2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-05-03 13:58:47 +00:00

Merge pull request #2684 from SchrodingersGat/po-target-date

Order target date improvements
This commit is contained in:
Oliver 2022-03-01 08:17:54 +11:00 committed by GitHub
commit c4d462b0b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 178 additions and 15 deletions

View File

@ -12,11 +12,14 @@ import common.models
INVENTREE_SW_VERSION = "0.7.0 dev" INVENTREE_SW_VERSION = "0.7.0 dev"
# InvenTree API version # 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 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 v26 -> 2022-02-17
- Adds API endpoint for uploading a BOM file and extracting data - Adds API endpoint for uploading a BOM file and extracting data

View File

@ -274,7 +274,7 @@ class POLineItemFilter(rest_filters.FilterSet):
model = models.PurchaseOrderLineItem model = models.PurchaseOrderLineItem
fields = [ fields = [
'order', 'order',
'part' 'part',
] ]
pending = rest_filters.BooleanFilter(label='pending', method='filter_pending') pending = rest_filters.BooleanFilter(label='pending', method='filter_pending')
@ -391,6 +391,7 @@ class POLineItemList(generics.ListCreateAPIView):
'reference', 'reference',
'SKU', 'SKU',
'total_price', 'total_price',
'target_date',
] ]
search_fields = [ search_fields = [
@ -401,11 +402,6 @@ class POLineItemList(generics.ListCreateAPIView):
'reference', 'reference',
] ]
filter_fields = [
'order',
'part'
]
class POLineItemDetail(generics.RetrieveUpdateDestroyAPIView): class POLineItemDetail(generics.RetrieveUpdateDestroyAPIView):
""" """
@ -703,6 +699,7 @@ class SOLineItemList(generics.ListCreateAPIView):
'part__name', 'part__name',
'quantity', 'quantity',
'reference', 'reference',
'target_date',
] ]
search_fields = [ search_fields = [

View File

@ -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'),
),
]

View File

@ -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(),
),
]

View File

@ -816,9 +816,18 @@ class OrderLineItem(models.Model):
Attributes: Attributes:
quantity: Number of items quantity: Number of items
reference: Reference text (e.g. customer reference) for this line item
note: Annotation for the 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: class Meta:
abstract = True 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')) 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): class PurchaseOrderLineItem(OrderLineItem):
""" Model for a purchase order line item. """ Model for a purchase order line item.
@ -846,7 +861,6 @@ class PurchaseOrderLineItem(OrderLineItem):
class Meta: class Meta:
unique_together = ( unique_together = (
('order', 'part', 'quantity', 'purchase_price')
) )
@staticmethod @staticmethod

View File

@ -12,7 +12,7 @@ from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Case, When, Value 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 import serializers
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
@ -28,7 +28,7 @@ from InvenTree.serializers import InvenTreeModelSerializer
from InvenTree.serializers import InvenTreeDecimalField from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.serializers import InvenTreeMoneySerializer from InvenTree.serializers import InvenTreeMoneySerializer
from InvenTree.serializers import ReferenceIndexingSerializerMixin from InvenTree.serializers import ReferenceIndexingSerializerMixin
from InvenTree.status_codes import StockStatus from InvenTree.status_codes import StockStatus, PurchaseOrderStatus, SalesOrderStatus
import order.models import order.models
@ -128,6 +128,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
Add some extra annotations to this queryset: Add some extra annotations to this queryset:
- Total price = purchase_price * quantity - Total price = purchase_price * quantity
- "Overdue" status (boolean field)
""" """
queryset = queryset.annotate( 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 return queryset
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -157,6 +167,8 @@ class POLineItemSerializer(InvenTreeModelSerializer):
quantity = serializers.FloatField(default=1) quantity = serializers.FloatField(default=1)
received = serializers.FloatField(default=0) received = serializers.FloatField(default=0)
overdue = serializers.BooleanField(required=False, read_only=True)
total_price = serializers.FloatField(read_only=True) total_price = serializers.FloatField(read_only=True)
part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True) part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True)
@ -187,6 +199,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
'notes', 'notes',
'order', 'order',
'order_detail', 'order_detail',
'overdue',
'part', 'part',
'part_detail', 'part_detail',
'supplier_part_detail', 'supplier_part_detail',
@ -196,6 +209,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
'purchase_price_string', 'purchase_price_string',
'destination', 'destination',
'destination_detail', 'destination_detail',
'target_date',
'total_price', 'total_price',
] ]
@ -601,6 +615,23 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
class SOLineItemSerializer(InvenTreeModelSerializer): class SOLineItemSerializer(InvenTreeModelSerializer):
""" Serializer for a SalesOrderLineItem object """ """ 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): def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False) part_detail = kwargs.pop('part_detail', False)
@ -622,6 +653,8 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
part_detail = PartBriefSerializer(source='part', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True) allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
overdue = serializers.BooleanField(required=False, read_only=True)
quantity = InvenTreeDecimalField() quantity = InvenTreeDecimalField()
allocated = serializers.FloatField(source='allocated_quantity', read_only=True) allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
@ -651,12 +684,14 @@ class SOLineItemSerializer(InvenTreeModelSerializer):
'notes', 'notes',
'order', 'order',
'order_detail', 'order_detail',
'overdue',
'part', 'part',
'part_detail', 'part_detail',
'sale_price', 'sale_price',
'sale_price_currency', 'sale_price_currency',
'sale_price_string', 'sale_price_string',
'shipped', 'shipped',
'target_date',
] ]

View File

@ -174,6 +174,7 @@ $('#new-po-line').click(function() {
value: '{{ order.supplier.currency }}', value: '{{ order.supplier.currency }}',
{% endif %} {% endif %}
}, },
target_date: {},
destination: {}, destination: {},
notes: {}, notes: {},
}, },
@ -210,7 +211,7 @@ $('#new-po-line').click(function() {
loadPurchaseOrderLineItemTable('#po-line-table', { loadPurchaseOrderLineItemTable('#po-line-table', {
order: {{ order.pk }}, order: {{ order.pk }},
supplier: {{ order.supplier.pk }}, supplier: {{ order.supplier.pk }},
{% if order.status == PurchaseOrderStatus.PENDING %} {% if roles.purchase_order.change %}
allow_edit: true, allow_edit: true,
{% else %} {% else %}
allow_edit: false, allow_edit: false,

View File

@ -238,6 +238,7 @@
reference: {}, reference: {},
sale_price: {}, sale_price: {},
sale_price_currency: {}, sale_price_currency: {},
target_date: {},
notes: {}, notes: {},
}, },
method: 'POST', method: 'POST',

View File

@ -1005,6 +1005,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
reference: {}, reference: {},
purchase_price: {}, purchase_price: {},
purchase_price_currency: {}, purchase_price_currency: {},
target_date: {},
destination: {}, destination: {},
notes: {}, notes: {},
}, },
@ -1046,7 +1047,11 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
], ],
{ {
success: function() { success: function() {
// Reload the line item table
$(table).bootstrapTable('refresh'); $(table).bootstrapTable('refresh');
// Reload the "received stock" table
$('#stock-table').bootstrapTable('refresh');
} }
} }
); );
@ -1186,6 +1191,28 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
return formatter.format(total); 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 += `<span class='fas fa-calendar-alt icon-red float-right' title='{% trans "This line item is overdue" %}'></span>`;
}
return html;
} else if (row.order_detail && row.order_detail.target_date) {
return `<em>${row.order_detail.target_date}</em>`;
} else {
return '-';
}
}
},
{ {
sortable: false, sortable: false,
field: 'received', field: 'received',
@ -1232,15 +1259,15 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
var pk = row.pk; 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) { if (options.allow_edit) {
html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}'); 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" %}'); 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 += `</div>`; html += `</div>`;
return html; return html;
@ -2283,6 +2310,28 @@ function loadSalesOrderLineItemTable(table, options={}) {
return formatter.format(total); 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 += `<span class='fas fa-calendar-alt icon-red float-right' title='{% trans "This line item is overdue" %}'></span>`;
}
return html;
} else if (row.order_detail && row.order_detail.target_date) {
return `<em>${row.order_detail.target_date}</em>`;
} else {
return '-';
}
}
}
]; ];
if (pending) { if (pending) {
@ -2426,6 +2475,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
reference: {}, reference: {},
sale_price: {}, sale_price: {},
sale_price_currency: {}, sale_price_currency: {},
target_date: {},
notes: {}, notes: {},
}, },
title: '{% trans "Edit Line Item" %}', title: '{% trans "Edit Line Item" %}',

View File

@ -905,6 +905,28 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) {
field: 'quantity', field: 'quantity',
title: '{% trans "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 += `<span class='fas fa-calendar-alt icon-red float-right' title='{% trans "This line item is overdue" %}'></span>`;
}
return html;
} else if (row.order_detail && row.order_detail.target_date) {
return `<em>${row.order_detail.target_date}</em>`;
} else {
return '-';
}
}
},
{ {
field: 'received', field: 'received',
title: '{% trans "Received" %}', title: '{% trans "Received" %}',