mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master' into 0.4.x
This commit is contained in:
commit
aea43924ae
@ -21,28 +21,15 @@ class AuthRequiredMiddleware(object):
|
||||
|
||||
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:
|
||||
"""
|
||||
Normally, a web-based session would use csrftoken based authentication.
|
||||
However when running an external application (e.g. the InvenTree app),
|
||||
we wish to use token-based auth to grab media files.
|
||||
|
||||
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.
|
||||
However when running an external application (e.g. the InvenTree app or Python library),
|
||||
we must validate the user token manually.
|
||||
"""
|
||||
|
||||
authorized = False
|
||||
@ -56,21 +43,24 @@ class AuthRequiredMiddleware(object):
|
||||
elif request.path_info.startswith('/accounts/'):
|
||||
authorized = True
|
||||
|
||||
elif 'Authorization' in request.headers.keys():
|
||||
auth = request.headers['Authorization'].strip()
|
||||
elif 'Authorization' in request.headers.keys() or 'authorization' in request.headers.keys():
|
||||
auth = request.headers.get('Authorization', request.headers.get('authorization')).strip()
|
||||
|
||||
if auth.startswith('Token') and len(auth.split()) == 2:
|
||||
token = auth.split()[1]
|
||||
if auth.lower().startswith('token') and len(auth.split()) == 2:
|
||||
token_key = auth.split()[1]
|
||||
|
||||
# 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/']
|
||||
|
||||
# Only allow token-auth for /media/ or /static/ dirs!
|
||||
if any([request.path_info.startswith(a) for a in allowed]):
|
||||
# Provide the user information to the request
|
||||
request.user = token.user
|
||||
authorized = True
|
||||
|
||||
except Token.DoesNotExist:
|
||||
logger.warning(f"Access denied for unknown token {token_key}")
|
||||
pass
|
||||
|
||||
# No authorization was found for the request
|
||||
if not authorized:
|
||||
# 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))
|
||||
|
||||
# Code to be executed for each request/response after
|
||||
# the view is called.
|
||||
response = self.get_response(request)
|
||||
|
||||
return response
|
||||
|
||||
|
@ -6,7 +6,8 @@ import json
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
@ -51,11 +52,14 @@ def schedule_task(taskname, **kwargs):
|
||||
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,
|
||||
in that it only runs once!
|
||||
|
||||
If workers are not running or force_sync flag
|
||||
is set then the task is ran synchronously.
|
||||
"""
|
||||
|
||||
try:
|
||||
@ -63,10 +67,48 @@ def offload_task(taskname, *args, **kwargs):
|
||||
except (AppRegistryNotReady):
|
||||
logger.warning("Could not offload task - app registry not ready")
|
||||
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.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():
|
||||
@ -84,7 +126,7 @@ def heartbeat():
|
||||
except AppRegistryNotReady:
|
||||
return
|
||||
|
||||
threshold = datetime.now() - timedelta(minutes=30)
|
||||
threshold = timezone.now() - timedelta(minutes=30)
|
||||
|
||||
# Delete heartbeat results more than half an hour old,
|
||||
# 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")
|
||||
return
|
||||
|
||||
threshold = datetime.now() - timedelta(days=30)
|
||||
threshold = timezone.now() - timedelta(days=30)
|
||||
|
||||
results = Success.objects.filter(
|
||||
started__lte=threshold
|
||||
|
@ -31,8 +31,6 @@ from stock.models import StockLocation, StockItem
|
||||
from common.models import InvenTreeSetting, ColorTheme
|
||||
from users.models import check_user_role, RuleSet
|
||||
|
||||
import InvenTree.tasks
|
||||
|
||||
from .forms import DeleteForm, EditUserForm, SetPasswordForm
|
||||
from .forms import SettingCategorySelectForm
|
||||
from .helpers import str2bool
|
||||
@ -827,8 +825,13 @@ class CurrencyRefreshView(RedirectView):
|
||||
On a POST request we will attempt to refresh the exchange rates
|
||||
"""
|
||||
|
||||
# Will block for a little bit
|
||||
InvenTree.tasks.update_exchange_rates()
|
||||
from InvenTree.tasks import offload_task
|
||||
|
||||
# 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'))
|
||||
|
||||
|
@ -9,6 +9,8 @@ import os
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Sum, Q, UniqueConstraint
|
||||
|
||||
@ -473,12 +475,32 @@ class SupplierPart(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return reverse('supplier-part-detail', kwargs={'pk': self.id})
|
||||
|
||||
def api_instance_filters(self):
|
||||
|
||||
return {
|
||||
'manufacturer_part': {
|
||||
'part': self.part.pk
|
||||
}
|
||||
}
|
||||
|
||||
class Meta:
|
||||
unique_together = ('part', 'supplier', 'SKU')
|
||||
|
||||
# This model was moved from the 'Part' app
|
||||
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,
|
||||
related_name='supplier_parts',
|
||||
verbose_name=_('Base Part'),
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
{% block form_alert %}
|
||||
{% 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" %}:
|
||||
<br>
|
||||
<ul>
|
||||
|
@ -1,25 +1,35 @@
|
||||
{% extends "part/part_base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block heading %}
|
||||
{% trans "Upload BOM File" %}
|
||||
{% block menubar %}
|
||||
<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 %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<div class='panel panel-default panel-inventree' id='panel-upload-file'>
|
||||
<div class='panel-heading'>
|
||||
{% block heading %}
|
||||
<h4>{% trans "Upload Bill of Materials" %}</h4>
|
||||
|
||||
{% block form_alert %}
|
||||
<div class='alert alert-info alert-block'>
|
||||
<b>{% trans "Requirements for BOM upload" %}:</b>
|
||||
<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 "Each part must already exist in the database" %}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ wizard.form.media }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class='panel-content'>
|
||||
{% block details %}
|
||||
|
||||
<p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %}
|
||||
{% if description %}- {{ description }}{% endif %}</p>
|
||||
@ -31,6 +41,16 @@
|
||||
{% 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>
|
||||
<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 "Each part must already exist in the database" %}</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<table class='table table-striped' style='margin-top: 12px; margin-bottom: 0px'>
|
||||
{{ wizard.management_form }}
|
||||
{% block form_content %}
|
||||
@ -46,4 +66,11 @@
|
||||
</form>
|
||||
{% endblock form_buttons_bottom %}
|
||||
|
||||
{% endblock %}
|
||||
{% endblock details %}
|
||||
</div>
|
||||
|
||||
{% endblock page_content %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
{% endblock js_ready %}
|
||||
|
@ -333,11 +333,111 @@
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
enableNavbar({
|
||||
label: 'part',
|
||||
toggleId: '#part-menu-toggle',
|
||||
// Load the "suppliers" tab
|
||||
onPanelLoad('suppliers', function() {
|
||||
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", {
|
||||
params: {
|
||||
@ -345,12 +445,19 @@
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// Load the "sales orders" tab
|
||||
onPanelLoad("sales-orders", function() {
|
||||
loadSalesOrderAllocationTable("#sales-order-allocation-table", {
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Load the "used in" tab
|
||||
onPanelLoad("used-in", function() {
|
||||
loadPartTable('#used-table',
|
||||
'{% url "api-part-list" %}',
|
||||
{
|
||||
@ -360,7 +467,10 @@
|
||||
filterTarget: '#filter-list-usedin',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Load the "BOM" tab
|
||||
onPanelLoad("bom", function() {
|
||||
// Load the BOM table data
|
||||
loadBomTable($("#bom-table"), {
|
||||
editable: {{ editing_enabled }},
|
||||
@ -370,16 +480,6 @@
|
||||
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"),
|
||||
[
|
||||
"#bom-item-delete",
|
||||
@ -488,20 +588,10 @@
|
||||
$("#print-bom-report").click(function() {
|
||||
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({
|
||||
});
|
||||
|
||||
@ -521,7 +611,10 @@
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Load the "variants" tab
|
||||
onPanelLoad("variants", function() {
|
||||
loadPartVariantTable($('#variants-table'), {{ part.pk }});
|
||||
|
||||
$('#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"), {
|
||||
url: "{% url 'api-po-list' %}",
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
onPanelLoad("sales-orders", function() {
|
||||
loadSalesOrderTable($("#sales-order-table"), {
|
||||
url: "{% url 'api-so-list' %}",
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$("#part-order2").click(function() {
|
||||
launchModalForm("{% url 'order-parts' %}", {
|
||||
@ -550,13 +666,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
loadSalesOrderTable($("#sales-order-table"), {
|
||||
url: "{% url 'api-so-list' %}",
|
||||
params: {
|
||||
part: {{ part.id }},
|
||||
},
|
||||
});
|
||||
|
||||
onPanelLoad("test-templates", function() {
|
||||
loadPartTestTemplateTable(
|
||||
$("#test-template-table"),
|
||||
{
|
||||
@ -567,12 +677,12 @@
|
||||
}
|
||||
);
|
||||
|
||||
function reloadTable() {
|
||||
$("#add-test-template").click(function() {
|
||||
|
||||
function reloadTestTemplateTable() {
|
||||
$("#test-template-table").bootstrapTable("refresh");
|
||||
}
|
||||
|
||||
$("#add-test-template").click(function() {
|
||||
|
||||
constructForm('{% url "api-part-test-template-list" %}', {
|
||||
method: 'POST',
|
||||
fields: {
|
||||
@ -587,8 +697,7 @@
|
||||
}
|
||||
},
|
||||
title: '{% trans "Add Test Result Template" %}',
|
||||
onSuccess: reloadTable
|
||||
});
|
||||
onSuccess: reloadTestTemplateTable
|
||||
});
|
||||
|
||||
$("#test-template-table").on('click', '.button-test-edit', function() {
|
||||
@ -605,7 +714,7 @@
|
||||
requires_attachment: {},
|
||||
},
|
||||
title: '{% trans "Edit Test Result Template" %}',
|
||||
onSuccess: reloadTable,
|
||||
onSuccess: reloadTestTemplateTable,
|
||||
});
|
||||
});
|
||||
|
||||
@ -617,10 +726,14 @@
|
||||
constructForm(url, {
|
||||
method: 'DELETE',
|
||||
title: '{% trans "Delete Test Result Template" %}',
|
||||
onSuccess: reloadTable,
|
||||
onSuccess: reloadTestTemplateTable,
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
onPanelLoad("part-stock", function() {
|
||||
$('#add-stock-item').click(function () {
|
||||
createNewStockItem({
|
||||
reload: true,
|
||||
@ -659,6 +772,7 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('#edit-notes').click(function() {
|
||||
constructForm('{% url "api-part-detail" part.pk %}', {
|
||||
@ -690,6 +804,7 @@
|
||||
);
|
||||
});
|
||||
|
||||
onPanelLoad("part-parameters", function() {
|
||||
loadPartParameterTable(
|
||||
'#parameter-table',
|
||||
'{% url "api-part-parameter-list" %}',
|
||||
@ -739,7 +854,9 @@
|
||||
reload: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
onPanelLoad("part-attachments", function() {
|
||||
loadAttachmentTable(
|
||||
'{% 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 %}
|
||||
|
||||
|
@ -213,6 +213,7 @@
|
||||
<p>
|
||||
<!-- Details show/hide button -->
|
||||
<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>
|
||||
</p>
|
||||
|
||||
@ -305,6 +306,11 @@
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
enableNavbar({
|
||||
label: 'part',
|
||||
toggleId: '#part-menu-toggle',
|
||||
});
|
||||
|
||||
{% if part.image %}
|
||||
$('#part-thumb').click(function() {
|
||||
showModalImage('{{ part.image.url }}');
|
||||
|
@ -3,6 +3,7 @@
|
||||
{% load static %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load markdownify %}
|
||||
|
||||
{% block menubar %}
|
||||
@ -152,7 +153,7 @@
|
||||
{
|
||||
stock_item: {{ item.pk }},
|
||||
part: {{ item.part.pk }},
|
||||
quantity: {{ item.quantity }},
|
||||
quantity: {{ item.quantity|unlocalize }},
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -72,7 +72,12 @@ function activatePanel(panelName, options={}) {
|
||||
|
||||
// Display the panel
|
||||
$(panel).addClass('panel-visible');
|
||||
$(panel).fadeIn(100);
|
||||
|
||||
// Load the data
|
||||
$(panel).trigger('fadeInStarted');
|
||||
|
||||
$(panel).fadeIn(100, function() {
|
||||
});
|
||||
|
||||
// Un-select all selectors
|
||||
$('.list-group-item').removeClass('active');
|
||||
@ -82,3 +87,22 @@ function activatePanel(panelName, options={}) {
|
||||
|
||||
$(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');
|
||||
|
||||
});
|
||||
}
|
@ -54,8 +54,12 @@ function editManufacturerPart(part, options={}) {
|
||||
|
||||
var url = `/api/company/part/manufacturer/${part}/`;
|
||||
|
||||
var fields = manufacturerPartFields();
|
||||
|
||||
fields.part.hidden = true;
|
||||
|
||||
constructForm(url, {
|
||||
fields: manufacturerPartFields(),
|
||||
fields: fields,
|
||||
title: '{% trans "Edit Manufacturer Part" %}',
|
||||
onSuccess: options.onSuccess
|
||||
});
|
||||
@ -157,8 +161,13 @@ function createSupplierPart(options={}) {
|
||||
|
||||
function editSupplierPart(part, options={}) {
|
||||
|
||||
var fields = supplierPartFields();
|
||||
|
||||
// Hide the "part" field
|
||||
fields.part.hidden = true;
|
||||
|
||||
constructForm(`/api/company/part/${part}/`, {
|
||||
fields: supplierPartFields(),
|
||||
fields: fields,
|
||||
title: '{% trans "Edit Supplier Part" %}',
|
||||
onSuccess: options.onSuccess
|
||||
});
|
||||
|
@ -263,11 +263,13 @@ function adjustStock(action, items, options={}) {
|
||||
required: true,
|
||||
api_url: `/api/stock/location/`,
|
||||
model: 'stocklocation',
|
||||
name: 'location',
|
||||
},
|
||||
notes: {
|
||||
label: '{% trans "Notes" %}',
|
||||
help_text: '{% trans "Stock transaction notes" %}',
|
||||
type: 'string',
|
||||
name: 'notes',
|
||||
}
|
||||
};
|
||||
|
||||
@ -665,6 +667,7 @@ function loadStockTable(table, options) {
|
||||
// List of user-params which override the default filters
|
||||
|
||||
options.params['location_detail'] = true;
|
||||
options.params['part_detail'] = true;
|
||||
|
||||
var params = options.params || {};
|
||||
|
||||
@ -1114,11 +1117,11 @@ function loadStockTable(table, options) {
|
||||
|
||||
|
||||
function stockAdjustment(action) {
|
||||
var items = $("#stock-table").bootstrapTable("getSelections");
|
||||
var items = $(table).bootstrapTable("getSelections");
|
||||
|
||||
adjustStock(action, items, {
|
||||
onSuccess: function() {
|
||||
$('#stock-table').bootstrapTable('refresh');
|
||||
$(table).bootstrapTable('refresh');
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1126,7 +1129,7 @@ function loadStockTable(table, options) {
|
||||
// Automatically link button callbacks
|
||||
|
||||
$('#multi-item-print-label').click(function() {
|
||||
var selections = $('#stock-table').bootstrapTable('getSelections');
|
||||
var selections = $(table).bootstrapTable('getSelections');
|
||||
|
||||
var items = [];
|
||||
|
||||
@ -1138,7 +1141,7 @@ function loadStockTable(table, options) {
|
||||
});
|
||||
|
||||
$('#multi-item-print-test-report').click(function() {
|
||||
var selections = $('#stock-table').bootstrapTable('getSelections');
|
||||
var selections = $(table).bootstrapTable('getSelections');
|
||||
|
||||
var items = [];
|
||||
|
||||
@ -1151,7 +1154,7 @@ function loadStockTable(table, options) {
|
||||
|
||||
if (global_settings.BARCODE_ENABLE) {
|
||||
$('#multi-item-barcode-scan-into-location').click(function() {
|
||||
var selections = $('#stock-table').bootstrapTable('getSelections');
|
||||
var selections = $(table).bootstrapTable('getSelections');
|
||||
|
||||
var items = [];
|
||||
|
||||
@ -1180,7 +1183,7 @@ function loadStockTable(table, options) {
|
||||
});
|
||||
|
||||
$("#multi-item-order").click(function() {
|
||||
var selections = $("#stock-table").bootstrapTable("getSelections");
|
||||
var selections = $(table).bootstrapTable("getSelections");
|
||||
|
||||
var stock = [];
|
||||
|
||||
@ -1197,7 +1200,7 @@ function loadStockTable(table, options) {
|
||||
|
||||
$("#multi-item-set-status").click(function() {
|
||||
// Select and set the STATUS field for selected stock items
|
||||
var selections = $("#stock-table").bootstrapTable('getSelections');
|
||||
var selections = $(table).bootstrapTable('getSelections');
|
||||
|
||||
// Select stock status
|
||||
var modal = '#modal-form';
|
||||
@ -1277,13 +1280,13 @@ function loadStockTable(table, options) {
|
||||
});
|
||||
|
||||
$.when.apply($, requests).done(function() {
|
||||
$("#stock-table").bootstrapTable('refresh');
|
||||
$(table).bootstrapTable('refresh');
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
$("#multi-item-delete").click(function() {
|
||||
var selections = $("#stock-table").bootstrapTable("getSelections");
|
||||
var selections = $(table).bootstrapTable("getSelections");
|
||||
|
||||
var stock = [];
|
||||
|
||||
|
@ -10,7 +10,7 @@ server {
|
||||
proxy_pass http://inventree-server:8000;
|
||||
|
||||
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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user