diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index d820dfb716..279225355d 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -311,6 +311,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..cf996547df 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -4,7 +4,6 @@ Top-level URL lookup for InvenTree application. Passes URL lookup downstream to each app as required. """ - from django.conf.urls import url, include from django.urls import path from django.contrib import admin @@ -169,9 +168,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/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/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/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/common/models.py b/InvenTree/common/models.py index 9ef5a4d0c3..fe6ddecbba 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,33 @@ 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) is not 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 +1239,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/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/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/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 7f5c6b8164..f08880a882 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -816,9 +816,18 @@ 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. + """ """ + 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 @@ -835,6 +844,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. @@ -846,7 +861,6 @@ class PurchaseOrderLineItem(OrderLineItem): class Meta: unique_together = ( - ('order', 'part', 'quantity', 'purchase_price') ) @staticmethod diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index cf8e701c68..2f4c1ea5df 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -12,7 +12,7 @@ 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 @@ -28,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 @@ -128,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( @@ -137,6 +138,15 @@ class POLineItemSerializer(InvenTreeModelSerializer): ) ) + 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): @@ -157,6 +167,8 @@ class POLineItemSerializer(InvenTreeModelSerializer): quantity = serializers.FloatField(default=1) received = serializers.FloatField(default=0) + overdue = serializers.BooleanField(required=False, read_only=True) + total_price = serializers.FloatField(read_only=True) part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True) @@ -187,6 +199,7 @@ class POLineItemSerializer(InvenTreeModelSerializer): 'notes', 'order', 'order_detail', + 'overdue', 'part', 'part_detail', 'supplier_part_detail', @@ -196,6 +209,7 @@ class POLineItemSerializer(InvenTreeModelSerializer): 'purchase_price_string', 'destination', 'destination_detail', + 'target_date', 'total_price', ] @@ -601,6 +615,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) @@ -622,6 +653,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) @@ -651,12 +684,14 @@ class SOLineItemSerializer(InvenTreeModelSerializer): 'notes', 'order', 'order_detail', + 'overdue', 'part', 'part_detail', 'sale_price', 'sale_price_currency', 'sale_price_string', 'shipped', + 'target_date', ] 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/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html index d0215777bb..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: {}, }, @@ -210,7 +211,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/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/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 @@
{% 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/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/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): """ diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index 2266b39048..a6cfda757f 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -122,7 +122,13 @@

{% trans "Sales Order Allocations" %}

-
+ +
+
+ {% 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( @@ -952,7 +965,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 +1078,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..fe18c68a38 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -59,13 +59,13 @@
{% trans "Creation Date" %} - {{ part.creation_date }} + {% render_date part.creation_date %} {% if part.creation_user %} {{ part.creation_user }} {% endif %} diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 627b925e23..7f0d7ff3cc 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,52 @@ 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 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) + + # 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 """ 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/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 diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index c52d101afb..ef71cfc05a 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -66,7 +66,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" %}
{% 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/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/js/dynamic/calendar.js b/InvenTree/templates/js/dynamic/calendar.js index 268337bd52..0b0ccc2915 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,33 @@ 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, options={}) { + + if (!date) { + return null; + } + + var fmt = user_settings.DATE_DISPLAY_FORMAT || 'YYYY-MM-DD'; + + if (options.showTime) { + fmt += ' HH:mm'; + } + + var m = moment(date); + + if (m.isValid()) { + return m.format(fmt); + } else { + // Invalid input string, simply return provided value + return date; + } +} 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 { 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 ab437df426..5f825c6738 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -923,11 +923,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', @@ -1005,6 +1011,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) { reference: {}, purchase_price: {}, purchase_price_currency: {}, + target_date: {}, destination: {}, notes: {}, }, @@ -1046,7 +1053,11 @@ function loadPurchaseOrderLineItemTable(table, options={}) { ], { success: function() { + // Reload the line item table $(table).bootstrapTable('refresh'); + + // Reload the "received stock" table + $('#stock-table').bootstrapTable('refresh'); } } ); @@ -1186,6 +1197,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 = renderDate(row.target_date); + + if (row.overdue) { + html += ``; + } + + return html; + + } else if (row.order_detail && row.order_detail.target_date) { + return `${renderDate(row.order_detail.target_date)}`; + } else { + return '-'; + } + } + }, { sortable: false, field: 'received', @@ -1232,15 +1265,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; @@ -1344,16 +1377,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, @@ -1505,9 +1547,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" %}'; } } }, @@ -2283,6 +2325,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 = renderDate(row.target_date); + + if (row.overdue) { + html += ``; + } + + return html; + + } else if (row.order_detail && row.order_detail.target_date) { + return `${renderDate(row.order_detail.target_date)}`; + } else { + return '-'; + } + } + } ]; if (pending) { @@ -2426,6 +2490,7 @@ function loadSalesOrderLineItemTable(table, options={}) { reference: {}, sale_price: {}, sale_price_currency: {}, + target_date: {}, notes: {}, }, title: '{% trans "Edit Line Item" %}', 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" %}', diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index 2d84f11e4a..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, @@ -1820,6 +1819,9 @@ function loadStockTable(table, options) { col = { field: 'stocktake_date', title: '{% trans "Stocktake" %}', + formatter: function(value) { + return renderDate(value); + } }; if (!options.params.ordering) { @@ -1833,6 +1835,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 +1849,9 @@ function loadStockTable(table, options) { col = { field: 'updated', title: '{% trans "Last Updated" %}', + formatter: function(value) { + return renderDate(value); + } }; if (!options.params.ordering) { @@ -2649,14 +2657,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, {showTime: true}); } }); 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}'`); } } 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 "Commit Date" %}{{ commit_date }}{% include "clip.html" %}{% trans "Commit Date" %}{% render_date commit_date %}{% include "clip.html" %}