From bae290d6058e8cf1b50780596d50041f226442ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 00:17:21 +0100 Subject: [PATCH 01/41] check git version and safe for runtime --- InvenTree/plugin/apps.py | 4 ++++ InvenTree/plugin/helpers.py | 20 ++++++++++++++++++++ InvenTree/plugin/registry.py | 1 + 3 files changed, 25 insertions(+) diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index dd75e3c8fb..4de6542bf1 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -10,6 +10,7 @@ from maintenance_mode.core import set_maintenance_mode from InvenTree.ready import isImportingData from plugin import registry +from plugin.helpers import check_git_version logger = logging.getLogger('inventree') @@ -34,3 +35,6 @@ class PluginAppConfig(AppConfig): # drop out of maintenance # makes sure we did not have an error in reloading and maintenance is still active set_maintenance_mode(False) + + # check git version + registry.git_is_modern = check_git_version() diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 2271c01d98..5afc8ea5fc 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -108,6 +108,26 @@ def get_git_log(path): output = 7 * [''] # pragma: no cover return {'hash': output[0], 'author': output[1], 'mail': output[2], 'date': output[3], 'message': output[4], 'verified': output[5], 'key': output[6]} +def check_git_version(): + """returns if the current git version supports modern features""" + + # get version string + try: + output = str(subprocess.check_output(['git', '--version'], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8') + except subprocess.CalledProcessError: # pragma: no cover + return False + + # process version string + try: + version = output[12:-1].split(".") + if len(version) > 1 and version[0] == '2': + if len(version) > 2 and int(version[1]) >= 22: + return True + except ValueError: + pass + + return False + class GitStatus: """ diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index aec38cc623..40a4a4e2d9 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -52,6 +52,7 @@ class PluginsRegistry: # flags self.is_loading = False self.apps_loading = True # Marks if apps were reloaded yet + self.git_is_modern = True # Is a modern version of git available # integration specific self.installed_apps = [] # Holds all added plugin_paths From 8550915528e518c002392059c792fde3eb2c2a99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 00:23:25 +0100 Subject: [PATCH 02/41] opimise coverage --- InvenTree/plugin/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 5afc8ea5fc..838dfeb275 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -123,7 +123,7 @@ def check_git_version(): if len(version) > 1 and version[0] == '2': if len(version) > 2 and int(version[1]) >= 22: return True - except ValueError: + except ValueError: # pragma: no cover pass return False From 1591597ef9a92bfb7f72809e3466666bcb118bf8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 00:23:46 +0100 Subject: [PATCH 03/41] check if modern git is used --- InvenTree/plugin/helpers.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 838dfeb275..2f0f80ab83 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -94,15 +94,18 @@ def get_git_log(path): """ Get dict with info of the last commit to file named in path """ - path = path.replace(os.path.dirname(settings.BASE_DIR), '')[1:] - command = ['git', 'log', '-n', '1', "--pretty=format:'%H%n%aN%n%aE%n%aI%n%f%n%G?%n%GK'", '--follow', '--', path] + from plugin import registry + output = None - try: - output = str(subprocess.check_output(command, cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8')[1:-1] - if output: - output = output.split('\n') - except subprocess.CalledProcessError: # pragma: no cover - pass + if registry.git_is_modern: + path = path.replace(os.path.dirname(settings.BASE_DIR), '')[1:] + command = ['git', 'log', '-n', '1', "--pretty=format:'%H%n%aN%n%aE%n%aI%n%f%n%G?%n%GK'", '--follow', '--', path] + try: + output = str(subprocess.check_output(command, cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8')[1:-1] + if output: + output = output.split('\n') + except subprocess.CalledProcessError: # pragma: no cover + pass if not output: output = 7 * [''] # pragma: no cover From 084f9d544460ac59f09071e22e1ea5c5afbc7c89 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 00:23:55 +0100 Subject: [PATCH 04/41] make more readable --- InvenTree/plugin/helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 2f0f80ab83..14c5184d01 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -109,6 +109,7 @@ def get_git_log(path): if not output: output = 7 * [''] # pragma: no cover + return {'hash': output[0], 'author': output[1], 'mail': output[2], 'date': output[3], 'message': output[4], 'verified': output[5], 'key': output[6]} def check_git_version(): From ca8df60d005beb6ff453ee0c91fdc151d41964a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 00:28:03 +0100 Subject: [PATCH 05/41] PEP fixes --- InvenTree/plugin/helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py index 14c5184d01..1a5089aefe 100644 --- a/InvenTree/plugin/helpers.py +++ b/InvenTree/plugin/helpers.py @@ -97,7 +97,7 @@ def get_git_log(path): from plugin import registry output = None - if registry.git_is_modern: + if registry.git_is_modern: path = path.replace(os.path.dirname(settings.BASE_DIR), '')[1:] command = ['git', 'log', '-n', '1', "--pretty=format:'%H%n%aN%n%aE%n%aI%n%f%n%G?%n%GK'", '--follow', '--', path] try: @@ -112,9 +112,10 @@ def get_git_log(path): return {'hash': output[0], 'author': output[1], 'mail': output[2], 'date': output[3], 'message': output[4], 'verified': output[5], 'key': output[6]} + def check_git_version(): """returns if the current git version supports modern features""" - + # get version string try: output = str(subprocess.check_output(['git', '--version'], cwd=os.path.dirname(settings.BASE_DIR)), 'utf-8') From f3f010c4bed0a71adb465fd41d1289a762428458 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 00:34:02 +0100 Subject: [PATCH 06/41] write error if too old git --- InvenTree/plugin/apps.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index 4de6542bf1..d46721962b 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -10,7 +10,7 @@ from maintenance_mode.core import set_maintenance_mode from InvenTree.ready import isImportingData from plugin import registry -from plugin.helpers import check_git_version +from plugin.helpers import check_git_version, log_error logger = logging.getLogger('inventree') @@ -38,3 +38,5 @@ class PluginAppConfig(AppConfig): # check git version registry.git_is_modern = check_git_version() + if not registry.git_is_modern: + log_error('Your enviroment has an outdated git version. This prevents InvenTree from loading plugin details.', 'load') From f80f9609231a4542be1cc8429983e61f9ae29724 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 00:34:28 +0100 Subject: [PATCH 07/41] translate error message --- InvenTree/plugin/apps.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index d46721962b..a10618e2be 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -5,6 +5,7 @@ import logging from django.apps import AppConfig from django.conf import settings +from django.utils.translation import ugettext_lazy as _ from maintenance_mode.core import set_maintenance_mode @@ -39,4 +40,4 @@ class PluginAppConfig(AppConfig): # check git version registry.git_is_modern = check_git_version() if not registry.git_is_modern: - log_error('Your enviroment has an outdated git version. This prevents InvenTree from loading plugin details.', 'load') + log_error(_('Your enviroment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load') From ba263187a5ad3a8b3291a537f8b0247662b58951 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Feb 2022 00:35:36 +0100 Subject: [PATCH 08/41] optimise coverage --- InvenTree/plugin/apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index a10618e2be..75063c6b3c 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -39,5 +39,5 @@ class PluginAppConfig(AppConfig): # check git version registry.git_is_modern = check_git_version() - if not registry.git_is_modern: + if not registry.git_is_modern: # pragma: no cover # simulating old git seems not worth it for coverage log_error(_('Your enviroment has an outdated git version. This prevents InvenTree from loading plugin details.'), 'load') From 557aa449049db348ff2a21269698d67d5b2e2470 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 14:26:01 +1100 Subject: [PATCH 09/41] Adds "target_date" field to PurchaseOrderLineItem and SalesOrderLineItem models - Allows different target dates to be specified for different line items - If not set (null) then the base "target_date" parameter for the parent order is used --- InvenTree/InvenTree/version.py | 5 +++- .../migrations/0062_auto_20220228_0321.py | 23 +++++++++++++++++++ InvenTree/order/models.py | 9 +++++++- InvenTree/order/serializers.py | 2 ++ 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 InvenTree/order/migrations/0062_auto_20220228_0321.py diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index b4314f6738..678f69d228 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -12,11 +12,14 @@ import common.models INVENTREE_SW_VERSION = "0.7.0 dev" # InvenTree API version -INVENTREE_API_VERSION = 26 +INVENTREE_API_VERSION = 27 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v27 -> 2022-02-28 + - Adds target_date field to individual line items for purchase orders and sales orders + v26 -> 2022-02-17 - Adds API endpoint for uploading a BOM file and extracting data diff --git a/InvenTree/order/migrations/0062_auto_20220228_0321.py b/InvenTree/order/migrations/0062_auto_20220228_0321.py new file mode 100644 index 0000000000..7f67a827a2 --- /dev/null +++ b/InvenTree/order/migrations/0062_auto_20220228_0321.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.10 on 2022-02-28 03:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0061_merge_0054_auto_20211201_2139_0060_auto_20211129_1339'), + ] + + operations = [ + migrations.AddField( + model_name='purchaseorderlineitem', + name='target_date', + field=models.DateField(blank=True, help_text='Target shipping date for this line item', null=True, verbose_name='Target Date'), + ), + migrations.AddField( + model_name='salesorderlineitem', + name='target_date', + field=models.DateField(blank=True, help_text='Target shipping date for this line item', null=True, verbose_name='Target Date'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 7f6192dfcd..4c04c1ba74 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -794,8 +794,9 @@ class OrderLineItem(models.Model): Attributes: quantity: Number of items + reference: Reference text (e.g. customer reference) for this line item note: Annotation for the item - + target_date: An (optional) date for expected shipment of this line item. """ class Meta: @@ -813,6 +814,12 @@ class OrderLineItem(models.Model): notes = models.CharField(max_length=500, blank=True, verbose_name=_('Notes'), help_text=_('Line item notes')) + target_date = models.DateField( + blank=True, null=True, + verbose_name=_('Target Date'), + help_text=_('Target shipping date for this line item'), + ) + class PurchaseOrderLineItem(OrderLineItem): """ Model for a purchase order line item. diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 5e78b3e3a3..251b088188 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -194,6 +194,7 @@ class POLineItemSerializer(InvenTreeModelSerializer): 'purchase_price_string', 'destination', 'destination_detail', + 'target_date', 'total_price', ] @@ -605,6 +606,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer): 'sale_price_currency', 'sale_price_string', 'shipped', + 'target_date', ] From df7713f6c28ea8574c82f83c6a7e8e4b6115e996 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 15:03:12 +1100 Subject: [PATCH 10/41] Adds "overdue" annotation field to POLineItem serializer --- InvenTree/order/api.py | 9 +++------ InvenTree/order/models.py | 8 ++++++++ InvenTree/order/serializers.py | 20 ++++++++++++++++++-- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index b5622d7ce8..66c61bd0d7 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -274,7 +274,7 @@ class POLineItemFilter(rest_filters.FilterSet): model = models.PurchaseOrderLineItem fields = [ 'order', - 'part' + 'part', ] pending = rest_filters.BooleanFilter(label='pending', method='filter_pending') @@ -391,6 +391,7 @@ class POLineItemList(generics.ListCreateAPIView): 'reference', 'SKU', 'total_price', + 'target_date', ] search_fields = [ @@ -401,11 +402,6 @@ class POLineItemList(generics.ListCreateAPIView): 'reference', ] - filter_fields = [ - 'order', - 'part' - ] - class POLineItemDetail(generics.RetrieveUpdateDestroyAPIView): """ @@ -703,6 +699,7 @@ class SOLineItemList(generics.ListCreateAPIView): 'part__name', 'quantity', 'reference', + 'target_date', ] search_fields = [ diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 4c04c1ba74..1618d336af 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -799,6 +799,14 @@ class OrderLineItem(models.Model): target_date: An (optional) date for expected shipment of this line item. """ + """ + Query filter for determining if an individual line item is "overdue": + - Amount received is less than the required quantity + - Target date is not None + - Target date is in the past + """ + OVERDUE_FILTER = Q(received__lt=F('quantity')) & ~Q(target_date=None) & Q(target_date__lt=datetime.now().date()) + class Meta: abstract = True diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 251b088188..be75bf1533 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -5,12 +5,14 @@ JSON serializers for the Order API # -*- coding: utf-8 -*- from __future__ import unicode_literals +from datetime import datetime + from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ValidationError as DjangoValidationError from django.db import models, transaction from django.db.models import Case, When, Value -from django.db.models import BooleanField, ExpressionWrapper, F +from django.db.models import BooleanField, ExpressionWrapper, F, Q from rest_framework import serializers from rest_framework.serializers import ValidationError @@ -26,7 +28,7 @@ from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeMoneySerializer from InvenTree.serializers import ReferenceIndexingSerializerMixin -from InvenTree.status_codes import StockStatus +from InvenTree.status_codes import StockStatus, PurchaseOrderStatus, SalesOrderStatus import order.models @@ -126,6 +128,7 @@ class POLineItemSerializer(InvenTreeModelSerializer): Add some extra annotations to this queryset: - Total price = purchase_price * quantity + - "Overdue" status (boolean field) """ queryset = queryset.annotate( @@ -135,6 +138,16 @@ class POLineItemSerializer(InvenTreeModelSerializer): ) ) + # Add an annotated 'overdue' field + queryset = queryset.annotate( + overdue=Case( + When(Q(order__status__in=PurchaseOrderStatus.OPEN) & order.models.OrderLineItem.OVERDUE_FILTER, + then=Value(True, output_field=BooleanField()), + ), + default=Value(False, output_field=BooleanField()), + ) + ) + return queryset def __init__(self, *args, **kwargs): @@ -155,6 +168,8 @@ class POLineItemSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField(default=1) received = serializers.FloatField(default=0) + overdue = serializers.BooleanField() + total_price = serializers.FloatField(read_only=True) part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True) @@ -185,6 +200,7 @@ class POLineItemSerializer(InvenTreeModelSerializer): 'notes', 'order', 'order_detail', + 'overdue', 'part', 'part_detail', 'supplier_part_detail', From 9e82b28e9d42583757ede46c51417163b79f4fd2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 15:03:39 +1100 Subject: [PATCH 11/41] Update PO line item table --- .../order/purchase_order_detail.html | 2 +- InvenTree/templates/js/translated/order.js | 35 ++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index d0215777bb..74ce484e7a 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -210,7 +210,7 @@ $('#new-po-line').click(function() { loadPurchaseOrderLineItemTable('#po-line-table', { order: {{ order.pk }}, supplier: {{ order.supplier.pk }}, - {% if order.status == PurchaseOrderStatus.PENDING %} + {% if roles.purchase_order.change %} allow_edit: true, {% else %} allow_edit: false, diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 008091bf15..3dfe112381 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -930,6 +930,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) { reference: {}, purchase_price: {}, purchase_price_currency: {}, + target_date: {}, destination: {}, notes: {}, }, @@ -971,7 +972,11 @@ function loadPurchaseOrderLineItemTable(table, options={}) { ], { success: function() { + // Reload the line item table $(table).bootstrapTable('refresh'); + + // Reload the "received stock" table + $('#stock-table').bootstrapTable('refresh'); } } ); @@ -1111,6 +1116,28 @@ function loadPurchaseOrderLineItemTable(table, options={}) { return formatter.format(total); } }, + { + sortable: true, + field: 'target_date', + switchable: true, + title: '{% trans "Target Date" %}', + formatter: function(value, row) { + if (row.target_date) { + var html = row.target_date; + + if (row.overdue) { + html += ``; + } + + return html; + + } else if (row.order_detail && row.order_detail.target_date) { + return `${row.order_detail.target_date}`; + } else { + return '-'; + } + } + }, { sortable: false, field: 'received', @@ -1157,15 +1184,15 @@ function loadPurchaseOrderLineItemTable(table, options={}) { var pk = row.pk; + if (options.allow_receive && row.received < row.quantity) { + html += makeIconButton('fa-sign-in-alt icon-green', 'button-line-receive', pk, '{% trans "Receive line item" %}'); + } + if (options.allow_edit) { html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}'); html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}'); } - if (options.allow_receive && row.received < row.quantity) { - html += makeIconButton('fa-sign-in-alt', 'button-line-receive', pk, '{% trans "Receive line item" %}'); - } - html += ``; return html; From 784189be758d0d88e126fe07fe8fd3e620acf10a Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 15:07:12 +1100 Subject: [PATCH 12/41] Update purchase order table from part context --- InvenTree/templates/js/translated/part.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 093aec388b..56b1ca6b75 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -905,6 +905,28 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) { field: 'quantity', title: '{% trans "Quantity" %}', }, + { + field: 'target_date', + title: '{% trans "Target Date" %}', + switchable: true, + sortable: true, + formatter: function(value, row) { + if (row.target_date) { + var html = row.target_date; + + if (row.overdue) { + html += ``; + } + + return html; + + } else if (row.order_detail && row.order_detail.target_date) { + return `${row.order_detail.target_date}`; + } else { + return '-'; + } + } + }, { field: 'received', title: '{% trans "Received" %}', From b451f3d149dd2ad283ec5f848175c651a17b668a Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 15:11:10 +1100 Subject: [PATCH 13/41] adds target_date field when adding a new line item to a purchase order --- InvenTree/order/serializers.py | 2 +- InvenTree/order/templates/order/purchase_order_detail.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index be75bf1533..56f10c3352 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -168,7 +168,7 @@ class POLineItemSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField(default=1) received = serializers.FloatField(default=0) - overdue = serializers.BooleanField() + overdue = serializers.BooleanField(required=False, read_only=True) total_price = serializers.FloatField(read_only=True) diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index 74ce484e7a..b9972d73fc 100644 --- a/InvenTree/order/templates/order/purchase_order_detail.html +++ b/InvenTree/order/templates/order/purchase_order_detail.html @@ -174,6 +174,7 @@ $('#new-po-line').click(function() { value: '{{ order.supplier.currency }}', {% endif %} }, + target_date: {}, destination: {}, notes: {}, }, From 4858787a780005cf35d9e69e41a4a34d6e494d04 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 15:24:01 +1100 Subject: [PATCH 14/41] Adds "overdue" field to sales order line item - API serializer - front end / UX tables --- InvenTree/order/serializers.py | 27 +++++++++++++++---- .../templates/order/sales_order_detail.html | 1 + InvenTree/templates/js/translated/order.js | 23 ++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 56f10c3352..477023226e 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -5,8 +5,6 @@ JSON serializers for the Order API # -*- coding: utf-8 -*- from __future__ import unicode_literals -from datetime import datetime - from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ValidationError as DjangoValidationError @@ -138,11 +136,10 @@ class POLineItemSerializer(InvenTreeModelSerializer): ) ) - # Add an annotated 'overdue' field queryset = queryset.annotate( overdue=Case( - When(Q(order__status__in=PurchaseOrderStatus.OPEN) & order.models.OrderLineItem.OVERDUE_FILTER, - then=Value(True, output_field=BooleanField()), + When( + Q(order__status__in=PurchaseOrderStatus.OPEN) & order.models.OrderLineItem.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()) ), default=Value(False, output_field=BooleanField()), ) @@ -566,6 +563,23 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): class SOLineItemSerializer(InvenTreeModelSerializer): """ Serializer for a SalesOrderLineItem object """ + @staticmethod + def annotate_queryset(queryset): + """ + Add some extra annotations to this queryset: + + - "Overdue" status (boolean field) + """ + + queryset = queryset.annotate( + overdue=Case( + When( + Q(order__status__in=SalesOrderStatus.OPEN) & order.models.OrderLineItem.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()), + ), + default=Value(False, output_field=BooleanField()), + ) + ) + def __init__(self, *args, **kwargs): part_detail = kwargs.pop('part_detail', False) @@ -587,6 +601,8 @@ class SOLineItemSerializer(InvenTreeModelSerializer): part_detail = PartBriefSerializer(source='part', many=False, read_only=True) allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True) + overdue = serializers.BooleanField(required=False, read_only=True) + quantity = InvenTreeDecimalField() allocated = serializers.FloatField(source='allocated_quantity', read_only=True) @@ -616,6 +632,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer): 'notes', 'order', 'order_detail', + 'overdue', 'part', 'part_detail', 'sale_price', diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 48b2542752..3676268f5c 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -238,6 +238,7 @@ reference: {}, sale_price: {}, sale_price_currency: {}, + target_date: {}, notes: {}, }, method: 'POST', diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 3dfe112381..83d6b92103 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -2235,6 +2235,28 @@ function loadSalesOrderLineItemTable(table, options={}) { return formatter.format(total); } }, + { + field: 'target_date', + title: '{% trans "Target Date" %}', + sortable: true, + switchable: true, + formatter: function(value, row) { + if (row.target_date) { + var html = row.target_date; + + if (row.overdue) { + html += ``; + } + + return html; + + } else if (row.order_detail && row.order_detail.target_date) { + return `${row.order_detail.target_date}`; + } else { + return '-'; + } + } + } ]; if (pending) { @@ -2378,6 +2400,7 @@ function loadSalesOrderLineItemTable(table, options={}) { reference: {}, sale_price: {}, sale_price_currency: {}, + target_date: {}, notes: {}, }, title: '{% trans "Edit Line Item" %}', From 7c82857cc7ca57f187e81438488bde0bab2c4d62 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 15:27:56 +1100 Subject: [PATCH 15/41] Remove unique_together requirement for purchaseorderlineitem - Allow a single purchase order to specify duplicate lines (e.g. split shipments / order scheduling) --- ...ter_purchaseorderlineitem_unique_together.py | 17 +++++++++++++++++ InvenTree/order/models.py | 1 - 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 InvenTree/order/migrations/0063_alter_purchaseorderlineitem_unique_together.py diff --git a/InvenTree/order/migrations/0063_alter_purchaseorderlineitem_unique_together.py b/InvenTree/order/migrations/0063_alter_purchaseorderlineitem_unique_together.py new file mode 100644 index 0000000000..1c73b6b437 --- /dev/null +++ b/InvenTree/order/migrations/0063_alter_purchaseorderlineitem_unique_together.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.10 on 2022-02-28 04:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0062_auto_20220228_0321'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='purchaseorderlineitem', + unique_together=set(), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 1618d336af..fb0ae1c2c5 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -839,7 +839,6 @@ class PurchaseOrderLineItem(OrderLineItem): class Meta: unique_together = ( - ('order', 'part', 'quantity', 'purchase_price') ) @staticmethod From b19719516bbd22c08c8c3e66f2b82c6a322764d3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 15:57:34 +1100 Subject: [PATCH 16/41] Adds a user-configurable setting to configure how dates are displayed (on the front end only!) --- InvenTree/common/models.py | 47 ++++++++++++++++++- .../templates/InvenTree/settings/setting.html | 4 ++ .../InvenTree/settings/user_display.html | 1 + InvenTree/templates/js/dynamic/settings.js | 7 ++- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 9ef5a4d0c3..927abd94cd 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -443,12 +443,12 @@ class BaseInvenTreeSetting(models.Model): except self.DoesNotExist: pass - def choices(self): + def choices(self, **kwargs): """ Return the available choices for this setting (or None if no choices are defined) """ - return self.__class__.get_setting_choices(self.key) + return self.__class__.get_setting_choices(self.key, **kwargs) def valid_options(self): """ @@ -462,6 +462,34 @@ class BaseInvenTreeSetting(models.Model): return [opt[0] for opt in choices] + def is_choice(self, **kwargs): + """ + Check if this setting is a "choice" field + """ + + return self.__class__.get_setting_choices(self.key, **kwargs) != None + + def as_choice(self, **kwargs): + """ + Render this setting as the "display" value of a choice field, + e.g. if the choices are: + [('A4', 'A4 paper'), ('A3', 'A3 paper')], + and the value is 'A4', + then display 'A4 paper' + """ + + choices = self.get_setting_choices(self.key, **kwargs) + + if not choices: + return self.value + + for value, display in choices: + if value == self.value: + return display + + return self.value + + def is_bool(self, **kwargs): """ Check if this setting is required to be a boolean value @@ -1212,6 +1240,21 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): 'default': False, 'validator': bool, }, + + 'DATE_DISPLAY_FORMAT': { + 'name': _('Date Format'), + 'description': _('Preferred format for displaying dates'), + 'default': 'YYYY-MM-DD', + 'choices': [ + ('YYYY-MM-DD', '2022-02-22'), + ('YYYY/MM/DD', '2022/22/22'), + ('DD-MM-YYYY', '22-02-2022'), + ('DD/MM/YYYY', '22/02/2022'), + ('MM-DD-YYYY', '02-22-2022'), + ('MM/DD/YYYY', '02/22/2022'), + ('MMM DD YYYY', 'Feb 22 2022'), + ] + } } class Meta: diff --git a/InvenTree/templates/InvenTree/settings/setting.html b/InvenTree/templates/InvenTree/settings/setting.html index 743e37bf8e..9ef6008292 100644 --- a/InvenTree/templates/InvenTree/settings/setting.html +++ b/InvenTree/templates/InvenTree/settings/setting.html @@ -28,7 +28,11 @@
{% if setting.value %} + {% if setting.is_choice %} + {{ setting.as_choice }} + {% else %} {{ setting.value }} + {% endif %} {% else %} {% trans "No value set" %} {% endif %} diff --git a/InvenTree/templates/InvenTree/settings/user_display.html b/InvenTree/templates/InvenTree/settings/user_display.html index 9f5c22f991..6432b858a3 100644 --- a/InvenTree/templates/InvenTree/settings/user_display.html +++ b/InvenTree/templates/InvenTree/settings/user_display.html @@ -15,6 +15,7 @@ {% include "InvenTree/settings/setting.html" with key="STICKY_HEADER" icon="fa-bars" user_setting=True %} + {% include "InvenTree/settings/setting.html" with key="DATE_DISPLAY_FORMAT" icon="fa-calendar-alt" user_setting=True %} {% include "InvenTree/settings/setting.html" with key="FORMS_CLOSE_USING_ESCAPE" icon="fa-window-close" user_setting=True %} {% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" user_setting=True %} diff --git a/InvenTree/templates/js/dynamic/settings.js b/InvenTree/templates/js/dynamic/settings.js index 133edfba20..4e7d36f72b 100644 --- a/InvenTree/templates/js/dynamic/settings.js +++ b/InvenTree/templates/js/dynamic/settings.js @@ -40,12 +40,15 @@ function editSetting(pk, options={}) { url = `/api/settings/user/${pk}/`; } + var reload_required = false; + // First, read the settings object from the server inventreeGet(url, {}, { success: function(response) { if (response.choices && response.choices.length > 0) { response.type = 'choice'; + reload_required = true; } // Construct the field @@ -89,7 +92,9 @@ function editSetting(pk, options={}) { var setting = response.key; - if (response.type == 'boolean') { + if (reload_required) { + location.reload(); + } else if (response.type == 'boolean') { var enabled = response.value.toString().toLowerCase() == 'true'; $(`#setting-value-${setting}`).prop('checked', enabled); } else { From c720c75b79d76d51d9e044268e934f5887111fcc Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 16:10:16 +1100 Subject: [PATCH 17/41] Adds javascript function for rendering a date according to user preference --- InvenTree/templates/js/dynamic/calendar.js | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/InvenTree/templates/js/dynamic/calendar.js b/InvenTree/templates/js/dynamic/calendar.js index 268337bd52..c730446bfb 100644 --- a/InvenTree/templates/js/dynamic/calendar.js +++ b/InvenTree/templates/js/dynamic/calendar.js @@ -7,6 +7,7 @@ clearEvents, endDate, startDate, + renderDate, */ /** @@ -32,3 +33,29 @@ function clearEvents(calendar) { event.remove(); }); } + + +/* + * Render the provided date in the user-specified format. + * + * The provided "date" variable is a string, nominally ISO format e.g. 2022-02-22 + * The user-configured setting DATE_DISPLAY_FORMAT determines how the date should be displayed. + */ + +function renderDate(date) { + + if (!date) { + return null; + } + + var fmt = user_settings.DATE_DISPLAY_FORMAT || 'YYYY-MM-DD'; + + var m = moment(date); + + if (m.isValid()) { + return m.format(fmt); + } else { + // Invalid input string, simply return provided value + return date; + } +} From 040f1805e0d81fb2433159970ed818dc52a4c7a4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 16:13:08 +1100 Subject: [PATCH 18/41] Pass dates displayed in front-end tables through the date formatter --- .../templates/js/translated/attachment.js | 3 +++ InvenTree/templates/js/translated/build.js | 9 +++++++++ InvenTree/templates/js/translated/order.js | 19 +++++++++++++++++-- InvenTree/templates/js/translated/stock.js | 18 ++++++++++-------- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/InvenTree/templates/js/translated/attachment.js b/InvenTree/templates/js/translated/attachment.js index 44061403fa..858ca8bd84 100644 --- a/InvenTree/templates/js/translated/attachment.js +++ b/InvenTree/templates/js/translated/attachment.js @@ -165,6 +165,9 @@ function loadAttachmentTable(url, options) { { field: 'upload_date', title: '{% trans "Upload Date" %}', + formatter: function(value) { + return renderDate(value); + } }, { field: 'actions', diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index fb8b870fad..1272670f7b 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1961,6 +1961,9 @@ function loadBuildTable(table, options) { field: 'creation_date', title: '{% trans "Created" %}', sortable: true, + formatter: function(value) { + return renderDate(value); + } }, { field: 'issued_by', @@ -1990,11 +1993,17 @@ function loadBuildTable(table, options) { field: 'target_date', title: '{% trans "Target Date" %}', sortable: true, + formatter: function(value) { + return renderDate(value); + } }, { field: 'completion_date', title: '{% trans "Completion Date" %}', sortable: true, + formatter: function(value) { + return renderDate(value); + } }, ], }); diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 008091bf15..1f5306b7f8 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -848,11 +848,17 @@ function loadPurchaseOrderTable(table, options) { field: 'creation_date', title: '{% trans "Date" %}', sortable: true, + formatter: function(value) { + return renderDate(value); + } }, { field: 'target_date', title: '{% trans "Target Date" %}', sortable: true, + formatter: function(value) { + return renderDate(value); + } }, { field: 'line_items', @@ -1269,16 +1275,25 @@ function loadSalesOrderTable(table, options) { sortable: true, field: 'creation_date', title: '{% trans "Creation Date" %}', + formatter: function(value) { + return renderDate(value); + } }, { sortable: true, field: 'target_date', title: '{% trans "Target Date" %}', + formatter: function(value) { + return renderDate(value); + } }, { sortable: true, field: 'shipment_date', title: '{% trans "Shipment Date" %}', + formatter: function(value) { + return renderDate(value); + } }, { sortable: true, @@ -1430,9 +1445,9 @@ function loadSalesOrderShipmentTable(table, options={}) { sortable: true, formatter: function(value, row) { if (value) { - return value; + return renderDate(value); } else { - return '{% trans "Not shipped" %}'; + return '{% trans "Not shipped" %}'; } } }, diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index 2d84f11e4a..327dd0ab71 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -1820,6 +1820,9 @@ function loadStockTable(table, options) { col = { field: 'stocktake_date', title: '{% trans "Stocktake" %}', + formatter: function(value) { + return renderDate(value); + } }; if (!options.params.ordering) { @@ -1833,6 +1836,9 @@ function loadStockTable(table, options) { title: '{% trans "Expiry Date" %}', visible: global_settings.STOCK_ENABLE_EXPIRY, switchable: global_settings.STOCK_ENABLE_EXPIRY, + formatter: function(value) { + return renderDate(value); + } }; if (!options.params.ordering) { @@ -1844,6 +1850,9 @@ function loadStockTable(table, options) { col = { field: 'updated', title: '{% trans "Last Updated" %}', + formatter: function(value) { + return renderDate(value); + } }; if (!options.params.ordering) { @@ -2649,14 +2658,7 @@ function loadStockTrackingTable(table, options) { title: '{% trans "Date" %}', sortable: true, formatter: function(value) { - var m = moment(value); - - if (m.isValid()) { - var html = m.format('dddd MMMM Do YYYY'); // + '
' + m.format('h:mm a'); - return html; - } - - return '{% trans "Invalid date" %}'; + return renderDate(value); } }); From 08946a411afc0046323e3fba721a25a2c63e2d29 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 16:29:00 +1100 Subject: [PATCH 19/41] Created template tag which renders date in templates based on user preference --- .../part/templatetags/inventree_extras.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 627b925e23..8aedf583de 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -5,6 +5,7 @@ This module provides template tags for extra functionality, over and above the built-in Django tags. """ +from datetime import date import os import sys @@ -43,6 +44,49 @@ def define(value, *args, **kwargs): return value +@register.simple_tag(takes_context=True) +def render_date(context, date_object): + """ + Renders a date according to the preference of the provided user + + Note that the user preference is stored using the formatting adopted by moment.js, + which differs from the python formatting! + """ + + if type(date_object) == str: + # If a string is passed, first convert it to a datetime + date_object = date.fromisoformat(date_object) + + # We may have already pre-cached the date format by calling this already! + user_date_format = context.get('user_date_format', None) + + if user_date_format is None: + + user = context.get('user', None) + + if user: + # User is specified - look for their date display preference + user_date_format = InvenTreeUserSetting.get_setting('DATE_DISPLAY_FORMAT', user=user) + else: + user_date_format = 'YYYY-MM-DD' + + # Convert the format string to Pythonic equivalent + replacements = [ + ('YYYY', '%Y'), + ('MMM', '%b'), + ('MM', '%m'), + ('DD', '%d'), + ] + + for o, n in replacements: + user_date_format = user_date_format.replace(o, n) + + # Update the context cache + context['user_date_format'] = user_date_format + + return date_object.strftime(user_date_format) + + @register.simple_tag() def decimal(x, *args, **kwargs): """ Simplified rendering of a decimal number """ From b00ae67d682a7b8b0be2ece8eb05130bcaabdc77 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 19:08:32 +1100 Subject: [PATCH 20/41] Pass dates in templates through the new template tag --- InvenTree/build/templates/build/build_base.html | 2 +- InvenTree/build/templates/build/detail.html | 10 +++++----- InvenTree/order/templates/order/order_base.html | 8 ++++---- InvenTree/order/templates/order/sales_order_base.html | 6 +++--- InvenTree/part/templates/part/detail.html | 4 ++-- InvenTree/part/templates/part/part_base.html | 2 +- .../templates/report/inventree_build_order_base.html | 4 ++-- InvenTree/stock/templates/stock/item_base.html | 4 ++-- InvenTree/templates/InvenTree/settings/plugin.html | 2 +- .../templates/InvenTree/settings/plugin_settings.html | 4 ++-- InvenTree/templates/about.html | 2 +- InvenTree/templates/version.html | 2 +- 12 files changed, 25 insertions(+), 25 deletions(-) diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index cd7126a801..4d2c77278c 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -151,7 +151,7 @@ src="{% static 'img/blank_image.png' %}" {% trans "Target Date" %}
- {{ build.target_date }} + {% render_date build.target_date %} {% if build.is_overdue %} {% trans "Overdue" %} {% endif %} diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index f85ec9afa6..1e31857ba5 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -18,7 +18,7 @@
- +
@@ -120,19 +120,19 @@
- +
- + {% if build.target_date %} {% else %} @@ -142,7 +142,7 @@ {% if build.completion_date %} - + {% else %} {% endif %} diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index af1e02fd54..c188e183d0 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -141,27 +141,27 @@ src="{% static 'img/blank_image.png' %}" - + {% if order.issue_date %} - + {% endif %} {% if order.target_date %} - + {% endif %} {% if order.status == PurchaseOrderStatus.COMPLETE %} - + {% endif %} {% if order.responsible %} diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index c8718d54d8..423090f917 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -155,13 +155,13 @@ src="{% static 'img/blank_image.png' %}" - + {% if order.target_date %} - + {% endif %} {% if order.shipment_date %} @@ -169,7 +169,7 @@ src="{% static 'img/blank_image.png' %}" - + {% if item.stocktake_date %} - + {% else %} {% endif %} diff --git a/InvenTree/templates/InvenTree/settings/plugin.html b/InvenTree/templates/InvenTree/settings/plugin.html index caee7c92bf..139ce0d41a 100644 --- a/InvenTree/templates/InvenTree/settings/plugin.html +++ b/InvenTree/templates/InvenTree/settings/plugin.html @@ -81,7 +81,7 @@ {% endif %} - + {% endfor %} diff --git a/InvenTree/templates/InvenTree/settings/plugin_settings.html b/InvenTree/templates/InvenTree/settings/plugin_settings.html index a670d71c34..9c5fa2d7c0 100644 --- a/InvenTree/templates/InvenTree/settings/plugin_settings.html +++ b/InvenTree/templates/InvenTree/settings/plugin_settings.html @@ -36,7 +36,7 @@ - + @@ -101,7 +101,7 @@ - + diff --git a/InvenTree/templates/about.html b/InvenTree/templates/about.html index 34884da9d1..333185725f 100644 --- a/InvenTree/templates/about.html +++ b/InvenTree/templates/about.html @@ -44,7 +44,7 @@ {% if commit_date %} - + {% endif %} {% endif %} diff --git a/InvenTree/templates/version.html b/InvenTree/templates/version.html index b702fd85f5..7f3b774257 100644 --- a/InvenTree/templates/version.html +++ b/InvenTree/templates/version.html @@ -2,7 +2,7 @@ InvenTree-Version: {% inventree_version %} Django Version: {% django_version %} {% inventree_commit_hash as hash %}{% if hash %}Commit Hash: {{ hash }}{% endif %} -{% inventree_commit_date as commit_date %}{% if commit_date %}Commit Date: {{ commit_date }}{% endif %} +{% inventree_commit_date as commit_date %}{% if commit_date %}Commit Date: {% render_date commit_date %}{% endif %} Database: {% inventree_db_engine %} Debug-Mode: {% inventree_in_debug_mode %} Deployed using Docker: {% inventree_docker_mode %} \ No newline at end of file From fd02f1b7615e37120d94695f042039f344d9bbc1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 19:14:20 +1100 Subject: [PATCH 21/41] Add option for display of "time" in addition to date --- InvenTree/templates/js/dynamic/calendar.js | 6 +++++- InvenTree/templates/js/translated/stock.js | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/dynamic/calendar.js b/InvenTree/templates/js/dynamic/calendar.js index c730446bfb..0b0ccc2915 100644 --- a/InvenTree/templates/js/dynamic/calendar.js +++ b/InvenTree/templates/js/dynamic/calendar.js @@ -42,7 +42,7 @@ function clearEvents(calendar) { * The user-configured setting DATE_DISPLAY_FORMAT determines how the date should be displayed. */ -function renderDate(date) { +function renderDate(date, options={}) { if (!date) { return null; @@ -50,6 +50,10 @@ function renderDate(date) { var fmt = user_settings.DATE_DISPLAY_FORMAT || 'YYYY-MM-DD'; + if (options.showTime) { + fmt += ' HH:mm'; + } + var m = moment(date); if (m.isValid()) { diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index 327dd0ab71..cf2538c1e5 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -2658,7 +2658,7 @@ function loadStockTrackingTable(table, options) { title: '{% trans "Date" %}', sortable: true, formatter: function(value) { - return renderDate(value); + return renderDate(value, {showTime: true}); } }); From 50a32a13f04082c344b8be99c594933a8564fcf1 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 19:15:12 +1100 Subject: [PATCH 22/41] PEP style fixes --- InvenTree/common/models.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 927abd94cd..fe6ddecbba 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -467,8 +467,8 @@ class BaseInvenTreeSetting(models.Model): Check if this setting is a "choice" field """ - return self.__class__.get_setting_choices(self.key, **kwargs) != None - + return self.__class__.get_setting_choices(self.key, **kwargs) is not None + def as_choice(self, **kwargs): """ Render this setting as the "display" value of a choice field, @@ -486,9 +486,8 @@ class BaseInvenTreeSetting(models.Model): for value, display in choices: if value == self.value: return display - + return self.value - def is_bool(self, **kwargs): """ From 7d19b21dffacb6d9be7358481c3253a322742097 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 19:22:22 +1100 Subject: [PATCH 23/41] JS linting --- InvenTree/templates/js/translated/stock.js | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index cf2538c1e5..1ca89368dc 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -25,7 +25,6 @@ modalSetContent, modalSetTitle, modalSubmit, - moment, openModal, printStockItemLabels, printTestReports, From 73484192a59edd7e82b78e6c23bdc418d4e2a258 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 22:47:41 +1100 Subject: [PATCH 24/41] Add "batch code" and "serial numbers" serializer fields when receiving stock items against a purchase order --- InvenTree/order/models.py | 75 ++++++++++++++++++++++------------ InvenTree/order/serializers.py | 67 ++++++++++++++++++++++++++---- 2 files changed, 107 insertions(+), 35 deletions(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 7f6192dfcd..3b1cad2ff5 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -398,12 +398,22 @@ class PurchaseOrder(Order): return self.lines.count() > 0 and self.pending_line_items().count() == 0 @transaction.atomic - def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, purchase_price=None, **kwargs): - """ Receive a line item (or partial line item) against this PO + def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, **kwargs): + """ + Receive a line item (or partial line item) against this PO """ + # Extract optional batch code for the new stock item + batch_code = kwargs.get('batch_code', '') + + # Extract optional list of serial numbers + serials = kwargs.get('serials', None) + + # Extract optional notes field notes = kwargs.get('notes', '') - barcode = kwargs.get('barcode', '') + + # Extract optional barcode field + barcode = kwargs.get('barcode', None) # Prevent null values for barcode if barcode is None: @@ -427,33 +437,44 @@ class PurchaseOrder(Order): # Create a new stock item if line.part and quantity > 0: - stock = stock_models.StockItem( - part=line.part.part, - supplier_part=line.part, - location=location, - quantity=quantity, - purchase_order=self, - status=status, - purchase_price=line.purchase_price, - uid=barcode - ) - stock.save(add_note=False) + # Determine if we should individually serialize the items, or not + if type(serials) is list and len(serials) > 0: + quantity = 1 + else: + serials = [None] - tracking_info = { - 'status': status, - 'purchaseorder': self.pk, - } + for sn in serials: - stock.add_tracking_entry( - StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER, - user, - notes=notes, - deltas=tracking_info, - location=location, - purchaseorder=self, - quantity=quantity - ) + stock = stock_models.StockItem( + part=line.part.part, + supplier_part=line.part, + location=location, + quantity=quantity, + purchase_order=self, + status=status, + batch=batch_code, + serial=sn, + purchase_price=line.purchase_price, + uid=barcode + ) + + stock.save(add_note=False) + + tracking_info = { + 'status': status, + 'purchaseorder': self.pk, + } + + stock.add_tracking_entry( + StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER, + user, + notes=notes, + deltas=tracking_info, + location=location, + purchaseorder=self, + quantity=quantity + ) # Update the number of parts received against the particular line item line.received += quantity diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 5e78b3e3a3..216bc63b11 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -5,6 +5,8 @@ JSON serializers for the Order API # -*- coding: utf-8 -*- from __future__ import unicode_literals +from decimal import Decimal + from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ValidationError as DjangoValidationError @@ -203,6 +205,17 @@ class POLineItemReceiveSerializer(serializers.Serializer): A serializer for receiving a single purchase order line item against a purchase order """ + class Meta: + fields = [ + 'barcode', + 'line_item', + 'location', + 'quantity', + 'status', + 'batch_code' + 'serial_numbers', + ] + line_item = serializers.PrimaryKeyRelatedField( queryset=order.models.PurchaseOrderLineItem.objects.all(), many=False, @@ -241,6 +254,22 @@ class POLineItemReceiveSerializer(serializers.Serializer): return quantity + batch_code = serializers.CharField( + label=_('Batch Code'), + help_text=_('Enter batch code for incoming stock items'), + required=False, + default='', + allow_blank=True, + ) + + serial_numbers = serializers.CharField( + label=_('Serial Numbers'), + help_text=_('Enter serial numbers for incoming stock items'), + required=False, + default='', + allow_blank=True, + ) + status = serializers.ChoiceField( choices=list(StockStatus.items()), default=StockStatus.OK, @@ -270,15 +299,35 @@ class POLineItemReceiveSerializer(serializers.Serializer): return barcode - class Meta: - fields = [ - 'barcode', - 'line_item', - 'location', - 'quantity', - 'status', - ] + def validate(self, data): + data = super().validate(data) + + line_item = data['line_item'] + quantity = data['quantity'] + serial_numbers = data.get('serial_numbers', '').strip() + + base_part = line_item.part.part + + # Does the quantity need to be "integer" (for trackable parts?) + if base_part.trackable: + + if Decimal(quantity) != int(quantity): + raise ValidationError({ + 'quantity': _('An integer quantity must be provided for trackable parts'), + }) + + # If serial numbers are provided + if serial_numbers: + try: + # Pass the serial numbers through to the parent serializer once validated + data['serials'] = extract_serial_numbers(serial_numbers, quantity, base_part.getLatestSerialNumberInt()) + except DjangoValidationError as e: + raise ValidationError({ + 'serial_numbers': e.messages, + }) + + return data class POReceiveSerializer(serializers.Serializer): """ @@ -366,6 +415,8 @@ class POReceiveSerializer(serializers.Serializer): request.user, status=item['status'], barcode=item.get('barcode', ''), + batch_code=item.get('batch_code', ''), + serials=item.get('serials', None), ) except (ValidationError, DjangoValidationError) as exc: # Catch model errors and re-throw as DRF errors From 9e0120599f21852f7da152614269f4ca87efff2d Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 22:48:15 +1100 Subject: [PATCH 25/41] Adds front-end javascript code to implement new serializer features --- InvenTree/templates/js/translated/forms.js | 17 ++--- InvenTree/templates/js/translated/order.js | 72 +++++++++++++++++++++- 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index 9ee3dcace3..63598a8676 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -1976,7 +1976,7 @@ function constructField(name, parameters, options) { html += `
`; // Does this input deserve "extra" decorators? - var extra = parameters.prefix != null; + var extra = (parameters.icon != null) || (parameters.prefix != null) || (parameters.prefixRaw != null); // Some fields can have 'clear' inputs associated with them if (!parameters.required && !parameters.read_only) { @@ -1998,9 +1998,13 @@ function constructField(name, parameters, options) { if (extra) { html += `
`; - + if (parameters.prefix) { html += `${parameters.prefix}`; + } else if (parameters.prefixRaw) { + html += parameters.prefixRaw; + } else if (parameters.icon) { + html += ``; } } @@ -2147,6 +2151,10 @@ function constructInputOptions(name, classes, type, parameters, options={}) { opts.push(`type='${type}'`); + if (parameters.title || parameters.help_text) { + opts.push(`title='${parameters.title || parameters.help_text}'`); + } + // Read only? if (parameters.read_only) { opts.push(`readonly=''`); @@ -2192,11 +2200,6 @@ function constructInputOptions(name, classes, type, parameters, options={}) { opts.push(`required=''`); } - // Custom mouseover title? - if (parameters.title != null) { - opts.push(`title='${parameters.title}'`); - } - // Placeholder? if (parameters.placeholder != null) { opts.push(`placeholder='${parameters.placeholder}'`); diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 008091bf15..0ec50022aa 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -476,6 +476,25 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { quantity = 0; } + // Prepend toggles to the quantity input + var toggle_batch = ` + + + + `; + + var toggle_serials = ` + + + + `; + + var prefix_buttons = toggle_batch; + + if (line_item.part_detail.trackable) { + prefix_buttons += toggle_serials; + } + // Quantity to Receive var quantity_input = constructField( `items_quantity_${pk}`, @@ -485,12 +504,49 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { value: quantity, title: '{% trans "Quantity to receive" %}', required: true, + prefixRaw: prefix_buttons, }, { hideLabels: true, } ); + // Add in options for "batch code" and "serial numbers" + var batch_input = constructField( + `items_batch_code_${pk}`, + { + type: 'string', + required: true, + label: '{% trans "Batch Code" %}', + help_text: '{% trans "Enter batch code for incoming stock items" %}', + prefixRaw: toggle_batch, + }, + { + hideLabels: true, + } + ); + + var sn_input = constructField( + `items_serial_numbers_${pk}`, + { + type: 'string', + required: true, + label: '{% trans "Serial Numbers" %}', + help_text: '{% trans "Enter serial numbers for incoming stock items" %}', + prefixRaw: toggle_serials, + }, + { + hideLabels: true + } + ); + + // Hidden inputs below the "quantity" field + var quantity_input_group = `${quantity_input}
${batch_input}
`; + + if (line_item.part_detail.trackable) { + quantity_input_group += `
${sn_input}
`; + } + // Construct list of StockItem status codes var choices = []; @@ -554,7 +610,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { ${line_item.received}
@@ -619,7 +628,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { ${destination_input} `; @@ -643,7 +652,7 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { - + From 7ad9f8852e300e329d59771aec4bbe10139e372f Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 23:31:48 +1100 Subject: [PATCH 29/41] PEP fix --- InvenTree/InvenTree/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index e260f71906..d71f7d87c7 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -409,7 +409,7 @@ def DownloadFile(data, filename, content_type='application/text', inline=False): def extract_serial_numbers(serials, expected_quantity, next_number: int): """ Attempt to extract serial numbers from an input string: - + Requirements: - Serial numbers can be either strings, or integers - Serial numbers can be split by whitespace / newline / commma chars From 0eba6f24767629de663d572d1c8ab12c3476e4b8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 23:39:11 +1100 Subject: [PATCH 30/41] Prevent operations on null dates --- InvenTree/part/templatetags/inventree_extras.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 8aedf583de..7f0d7ff3cc 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -53,6 +53,9 @@ def render_date(context, date_object): which differs from the python formatting! """ + if date_object is None: + return None + if type(date_object) == str: # If a string is passed, first convert it to a datetime date_object = date.fromisoformat(date_object) From 9f5618a51fabf78257fbbddaf31c18ff39e15638 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 23:57:28 +1100 Subject: [PATCH 31/41] Adds option to reload a form after success, rather than dismissing it --- InvenTree/templates/js/translated/forms.js | 53 +++++++++++++++------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index 9ee3dcace3..d5be70e4fe 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -934,16 +934,14 @@ function getFormFieldValue(name, field={}, options={}) { */ function handleFormSuccess(response, options) { - // Close the modal - if (!options.preventClose) { - // Note: The modal will be deleted automatically after closing - $(options.modal).modal('hide'); - } - // Display any required messages // Should we show alerts immediately or cache them? var cache = (options.follow && response.url) || options.redirect || options.reload; + if (options.reloadFormAfterSuccess) { + cache = false; + } + // Display any messages if (response && (response.success || options.successMessage)) { showAlertOrCache(response.success || options.successMessage, cache, {style: 'success'}); @@ -960,21 +958,42 @@ function handleFormSuccess(response, options) { if (response && response.danger) { showAlertOrCache(response.danger, cache, {style: 'danger'}); } - + if (options.onSuccess) { // Callback function options.onSuccess(response, options); } - if (options.follow && response.url) { - // Follow the returned URL - window.location.href = response.url; - } else if (options.reload) { - // Reload the current page - location.reload(); - } else if (options.redirect) { - // Redirect to a specified URL - window.location.href = options.redirect; + if (options.reloadFormAfterSuccess) { + // Instead of closing the form and going somewhere else, + // reload (empty) the form so the user can input more data + + // Reset the status of the "submit" button + if (options.modal) { + $(options.modal).find('#modal-form-submit').prop('disabled', false); + } + + // Remove any error flags from the form + clearFormErrors(options); + + } else { + + // Close the modal + if (!options.preventClose) { + // Note: The modal will be deleted automatically after closing + $(options.modal).modal('hide'); + } + + if (options.follow && response.url) { + // Follow the returned URL + window.location.href = response.url; + } else if (options.reload) { + // Reload the current page + location.reload(); + } else if (options.redirect) { + // Redirect to a specified URL + window.location.href = options.redirect; + } } } @@ -988,6 +1007,8 @@ function clearFormErrors(options={}) { if (options && options.modal) { // Remove the individual error messages $(options.modal).find('.form-error-message').remove(); + + $(options.modal).find('.modal-content').removeClass('modal-error'); // Remove the "has error" class $(options.modal).find('.form-field-error').removeClass('form-field-error'); From 7170e16ae7a3e948228e588ecd9a9fa9c062131e Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 1 Mar 2022 00:05:30 +1100 Subject: [PATCH 32/41] Adds ability to display "success" messages inside a persistant modal dialog --- .../static/script/inventree/notification.js | 5 +++-- InvenTree/templates/js/translated/forms.js | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/notification.js b/InvenTree/InvenTree/static/script/inventree/notification.js index f6bdf3bc57..c4816d4b5c 100644 --- a/InvenTree/InvenTree/static/script/inventree/notification.js +++ b/InvenTree/InvenTree/static/script/inventree/notification.js @@ -37,7 +37,6 @@ function showAlertOrCache(message, cache, options={}) { if (cache) { addCachedAlert(message, options); } else { - showMessage(message, options); } } @@ -82,6 +81,8 @@ function showMessage(message, options={}) { var timeout = options.timeout || 5000; + var target = options.target || $('#alerts'); + var details = ''; if (options.details) { @@ -111,7 +112,7 @@ function showMessage(message, options={}) { `; - $('#alerts').append(html); + target.append(html); // Remove the alert automatically after a specified period of time $(`#alert-${id}`).delay(timeout).slideUp(200, function() { diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index d5be70e4fe..c288ada27f 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -942,9 +942,22 @@ function handleFormSuccess(response, options) { cache = false; } + var msg_target = null; + + if (options.modal && options.reloadFormAfterSuccess) { + // If the modal is persistant, the target for any messages should be the modal! + msg_target = $(options.modal).find('#pre-form-content'); + } + // Display any messages if (response && (response.success || options.successMessage)) { - showAlertOrCache(response.success || options.successMessage, cache, {style: 'success'}); + showAlertOrCache( + response.success || options.successMessage, + cache, + { + style: 'success', + target: msg_target, + }); } if (response && response.info) { From 42a75863fe2c4b214f4aebf365442f9d0c83596b Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 1 Mar 2022 00:25:14 +1100 Subject: [PATCH 33/41] Adds a "persist" option for modal forms --- InvenTree/part/templates/part/category.html | 4 ++ InvenTree/templates/js/translated/forms.js | 47 +++++++++++++++++---- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/InvenTree/part/templates/part/category.html b/InvenTree/part/templates/part/category.html index 4de0005672..e02c77509d 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -313,6 +313,10 @@ fields: fields, groups: partGroups(), title: '{% trans "Create Part" %}', + reloadFormAfterSuccess: true, + persist: true, + persistMessage: '{% trans "Create another part after this one" %}', + successMessage: '{% trans "Part created successfully" %}', onSuccess: function(data) { // Follow the new part location.href = `/part/${data.pk}/`; diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index c288ada27f..2946b77e4e 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -542,6 +542,11 @@ function constructFormBody(fields, options) { insertConfirmButton(options); } + // Insert "persist" button (if required) + if (options.persist) { + insertPersistButton(options); + } + // Display the modal $(modal).modal('show'); @@ -616,6 +621,22 @@ function insertConfirmButton(options) { } +/* Add a checkbox to select if the modal will stay open after success */ +function insertPersistButton(options) { + + var message = options.persistMessage || '{% trans "Keep this form open" %}'; + + var html = ` +
+ + +
+ `; + + $(options.modal).find('#modal-footer-buttons').append(html); +} + + /* * Extract all specified form values as a single object */ @@ -938,13 +959,23 @@ function handleFormSuccess(response, options) { // Should we show alerts immediately or cache them? var cache = (options.follow && response.url) || options.redirect || options.reload; - if (options.reloadFormAfterSuccess) { + // Should the form "persist"? + var persist = false; + + if (options.persist && options.modal) { + // Determine if this form should "persist", or be dismissed? + var chk = $(options.modal).find('#modal-persist'); + + persist = chk.exists() && chk.prop('checked'); + } + + if (persist) { cache = false; } var msg_target = null; - if (options.modal && options.reloadFormAfterSuccess) { + if (persist) { // If the modal is persistant, the target for any messages should be the modal! msg_target = $(options.modal).find('#pre-form-content'); } @@ -971,13 +1002,8 @@ function handleFormSuccess(response, options) { if (response && response.danger) { showAlertOrCache(response.danger, cache, {style: 'danger'}); } - - if (options.onSuccess) { - // Callback function - options.onSuccess(response, options); - } - if (options.reloadFormAfterSuccess) { + if (persist) { // Instead of closing the form and going somewhere else, // reload (empty) the form so the user can input more data @@ -997,6 +1023,11 @@ function handleFormSuccess(response, options) { $(options.modal).modal('hide'); } + if (options.onSuccess) { + // Callback function + options.onSuccess(response, options); + } + if (options.follow && response.url) { // Follow the returned URL window.location.href = response.url; From 63d052e1ca4e7931072688f1fa4cc39f9b3117bd Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 1 Mar 2022 00:37:27 +1100 Subject: [PATCH 34/41] Fixes for unit tests - Prioritize "integer" values when extracting serial numbers --- InvenTree/InvenTree/helpers.py | 10 +++++++--- InvenTree/InvenTree/status.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index d71f7d87c7..b5cecf764d 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -439,7 +439,7 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): # Helper function to check for duplicated numbers def add_sn(sn): if sn in numbers: - errors.append(_('Duplicate serial: {sn}').format(n=n)) + errors.append(_('Duplicate serial: {sn}').format(sn=sn)) else: numbers.append(sn) @@ -504,10 +504,14 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): errors.append(_("Invalid group: {g}").format(g=group)) # At this point, we assume that the "group" is just a single serial value - # Note: At this point it is treated only as a string elif group: - add_sn(group) + try: + # First attempt to add as an integer value + add_sn(int(group)) + except (ValueError): + # As a backup, add as a string value + add_sn(group) # No valid input group detected else: diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py index cc9df701c6..181f431574 100644 --- a/InvenTree/InvenTree/status.py +++ b/InvenTree/InvenTree/status.py @@ -14,6 +14,8 @@ from django_q.monitor import Stat from django.conf import settings +import InvenTree.ready + logger = logging.getLogger("inventree") @@ -56,6 +58,12 @@ def is_email_configured(): configured = True + if InvenTree.ready.isInTestMode(): + return False + + if InvenTree.ready.isImportingData(): + return False + if not settings.EMAIL_HOST: configured = False @@ -89,6 +97,14 @@ def check_system_health(**kwargs): result = True + if InvenTree.ready.isInTestMode(): + # Do not perform further checks if we are running unit tests + return False + + if InvenTree.ready.isImportingData(): + # Do not perform further checks if we are importing data + return False + if not is_worker_running(**kwargs): # pragma: no cover result = False logger.warning(_("Background worker check failed")) From b2e0241b1274f390a049a4d314a612d59859227e Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 1 Mar 2022 00:57:53 +1100 Subject: [PATCH 35/41] Adds option to "obfuscate" the admin URL - By default, uses '/admin/' - Can be set in config yaml file - Can be set by environment variable --- InvenTree/InvenTree/settings.py | 8 ++++++++ InvenTree/InvenTree/urls.py | 8 ++++---- InvenTree/templates/navbar.html | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index c45be06f4b..121f1b6383 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -309,6 +309,14 @@ if DEBUG and CONFIG.get('debug_toolbar', False): # pragma: no cover INSTALLED_APPS.append('debug_toolbar') MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') +# InvenTree URL configuration + +# Base URL for admin pages (default="admin") +INVENTREE_ADMIN_URL = get_setting( + 'INVENTREE_ADMIN_URL', + CONFIG.get('admin_url', 'admin'), +) + ROOT_URLCONF = 'InvenTree.urls' TEMPLATES = [ diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index b1941da8db..6c444497b9 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -4,7 +4,7 @@ Top-level URL lookup for InvenTree application. Passes URL lookup downstream to each app as required. """ - +from django.conf import settings from django.conf.urls import url, include from django.urls import path from django.contrib import admin @@ -169,9 +169,9 @@ frontendpatterns = [ url(r'^stats/', DatabaseStatsView.as_view(), name='stats'), # admin sites - url(r'^admin/error_log/', include('error_report.urls')), - url(r'^admin/shell/', include('django_admin_shell.urls')), - url(r'^admin/', admin.site.urls, name='inventree-admin'), + url(f'^{settings.INVENTREE_ADMIN_URL}/error_log/', include('error_report.urls')), + url(f'^{settings.INVENTREE_ADMIN_URL}/shell/', include('django_admin_shell.urls')), + url(f'^{settings.INVENTREE_ADMIN_URL}/', admin.site.urls, name='inventree-admin'), # DB user sessions url(r'^accounts/sessions/other/delete/$', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ), diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 7fd4a76eff..e898b5fafa 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -108,7 +108,7 @@
{% trans "Created" %}{{ build.creation_date }}{% render_date build.creation_date %}
{% trans "Target Date" %} - {{ build.target_date }}{% if build.is_overdue %} {% endif %} + {% render_date build.target_date %}{% if build.is_overdue %} {% endif %} {% trans "No target date set" %} {% trans "Completed" %}{{ build.completion_date }}{% if build.completed_by %}{{ build.completed_by }}{% endif %}{% render_date build.completion_date %}{% if build.completed_by %}{{ build.completed_by }}{% endif %}{% trans "Build not complete" %}
{% trans "Created" %}{{ order.creation_date }}{{ order.created_by }}{% render_date order.creation_date %}{{ order.created_by }}
{% trans "Issued" %}{{ order.issue_date }}{% render_date order.issue_date %}
{% trans "Target Date" %}{{ order.target_date }}{% render_date order.target_date %}
{% trans "Received" %}{{ order.complete_date }}{{ order.received_by }}{% render_date order.complete_date %}{{ order.received_by }}
{% trans "Created" %}{{ order.creation_date }}{{ order.created_by }}{% render_date order.creation_date %}{{ order.created_by }}
{% trans "Target Date" %}{{ order.target_date }}{% render_date order.target_date %}
{% trans "Completed" %} - {{ order.shipment_date }} + {% render_date order.shipment_date %} {% if order.shipped_by %} {{ order.shipped_by }} {% endif %} diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 2266b39048..1cfd6c13e2 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -952,7 +952,7 @@ {% if price_history %} var purchasepricedata = { labels: [ - {% for line in price_history %}'{{ line.date }}',{% endfor %} + {% for line in price_history %}'{% render_date line.date %}',{% endfor %} ], datasets: [{ label: '{% blocktrans %}Purchase Unit Price - {{currency}}{% endblocktrans %}', @@ -1065,7 +1065,7 @@ {% if sale_history %} var salepricedata = { labels: [ - {% for line in sale_history %}'{{ line.date }}',{% endfor %} + {% for line in sale_history %}'{% render_date line.date %}',{% endfor %} ], datasets: [{ label: '{% blocktrans %}Unit Price - {{currency}}{% endblocktrans %}', diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index d2505a57f7..823bb1a4f3 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -312,7 +312,7 @@ {% trans "Creation Date" %} - {{ part.creation_date }} + {% render_date part.creation_date %} {% if part.creation_user %} {{ part.creation_user }} {% endif %} diff --git a/InvenTree/report/templates/report/inventree_build_order_base.html b/InvenTree/report/templates/report/inventree_build_order_base.html index 20e6a6ffd0..06d8abae65 100644 --- a/InvenTree/report/templates/report/inventree_build_order_base.html +++ b/InvenTree/report/templates/report/inventree_build_order_base.html @@ -120,13 +120,13 @@ content: "v{{report_revision}} - {{ date.isoformat }}";
{% trans "Issued" %}{{ build.creation_date }}{% render_date build.creation_date %}
{% trans "Target Date" %} {% if build.target_date %} - {{ build.target_date }} + {% render_date build.target_date %} {% else %} Not specified {% endif %} diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index c52d101afb..c612abe929 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -187,7 +187,7 @@ {% trans "Expiry Date" %} - {{ item.expiry_date }} + {% render_date item.expiry_date %} {% if item.is_expired %} {% trans "Expired" %} {% elif item.is_stale %} @@ -205,7 +205,7 @@ {% trans "Last Stocktake" %}{{ item.stocktake_date }} {{ item.stocktake_user }}{% render_date item.stocktake_date %} {{ item.stocktake_user }}{% trans "No stocktake performed" %} {{ plugin.author }}{{ plugin.pub_date }}{% render_date plugin.pub_date %} {% if plugin.version %}{{ plugin.version }}{% endif %}
{% trans "Date" %}{{ plugin.pub_date }}{% include "clip.html" %}{% render_date plugin.pub_date %}{% include "clip.html" %}
{% trans "Commit Date" %}{{ plugin.package.date }}{% include "clip.html" %}{% trans "Commit Date" %}{% render_date plugin.package.date %}{% include "clip.html" %}
{% trans "Commit Date" %}{{ commit_date }}{% include "clip.html" %}{% trans "Commit Date" %}{% render_date commit_date %}{% include "clip.html" %}
- ${quantity_input} + ${quantity_input_group} ${status_input} @@ -678,13 +734,23 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { var location = getFormFieldValue(`items_location_${pk}`, {}, opts); if (quantity != null) { - data.items.push({ + + var line = { line_item: pk, quantity: quantity, status: status, location: location, - }); + }; + if (getFormFieldElement(`items_batch_code_${pk}`).exists()) { + line.batch_code = getFormFieldValue(`items_batch_code_${pk}`); + } + + if (getFormFieldElement(`items_serial_numbers_${pk}`).exists()) { + line.serial_numbers = getFormFieldValue(`items_serial_numbers_${pk}`); + } + + data.items.push(line); item_pk_values.push(pk); } From 421db61f21b89b04ea244d580d633fb4f3c07144 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 23:09:57 +1100 Subject: [PATCH 26/41] Adding unit testing for new features --- InvenTree/order/models.py | 5 +- InvenTree/order/serializers.py | 1 + InvenTree/order/test_api.py | 102 +++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 3b1cad2ff5..7f5c6b8164 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -440,8 +440,9 @@ class PurchaseOrder(Order): # Determine if we should individually serialize the items, or not if type(serials) is list and len(serials) > 0: - quantity = 1 + serialize = True else: + serialize = False serials = [None] for sn in serials: @@ -450,7 +451,7 @@ class PurchaseOrder(Order): part=line.part.part, supplier_part=line.part, location=location, - quantity=quantity, + quantity=1 if serialize else quantity, purchase_order=self, status=status, batch=batch_code, diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 216bc63b11..cf8e701c68 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -329,6 +329,7 @@ class POLineItemReceiveSerializer(serializers.Serializer): return data + class POReceiveSerializer(serializers.Serializer): """ Serializer for receiving items against a purchase order diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py index 2ea2086431..b6fee8a218 100644 --- a/InvenTree/order/test_api.py +++ b/InvenTree/order/test_api.py @@ -529,6 +529,108 @@ class PurchaseOrderReceiveTest(OrderTest): self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-123').exists()) self.assertTrue(StockItem.objects.filter(uid='MY-UNIQUE-BARCODE-456').exists()) + def test_batch_code(self): + """ + Test that we can supply a 'batch code' when receiving items + """ + + line_1 = models.PurchaseOrderLineItem.objects.get(pk=1) + line_2 = models.PurchaseOrderLineItem.objects.get(pk=2) + + self.assertEqual(StockItem.objects.filter(supplier_part=line_1.part).count(), 0) + self.assertEqual(StockItem.objects.filter(supplier_part=line_2.part).count(), 0) + + data = { + 'items': [ + { + 'line_item': 1, + 'quantity': 10, + 'batch_code': 'abc-123', + }, + { + 'line_item': 2, + 'quantity': 10, + 'batch_code': 'xyz-789', + } + ], + 'location': 1, + } + + n = StockItem.objects.count() + + self.post( + self.url, + data, + expected_code=201, + ) + + # Check that two new stock items have been created! + self.assertEqual(n + 2, StockItem.objects.count()) + + item_1 = StockItem.objects.filter(supplier_part=line_1.part).first() + item_2 = StockItem.objects.filter(supplier_part=line_2.part).first() + + self.assertEqual(item_1.batch, 'abc-123') + self.assertEqual(item_2.batch, 'xyz-789') + + def test_serial_numbers(self): + """ + Test that we can supply a 'serial number' when receiving items + """ + + line_1 = models.PurchaseOrderLineItem.objects.get(pk=1) + line_2 = models.PurchaseOrderLineItem.objects.get(pk=2) + + self.assertEqual(StockItem.objects.filter(supplier_part=line_1.part).count(), 0) + self.assertEqual(StockItem.objects.filter(supplier_part=line_2.part).count(), 0) + + data = { + 'items': [ + { + 'line_item': 1, + 'quantity': 10, + 'batch_code': 'abc-123', + 'serial_numbers': '100+', + }, + { + 'line_item': 2, + 'quantity': 10, + 'batch_code': 'xyz-789', + } + ], + 'location': 1, + } + + n = StockItem.objects.count() + + self.post( + self.url, + data, + expected_code=201, + ) + + # Check that the expected number of stock items has been created + self.assertEqual(n + 11, StockItem.objects.count()) + + # 10 serialized stock items created for the first line item + self.assertEqual(StockItem.objects.filter(supplier_part=line_1.part).count(), 10) + + # Check that the correct serial numbers have been allocated + for i in range(100, 110): + item = StockItem.objects.get(serial_int=i) + self.assertEqual(item.serial, str(i)) + self.assertEqual(item.quantity, 1) + self.assertEqual(item.batch, 'abc-123') + + # A single stock item (quantity 10) created for the second line item + items = StockItem.objects.filter(supplier_part=line_2.part) + self.assertEqual(items.count(), 1) + + item = items.first() + + self.assertEqual(item.quantity, 10) + self.assertEqual(item.batch, 'xyz-789') + class SalesOrderTest(OrderTest): """ From fdc2cae6bac0daa7b1bfe640c487c8e9b5c48842 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 23:31:12 +1100 Subject: [PATCH 27/41] Fix some existing problems with the extract_serial_numbers helper function - Serial numbers don't really have to be "numbers" - Allow any text value once other higher-level checks have been performed --- InvenTree/InvenTree/helpers.py | 44 ++++++++++++++++------------------ 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py index 2595f8b5c3..e260f71906 100644 --- a/InvenTree/InvenTree/helpers.py +++ b/InvenTree/InvenTree/helpers.py @@ -407,14 +407,16 @@ def DownloadFile(data, filename, content_type='application/text', inline=False): def extract_serial_numbers(serials, expected_quantity, next_number: int): - """ Attempt to extract serial numbers from an input string. - - Serial numbers must be integer values - - Serial numbers must be positive - - Serial numbers can be split by whitespace / newline / commma chars - - Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20 - - Serial numbers can be defined as ~ for getting the next available serial number - - Serial numbers can be supplied as + for getting all expecteded numbers starting from - - Serial numbers can be supplied as + for getting numbers starting from + """ + Attempt to extract serial numbers from an input string: + + Requirements: + - Serial numbers can be either strings, or integers + - Serial numbers can be split by whitespace / newline / commma chars + - Serial numbers can be supplied as an inclusive range using hyphen char e.g. 10-20 + - Serial numbers can be defined as ~ for getting the next available serial number + - Serial numbers can be supplied as + for getting all expecteded numbers starting from + - Serial numbers can be supplied as + for getting numbers starting from Args: serials: input string with patterns @@ -428,17 +430,18 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): if '~' in serials: serials = serials.replace('~', str(next_number)) + # Split input string by whitespace or comma (,) characters groups = re.split("[\s,]+", serials) numbers = [] errors = [] - # helpers - def number_add(n): - if n in numbers: - errors.append(_('Duplicate serial: {n}').format(n=n)) + # Helper function to check for duplicated numbers + def add_sn(sn): + if sn in numbers: + errors.append(_('Duplicate serial: {sn}').format(n=n)) else: - numbers.append(n) + numbers.append(sn) try: expected_quantity = int(expected_quantity) @@ -466,7 +469,7 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): if a < b: for n in range(a, b + 1): - number_add(n) + add_sn(n) else: errors.append(_("Invalid group: {g}").format(g=group)) @@ -495,21 +498,16 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int): end = start + expected_quantity for n in range(start, end): - number_add(n) + add_sn(n) # no case else: errors.append(_("Invalid group: {g}").format(g=group)) - # Group should be a number + # At this point, we assume that the "group" is just a single serial value + # Note: At this point it is treated only as a string elif group: - # try conversion - try: - number = int(group) - except: - # seem like it is not a number - raise ValidationError(_(f"Invalid group {group}")) - number_add(number) + add_sn(group) # No valid input group detected else: From 615a954e094c748c4984d0c6d8429fc0ce2cf88e Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 23:31:23 +1100 Subject: [PATCH 28/41] Refactor UI for adding batch code and serial numbers --- InvenTree/templates/js/translated/forms.js | 2 +- InvenTree/templates/js/translated/helpers.js | 4 ++ InvenTree/templates/js/translated/order.js | 49 ++++++++++++-------- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index 63598a8676..b3f54033e0 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -1884,7 +1884,7 @@ function getFieldName(name, options={}) { * - Field description (help text) * - Field errors */ -function constructField(name, parameters, options) { +function constructField(name, parameters, options={}) { var html = ''; diff --git a/InvenTree/templates/js/translated/helpers.js b/InvenTree/templates/js/translated/helpers.js index 2d35513e78..1925cb47ac 100644 --- a/InvenTree/templates/js/translated/helpers.js +++ b/InvenTree/templates/js/translated/helpers.js @@ -116,6 +116,10 @@ function makeIconButton(icon, cls, pk, title, options={}) { extraProps += `disabled='true' `; } + if (options.collapseTarget) { + extraProps += `data-bs-toggle='collapse' href='#${options.collapseTarget}'`; + } + html += ``; diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 0ec50022aa..ab437df426 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -489,12 +489,6 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { `; - var prefix_buttons = toggle_batch; - - if (line_item.part_detail.trackable) { - prefix_buttons += toggle_serials; - } - // Quantity to Receive var quantity_input = constructField( `items_quantity_${pk}`, @@ -504,7 +498,6 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { value: quantity, title: '{% trans "Quantity to receive" %}', required: true, - prefixRaw: prefix_buttons, }, { hideLabels: true, @@ -516,13 +509,10 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { `items_batch_code_${pk}`, { type: 'string', - required: true, + required: false, label: '{% trans "Batch Code" %}', help_text: '{% trans "Enter batch code for incoming stock items" %}', prefixRaw: toggle_batch, - }, - { - hideLabels: true, } ); @@ -530,13 +520,10 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { `items_serial_numbers_${pk}`, { type: 'string', - required: true, + required: false, label: '{% trans "Serial Numbers" %}', help_text: '{% trans "Enter serial numbers for incoming stock items" %}', prefixRaw: toggle_serials, - }, - { - hideLabels: true } ); @@ -584,16 +571,38 @@ function receivePurchaseOrderItems(order_id, line_items, options={}) { ); // Button to remove the row - var delete_button = `
`; + var buttons = `
`; - delete_button += makeIconButton( + buttons += makeIconButton( + 'fa-layer-group', + 'button-row-add-batch', + pk, + '{% trans "Add batch code" %}', + { + collapseTarget: `div-batch-${pk}` + } + ); + + if (line_item.part_detail.trackable) { + buttons += makeIconButton( + 'fa-hashtag', + 'button-row-add-serials', + pk, + '{% trans "Add serial numbers" %}', + { + collapseTarget: `div-serials-${pk}`, + } + ); + } + + buttons += makeIconButton( 'fa-times icon-red', 'button-row-remove', pk, '{% trans "Remove row" %}', ); - delete_button += '
'; + buttons += '
'; var html = `
- ${delete_button} + ${buttons}
{% trans "Order Code" %} {% trans "Ordered" %} {% trans "Received" %}{% trans "Receive" %}{% trans "Quantity to Receive" %} {% trans "Status" %} {% trans "Destination" %}
+ +
+
+ {% include "filter_list.html" with id="salesorderallocation" %} +
+
+
@@ -342,7 +348,12 @@

{% trans "Build Order Allocations" %}

-
+
+
+ {% include "filter_list.html" with id="buildorderallocation" %} +
+
+
@@ -722,6 +733,7 @@ }); // Load the BOM table data in the pricing view + {% if part.has_bom and roles.sales_order.view %} loadBomTable($("#bom-pricing-table"), { editable: false, bom_url: "{% url 'api-bom-list' %}", @@ -729,6 +741,7 @@ parent_id: {{ part.id }} , sub_part_detail: true, }); + {% endif %} onPanelLoad("purchase-orders", function() { loadPartPurchaseOrderTable( diff --git a/InvenTree/templates/js/translated/tables.js b/InvenTree/templates/js/translated/tables.js index 4c9bec0476..c2418dbe78 100644 --- a/InvenTree/templates/js/translated/tables.js +++ b/InvenTree/templates/js/translated/tables.js @@ -278,7 +278,7 @@ $.fn.inventreeTable = function(options) { } }); } else { - console.log(`Could not get list of visible columns for column '${tableName}'`); + console.log(`Could not get list of visible columns for table '${tableName}'`); } } From 28a7ad7f0e285d57531c97c2532b0aabb004690d Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 1 Mar 2022 23:53:33 +1100 Subject: [PATCH 39/41] Bug fix for BuildOrder.bom_items - Now uses the query generator provided by the Part model - No more code duplication - More importantly, no more code duplication which is WRONG! --- InvenTree/build/models.py | 4 +--- InvenTree/part/models.py | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 095a8cf70c..01c2c781e9 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -383,9 +383,7 @@ class Build(MPTTModel, ReferenceIndexingMixin): Returns the BOM items for the part referenced by this BuildOrder """ - return self.part.bom_items.all().prefetch_related( - 'sub_part' - ) + return self.part.get_bom_items() @property def tracked_bom_items(self): diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 33ad8bf612..09e1f77542 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1453,7 +1453,9 @@ class Part(MPTTModel): By default, will include inherited BOM items """ - return BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited)) + queryset = BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited)) + + return queryset.prefetch_related('sub_part') def get_installed_part_options(self, include_inherited=True, include_variants=True): """ From aeb9dfe37179efc173206d9178fa1e1622f00282 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 1 Mar 2022 23:58:30 +1100 Subject: [PATCH 40/41] Allows deletion of serialized stock --- InvenTree/stock/models.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 64b47da6d3..14132d297b 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -909,7 +909,6 @@ class StockItem(MPTTModel): """ Can this stock item be deleted? It can NOT be deleted under the following circumstances: - Has installed stock items - - Has a serial number and is tracked - Is installed inside another StockItem - It has been assigned to a SalesOrder - It has been assigned to a BuildOrder @@ -918,9 +917,6 @@ class StockItem(MPTTModel): if self.installed_item_count() > 0: return False - if self.part.trackable and self.serial is not None: - return False - if self.sales_order is not None: return False From f6b3760bb5d5025f5a974ef9985f31c078035261 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 2 Mar 2022 00:05:02 +1100 Subject: [PATCH 41/41] Make UI elements more consistent Ref: https://github.com/inventree/InvenTree/issues/2692 --- InvenTree/part/templates/part/part_base.html | 4 ++-- InvenTree/stock/templates/stock/item_base.html | 2 +- InvenTree/templates/stock_table.html | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 823bb1a4f3..fe18c68a38 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -59,13 +59,13 @@