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/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/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_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 ab437df426..4a71c7d7f9 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -1005,6 +1005,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) { reference: {}, purchase_price: {}, purchase_price_currency: {}, + target_date: {}, destination: {}, notes: {}, }, @@ -1046,7 +1047,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 +1191,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', @@ -1232,15 +1259,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; @@ -2283,6 +2310,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) { @@ -2426,6 +2475,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" %}',