mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into plugin-2037
This commit is contained in:
commit
a95b298c62
@ -92,6 +92,12 @@ DEBUG = _is_true(get_setting(
|
|||||||
CONFIG.get('debug', True)
|
CONFIG.get('debug', True)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# Determine if we are running in "demo mode"
|
||||||
|
DEMO_MODE = _is_true(get_setting(
|
||||||
|
'INVENTREE_DEMO',
|
||||||
|
CONFIG.get('demo', False)
|
||||||
|
))
|
||||||
|
|
||||||
DOCKER = _is_true(get_setting(
|
DOCKER = _is_true(get_setting(
|
||||||
'INVENTREE_DOCKER',
|
'INVENTREE_DOCKER',
|
||||||
False
|
False
|
||||||
@ -239,7 +245,10 @@ STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes')
|
|||||||
MEDIA_URL = '/media/'
|
MEDIA_URL = '/media/'
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
logger.info("InvenTree running in DEBUG mode")
|
logger.info("InvenTree running with DEBUG enabled")
|
||||||
|
|
||||||
|
if DEMO_MODE:
|
||||||
|
logger.warning("InvenTree running in DEMO mode")
|
||||||
|
|
||||||
logger.debug(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
|
logger.debug(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
|
||||||
logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'")
|
logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'")
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 461 KiB |
BIN
InvenTree/InvenTree/static/img/paper_splash_large.jpg
Normal file
BIN
InvenTree/InvenTree/static/img/paper_splash_large.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 MiB |
@ -1,13 +0,0 @@
|
|||||||
from rest_framework.views import exception_handler
|
|
||||||
|
|
||||||
|
|
||||||
def api_exception_handler(exc, context):
|
|
||||||
response = exception_handler(exc, context)
|
|
||||||
|
|
||||||
# Now add the HTTP status code to the response.
|
|
||||||
if response is not None:
|
|
||||||
|
|
||||||
data = {'error': response.data}
|
|
||||||
response.data = data
|
|
||||||
|
|
||||||
return response
|
|
@ -12,11 +12,16 @@ import common.models
|
|||||||
INVENTREE_SW_VERSION = "0.6.0 dev"
|
INVENTREE_SW_VERSION = "0.6.0 dev"
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 17
|
INVENTREE_API_VERSION = 18
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v18 -> 2021-11-15
|
||||||
|
- Adds the ability to filter BomItem API by "uses" field
|
||||||
|
- This returns a list of all BomItems which "use" the specified part
|
||||||
|
- Includes inherited BomItem objects
|
||||||
|
|
||||||
v17 -> 2021-11-09
|
v17 -> 2021-11-09
|
||||||
- Adds API endpoints for GLOBAL and USER settings objects
|
- Adds API endpoints for GLOBAL and USER settings objects
|
||||||
- Ref: https://github.com/inventree/InvenTree/pull/2275
|
- Ref: https://github.com/inventree/InvenTree/pull/2275
|
||||||
|
@ -832,18 +832,6 @@ class PartList(generics.ListCreateAPIView):
|
|||||||
|
|
||||||
queryset = super().filter_queryset(queryset)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
# Filter by "uses" query - Limit to parts which use the provided part
|
|
||||||
uses = params.get('uses', None)
|
|
||||||
|
|
||||||
if uses:
|
|
||||||
try:
|
|
||||||
uses = Part.objects.get(pk=uses)
|
|
||||||
|
|
||||||
queryset = queryset.filter(uses.get_used_in_filter())
|
|
||||||
|
|
||||||
except (ValueError, Part.DoesNotExist):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Exclude specific part ID values?
|
# Exclude specific part ID values?
|
||||||
exclude_id = []
|
exclude_id = []
|
||||||
|
|
||||||
@ -1040,13 +1028,19 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
|
|||||||
serializer_class = part_serializers.PartParameterTemplateSerializer
|
serializer_class = part_serializers.PartParameterTemplateSerializer
|
||||||
|
|
||||||
filter_backends = [
|
filter_backends = [
|
||||||
|
DjangoFilterBackend,
|
||||||
filters.OrderingFilter,
|
filters.OrderingFilter,
|
||||||
|
filters.SearchFilter,
|
||||||
]
|
]
|
||||||
|
|
||||||
filter_fields = [
|
filter_fields = [
|
||||||
'name',
|
'name',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
'name',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class PartParameterList(generics.ListCreateAPIView):
|
class PartParameterList(generics.ListCreateAPIView):
|
||||||
""" API endpoint for accessing a list of PartParameter objects
|
""" API endpoint for accessing a list of PartParameter objects
|
||||||
@ -1211,6 +1205,54 @@ class BomList(generics.ListCreateAPIView):
|
|||||||
except (ValueError, Part.DoesNotExist):
|
except (ValueError, Part.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
"""
|
||||||
|
Filter by 'uses'?
|
||||||
|
|
||||||
|
Here we pass a part ID and return BOM items for any assemblies which "use" (or "require") that part.
|
||||||
|
|
||||||
|
There are multiple ways that an assembly can "use" a sub-part:
|
||||||
|
|
||||||
|
A) Directly specifying the sub_part in a BomItem field
|
||||||
|
B) Specifing a "template" part with inherited=True
|
||||||
|
C) Allowing variant parts to be substituted
|
||||||
|
D) Allowing direct substitute parts to be specified
|
||||||
|
|
||||||
|
- BOM items which are "inherited" by parts which are variants of the master BomItem
|
||||||
|
"""
|
||||||
|
uses = params.get('uses', None)
|
||||||
|
|
||||||
|
if uses is not None:
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Extract the part we are interested in
|
||||||
|
uses_part = Part.objects.get(pk=uses)
|
||||||
|
|
||||||
|
# Construct the database query in multiple parts
|
||||||
|
|
||||||
|
# A) Direct specification of sub_part
|
||||||
|
q_A = Q(sub_part=uses_part)
|
||||||
|
|
||||||
|
# B) BomItem is inherited and points to a "parent" of this part
|
||||||
|
parents = uses_part.get_ancestors(include_self=False)
|
||||||
|
|
||||||
|
q_B = Q(
|
||||||
|
inherited=True,
|
||||||
|
sub_part__in=parents
|
||||||
|
)
|
||||||
|
|
||||||
|
# C) Substitution of variant parts
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
# D) Specification of individual substitutes
|
||||||
|
# TODO
|
||||||
|
|
||||||
|
q = q_A | q_B
|
||||||
|
|
||||||
|
queryset = queryset.filter(q)
|
||||||
|
|
||||||
|
except (ValueError, Part.DoesNotExist):
|
||||||
|
pass
|
||||||
|
|
||||||
if self.include_pricing():
|
if self.include_pricing():
|
||||||
queryset = self.annotate_pricing(queryset)
|
queryset = self.annotate_pricing(queryset)
|
||||||
|
|
||||||
|
@ -1,13 +1,29 @@
|
|||||||
# Generated by Django 2.2 on 2019-05-20 12:04
|
# Generated by Django 2.2 on 2019-05-20 12:04
|
||||||
|
|
||||||
import InvenTree.validators
|
import os
|
||||||
from django.conf import settings
|
|
||||||
import django.core.validators
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
import django.core.validators
|
||||||
|
|
||||||
|
import InvenTree.validators
|
||||||
import part.models
|
import part.models
|
||||||
|
|
||||||
|
|
||||||
|
def attach_file(instance, filename):
|
||||||
|
"""
|
||||||
|
Generate a filename for the uploaded attachment.
|
||||||
|
|
||||||
|
2021-11-17 - This was moved here from part.models.py,
|
||||||
|
as the function itself is no longer used,
|
||||||
|
but is still required for migration
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Construct a path to store a file attachment
|
||||||
|
return os.path.join('part_files', str(instance.part.id), filename)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
@ -61,7 +77,7 @@ class Migration(migrations.Migration):
|
|||||||
name='PartAttachment',
|
name='PartAttachment',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('attachment', models.FileField(help_text='Select file to attach', upload_to=part.models.attach_file)),
|
('attachment', models.FileField(help_text='Select file to attach', upload_to=attach_file)),
|
||||||
('comment', models.CharField(help_text='File comment', max_length=100)),
|
('comment', models.CharField(help_text='File comment', max_length=100)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -2135,20 +2135,6 @@ def after_save_part(sender, instance: Part, created, **kwargs):
|
|||||||
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance)
|
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance)
|
||||||
|
|
||||||
|
|
||||||
def attach_file(instance, filename):
|
|
||||||
""" Function for storing a file for a PartAttachment
|
|
||||||
|
|
||||||
Args:
|
|
||||||
instance: Instance of a PartAttachment object
|
|
||||||
filename: name of uploaded file
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
path to store file, format: 'part_file_<pk>_filename'
|
|
||||||
"""
|
|
||||||
# Construct a path to store a file attachment
|
|
||||||
return os.path.join('part_files', str(instance.part.id), filename)
|
|
||||||
|
|
||||||
|
|
||||||
class PartAttachment(InvenTreeAttachment):
|
class PartAttachment(InvenTreeAttachment):
|
||||||
"""
|
"""
|
||||||
Model for storing file attachments against a Part object
|
Model for storing file attachments against a Part object
|
||||||
|
@ -388,9 +388,7 @@
|
|||||||
{% if part.variant_of %}
|
{% if part.variant_of %}
|
||||||
<li><a class='dropdown-item' href='#' id='bom-duplicate'><span class='fas fa-clone'></span> {% trans "Copy BOM" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='bom-duplicate'><span class='fas fa-clone'></span> {% trans "Copy BOM" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not part.is_bom_valid %}
|
|
||||||
<li><a class='dropdown-item' href='#' id='validate-bom'><span class='fas fa-clipboard-check icon-green'></span> {% trans "Validate BOM" %}</a></li>
|
<li><a class='dropdown-item' href='#' id='validate-bom'><span class='fas fa-clipboard-check icon-green'></span> {% trans "Validate BOM" %}</a></li>
|
||||||
{% endif %}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -649,14 +647,10 @@
|
|||||||
|
|
||||||
// Load the "used in" tab
|
// Load the "used in" tab
|
||||||
onPanelLoad("used-in", function() {
|
onPanelLoad("used-in", function() {
|
||||||
loadPartTable('#used-table',
|
|
||||||
'{% url "api-part-list" %}',
|
loadUsedInTable(
|
||||||
{
|
'#used-table',
|
||||||
params: {
|
{{ part.pk }},
|
||||||
uses: {{ part.pk }},
|
|
||||||
},
|
|
||||||
filterTarget: '#filter-list-usedin',
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -90,6 +90,13 @@ def inventree_in_debug_mode(*args, **kwargs):
|
|||||||
return djangosettings.DEBUG
|
return djangosettings.DEBUG
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag()
|
||||||
|
def inventree_demo_mode(*args, **kwargs):
|
||||||
|
""" Return True if the server is running in DEMO mode """
|
||||||
|
|
||||||
|
return djangosettings.DEMO_MODE
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def inventree_docker_mode(*args, **kwargs):
|
def inventree_docker_mode(*args, **kwargs):
|
||||||
""" Return True if the server is running as a Docker image """
|
""" Return True if the server is running as a Docker image """
|
||||||
|
@ -1123,6 +1123,59 @@ class BomItemTest(InvenTreeAPITestCase):
|
|||||||
response = self.get(url, expected_code=200)
|
response = self.get(url, expected_code=200)
|
||||||
self.assertEqual(len(response.data), 5)
|
self.assertEqual(len(response.data), 5)
|
||||||
|
|
||||||
|
def test_bom_item_uses(self):
|
||||||
|
"""
|
||||||
|
Tests for the 'uses' field
|
||||||
|
"""
|
||||||
|
|
||||||
|
url = reverse('api-bom-list')
|
||||||
|
|
||||||
|
# Test that the direct 'sub_part' association works
|
||||||
|
|
||||||
|
assemblies = []
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
assy = Part.objects.create(
|
||||||
|
name=f"Assy_{i}",
|
||||||
|
description="An assembly made of other parts",
|
||||||
|
active=True,
|
||||||
|
assembly=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assemblies.append(assy)
|
||||||
|
|
||||||
|
components = []
|
||||||
|
|
||||||
|
# Create some sub-components
|
||||||
|
for i in range(5):
|
||||||
|
|
||||||
|
cmp = Part.objects.create(
|
||||||
|
name=f"Component_{i}",
|
||||||
|
description="A sub component",
|
||||||
|
active=True,
|
||||||
|
component=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for j in range(i):
|
||||||
|
# Create a BOM item
|
||||||
|
BomItem.objects.create(
|
||||||
|
quantity=10,
|
||||||
|
part=assemblies[j],
|
||||||
|
sub_part=cmp,
|
||||||
|
)
|
||||||
|
|
||||||
|
components.append(cmp)
|
||||||
|
|
||||||
|
response = self.get(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'uses': cmp.pk,
|
||||||
|
},
|
||||||
|
expected_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(response.data), i)
|
||||||
|
|
||||||
|
|
||||||
class PartParameterTest(InvenTreeAPITestCase):
|
class PartParameterTest(InvenTreeAPITestCase):
|
||||||
"""
|
"""
|
||||||
|
@ -12,12 +12,15 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block actions %}
|
{% block actions %}
|
||||||
|
{% inventree_demo_mode as demo %}
|
||||||
|
{% if not demo %}
|
||||||
<div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'>
|
<div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'>
|
||||||
<span class='fas fa-user-cog'></span> {% trans "Edit" %}
|
<span class='fas fa-user-cog'></span> {% trans "Edit" %}
|
||||||
</div>
|
</div>
|
||||||
<div class='btn btn-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'>
|
<div class='btn btn-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'>
|
||||||
<span class='fas fa-key'></span> {% trans "Set Password" %}
|
<span class='fas fa-key'></span> {% trans "Set Password" %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{% extends "account/base.html" %}
|
{% extends "account/base.html" %}
|
||||||
|
|
||||||
|
{% load inventree_extras %}
|
||||||
{% load i18n account socialaccount crispy_forms_tags inventree_extras %}
|
{% load i18n account socialaccount crispy_forms_tags inventree_extras %}
|
||||||
|
|
||||||
{% block head_title %}{% trans "Sign In" %}{% endblock %}
|
{% block head_title %}{% trans "Sign In" %}{% endblock %}
|
||||||
@ -10,6 +11,7 @@
|
|||||||
{% settings_value 'LOGIN_ENABLE_PWD_FORGOT' as enable_pwd_forgot %}
|
{% settings_value 'LOGIN_ENABLE_PWD_FORGOT' as enable_pwd_forgot %}
|
||||||
{% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %}
|
{% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %}
|
||||||
{% mail_configured as mail_conf %}
|
{% mail_configured as mail_conf %}
|
||||||
|
{% inventree_demo_mode as demo %}
|
||||||
|
|
||||||
<h1>{% trans "Sign In" %}</h1>
|
<h1>{% trans "Sign In" %}</h1>
|
||||||
|
|
||||||
@ -36,9 +38,16 @@ for a account and sign in below:{% endblocktrans %}</p>
|
|||||||
<div class="btn-group float-right" role="group">
|
<div class="btn-group float-right" role="group">
|
||||||
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
|
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
|
||||||
</div>
|
</div>
|
||||||
{% if mail_conf and enable_pwd_forgot %}
|
{% if mail_conf and enable_pwd_forgot and not demo %}
|
||||||
<a class="" href="{% url 'account_reset_password' %}"><small>{% trans "Forgot Password?" %}</small></a>
|
<a class="" href="{% url 'account_reset_password' %}"><small>{% trans "Forgot Password?" %}</small></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if demo %}
|
||||||
|
<p>
|
||||||
|
<h6>
|
||||||
|
{% trans "InvenTree demo instance" %} - <a href='https://inventree.readthedocs.io/en/latest/demo/'>{% trans "Click here for login details" %}</a>
|
||||||
|
</h6>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if enable_sso %}
|
{% if enable_sso %}
|
||||||
|
@ -87,24 +87,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<main class='col ps-md-2 pt-2 pe-2'>
|
<main class='col ps-md-2 pt-2 pe-2'>
|
||||||
|
|
||||||
|
{% block alerts %}
|
||||||
|
<div class='notification-area' id='alerts'>
|
||||||
|
<!-- Div for displayed alerts -->
|
||||||
{% if server_restart_required %}
|
{% if server_restart_required %}
|
||||||
<div class='notification-area' id='restart-required'>
|
|
||||||
<div id='alert-restart-server' class='alert alert-danger' role='alert'>
|
<div id='alert-restart-server' class='alert alert-danger' role='alert'>
|
||||||
<span class='fas fa-server'></span>
|
<span class='fas fa-server'></span>
|
||||||
<b>{% trans "Server Restart Required" %}</b>
|
<b>{% trans "Server Restart Required" %}</b>
|
||||||
<small>
|
<small>
|
||||||
<br>
|
<br>
|
||||||
{% trans "A configuration option has been changed which requires a server restart" %}.
|
{% trans "A configuration option has been changed which requires a server restart" %}. {% trans "Contact your system administrator for further information" %}
|
||||||
<br>
|
|
||||||
{% trans "Contact your system administrator for further information" %}
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% block alerts %}
|
|
||||||
<div class='notification-area' id='alerts'>
|
|
||||||
<!-- Div for displayed alerts -->
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -217,8 +217,10 @@ function showApiError(xhr, url) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url) {
|
||||||
message += '<hr>';
|
message += '<hr>';
|
||||||
message += `URL: ${url}`;
|
message += `URL: ${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
showMessage(title, {
|
showMessage(title, {
|
||||||
style: 'danger',
|
style: 'danger',
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
/* exported
|
/* exported
|
||||||
newPartFromBomWizard,
|
newPartFromBomWizard,
|
||||||
loadBomTable,
|
loadBomTable,
|
||||||
|
loadUsedInTable,
|
||||||
removeRowFromBomWizard,
|
removeRowFromBomWizard,
|
||||||
removeColFromBomWizard,
|
removeColFromBomWizard,
|
||||||
*/
|
*/
|
||||||
@ -311,7 +312,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function loadBomTable(table, options) {
|
function loadBomTable(table, options={}) {
|
||||||
/* Load a BOM table with some configurable options.
|
/* Load a BOM table with some configurable options.
|
||||||
*
|
*
|
||||||
* Following options are available:
|
* Following options are available:
|
||||||
@ -395,7 +396,7 @@ function loadBomTable(table, options) {
|
|||||||
|
|
||||||
var sub_part = row.sub_part_detail;
|
var sub_part = row.sub_part_detail;
|
||||||
|
|
||||||
html += makePartIcons(row.sub_part_detail);
|
html += makePartIcons(sub_part);
|
||||||
|
|
||||||
if (row.substitutes && row.substitutes.length > 0) {
|
if (row.substitutes && row.substitutes.length > 0) {
|
||||||
html += makeIconBadge('fa-exchange-alt', '{% trans "Substitutes Available" %}');
|
html += makeIconBadge('fa-exchange-alt', '{% trans "Substitutes Available" %}');
|
||||||
@ -672,8 +673,9 @@ function loadBomTable(table, options) {
|
|||||||
|
|
||||||
table.treegrid('collapseAll');
|
table.treegrid('collapseAll');
|
||||||
},
|
},
|
||||||
error: function() {
|
error: function(xhr) {
|
||||||
console.log('Error requesting BOM for part=' + part_pk);
|
console.log('Error requesting BOM for part=' + part_pk);
|
||||||
|
showApiError(xhr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -835,3 +837,166 @@ function loadBomTable(table, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Load a table which shows the assemblies which "require" a certain part.
|
||||||
|
*
|
||||||
|
* Arguments:
|
||||||
|
* - table: The ID string of the table element e.g. '#used-in-table'
|
||||||
|
* - part_id: The ID (PK) of the part we are interested in
|
||||||
|
*
|
||||||
|
* Options:
|
||||||
|
* -
|
||||||
|
*
|
||||||
|
* The following "options" are available.
|
||||||
|
*/
|
||||||
|
function loadUsedInTable(table, part_id, options={}) {
|
||||||
|
|
||||||
|
var params = options.params || {};
|
||||||
|
|
||||||
|
params.uses = part_id;
|
||||||
|
params.part_detail = true;
|
||||||
|
params.sub_part_detail = true,
|
||||||
|
params.show_pricing = global_settings.PART_SHOW_PRICE_IN_BOM;
|
||||||
|
|
||||||
|
var filters = {};
|
||||||
|
|
||||||
|
if (!options.disableFilters) {
|
||||||
|
filters = loadTableFilters('usedin');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var key in params) {
|
||||||
|
filters[key] = params[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFilterList('usedin', $(table), options.filterTarget || '#filter-list-usedin');
|
||||||
|
|
||||||
|
function loadVariantData(row) {
|
||||||
|
// Load variants information for inherited BOM rows
|
||||||
|
|
||||||
|
inventreeGet(
|
||||||
|
'{% url "api-part-list" %}',
|
||||||
|
{
|
||||||
|
assembly: true,
|
||||||
|
ancestor: row.part,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
success: function(variantData) {
|
||||||
|
// Iterate through each variant item
|
||||||
|
for (var jj = 0; jj < variantData.length; jj++) {
|
||||||
|
variantData[jj].parent = row.pk;
|
||||||
|
|
||||||
|
var variant = variantData[jj];
|
||||||
|
|
||||||
|
// Add this variant to the table, augmented
|
||||||
|
$(table).bootstrapTable('append', [{
|
||||||
|
// Point the parent to the "master" assembly row
|
||||||
|
parent: row.pk,
|
||||||
|
part: variant.pk,
|
||||||
|
part_detail: variant,
|
||||||
|
sub_part: row.sub_part,
|
||||||
|
sub_part_detail: row.sub_part_detail,
|
||||||
|
quantity: row.quantity,
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
showApiError(xhr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(table).inventreeTable({
|
||||||
|
url: options.url || '{% url "api-bom-list" %}',
|
||||||
|
name: options.table_name || 'usedin',
|
||||||
|
sortable: true,
|
||||||
|
search: true,
|
||||||
|
showColumns: true,
|
||||||
|
queryParams: filters,
|
||||||
|
original: params,
|
||||||
|
rootParentId: 'top-level-item',
|
||||||
|
idField: 'pk',
|
||||||
|
uniqueId: 'pk',
|
||||||
|
parentIdField: 'parent',
|
||||||
|
treeShowField: 'part',
|
||||||
|
onLoadSuccess: function(tableData) {
|
||||||
|
// Once the initial data are loaded, check if there are any "inherited" BOM lines
|
||||||
|
for (var ii = 0; ii < tableData.length; ii++) {
|
||||||
|
var row = tableData[ii];
|
||||||
|
|
||||||
|
// This is a "top level" item in the table
|
||||||
|
row.parent = 'top-level-item';
|
||||||
|
|
||||||
|
// Ignore this row as it is not "inherited" by variant parts
|
||||||
|
if (!row.inherited) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadVariantData(row);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPostBody: function() {
|
||||||
|
$(table).treegrid({
|
||||||
|
treeColumn: 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
field: 'pk',
|
||||||
|
title: 'ID',
|
||||||
|
visible: false,
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'part',
|
||||||
|
title: '{% trans "Assembly" %}',
|
||||||
|
switchable: false,
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var url = `/part/${value}/?display=bom`;
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
var part = row.part_detail;
|
||||||
|
|
||||||
|
html += imageHoverIcon(part.thumbnail);
|
||||||
|
html += renderLink(part.full_name, url);
|
||||||
|
html += makePartIcons(part);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'sub_part',
|
||||||
|
title: '{% trans "Required Part" %}',
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var url = `/part/${value}/`;
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
var sub_part = row.sub_part_detail;
|
||||||
|
|
||||||
|
html += imageHoverIcon(sub_part.thumbnail);
|
||||||
|
html += renderLink(sub_part.full_name, url);
|
||||||
|
html += makePartIcons(sub_part);
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'quantity',
|
||||||
|
title: '{% trans "Required Quantity" %}',
|
||||||
|
formatter: function(value, row) {
|
||||||
|
var html = value;
|
||||||
|
|
||||||
|
if (row.parent && row.parent != 'top-level-item') {
|
||||||
|
html += ` <em>({% trans "Inherited from parent BOM" %})</em>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -281,23 +281,24 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
// One blank slate, please
|
// One blank slate, please
|
||||||
element.empty();
|
element.empty();
|
||||||
|
|
||||||
element.append(`<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-redo-alt'></span></button>`);
|
var buttons = '';
|
||||||
|
|
||||||
// Callback for reloading the table
|
buttons += `<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-redo-alt'></span></button>`;
|
||||||
element.find(`#reload-${tableKey}`).click(function() {
|
|
||||||
$(table).bootstrapTable('refresh');
|
|
||||||
});
|
|
||||||
|
|
||||||
// If there are no filters defined for this table, exit now
|
// If there are filters defined for this table, add more buttons
|
||||||
if (jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) {
|
if (!jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) {
|
||||||
return;
|
buttons += `<button id='${add}' title='{% trans "Add new filter" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-filter'></span></button>`;
|
||||||
}
|
|
||||||
|
|
||||||
element.append(`<button id='${add}' title='{% trans "Add new filter" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-filter'></span></button>`);
|
|
||||||
|
|
||||||
if (Object.keys(filters).length > 0) {
|
if (Object.keys(filters).length > 0) {
|
||||||
element.append(`<button id='${clear}' title='{% trans "Clear all filters" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-backspace icon-red'></span></button>`);
|
buttons += `<button id='${clear}' title='{% trans "Clear all filters" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-backspace icon-red'></span></button>`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
element.html(`
|
||||||
|
<div class='btn-group' role='group'>
|
||||||
|
${buttons}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
for (var key in filters) {
|
for (var key in filters) {
|
||||||
var value = getFilterOptionValue(tableKey, key, filters[key]);
|
var value = getFilterOptionValue(tableKey, key, filters[key]);
|
||||||
@ -307,6 +308,11 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`);
|
element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Callback for reloading the table
|
||||||
|
element.find(`#reload-${tableKey}`).click(function() {
|
||||||
|
$(table).bootstrapTable('refresh');
|
||||||
|
});
|
||||||
|
|
||||||
// Add a callback for adding a new filter
|
// Add a callback for adding a new filter
|
||||||
element.find(`#${add}`).click(function clicked() {
|
element.find(`#${add}`).click(function clicked() {
|
||||||
|
|
||||||
@ -316,10 +322,12 @@ function setupFilterList(tableKey, table, target) {
|
|||||||
|
|
||||||
var html = '';
|
var html = '';
|
||||||
|
|
||||||
|
html += `<div class='input-group'>`;
|
||||||
html += generateAvailableFilterList(tableKey);
|
html += generateAvailableFilterList(tableKey);
|
||||||
html += generateFilterInput(tableKey);
|
html += generateFilterInput(tableKey);
|
||||||
|
|
||||||
html += `<button title='{% trans "Create filter" %}' class='btn btn-outline-secondary filter-button' id='${make}'><span class='fas fa-plus'></span></button>`;
|
html += `<button title='{% trans "Create filter" %}' class='btn btn-outline-secondary filter-button' id='${make}'><span class='fas fa-plus'></span></button>`;
|
||||||
|
html += `</div>`;
|
||||||
|
|
||||||
element.append(html);
|
element.append(html);
|
||||||
|
|
||||||
|
@ -924,8 +924,8 @@ function handleFormSuccess(response, options) {
|
|||||||
var cache = (options.follow && response.url) || options.redirect || options.reload;
|
var cache = (options.follow && response.url) || options.redirect || options.reload;
|
||||||
|
|
||||||
// Display any messages
|
// Display any messages
|
||||||
if (response && response.success) {
|
if (response && (response.success || options.successMessage)) {
|
||||||
showAlertOrCache(response.success, cache, {style: 'success'});
|
showAlertOrCache(response.success || options.successMessage, cache, {style: 'success'});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && response.info) {
|
if (response && response.info) {
|
||||||
|
@ -331,6 +331,7 @@ function editPart(pk) {
|
|||||||
groups: groups,
|
groups: groups,
|
||||||
title: '{% trans "Edit Part" %}',
|
title: '{% trans "Edit Part" %}',
|
||||||
reload: true,
|
reload: true,
|
||||||
|
successMessage: '{% trans "Part edited" %}',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,10 +77,22 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
// Filters for the "used in" table
|
// Filters for the "used in" table
|
||||||
if (tableKey == 'usedin') {
|
if (tableKey == 'usedin') {
|
||||||
return {
|
return {
|
||||||
|
'inherited': {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Inherited" %}',
|
||||||
|
},
|
||||||
|
'optional': {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Optional" %}',
|
||||||
|
},
|
||||||
'part_active': {
|
'part_active': {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Active" %}',
|
title: '{% trans "Active" %}',
|
||||||
},
|
},
|
||||||
|
'part_trackable': {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Trackable" %}',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
{% settings_value 'BARCODE_ENABLE' as barcodes %}
|
{% settings_value 'BARCODE_ENABLE' as barcodes %}
|
||||||
{% settings_value 'STICKY_HEADER' user=request.user as sticky %}
|
{% settings_value 'STICKY_HEADER' user=request.user as sticky %}
|
||||||
{% navigation_enabled as plugin_nav %}
|
{% navigation_enabled as plugin_nav %}
|
||||||
|
{% inventree_demo_mode as demo %}
|
||||||
|
|
||||||
<nav class="navbar {% if sticky %}fixed-top{% endif %} navbar-expand-lg navbar-light">
|
<nav class="navbar {% if sticky %}fixed-top{% endif %} navbar-expand-lg navbar-light">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@ -83,6 +84,9 @@
|
|||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
{% if demo %}
|
||||||
|
{% include "navbar_demo.html" %}
|
||||||
|
{% endif %}
|
||||||
{% include "search_form.html" %}
|
{% include "search_form.html" %}
|
||||||
<ul class='navbar-nav flex-row'>
|
<ul class='navbar-nav flex-row'>
|
||||||
{% if barcodes %}
|
{% if barcodes %}
|
||||||
@ -103,7 +107,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'>
|
<ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'>
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff and not demo %}
|
||||||
<li><a class='dropdown-item' href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li>
|
<li><a class='dropdown-item' href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a class='dropdown-item' href="{% url 'account_logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>
|
<li><a class='dropdown-item' href="{% url 'account_logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>
|
||||||
|
12
InvenTree/templates/navbar_demo.html
Normal file
12
InvenTree/templates/navbar_demo.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
{% include "spacer.html" %}
|
||||||
|
<div class='flex'>
|
||||||
|
<h6>
|
||||||
|
{% trans "InvenTree demo mode" %}
|
||||||
|
<a href='https://inventree.readthedocs.io/en/latest/demo/'>
|
||||||
|
<span class='fas fa-info-circle'></span>
|
||||||
|
</a>
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
{% include "spacer.html" %}
|
||||||
|
{% include "spacer.html" %}
|
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
<form class="d-flex" action="{% url 'search' %}" method='post'>
|
<form class="d-flex" action="{% url 'search' %}" method='post'>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<div class='input-group'>
|
||||||
<input type="text" name='search' class="form-control" aria-label='{% trans "Search" %}' id="search-bar" placeholder="{% trans 'Search' %}"{% if query_text %} value="{{ query }}"{% endif %}>
|
<input type="text" name='search' class="form-control" aria-label='{% trans "Search" %}' id="search-bar" placeholder="{% trans 'Search' %}"{% if query_text %} value="{{ query }}"{% endif %}>
|
||||||
<button type="submit" id='search-submit' class="btn btn-secondary" title='{% trans "Search" %}'>
|
<button type="submit" id='search-submit' class="btn btn-secondary" title='{% trans "Search" %}'>
|
||||||
<span class='fas fa-search'></span>
|
<span class='fas fa-search'></span>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -27,6 +27,9 @@ function {{ label }}StatusDisplay(key, options={}) {
|
|||||||
label = {{ label }}Codes[key].label;
|
label = {{ label }}Codes[key].label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback option for label
|
||||||
|
label = label || 'bg-dark';
|
||||||
|
|
||||||
if (value == null || value.length == 0) {
|
if (value == null || value.length == 0) {
|
||||||
value = key;
|
value = key;
|
||||||
label = '';
|
label = '';
|
||||||
|
@ -30,6 +30,7 @@ flake8==3.8.3 # PEP checking
|
|||||||
gunicorn>=20.1.0 # Gunicorn web server
|
gunicorn>=20.1.0 # Gunicorn web server
|
||||||
importlib_metadata # Backport for importlib.metadata
|
importlib_metadata # Backport for importlib.metadata
|
||||||
inventree # Install the latest version of the InvenTree API python library
|
inventree # Install the latest version of the InvenTree API python library
|
||||||
|
markdown==3.3.4 # Force particular version of markdown
|
||||||
pep8-naming==0.11.1 # PEP naming convention extension
|
pep8-naming==0.11.1 # PEP naming convention extension
|
||||||
pillow==8.3.2 # Image manipulation
|
pillow==8.3.2 # Image manipulation
|
||||||
py-moneyed==0.8.0 # Specific version requirement for py-moneyed
|
py-moneyed==0.8.0 # Specific version requirement for py-moneyed
|
||||||
|
Loading…
x
Reference in New Issue
Block a user