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" %}',