diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 1b79eac59f..7ce41b2d4c 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -92,6 +92,12 @@ DEBUG = _is_true(get_setting( 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( 'INVENTREE_DOCKER', False @@ -239,7 +245,10 @@ STATIC_COLOR_THEMES_DIR = os.path.join(STATIC_ROOT, 'css', 'color-themes') MEDIA_URL = '/media/' 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"STATIC_ROOT: '{STATIC_ROOT}'") diff --git a/InvenTree/InvenTree/static/img/paper_splash.jpg b/InvenTree/InvenTree/static/img/paper_splash.jpg index e466708f3f..c7da1d6092 100644 Binary files a/InvenTree/InvenTree/static/img/paper_splash.jpg and b/InvenTree/InvenTree/static/img/paper_splash.jpg differ diff --git a/InvenTree/InvenTree/static/img/paper_splash_large.jpg b/InvenTree/InvenTree/static/img/paper_splash_large.jpg new file mode 100644 index 0000000000..e466708f3f Binary files /dev/null and b/InvenTree/InvenTree/static/img/paper_splash_large.jpg differ diff --git a/InvenTree/InvenTree/utils.py b/InvenTree/InvenTree/utils.py deleted file mode 100644 index dc28da81a0..0000000000 --- a/InvenTree/InvenTree/utils.py +++ /dev/null @@ -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 diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 935a0bed37..ac6e268f78 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -12,11 +12,16 @@ import common.models INVENTREE_SW_VERSION = "0.6.0 dev" # 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 +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 - Adds API endpoints for GLOBAL and USER settings objects - Ref: https://github.com/inventree/InvenTree/pull/2275 diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index b08834445c..eeb8ec1255 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -832,18 +832,6 @@ class PartList(generics.ListCreateAPIView): 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_id = [] @@ -1040,13 +1028,19 @@ class PartParameterTemplateList(generics.ListCreateAPIView): serializer_class = part_serializers.PartParameterTemplateSerializer filter_backends = [ + DjangoFilterBackend, filters.OrderingFilter, + filters.SearchFilter, ] filter_fields = [ 'name', ] + search_fields = [ + 'name', + ] + class PartParameterList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PartParameter objects @@ -1211,6 +1205,54 @@ class BomList(generics.ListCreateAPIView): except (ValueError, Part.DoesNotExist): 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(): queryset = self.annotate_pricing(queryset) diff --git a/InvenTree/part/migrations/0001_initial.py b/InvenTree/part/migrations/0001_initial.py index e5183e2b5c..066d1afa4b 100644 --- a/InvenTree/part/migrations/0001_initial.py +++ b/InvenTree/part/migrations/0001_initial.py @@ -1,13 +1,29 @@ # Generated by Django 2.2 on 2019-05-20 12:04 -import InvenTree.validators -from django.conf import settings -import django.core.validators +import os + from django.db import migrations, models import django.db.models.deletion +from django.conf import settings +import django.core.validators + +import InvenTree.validators 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): initial = True @@ -61,7 +77,7 @@ class Migration(migrations.Migration): name='PartAttachment', fields=[ ('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)), ], ), diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index e77008b076..cec21becc2 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -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) -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__filename' - """ - # Construct a path to store a file attachment - return os.path.join('part_files', str(instance.part.id), filename) - - class PartAttachment(InvenTreeAttachment): """ Model for storing file attachments against a Part object diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 39ed011861..0d05665f7d 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -388,9 +388,7 @@ {% if part.variant_of %}
  • {% trans "Copy BOM" %}
  • {% endif %} - {% if not part.is_bom_valid %}
  • {% trans "Validate BOM" %}
  • - {% endif %} @@ -649,14 +647,10 @@ // Load the "used in" tab onPanelLoad("used-in", function() { - loadPartTable('#used-table', - '{% url "api-part-list" %}', - { - params: { - uses: {{ part.pk }}, - }, - filterTarget: '#filter-list-usedin', - } + + loadUsedInTable( + '#used-table', + {{ part.pk }}, ); }); diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 42c66cf78e..e31fb9e398 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -90,6 +90,13 @@ def inventree_in_debug_mode(*args, **kwargs): 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() def inventree_docker_mode(*args, **kwargs): """ Return True if the server is running as a Docker image """ diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index ec377bd513..b16de1b9d7 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -1123,6 +1123,59 @@ class BomItemTest(InvenTreeAPITestCase): response = self.get(url, expected_code=200) 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): """ diff --git a/InvenTree/templates/InvenTree/settings/user.html b/InvenTree/templates/InvenTree/settings/user.html index 2b2637330c..d22c89954f 100644 --- a/InvenTree/templates/InvenTree/settings/user.html +++ b/InvenTree/templates/InvenTree/settings/user.html @@ -12,12 +12,15 @@ {% endblock %} {% block actions %} +{% inventree_demo_mode as demo %} +{% if not demo %}
    {% trans "Edit" %}
    {% trans "Set Password" %}
    +{% endif %} {% endblock %} {% block content %} diff --git a/InvenTree/templates/account/login.html b/InvenTree/templates/account/login.html index fbe48224b4..6e62560bfa 100644 --- a/InvenTree/templates/account/login.html +++ b/InvenTree/templates/account/login.html @@ -1,5 +1,6 @@ {% extends "account/base.html" %} +{% load inventree_extras %} {% load i18n account socialaccount crispy_forms_tags inventree_extras %} {% 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_SSO' as enable_sso %} {% mail_configured as mail_conf %} +{% inventree_demo_mode as demo %}

    {% trans "Sign In" %}

    @@ -36,9 +38,16 @@ for a account and sign in below:{% endblocktrans %}

    - {% if mail_conf and enable_pwd_forgot %} + {% if mail_conf and enable_pwd_forgot and not demo %} {% trans "Forgot Password?" %} {% endif %} + {% if demo %} +

    +

    + {% trans "InvenTree demo instance" %} - {% trans "Click here for login details" %} +
    +

    + {% endif %} {% if enable_sso %} diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 6dc0d7d78a..262a749bfa 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -86,25 +86,20 @@
    - - {% if server_restart_required %} -
    + + {% block alerts %} +
    + + {% if server_restart_required %} -
    - {% endif %} - - {% block alerts %} -
    - + {% endif %}
    {% endblock %} diff --git a/InvenTree/templates/js/translated/api.js b/InvenTree/templates/js/translated/api.js index 15a74a9a71..735ce0a676 100644 --- a/InvenTree/templates/js/translated/api.js +++ b/InvenTree/templates/js/translated/api.js @@ -217,8 +217,10 @@ function showApiError(xhr, url) { break; } - message += '
    '; - message += `URL: ${url}`; + if (url) { + message += '
    '; + message += `URL: ${url}`; + } showMessage(title, { style: 'danger', diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index ee04cb8660..1885624dd8 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -16,6 +16,7 @@ /* exported newPartFromBomWizard, loadBomTable, + loadUsedInTable, removeRowFromBomWizard, 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. * * Following options are available: @@ -395,7 +396,7 @@ function loadBomTable(table, options) { var sub_part = row.sub_part_detail; - html += makePartIcons(row.sub_part_detail); + html += makePartIcons(sub_part); if (row.substitutes && row.substitutes.length > 0) { html += makeIconBadge('fa-exchange-alt', '{% trans "Substitutes Available" %}'); @@ -672,8 +673,9 @@ function loadBomTable(table, options) { table.treegrid('collapseAll'); }, - error: function() { + error: function(xhr) { 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 += ` ({% trans "Inherited from parent BOM" %})`; + } + + return html; + } + } + ] + }); +} diff --git a/InvenTree/templates/js/translated/filters.js b/InvenTree/templates/js/translated/filters.js index 4383f0a096..227fbb8009 100644 --- a/InvenTree/templates/js/translated/filters.js +++ b/InvenTree/templates/js/translated/filters.js @@ -281,23 +281,24 @@ function setupFilterList(tableKey, table, target) { // One blank slate, please element.empty(); - element.append(``); + var buttons = ''; - // Callback for reloading the table - element.find(`#reload-${tableKey}`).click(function() { - $(table).bootstrapTable('refresh'); - }); + buttons += ``; - // If there are no filters defined for this table, exit now - if (jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) { - return; + // If there are filters defined for this table, add more buttons + if (!jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) { + buttons += ``; + + if (Object.keys(filters).length > 0) { + buttons += ``; + } } - element.append(``); - - if (Object.keys(filters).length > 0) { - element.append(``); - } + element.html(` +
    + ${buttons} +
    + `); for (var key in filters) { var value = getFilterOptionValue(tableKey, key, filters[key]); @@ -307,6 +308,11 @@ function setupFilterList(tableKey, table, target) { element.append(`
    ${title} = ${value}x
    `); } + // Callback for reloading the table + element.find(`#reload-${tableKey}`).click(function() { + $(table).bootstrapTable('refresh'); + }); + // Add a callback for adding a new filter element.find(`#${add}`).click(function clicked() { @@ -316,10 +322,12 @@ function setupFilterList(tableKey, table, target) { var html = ''; + html += `
    `; html += generateAvailableFilterList(tableKey); html += generateFilterInput(tableKey); html += ``; + html += `
    `; element.append(html); diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index 2f25fef259..fd1668cc77 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -924,8 +924,8 @@ function handleFormSuccess(response, options) { var cache = (options.follow && response.url) || options.redirect || options.reload; // Display any messages - if (response && response.success) { - showAlertOrCache(response.success, cache, {style: 'success'}); + if (response && (response.success || options.successMessage)) { + showAlertOrCache(response.success || options.successMessage, cache, {style: 'success'}); } if (response && response.info) { diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index dc1adf8837..89e09a314e 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -331,6 +331,7 @@ function editPart(pk) { groups: groups, title: '{% trans "Edit Part" %}', reload: true, + successMessage: '{% trans "Part edited" %}', }); } diff --git a/InvenTree/templates/js/translated/table_filters.js b/InvenTree/templates/js/translated/table_filters.js index 537adefee9..903774f8e5 100644 --- a/InvenTree/templates/js/translated/table_filters.js +++ b/InvenTree/templates/js/translated/table_filters.js @@ -77,10 +77,22 @@ function getAvailableTableFilters(tableKey) { // Filters for the "used in" table if (tableKey == 'usedin') { return { + 'inherited': { + type: 'bool', + title: '{% trans "Inherited" %}', + }, + 'optional': { + type: 'bool', + title: '{% trans "Optional" %}', + }, 'part_active': { type: 'bool', title: '{% trans "Active" %}', }, + 'part_trackable': { + type: 'bool', + title: '{% trans "Trackable" %}', + }, }; } diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 02f7b2e6b7..0ee6bb021e 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -6,6 +6,7 @@ {% settings_value 'BARCODE_ENABLE' as barcodes %} {% settings_value 'STICKY_HEADER' user=request.user as sticky %} {% navigation_enabled as plugin_nav %} +{% inventree_demo_mode as demo %}