Merge remote-tracking branch 'inventree/master' into 0.4.x

This commit is contained in:
Oliver 2021-08-11 00:29:36 +10:00
commit aea43924ae
13 changed files with 717 additions and 559 deletions

View File

@ -21,28 +21,15 @@ class AuthRequiredMiddleware(object):
assert hasattr(request, 'user') assert hasattr(request, 'user')
response = self.get_response(request) # API requests are handled by the DRF library
if request.path_info.startswith('/api/'):
return self.get_response(request)
if not request.user.is_authenticated: if not request.user.is_authenticated:
""" """
Normally, a web-based session would use csrftoken based authentication. Normally, a web-based session would use csrftoken based authentication.
However when running an external application (e.g. the InvenTree app), However when running an external application (e.g. the InvenTree app or Python library),
we wish to use token-based auth to grab media files. we must validate the user token manually.
So, we will allow token-based authentication but ONLY for the /media/ directory.
What problem is this solving?
- The InvenTree mobile app does not use csrf token auth
- Token auth is used by the Django REST framework, but that is under the /api/ endpoint
- Media files (e.g. Part images) are required to be served to the app
- We do not want to make /media/ files accessible without login!
There is PROBABLY a better way of going about this?
a) Allow token-based authentication against a user?
b) Serve /media/ files in a duplicate location e.g. /api/media/ ?
c) Is there a "standard" way of solving this problem?
My [google|stackoverflow]-fu has failed me. So this hack has been created.
""" """
authorized = False authorized = False
@ -56,21 +43,24 @@ class AuthRequiredMiddleware(object):
elif request.path_info.startswith('/accounts/'): elif request.path_info.startswith('/accounts/'):
authorized = True authorized = True
elif 'Authorization' in request.headers.keys(): elif 'Authorization' in request.headers.keys() or 'authorization' in request.headers.keys():
auth = request.headers['Authorization'].strip() auth = request.headers.get('Authorization', request.headers.get('authorization')).strip()
if auth.startswith('Token') and len(auth.split()) == 2: if auth.lower().startswith('token') and len(auth.split()) == 2:
token = auth.split()[1] token_key = auth.split()[1]
# Does the provided token match a valid user? # Does the provided token match a valid user?
if Token.objects.filter(key=token).exists(): try:
token = Token.objects.get(key=token_key)
allowed = ['/api/', '/media/'] # Provide the user information to the request
request.user = token.user
# Only allow token-auth for /media/ or /static/ dirs!
if any([request.path_info.startswith(a) for a in allowed]):
authorized = True authorized = True
except Token.DoesNotExist:
logger.warning(f"Access denied for unknown token {token_key}")
pass
# No authorization was found for the request # No authorization was found for the request
if not authorized: if not authorized:
# A logout request will redirect the user to the login screen # A logout request will redirect the user to the login screen
@ -92,8 +82,7 @@ class AuthRequiredMiddleware(object):
return redirect('%s?next=%s' % (reverse_lazy('login'), request.path)) return redirect('%s?next=%s' % (reverse_lazy('login'), request.path))
# Code to be executed for each request/response after response = self.get_response(request)
# the view is called.
return response return response

View File

@ -6,7 +6,8 @@ import json
import requests import requests
import logging import logging
from datetime import datetime, timedelta from datetime import timedelta
from django.utils import timezone
from django.core.exceptions import AppRegistryNotReady from django.core.exceptions import AppRegistryNotReady
from django.db.utils import OperationalError, ProgrammingError from django.db.utils import OperationalError, ProgrammingError
@ -51,11 +52,14 @@ def schedule_task(taskname, **kwargs):
pass pass
def offload_task(taskname, *args, **kwargs): def offload_task(taskname, force_sync=False, *args, **kwargs):
""" """
Create an AsyncTask. Create an AsyncTask if workers are running.
This is different to a 'scheduled' task, This is different to a 'scheduled' task,
in that it only runs once! in that it only runs once!
If workers are not running or force_sync flag
is set then the task is ran synchronously.
""" """
try: try:
@ -63,10 +67,48 @@ def offload_task(taskname, *args, **kwargs):
except (AppRegistryNotReady): except (AppRegistryNotReady):
logger.warning("Could not offload task - app registry not ready") logger.warning("Could not offload task - app registry not ready")
return return
import importlib
from InvenTree.status import is_worker_running
if is_worker_running() and not force_sync:
# Running as asynchronous task
try:
task = AsyncTask(taskname, *args, **kwargs) task = AsyncTask(taskname, *args, **kwargs)
task.run() task.run()
except ImportError:
logger.warning(f"WARNING: '{taskname}' not started - Function not found")
else:
# Split path
try:
app, mod, func = taskname.split('.')
app_mod = app + '.' + mod
except ValueError:
logger.warning(f"WARNING: '{taskname}' not started - Malformed function path")
return
# Import module from app
try:
_mod = importlib.import_module(app_mod)
except ModuleNotFoundError:
logger.warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'")
return
# Retrieve function
try:
_func = getattr(_mod, func)
except AttributeError:
# getattr does not work for local import
_func = None
try:
if not _func:
_func = eval(func)
except NameError:
logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'")
return
# Workers are not running: run it as synchronous task
_func()
def heartbeat(): def heartbeat():
@ -84,7 +126,7 @@ def heartbeat():
except AppRegistryNotReady: except AppRegistryNotReady:
return return
threshold = datetime.now() - timedelta(minutes=30) threshold = timezone.now() - timedelta(minutes=30)
# Delete heartbeat results more than half an hour old, # Delete heartbeat results more than half an hour old,
# otherwise they just create extra noise # otherwise they just create extra noise
@ -108,7 +150,7 @@ def delete_successful_tasks():
logger.info("Could not perform 'delete_successful_tasks' - App registry not ready") logger.info("Could not perform 'delete_successful_tasks' - App registry not ready")
return return
threshold = datetime.now() - timedelta(days=30) threshold = timezone.now() - timedelta(days=30)
results = Success.objects.filter( results = Success.objects.filter(
started__lte=threshold started__lte=threshold

View File

@ -31,8 +31,6 @@ from stock.models import StockLocation, StockItem
from common.models import InvenTreeSetting, ColorTheme from common.models import InvenTreeSetting, ColorTheme
from users.models import check_user_role, RuleSet from users.models import check_user_role, RuleSet
import InvenTree.tasks
from .forms import DeleteForm, EditUserForm, SetPasswordForm from .forms import DeleteForm, EditUserForm, SetPasswordForm
from .forms import SettingCategorySelectForm from .forms import SettingCategorySelectForm
from .helpers import str2bool from .helpers import str2bool
@ -827,8 +825,13 @@ class CurrencyRefreshView(RedirectView):
On a POST request we will attempt to refresh the exchange rates On a POST request we will attempt to refresh the exchange rates
""" """
# Will block for a little bit from InvenTree.tasks import offload_task
InvenTree.tasks.update_exchange_rates()
# Define associated task from InvenTree.tasks list of methods
taskname = 'InvenTree.tasks.update_exchange_rates'
# Run it
offload_task(taskname, force_sync=True)
return redirect(reverse_lazy('settings')) return redirect(reverse_lazy('settings'))

View File

@ -9,6 +9,8 @@ import os
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Sum, Q, UniqueConstraint from django.db.models import Sum, Q, UniqueConstraint
@ -473,12 +475,32 @@ class SupplierPart(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('supplier-part-detail', kwargs={'pk': self.id}) return reverse('supplier-part-detail', kwargs={'pk': self.id})
def api_instance_filters(self):
return {
'manufacturer_part': {
'part': self.part.pk
}
}
class Meta: class Meta:
unique_together = ('part', 'supplier', 'SKU') unique_together = ('part', 'supplier', 'SKU')
# This model was moved from the 'Part' app # This model was moved from the 'Part' app
db_table = 'part_supplierpart' db_table = 'part_supplierpart'
def clean(self):
super().clean()
# Ensure that the linked manufacturer_part points to the same part!
if self.manufacturer_part and self.part:
if not self.manufacturer_part.part == self.part:
raise ValidationError({
'manufacturer_part': _("Linked manufacturer part must reference the same base part"),
})
part = models.ForeignKey('part.Part', on_delete=models.CASCADE, part = models.ForeignKey('part.Part', on_delete=models.CASCADE,
related_name='supplier_parts', related_name='supplier_parts',
verbose_name=_('Base Part'), verbose_name=_('Base Part'),

View File

@ -5,7 +5,7 @@
{% block form_alert %} {% block form_alert %}
{% if missing_columns and missing_columns|length > 0 %} {% if missing_columns and missing_columns|length > 0 %}
<div class='alert alert-danger alert-block' role='alert'> <div class='alert alert-danger alert-block' style='margin-top:12px;' role='alert'>
{% trans "Missing selections for the following required columns" %}: {% trans "Missing selections for the following required columns" %}:
<br> <br>
<ul> <ul>

View File

@ -1,49 +1,76 @@
{% extends "part/part_base.html" %} {% extends "part/part_base.html" %}
{% load static %}
{% load i18n %}
{% load inventree_extras %} {% load inventree_extras %}
{% load i18n %}
{% load static %}
{% block heading %} {% block menubar %}
{% trans "Upload BOM File" %} <ul class='list-group'>
<li class='list-group-item'>
<a href='#' id='part-menu-toggle'>
<span class='menu-tab-icon fas fa-expand-arrows-alt'></span>
</a>
</li>
<li class='list-group-item' title='{% trans "Return To BOM" %}'>
<a href='{% url "part-detail" part.id %}' id='select-upload-file' class='nav-toggle'>
<span class='fas fa-undo side-icon'></span>
{% trans "Return To BOM" %}
</a>
</li>
</ul>
{% endblock %} {% endblock %}
{% block page_content %} {% block page_content %}
<h4>{% trans "Upload Bill of Materials" %}</h4> <div class='panel panel-default panel-inventree' id='panel-upload-file'>
<div class='panel-heading'>
{% block heading %}
<h4>{% trans "Upload Bill of Materials" %}</h4>
{{ wizard.form.media }}
{% endblock %}
</div>
<div class='panel-content'>
{% block details %}
{% block form_alert %} <p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %}
<div class='alert alert-info alert-block'> {% if description %}- {{ description }}{% endif %}</p>
<form action="" method="post" class='js-modal-form' enctype="multipart/form-data">
{% csrf_token %}
{% load crispy_forms_tags %}
{% block form_buttons_top %}
{% endblock form_buttons_top %}
{% block form_alert %}
<div class='alert alert-info alert-block'>
<b>{% trans "Requirements for BOM upload" %}:</b> <b>{% trans "Requirements for BOM upload" %}:</b>
<ul> <ul>
<li>{% trans "The BOM file must contain the required named columns as provided in the " %} <b><a href="/part/bom_template/">{% trans "BOM Upload Template" %}</a></b></li> <li>{% trans "The BOM file must contain the required named columns as provided in the " %} <b><a href="/part/bom_template/">{% trans "BOM Upload Template" %}</a></b></li>
<li>{% trans "Each part must already exist in the database" %}</li> <li>{% trans "Each part must already exist in the database" %}</li>
</ul> </ul>
</div> </div>
{% endblock %} {% endblock %}
<p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %} <table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'>
{% if description %}- {{ description }}{% endif %}</p> {{ wizard.management_form }}
{% block form_content %}
{% crispy wizard.form %}
{% endblock form_content %}
</table>
<form action="" method="post" class='js-modal-form' enctype="multipart/form-data"> {% block form_buttons_bottom %}
{% csrf_token %} {% if wizard.steps.prev %}
{% load crispy_forms_tags %} <button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button>
{% endif %}
<button type="submit" class="save btn btn-default">{% trans "Upload File" %}</button>
</form>
{% endblock form_buttons_bottom %}
{% block form_buttons_top %} {% endblock details %}
{% endblock form_buttons_top %} </div>
<table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'> {% endblock page_content %}
{{ wizard.management_form }}
{% block form_content %}
{% crispy wizard.form %}
{% endblock form_content %}
</table>
{% block form_buttons_bottom %} {% block js_ready %}
{% if wizard.steps.prev %} {{ block.super }}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="save btn btn-default">{% trans "Previous Step" %}</button> {% endblock js_ready %}
{% endif %}
<button type="submit" class="save btn btn-default">{% trans "Upload File" %}</button>
</form>
{% endblock form_buttons_bottom %}
{% endblock %}

View File

@ -333,11 +333,111 @@
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
enableNavbar({ // Load the "suppliers" tab
label: 'part', onPanelLoad('suppliers', function() {
toggleId: '#part-menu-toggle', function reloadSupplierPartTable() {
$('#supplier-part-table').bootstrapTable('refresh');
}
$('#supplier-create').click(function () {
createSupplierPart({
part: {{ part.pk }},
onSuccess: reloadSupplierPartTable,
});
}); });
$("#supplier-part-delete").click(function() {
var selections = $("#supplier-part-table").bootstrapTable("getSelections");
var requests = [];
showQuestionDialog(
'{% trans "Delete Supplier Parts?" %}',
'{% trans "All selected supplier parts will be deleted" %}',
{
accept: function() {
selections.forEach(function(part) {
var url = `/api/company/part/${part.pk}/`;
requests.push(inventreeDelete(url));
});
$.when.apply($, requests).done(function() {
reloadSupplierPartTable();
});
}
}
);
});
loadSupplierPartTable(
"#supplier-part-table",
"{% url 'api-supplier-part-list' %}",
{
params: {
part: {{ part.id }},
part_detail: false,
supplier_detail: true,
manufacturer_detail: true,
},
}
);
linkButtonsToSelection($("#supplier-part-table"), ['#supplier-part-options']);
loadManufacturerPartTable(
'#manufacturer-part-table',
"{% url 'api-manufacturer-part-list' %}",
{
params: {
part: {{ part.id }},
part_detail: true,
manufacturer_detail: true,
},
}
);
linkButtonsToSelection($("#manufacturer-part-table"), ['#manufacturer-part-options']);
$("#manufacturer-part-delete").click(function() {
var selections = $("#manufacturer-part-table").bootstrapTable("getSelections");
deleteManufacturerParts(selections, {
onSuccess: function() {
$("#manufacturer-part-table").bootstrapTable("refresh");
}
});
});
$('#manufacturer-create').click(function () {
createManufacturerPart({
part: {{ part.pk }},
onSuccess: function() {
$("#manufacturer-part-table").bootstrapTable("refresh");
}
});
});
});
// Load the "builds" tab
onPanelLoad("build-orders", function() {
$("#start-build").click(function() {
newBuildOrder({
part: {{ part.pk }},
});
});
loadBuildTable($("#build-table"), {
url: "{% url 'api-build-list' %}",
params: {
part: {{ part.id }},
}
});
loadBuildOrderAllocationTable("#build-order-allocation-table", { loadBuildOrderAllocationTable("#build-order-allocation-table", {
params: { params: {
@ -345,12 +445,19 @@
} }
}); });
});
// Load the "sales orders" tab
onPanelLoad("sales-orders", function() {
loadSalesOrderAllocationTable("#sales-order-allocation-table", { loadSalesOrderAllocationTable("#sales-order-allocation-table", {
params: { params: {
part: {{ part.id }}, part: {{ part.id }},
} }
}); });
});
// Load the "used in" tab
onPanelLoad("used-in", function() {
loadPartTable('#used-table', loadPartTable('#used-table',
'{% url "api-part-list" %}', '{% url "api-part-list" %}',
{ {
@ -360,7 +467,10 @@
filterTarget: '#filter-list-usedin', filterTarget: '#filter-list-usedin',
} }
); );
});
// Load the "BOM" tab
onPanelLoad("bom", function() {
// Load the BOM table data // Load the BOM table data
loadBomTable($("#bom-table"), { loadBomTable($("#bom-table"), {
editable: {{ editing_enabled }}, editable: {{ editing_enabled }},
@ -370,16 +480,6 @@
sub_part_detail: true, sub_part_detail: true,
}); });
// Load the BOM table data in the pricing view
loadBomTable($("#bom-pricing-table"), {
editable: {{ editing_enabled }},
bom_url: "{% url 'api-bom-list' %}",
part_url: "{% url 'api-part-list' %}",
parent_id: {{ part.id }} ,
sub_part_detail: true,
});
linkButtonsToSelection($("#bom-table"), linkButtonsToSelection($("#bom-table"),
[ [
"#bom-item-delete", "#bom-item-delete",
@ -488,20 +588,10 @@
$("#print-bom-report").click(function() { $("#print-bom-report").click(function() {
printBomReports([{{ part.pk }}]); printBomReports([{{ part.pk }}]);
}); });
$("#start-build").click(function() {
newBuildOrder({
part: {{ part.pk }},
});
});
loadBuildTable($("#build-table"), {
url: "{% url 'api-build-list' %}",
params: {
part: {{ part.id }},
}
}); });
// Load the "related parts" tab
onPanelLoad("related-parts", function() {
$('#table-related-part').inventreeTable({ $('#table-related-part').inventreeTable({
}); });
@ -521,7 +611,10 @@
reload: true, reload: true,
}); });
}); });
});
// Load the "variants" tab
onPanelLoad("variants", function() {
loadPartVariantTable($('#variants-table'), {{ part.pk }}); loadPartVariantTable($('#variants-table'), {{ part.pk }});
$('#new-variant').click(function() { $('#new-variant').click(function() {
@ -533,13 +626,36 @@
} }
); );
}); });
});
// Load the BOM table data in the pricing view
loadBomTable($("#bom-pricing-table"), {
editable: {{ editing_enabled }},
bom_url: "{% url 'api-bom-list' %}",
part_url: "{% url 'api-part-list' %}",
parent_id: {{ part.id }} ,
sub_part_detail: true,
});
onPanelLoad("purchase-orders", function() {
loadPurchaseOrderTable($("#purchase-order-table"), { loadPurchaseOrderTable($("#purchase-order-table"), {
url: "{% url 'api-po-list' %}", url: "{% url 'api-po-list' %}",
params: { params: {
part: {{ part.id }}, part: {{ part.id }},
}, },
}); });
});
onPanelLoad("sales-orders", function() {
loadSalesOrderTable($("#sales-order-table"), {
url: "{% url 'api-so-list' %}",
params: {
part: {{ part.id }},
},
});
});
$("#part-order2").click(function() { $("#part-order2").click(function() {
launchModalForm("{% url 'order-parts' %}", { launchModalForm("{% url 'order-parts' %}", {
@ -550,13 +666,7 @@
}); });
}); });
loadSalesOrderTable($("#sales-order-table"), { onPanelLoad("test-templates", function() {
url: "{% url 'api-so-list' %}",
params: {
part: {{ part.id }},
},
});
loadPartTestTemplateTable( loadPartTestTemplateTable(
$("#test-template-table"), $("#test-template-table"),
{ {
@ -567,12 +677,12 @@
} }
); );
function reloadTable() { $("#add-test-template").click(function() {
function reloadTestTemplateTable() {
$("#test-template-table").bootstrapTable("refresh"); $("#test-template-table").bootstrapTable("refresh");
} }
$("#add-test-template").click(function() {
constructForm('{% url "api-part-test-template-list" %}', { constructForm('{% url "api-part-test-template-list" %}', {
method: 'POST', method: 'POST',
fields: { fields: {
@ -587,8 +697,7 @@
} }
}, },
title: '{% trans "Add Test Result Template" %}', title: '{% trans "Add Test Result Template" %}',
onSuccess: reloadTable onSuccess: reloadTestTemplateTable
});
}); });
$("#test-template-table").on('click', '.button-test-edit', function() { $("#test-template-table").on('click', '.button-test-edit', function() {
@ -605,7 +714,7 @@
requires_attachment: {}, requires_attachment: {},
}, },
title: '{% trans "Edit Test Result Template" %}', title: '{% trans "Edit Test Result Template" %}',
onSuccess: reloadTable, onSuccess: reloadTestTemplateTable,
}); });
}); });
@ -617,10 +726,14 @@
constructForm(url, { constructForm(url, {
method: 'DELETE', method: 'DELETE',
title: '{% trans "Delete Test Result Template" %}', title: '{% trans "Delete Test Result Template" %}',
onSuccess: reloadTable, onSuccess: reloadTestTemplateTable,
}); });
}); });
});
});
onPanelLoad("part-stock", function() {
$('#add-stock-item').click(function () { $('#add-stock-item').click(function () {
createNewStockItem({ createNewStockItem({
reload: true, reload: true,
@ -659,6 +772,7 @@
} }
}); });
}); });
});
$('#edit-notes').click(function() { $('#edit-notes').click(function() {
constructForm('{% url "api-part-detail" part.pk %}', { constructForm('{% url "api-part-detail" part.pk %}', {
@ -690,6 +804,7 @@
); );
}); });
onPanelLoad("part-parameters", function() {
loadPartParameterTable( loadPartParameterTable(
'#parameter-table', '#parameter-table',
'{% url "api-part-parameter-list" %}', '{% url "api-part-parameter-list" %}',
@ -739,7 +854,9 @@
reload: true, reload: true,
}); });
}); });
});
onPanelLoad("part-attachments", function() {
loadAttachmentTable( loadAttachmentTable(
'{% url "api-part-attachment-list" %}', '{% url "api-part-attachment-list" %}',
{ {
@ -803,93 +920,8 @@
} }
) )
}); });
function reloadSupplierPartTable() {
$('#supplier-part-table').bootstrapTable('refresh');
}
$('#supplier-create').click(function () {
createSupplierPart({
part: {{ part.pk }},
onSuccess: reloadSupplierPartTable,
});
}); });
$("#supplier-part-delete").click(function() {
var selections = $("#supplier-part-table").bootstrapTable("getSelections");
var requests = [];
showQuestionDialog(
'{% trans "Delete Supplier Parts?" %}',
'{% trans "All selected supplier parts will be deleted" %}',
{
accept: function() {
selections.forEach(function(part) {
var url = `/api/company/part/${part.pk}/`;
requests.push(inventreeDelete(url));
});
$.when.apply($, requests).done(function() {
reloadSupplierPartTable();
});
}
}
);
});
loadSupplierPartTable(
"#supplier-part-table",
"{% url 'api-supplier-part-list' %}",
{
params: {
part: {{ part.id }},
part_detail: false,
supplier_detail: true,
manufacturer_detail: true,
},
}
);
linkButtonsToSelection($("#supplier-part-table"), ['#supplier-part-options']);
loadManufacturerPartTable(
'#manufacturer-part-table',
"{% url 'api-manufacturer-part-list' %}",
{
params: {
part: {{ part.id }},
part_detail: true,
manufacturer_detail: true,
},
}
);
linkButtonsToSelection($("#manufacturer-part-table"), ['#manufacturer-part-options']);
$("#manufacturer-part-delete").click(function() {
var selections = $("#manufacturer-part-table").bootstrapTable("getSelections");
deleteManufacturerParts(selections, {
onSuccess: function() {
$("#manufacturer-part-table").bootstrapTable("refresh");
}
});
});
$('#manufacturer-create').click(function () {
createManufacturerPart({
part: {{ part.pk }},
onSuccess: function() {
$("#manufacturer-part-table").bootstrapTable("refresh");
}
});
});
{% default_currency as currency %} {% default_currency as currency %}

View File

@ -213,6 +213,7 @@
<p> <p>
<!-- Details show/hide button --> <!-- Details show/hide button -->
<button id="toggle-part-details" class="btn btn-primary" data-toggle="collapse" data-target="#collapsible-part-details" value="show"> <button id="toggle-part-details" class="btn btn-primary" data-toggle="collapse" data-target="#collapsible-part-details" value="show">
<span class="fas fa-chevron-down"></span> {% trans "Show Part Details" %}
</button> </button>
</p> </p>
@ -305,6 +306,11 @@
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
enableNavbar({
label: 'part',
toggleId: '#part-menu-toggle',
});
{% if part.image %} {% if part.image %}
$('#part-thumb').click(function() { $('#part-thumb').click(function() {
showModalImage('{{ part.image.url }}'); showModalImage('{{ part.image.url }}');

View File

@ -3,6 +3,7 @@
{% load static %} {% load static %}
{% load inventree_extras %} {% load inventree_extras %}
{% load i18n %} {% load i18n %}
{% load l10n %}
{% load markdownify %} {% load markdownify %}
{% block menubar %} {% block menubar %}
@ -152,7 +153,7 @@
{ {
stock_item: {{ item.pk }}, stock_item: {{ item.pk }},
part: {{ item.part.pk }}, part: {{ item.part.pk }},
quantity: {{ item.quantity }}, quantity: {{ item.quantity|unlocalize }},
} }
); );

View File

@ -72,7 +72,12 @@ function activatePanel(panelName, options={}) {
// Display the panel // Display the panel
$(panel).addClass('panel-visible'); $(panel).addClass('panel-visible');
$(panel).fadeIn(100);
// Load the data
$(panel).trigger('fadeInStarted');
$(panel).fadeIn(100, function() {
});
// Un-select all selectors // Un-select all selectors
$('.list-group-item').removeClass('active'); $('.list-group-item').removeClass('active');
@ -82,3 +87,22 @@ function activatePanel(panelName, options={}) {
$(select).parent('.list-group-item').addClass('active'); $(select).parent('.list-group-item').addClass('active');
} }
function onPanelLoad(panel, callback) {
// One-time callback when a panel is first displayed
// Used to implement lazy-loading, rather than firing
// multiple AJAX queries when the page is first loaded.
var panelId = `#panel-${panel}`;
$(panelId).on('fadeInStarted', function(e) {
// Trigger the callback
callback();
// Turn off the event
$(panelId).off('fadeInStarted');
});
}

View File

@ -54,8 +54,12 @@ function editManufacturerPart(part, options={}) {
var url = `/api/company/part/manufacturer/${part}/`; var url = `/api/company/part/manufacturer/${part}/`;
var fields = manufacturerPartFields();
fields.part.hidden = true;
constructForm(url, { constructForm(url, {
fields: manufacturerPartFields(), fields: fields,
title: '{% trans "Edit Manufacturer Part" %}', title: '{% trans "Edit Manufacturer Part" %}',
onSuccess: options.onSuccess onSuccess: options.onSuccess
}); });
@ -157,8 +161,13 @@ function createSupplierPart(options={}) {
function editSupplierPart(part, options={}) { function editSupplierPart(part, options={}) {
var fields = supplierPartFields();
// Hide the "part" field
fields.part.hidden = true;
constructForm(`/api/company/part/${part}/`, { constructForm(`/api/company/part/${part}/`, {
fields: supplierPartFields(), fields: fields,
title: '{% trans "Edit Supplier Part" %}', title: '{% trans "Edit Supplier Part" %}',
onSuccess: options.onSuccess onSuccess: options.onSuccess
}); });

View File

@ -263,11 +263,13 @@ function adjustStock(action, items, options={}) {
required: true, required: true,
api_url: `/api/stock/location/`, api_url: `/api/stock/location/`,
model: 'stocklocation', model: 'stocklocation',
name: 'location',
}, },
notes: { notes: {
label: '{% trans "Notes" %}', label: '{% trans "Notes" %}',
help_text: '{% trans "Stock transaction notes" %}', help_text: '{% trans "Stock transaction notes" %}',
type: 'string', type: 'string',
name: 'notes',
} }
}; };
@ -665,6 +667,7 @@ function loadStockTable(table, options) {
// List of user-params which override the default filters // List of user-params which override the default filters
options.params['location_detail'] = true; options.params['location_detail'] = true;
options.params['part_detail'] = true;
var params = options.params || {}; var params = options.params || {};
@ -1114,11 +1117,11 @@ function loadStockTable(table, options) {
function stockAdjustment(action) { function stockAdjustment(action) {
var items = $("#stock-table").bootstrapTable("getSelections"); var items = $(table).bootstrapTable("getSelections");
adjustStock(action, items, { adjustStock(action, items, {
onSuccess: function() { onSuccess: function() {
$('#stock-table').bootstrapTable('refresh'); $(table).bootstrapTable('refresh');
} }
}); });
} }
@ -1126,7 +1129,7 @@ function loadStockTable(table, options) {
// Automatically link button callbacks // Automatically link button callbacks
$('#multi-item-print-label').click(function() { $('#multi-item-print-label').click(function() {
var selections = $('#stock-table').bootstrapTable('getSelections'); var selections = $(table).bootstrapTable('getSelections');
var items = []; var items = [];
@ -1138,7 +1141,7 @@ function loadStockTable(table, options) {
}); });
$('#multi-item-print-test-report').click(function() { $('#multi-item-print-test-report').click(function() {
var selections = $('#stock-table').bootstrapTable('getSelections'); var selections = $(table).bootstrapTable('getSelections');
var items = []; var items = [];
@ -1151,7 +1154,7 @@ function loadStockTable(table, options) {
if (global_settings.BARCODE_ENABLE) { if (global_settings.BARCODE_ENABLE) {
$('#multi-item-barcode-scan-into-location').click(function() { $('#multi-item-barcode-scan-into-location').click(function() {
var selections = $('#stock-table').bootstrapTable('getSelections'); var selections = $(table).bootstrapTable('getSelections');
var items = []; var items = [];
@ -1180,7 +1183,7 @@ function loadStockTable(table, options) {
}); });
$("#multi-item-order").click(function() { $("#multi-item-order").click(function() {
var selections = $("#stock-table").bootstrapTable("getSelections"); var selections = $(table).bootstrapTable("getSelections");
var stock = []; var stock = [];
@ -1197,7 +1200,7 @@ function loadStockTable(table, options) {
$("#multi-item-set-status").click(function() { $("#multi-item-set-status").click(function() {
// Select and set the STATUS field for selected stock items // Select and set the STATUS field for selected stock items
var selections = $("#stock-table").bootstrapTable('getSelections'); var selections = $(table).bootstrapTable('getSelections');
// Select stock status // Select stock status
var modal = '#modal-form'; var modal = '#modal-form';
@ -1277,13 +1280,13 @@ function loadStockTable(table, options) {
}); });
$.when.apply($, requests).done(function() { $.when.apply($, requests).done(function() {
$("#stock-table").bootstrapTable('refresh'); $(table).bootstrapTable('refresh');
}); });
}) })
}); });
$("#multi-item-delete").click(function() { $("#multi-item-delete").click(function() {
var selections = $("#stock-table").bootstrapTable("getSelections"); var selections = $(table).bootstrapTable("getSelections");
var stock = []; var stock = [];

View File

@ -10,7 +10,7 @@ server {
proxy_pass http://inventree-server:8000; proxy_pass http://inventree-server:8000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host; proxy_set_header Host $http_host;
proxy_redirect off; proxy_redirect off;