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:
commit
c4d462b0b4
@ -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
|
||||||
|
|
||||||
|
@ -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 = [
|
||||||
|
23
InvenTree/order/migrations/0062_auto_20220228_0321.py
Normal file
23
InvenTree/order/migrations/0062_auto_20220228_0321.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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(),
|
||||||
|
),
|
||||||
|
]
|
@ -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
|
||||||
|
@ -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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -238,6 +238,7 @@
|
|||||||
reference: {},
|
reference: {},
|
||||||
sale_price: {},
|
sale_price: {},
|
||||||
sale_price_currency: {},
|
sale_price_currency: {},
|
||||||
|
target_date: {},
|
||||||
notes: {},
|
notes: {},
|
||||||
},
|
},
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -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" %}',
|
||||||
|
@ -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" %}',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user