\d+)/$', SOLineItemDetail.as_view(), name='api-so-line-detail'),
+ url(r'^$', SOLineItemList.as_view(), name='api-so-line-list'),
+ ])),
+
+ # API endpoints for sales order allocations
+ url(r'^so-allocation', include([
+
+ # List all sales order allocations
+ url(r'^.*$', SOAllocationList.as_view(), name='api-so-allocation-list'),
+ ])),
]
diff --git a/InvenTree/order/fixtures/order.yaml b/InvenTree/order/fixtures/order.yaml
index 6b65c20786..769f7702e5 100644
--- a/InvenTree/order/fixtures/order.yaml
+++ b/InvenTree/order/fixtures/order.yaml
@@ -68,6 +68,7 @@
order: 1
part: 1
quantity: 100
+ destination: 5 # Desk/Drawer_1
# 250 x ACME0002 (M2x4 LPHS)
# Partially received (50)
@@ -95,3 +96,10 @@
part: 3
quantity: 100
+# 1 x R_4K7_0603
+- model: order.purchaseorderlineitem
+ pk: 23
+ fields:
+ order: 1
+ part: 5
+ quantity: 1
diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py
index ceb633688a..48b5245a5f 100644
--- a/InvenTree/order/forms.py
+++ b/InvenTree/order/forms.py
@@ -86,12 +86,17 @@ class ShipSalesOrderForm(HelperForm):
class ReceivePurchaseOrderForm(HelperForm):
- location = TreeNodeChoiceField(queryset=StockLocation.objects.all(), required=True, label=_('Location'), help_text=_('Receive parts to this location'))
+ location = TreeNodeChoiceField(
+ queryset=StockLocation.objects.all(),
+ required=True,
+ label=_("Destination"),
+ help_text=_("Receive parts to this location"),
+ )
class Meta:
model = PurchaseOrder
fields = [
- 'location',
+ "location",
]
@@ -202,6 +207,7 @@ class EditPurchaseOrderLineItemForm(HelperForm):
'quantity',
'reference',
'purchase_price',
+ 'destination',
'notes',
]
diff --git a/InvenTree/order/migrations/0046_purchaseorderlineitem_destination.py b/InvenTree/order/migrations/0046_purchaseorderlineitem_destination.py
new file mode 100644
index 0000000000..fa08c91e0d
--- /dev/null
+++ b/InvenTree/order/migrations/0046_purchaseorderlineitem_destination.py
@@ -0,0 +1,29 @@
+# Generated by Django 3.2 on 2021-05-13 22:38
+
+from django.db import migrations
+import django.db.models.deletion
+import mptt.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("stock", "0063_auto_20210511_2343"),
+ ("order", "0045_auto_20210504_1946"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="purchaseorderlineitem",
+ name="destination",
+ field=mptt.fields.TreeForeignKey(
+ blank=True,
+ help_text="Where does the Purchaser want this item to be stored?",
+ null=True,
+ on_delete=django.db.models.deletion.DO_NOTHING,
+ related_name="po_lines",
+ to="stock.stocklocation",
+ verbose_name="Destination",
+ ),
+ ),
+ ]
diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py
index 49504b6d89..c150331f94 100644
--- a/InvenTree/order/models.py
+++ b/InvenTree/order/models.py
@@ -20,6 +20,7 @@ from django.utils.translation import ugettext_lazy as _
from common.settings import currency_code_default
from markdownx.models import MarkdownxField
+from mptt.models import TreeForeignKey
from djmoney.models.fields import MoneyField
@@ -672,6 +673,29 @@ class PurchaseOrderLineItem(OrderLineItem):
help_text=_('Unit purchase price'),
)
+ destination = TreeForeignKey(
+ 'stock.StockLocation', on_delete=models.DO_NOTHING,
+ verbose_name=_('Destination'),
+ related_name='po_lines',
+ blank=True, null=True,
+ help_text=_('Where does the Purchaser want this item to be stored?')
+ )
+
+ def get_destination(self):
+ """Show where the line item is or should be placed"""
+ # NOTE: If a line item gets split when recieved, only an arbitrary
+ # stock items location will be reported as the location for the
+ # entire line.
+ for stock in stock_models.StockItem.objects.filter(
+ supplier_part=self.part, purchase_order=self.order
+ ):
+ if stock.location:
+ return stock.location
+ if self.destination:
+ return self.destination
+ if self.part and self.part.part and self.part.part.default_location:
+ return self.part.part.default_location
+
def remaining(self):
""" Calculate the number of items remaining to be received """
r = self.quantity - self.received
diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py
index 6091140313..9efbf947bb 100644
--- a/InvenTree/order/serializers.py
+++ b/InvenTree/order/serializers.py
@@ -17,6 +17,8 @@ from InvenTree.serializers import InvenTreeAttachmentSerializerField
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
from part.serializers import PartBriefSerializer
+from stock.serializers import LocationBriefSerializer
+from stock.serializers import StockItemSerializer, LocationSerializer
from .models import PurchaseOrder, PurchaseOrderLineItem
from .models import PurchaseOrderAttachment, SalesOrderAttachment
@@ -41,7 +43,7 @@ class POSerializer(InvenTreeModelSerializer):
"""
Add extra information to the queryset
- - Number of liens in the PurchaseOrder
+ - Number of lines in the PurchaseOrder
- Overdue status of the PurchaseOrder
"""
@@ -116,6 +118,8 @@ class POLineItemSerializer(InvenTreeModelSerializer):
purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
+ destination = LocationBriefSerializer(source='get_destination', read_only=True)
+
class Meta:
model = PurchaseOrderLineItem
@@ -132,6 +136,7 @@ class POLineItemSerializer(InvenTreeModelSerializer):
'purchase_price',
'purchase_price_currency',
'purchase_price_string',
+ 'destination',
]
@@ -232,11 +237,38 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
This includes some fields from the related model objects.
"""
- location_path = serializers.CharField(source='get_location_path')
- location_id = serializers.IntegerField(source='get_location')
- serial = serializers.CharField(source='get_serial')
- po = serializers.CharField(source='get_po')
- quantity = serializers.FloatField()
+ part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True)
+ order = serializers.PrimaryKeyRelatedField(source='line.order', many=False, read_only=True)
+ serial = serializers.CharField(source='get_serial', read_only=True)
+ quantity = serializers.FloatField(read_only=True)
+ location = serializers.PrimaryKeyRelatedField(source='item.location', many=False, read_only=True)
+
+ # Extra detail fields
+ order_detail = SalesOrderSerializer(source='line.order', many=False, read_only=True)
+ part_detail = PartBriefSerializer(source='item.part', many=False, read_only=True)
+ item_detail = StockItemSerializer(source='item', many=False, read_only=True)
+ location_detail = LocationSerializer(source='item.location', many=False, read_only=True)
+
+ def __init__(self, *args, **kwargs):
+
+ order_detail = kwargs.pop('order_detail', False)
+ part_detail = kwargs.pop('part_detail', False)
+ item_detail = kwargs.pop('item_detail', False)
+ location_detail = kwargs.pop('location_detail', False)
+
+ super().__init__(*args, **kwargs)
+
+ if not order_detail:
+ self.fields.pop('order_detail')
+
+ if not part_detail:
+ self.fields.pop('part_detail')
+
+ if not item_detail:
+ self.fields.pop('item_detail')
+
+ if not location_detail:
+ self.fields.pop('location_detail')
class Meta:
model = SalesOrderAllocation
@@ -246,10 +278,14 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
'line',
'serial',
'quantity',
- 'location_id',
- 'location_path',
- 'po',
+ 'location',
+ 'location_detail',
'item',
+ 'item_detail',
+ 'order',
+ 'order_detail',
+ 'part',
+ 'part_detail',
]
diff --git a/InvenTree/order/templates/order/purchase_order_detail.html b/InvenTree/order/templates/order/purchase_order_detail.html
index ef844409fd..0ad73923a2 100644
--- a/InvenTree/order/templates/order/purchase_order_detail.html
+++ b/InvenTree/order/templates/order/purchase_order_detail.html
@@ -117,6 +117,7 @@ $("#po-table").inventreeTable({
part_detail: true,
},
url: "{% url 'api-po-line-list' %}",
+ showFooter: true,
columns: [
{
field: 'pk',
@@ -137,6 +138,9 @@ $("#po-table").inventreeTable({
return '-';
}
},
+ footerFormatter: function() {
+ return '{% trans "Total" %}'
+ }
},
{
field: 'part_detail.description',
@@ -172,7 +176,14 @@ $("#po-table").inventreeTable({
{
sortable: true,
field: 'quantity',
- title: '{% trans "Quantity" %}'
+ title: '{% trans "Quantity" %}',
+ footerFormatter: function(data) {
+ return data.map(function (row) {
+ return +row['quantity']
+ }).reduce(function (sum, i) {
+ return sum + i
+ }, 0)
+ }
},
{
sortable: true,
@@ -182,6 +193,25 @@ $("#po-table").inventreeTable({
return row.purchase_price_string || row.purchase_price;
}
},
+ {
+ sortable: true,
+ title: '{% trans "Total price" %}',
+ formatter: function(value, row) {
+ var total = row.purchase_price * row.quantity;
+ var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.purchase_price_currency});
+ return formatter.format(total)
+ },
+ footerFormatter: function(data) {
+ var total = data.map(function (row) {
+ return +row['purchase_price']*row['quantity']
+ }).reduce(function (sum, i) {
+ return sum + i
+ }, 0)
+ var currency = (data.slice(-1)[0] && data.slice(-1)[0].purchase_price_currency) || 'USD';
+ var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: currency});
+ return formatter.format(total)
+ }
+ },
{
sortable: true,
field: 'received',
@@ -204,6 +234,10 @@ $("#po-table").inventreeTable({
return (progressA < progressB) ? 1 : -1;
}
},
+ {
+ field: 'destination.pathstring',
+ title: '{% trans "Destination" %}',
+ },
{
field: 'notes',
title: '{% trans "Notes" %}',
diff --git a/InvenTree/order/templates/order/receive_parts.html b/InvenTree/order/templates/order/receive_parts.html
index 35ce4b6513..bfddf01ce9 100644
--- a/InvenTree/order/templates/order/receive_parts.html
+++ b/InvenTree/order/templates/order/receive_parts.html
@@ -22,6 +22,7 @@
{% trans "Received" %} |
{% trans "Receive" %} |
{% trans "Status" %} |
+ {% trans "Destination" %} |
|
{% for line in lines %}
@@ -53,6 +54,9 @@
+
+ {{ line.get_destination }}
+ |
|