diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 2f27dd602d..fa1d4dc5c9 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -153,11 +153,13 @@ class InvenTreeSetting(models.Model): 'SALESORDER_REFERENCE_PREFIX': { 'name': _('Sales Order Reference Prefix'), 'description': _('Prefix value for sales order reference'), + 'default': 'SO', }, 'PURCHASEORDER_REFERENCE_PREFIX': { 'name': _('Purchase Order Reference Prefix'), 'description': _('Prefix value for purchase order reference'), + 'default': 'PO', }, } diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 4a9dbfa2ac..6b8a3c81e0 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -266,6 +266,17 @@ class SOList(generics.ListCreateAPIView): else: queryset = queryset.exclude(status__in=SalesOrderStatus.OPEN) + # Filter by 'overdue' status + overdue = params.get('overdue', None) + + if overdue is not None: + overdue = str2bool(overdue) + + if overdue: + queryset = queryset.filter(SalesOrder.OVERDUE_FILTER) + else: + queryset = queryset.exclude(SalesOrder.OVERDUE_FILTER) + status = params.get('status', None) if status is not None: diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index aa0c897c8a..d797a4e42d 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -128,6 +128,13 @@ class EditSalesOrderForm(HelperForm): super().__init__(*args, **kwargs) + # TODO: Improve this using a better date picker + target_date = forms.DateField( + widget=forms.DateInput( + attrs={'type': 'date'}, + ) + ) + class Meta: model = SalesOrder fields = [ @@ -135,6 +142,7 @@ class EditSalesOrderForm(HelperForm): 'customer', 'customer_reference', 'description', + 'target_date', 'link' ] diff --git a/InvenTree/order/migrations/0040_salesorder_target_date.py b/InvenTree/order/migrations/0040_salesorder_target_date.py new file mode 100644 index 0000000000..29a90a6ad5 --- /dev/null +++ b/InvenTree/order/migrations/0040_salesorder_target_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.7 on 2020-12-18 01:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0039_auto_20201112_2203'), + ] + + operations = [ + migrations.AddField( + model_name='salesorder', + name='target_date', + field=models.DateField(blank=True, help_text='Target date for order completion. Order will be overdue after this date.', null=True, verbose_name='Target completion date'), + ), + ] diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index b2eb59c52e..9045ab86d7 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -9,7 +9,7 @@ from datetime import datetime from decimal import Decimal from django.db import models, transaction -from django.db.models import F, Sum +from django.db.models import Q, F, Sum from django.db.models.functions import Coalesce from django.core.validators import MinValueValidator from django.core.exceptions import ValidationError @@ -26,7 +26,7 @@ from stock import models as stock_models from company.models import Company, SupplierPart from InvenTree.fields import RoundingDecimalField -from InvenTree.helpers import decimal2string, increment +from InvenTree.helpers import decimal2string, increment, getSetting from InvenTree.status_codes import PurchaseOrderStatus, SalesOrderStatus, StockStatus from InvenTree.models import InvenTreeAttachment @@ -49,8 +49,6 @@ class Order(models.Model): """ - ORDER_PREFIX = "" - @classmethod def getNextOrderNumber(cls): """ @@ -88,16 +86,6 @@ class Order(models.Model): return new_ref - def __str__(self): - el = [] - - if self.ORDER_PREFIX: - el.append(self.ORDER_PREFIX) - - el.append(self.reference) - - return " ".join(el) - def save(self, *args, **kwargs): if not self.creation_date: self.creation_date = datetime.now().date() @@ -132,11 +120,12 @@ class PurchaseOrder(Order): supplier_reference: Optional field for supplier order reference code received_by: User that received the goods """ - - ORDER_PREFIX = "PO" def __str__(self): - return "PO {ref} - {company}".format(ref=self.reference, company=self.supplier.name) + + prefix = getSetting('PURCHASEORDER_REFERENCE_PREFIX') + + return f"{prefix}{self.reference} - {self.supplier.name}" status = models.PositiveIntegerField(default=PurchaseOrderStatus.PENDING, choices=PurchaseOrderStatus.items(), help_text=_('Purchase order status')) @@ -307,10 +296,16 @@ class SalesOrder(Order): Attributes: customer: Reference to the company receiving the goods in the order customer_reference: Optional field for customer order reference code + target_date: Target date for SalesOrder completion (optional) """ + OVERDUE_FILTER = Q(status__in=SalesOrderStatus.OPEN) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date()) + def __str__(self): - return "SO {ref} - {company}".format(ref=self.reference, company=self.customer.name) + + prefix = getSetting('SALESORDER_REFERENCE_PREFIX') + + return f"{prefix}{self.reference} - {self.customer.name}" def get_absolute_url(self): return reverse('so-detail', kwargs={'pk': self.id}) @@ -329,6 +324,12 @@ class SalesOrder(Order): customer_reference = models.CharField(max_length=64, blank=True, help_text=_("Customer order reference code")) + target_date = models.DateField( + null=True, blank=True, + verbose_name=_('Target completion date'), + help_text=_('Target date for order completion. Order will be overdue after this date.') + ) + shipment_date = models.DateField(blank=True, null=True) shipped_by = models.ForeignKey( @@ -338,6 +339,23 @@ class SalesOrder(Order): related_name='+' ) + @property + def is_overdue(self): + """ + Returns true if this SalesOrder is "overdue": + + - Not completed + - Target date is "in the past" + """ + + # Order cannot be deemed overdue if target_date is not set + if self.target_date is None: + return False + + today = datetime.now().date() + + return self.is_pending and self.target_date < today + @property def is_pending(self): return self.status == SalesOrderStatus.PENDING diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index c91ec6e02d..f0505728f6 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -9,6 +9,9 @@ from rest_framework import serializers from sql_util.utils import SubqueryCount +from django.db.models import Case, When, Value +from django.db.models import BooleanField + from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeAttachmentSerializerField @@ -152,12 +155,24 @@ class SalesOrderSerializer(InvenTreeModelSerializer): def annotate_queryset(queryset): """ Add extra information to the queryset + + - Number of line items in the SalesOrder + - Overdue status of the SalesOrder """ queryset = queryset.annotate( line_items=SubqueryCount('lines') ) + queryset = queryset.annotate( + overdue=Case( + When( + SalesOrder.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()), + ), + default=Value(False, output_field=BooleanField()) + ) + ) + return queryset customer_detail = CompanyBriefSerializer(source='customer', many=False, read_only=True) @@ -166,24 +181,27 @@ class SalesOrderSerializer(InvenTreeModelSerializer): status_text = serializers.CharField(source='get_status_display', read_only=True) + overdue = serializers.BooleanField() + class Meta: model = SalesOrder fields = [ 'pk', - 'shipment_date', 'creation_date', - 'description', - 'line_items', - 'link', - 'reference', 'customer', 'customer_detail', 'customer_reference', + 'description', + 'line_items', + 'link', + 'notes', + 'overdue', + 'reference', 'status', 'status_text', 'shipment_date', - 'notes', + 'target_date', ] read_only_fields = [ diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 59ff0eeda8..378020850b 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -37,6 +37,9 @@ src="{% static 'img/blank_image.png' %}"

{% sales_order_status_label order.status large=True %} + {% if order.is_overdue %} + {% trans "Overdue" %} + {% endif %}


{{ order.description }}

@@ -74,7 +77,12 @@ src="{% static 'img/blank_image.png' %}" {% trans "Order Status" %} - {% sales_order_status_label order.status %} + + {% sales_order_status_label order.status %} + {% if order.is_overdue %} + {% trans "Overdue" %} + {% endif %} + @@ -100,6 +108,13 @@ src="{% static 'img/blank_image.png' %}" {% trans "Created" %} {{ order.creation_date }}{{ order.created_by }} + {% if order.target_date %} + + + {% trans "Target Date" %} + {{ order.target_date }} + + {% endif %} {% if order.shipment_date %} diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py index 7b118dc60e..47dfe63d0f 100644 --- a/InvenTree/order/tests.py +++ b/InvenTree/order/tests.py @@ -31,11 +31,11 @@ class OrderTest(TestCase): self.assertEqual(order.get_absolute_url(), '/order/purchase-order/1/') - self.assertEqual(str(order), 'PO 0001 - ACME') + self.assertEqual(str(order), 'PO0001 - ACME') line = PurchaseOrderLineItem.objects.get(pk=1) - self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO 0001 - ACME)") + self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO0001 - ACME)") def test_increment(self): diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 9bf7294335..b479575a35 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -130,7 +130,7 @@ class CategoryParameters(generics.ListAPIView): """ try: - cat_id = int(self.request.query_params.get('category', None)) + cat_id = int(self.kwargs.get('pk', None)) except TypeError: cat_id = None fetch_parent = str2bool(self.request.query_params.get('fetch_parent', 'true')) @@ -910,8 +910,8 @@ part_api_urls = [ # Base URL for PartCategory API endpoints url(r'^category/', include([ + url(r'^(?P\d+)/parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'), url(r'^(?P\d+)/?', CategoryDetail.as_view(), name='api-part-category-detail'), - url(r'^parameters/?', CategoryParameters.as_view(), name='api-part-category-parameters'), url(r'^$', CategoryList.as_view(), name='api-part-category-list'), ])), diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index ea26e5e5c7..cf0c92899a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -594,7 +594,7 @@ class Part(MPTTModel): # User can decide whether duplicate IPN (Internal Part Number) values are allowed allow_duplicate_ipn = common.models.InvenTreeSetting.get_setting('PART_ALLOW_DUPLICATE_IPN') - if not allow_duplicate_ipn: + if self.IPN is not None and not allow_duplicate_ipn: parts = Part.objects.filter(IPN__iexact=self.IPN) parts = parts.exclude(pk=self.pk) diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 5e9d26c5cf..74e93fecc0 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -114,7 +114,7 @@ InvenTree | {% trans "Stock Item" %} - {{ item }} {% if roles.stock.change and not item.is_building %}
- +
@@ -112,6 +113,14 @@ loadSalesOrderTable("#so-outstanding-table", { }, }); +loadSalesOrderTable("#so-overdue-table", { + url: "{% url 'api-so-list' %}", + params: { + overdue: true, + customer_detail: true, + } +}); + $("#latest-parts-table").on('load-success.bs.table', function() { var count = $("#latest-parts-table").bootstrapTable('getData').length; @@ -166,4 +175,10 @@ $("#so-outstanding-table").on('load-success.bs.table', function() { $("#so-outstanding-count").html(count); }); +$("#so-overdue-table").on('load-success.bs.table', function() { + var count = $("#so-overdue-table").bootstrapTable('getData').length; + + $("#so-overdue-count").html(count); +}); + {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/InvenTree/settings/category.html b/InvenTree/templates/InvenTree/settings/category.html index c84573e8b4..07dd0df342 100644 --- a/InvenTree/templates/InvenTree/settings/category.html +++ b/InvenTree/templates/InvenTree/settings/category.html @@ -45,7 +45,7 @@ {% if category %} $("#param-table").inventreeTable({ - url: "{% url 'api-part-category-parameters' category.pk %}", + url: "{% url 'api-part-category-parameters' pk=category.pk %}", queryParams: { ordering: 'name', }, @@ -58,7 +58,7 @@ switchable: false, }, { - field: 'parameter_template_detail.name', + field: 'parameter_template.name', title: '{% trans "Parameter Template" %}', sortable: 'true', }, diff --git a/InvenTree/templates/InvenTree/so_overdue.html b/InvenTree/templates/InvenTree/so_overdue.html new file mode 100644 index 0000000000..bf2b64a1e3 --- /dev/null +++ b/InvenTree/templates/InvenTree/so_overdue.html @@ -0,0 +1,15 @@ +{% extends "collapse_index.html" %} + +{% load i18n %} + +{% block collapse_title %} + +{% trans "Overdue Sales Orders" %} +{% endblock %} + +{% block collapse_content %} + + +
+ +{% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/js/order.js b/InvenTree/templates/js/order.js index 69d4f584d9..18c315d17e 100644 --- a/InvenTree/templates/js/order.js +++ b/InvenTree/templates/js/order.js @@ -235,7 +235,13 @@ function loadSalesOrderTable(table, options) { value = `${prefix}${value}`; } - return renderLink(value, `/order/sales-order/${row.pk}/`); + var html = renderLink(value, `/order/sales-order/${row.pk}/`); + + if (row.overdue) { + html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Order is overdue" %}'); + } + + return html; }, }, { @@ -269,6 +275,11 @@ function loadSalesOrderTable(table, options) { field: 'creation_date', title: '{% trans "Creation Date" %}', }, + { + sortable: true, + field: 'target_date', + title: '{% trans "Target Date" %}', + }, { sortable: true, field: 'shipment_date', diff --git a/InvenTree/templates/js/table_filters.js b/InvenTree/templates/js/table_filters.js index f1f5c12732..d2ea26d8c3 100644 --- a/InvenTree/templates/js/table_filters.js +++ b/InvenTree/templates/js/table_filters.js @@ -217,6 +217,10 @@ function getAvailableTableFilters(tableKey) { type: 'bool', title: '{% trans "Outstanding" %}', }, + overdue: { + type: 'bool', + title: '{% trans "Overdue" %}', + }, }; }