From 4ea4456517b607a8d38b36b75907819d0bdf1962 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Feb 2021 23:56:47 +1100 Subject: [PATCH 1/3] Add API LIST endpoint for SalesOrderAllocations --- InvenTree/order/api.py | 84 ++++++++++++++++--- InvenTree/order/serializers.py | 14 ++-- .../templates/order/sales_order_detail.html | 4 +- 3 files changed, 82 insertions(+), 20 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index ce75a47697..700b97f67a 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -22,10 +22,10 @@ from .models import PurchaseOrder, PurchaseOrderLineItem from .models import PurchaseOrderAttachment from .serializers import POSerializer, POLineItemSerializer, POAttachmentSerializer -from .models import SalesOrder, SalesOrderLineItem +from .models import SalesOrder, SalesOrderLineItem, SalesOrderAllocation from .models import SalesOrderAttachment from .serializers import SalesOrderSerializer, SOLineItemSerializer, SOAttachmentSerializer - +from .serializers import SalesOrderAllocationSerializer class POList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PurchaseOrder objects @@ -427,6 +427,56 @@ class SOLineItemDetail(generics.RetrieveUpdateAPIView): serializer_class = SOLineItemSerializer +class SOAllocationList(generics.ListCreateAPIView): + """ + API endpoint for listing SalesOrderAllocation objects + """ + + queryset = SalesOrderAllocation.objects.all() + serializer_class = SalesOrderAllocationSerializer + + def filter_queryset(self, queryset): + + queryset = super().filter_queryset(queryset) + + # Filter by order + params = self.request.query_params + + # Filter by "part" reference + part = params.get('part', None) + + if part is not None: + queryset = queryset.filter(item__part=part) + + # Filter by "order" reference + order = params.get('order', None) + + if order is not None: + queryset = queryset.filter(line__order=order) + + # Filter by "outstanding" order status + outstanding = params.get('outstanding', None) + + if outstanding is not None: + outstanding = str2bool(outstanding) + + if outstanding: + queryset = queryset.filter(line__order__status__in=SalesOrderStatus.OPEN) + else: + queryset = queryset.exclude(line__order__status__in=SalesOrderStatus.OPEN) + + return queryset + + filter_backends = [ + DjangoFilterBackend, + ] + + # Default filterable fields + filter_fields = [ + 'item', + ] + + class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin): """ API endpoint for listing (and creating) a PurchaseOrderAttachment (file upload) @@ -435,10 +485,6 @@ class POAttachmentList(generics.ListCreateAPIView, AttachmentMixin): queryset = PurchaseOrderAttachment.objects.all() serializer_class = POAttachmentSerializer - filter_fields = [ - 'order', - ] - order_api_urls = [ # API endpoints for purchase orders @@ -453,14 +499,26 @@ order_api_urls = [ url(r'^po-line/$', POLineItemList.as_view(), name='api-po-line-list'), # API endpoints for sales ordesr - url(r'^so/(?P\d+)/$', SODetail.as_view(), name='api-so-detail'), - url(r'so/attachment/', include([ - url(r'^.*$', SOAttachmentList.as_view(), name='api-so-attachment-list'), + url(r'^so/', include([ + url(r'^(?P\d+)/$', SODetail.as_view(), name='api-so-detail'), + url(r'attachment/', include([ + url(r'^.*$', SOAttachmentList.as_view(), name='api-so-attachment-list'), + ])), + + # List all sales orders + url(r'^.*$', SOList.as_view(), name='api-so-list'), ])), - url(r'^so/.*$', SOList.as_view(), name='api-so-list'), - # API endpoints for sales order line items - url(r'^so-line/(?P\d+)/$', SOLineItemDetail.as_view(), name='api-so-line-detail'), - url(r'^so-line/$', SOLineItemList.as_view(), name='api-so-line-list'), + url(r'^so-line/', include([ + url(r'^(?P\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/serializers.py b/InvenTree/order/serializers.py index a04798c303..51a0d6ebf0 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -232,10 +232,12 @@ 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') - quantity = serializers.FloatField() + location_path = serializers.CharField(source='get_location_path', read_only=True) + location = serializers.IntegerField(source='get_location', read_only=True) + 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) class Meta: model = SalesOrderAllocation @@ -245,7 +247,9 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): 'line', 'serial', 'quantity', - 'location_id', + 'order', + 'part', + 'location', 'location_path', 'item', ] diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 8e3128e1f3..d4a004a73c 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -78,10 +78,10 @@ function showAllocationSubTable(index, row, element) { }, }, { - field: 'location_id', + field: 'location', title: 'Location', formatter: function(value, row, index, field) { - return renderLink(row.location_path, `/stock/location/${row.location_id}/`); + return renderLink(row.location_path, `/stock/location/${row.location}/`); }, }, { From 77df82c46d15f8a59a5ea9a32195baf64787817c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 18 Feb 2021 00:07:44 +1100 Subject: [PATCH 2/3] Add optional detail elements to SOAllocation API --- InvenTree/order/api.py | 10 ++++++++++ InvenTree/order/serializers.py | 34 ++++++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 700b97f67a..1b376e9a32 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -435,6 +435,16 @@ class SOAllocationList(generics.ListCreateAPIView): queryset = SalesOrderAllocation.objects.all() serializer_class = SalesOrderAllocationSerializer + def get_serializer(self, *args, **kwargs): + + params = self.request.query_params + + kwargs['part_detail'] = str2bool(params.get('part_detail', False)) + kwargs['item_detail'] = str2bool(params.get('item_detail', False)) + kwargs['order_detail'] = str2bool(params.get('order_detail', False)) + + return self.serializer_class(*args, **kwargs) + def filter_queryset(self, queryset): queryset = super().filter_queryset(queryset) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 51a0d6ebf0..d9d6143253 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -17,6 +17,7 @@ from InvenTree.serializers import InvenTreeAttachmentSerializerField from company.serializers import CompanyBriefSerializer, SupplierPartSerializer from part.serializers import PartBriefSerializer +from stock.serializers import StockItemSerializer from .models import PurchaseOrder, PurchaseOrderLineItem from .models import PurchaseOrderAttachment, SalesOrderAttachment @@ -232,13 +233,33 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): This includes some fields from the related model objects. """ - location_path = serializers.CharField(source='get_location_path', read_only=True) - location = serializers.IntegerField(source='get_location', read_only=True) 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) + # 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) + + 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) + + 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') + class Meta: model = SalesOrderAllocation @@ -247,11 +268,12 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): 'line', 'serial', 'quantity', - 'order', - 'part', - 'location', - 'location_path', 'item', + 'item_detail', + 'order', + 'order_detail', + 'part', + 'part_detail', ] From 228349bea615ecc45eacc22bd3cc06df5dcf45bb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 18 Feb 2021 00:36:51 +1100 Subject: [PATCH 3/3] Add 'location_detail' filter --- InvenTree/order/api.py | 1 + InvenTree/order/serializers.py | 10 ++- InvenTree/part/templates/part/allocation.html | 10 +++ InvenTree/templates/js/order.js | 82 +++++++++++++++++++ 4 files changed, 102 insertions(+), 1 deletion(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 1b376e9a32..54a88f19be 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -442,6 +442,7 @@ class SOAllocationList(generics.ListCreateAPIView): kwargs['part_detail'] = str2bool(params.get('part_detail', False)) kwargs['item_detail'] = str2bool(params.get('item_detail', False)) kwargs['order_detail'] = str2bool(params.get('order_detail', False)) + kwargs['location_detail'] = str2bool(params.get('location_detail', False)) return self.serializer_class(*args, **kwargs) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index d9d6143253..aa6b05fe39 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -17,7 +17,7 @@ from InvenTree.serializers import InvenTreeAttachmentSerializerField from company.serializers import CompanyBriefSerializer, SupplierPartSerializer from part.serializers import PartBriefSerializer -from stock.serializers import StockItemSerializer +from stock.serializers import StockItemSerializer, LocationSerializer from .models import PurchaseOrder, PurchaseOrderLineItem from .models import PurchaseOrderAttachment, SalesOrderAttachment @@ -237,17 +237,20 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): 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) @@ -260,6 +263,9 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): if not item_detail: self.fields.pop('item_detail') + if not location_detail: + self.fields.pop('location_detail') + class Meta: model = SalesOrderAllocation @@ -268,6 +274,8 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): 'line', 'serial', 'quantity', + 'location', + 'location_detail', 'item', 'item_detail', 'order', diff --git a/InvenTree/part/templates/part/allocation.html b/InvenTree/part/templates/part/allocation.html index e574742ad5..93acf6ec4a 100644 --- a/InvenTree/part/templates/part/allocation.html +++ b/InvenTree/part/templates/part/allocation.html @@ -9,6 +9,10 @@

{% trans "Part Stock Allocations" %}

+
+ +
+ @@ -35,6 +39,12 @@ {% block js_ready %} + loadSalesOrderAllocationTable("#sales-order-table", { + params: { + part: {{ part.id }}, + } + }); + $("#build-table").inventreeTable({ columns: [ { diff --git a/InvenTree/templates/js/order.js b/InvenTree/templates/js/order.js index 53063cd709..fcaf54ef63 100644 --- a/InvenTree/templates/js/order.js +++ b/InvenTree/templates/js/order.js @@ -304,3 +304,85 @@ function loadSalesOrderTable(table, options) { ], }); } + + +function loadSalesOrderAllocationTable(table, options={}) { + /** + * Load a table with SalesOrderAllocation items + */ + + options.params = options.params || {}; + + options.params['location_detail'] = true; + options.params['part_detail'] = true; + options.params['item_detail'] = true; + options.params['order_detail'] = true; + + var filters = loadTableFilters("salesorderallocation"); + + for (var key in options.params) { + filters[key] = options.params[key]; + } + + setupFilterList("salesorderallocation", $(table)); + + $(table).inventreeTable({ + url: '{% url "api-so-allocation-list" %}', + queryParams: filters, + name: 'salesorderallocation', + groupBy: false, + original: options.params, + formatNoMatches: function() { return '{% trans "No sales order allocations found" %}'; }, + columns: [ + { + field: 'pk', + visible: false, + switchable: false, + }, + { + field: 'order', + title: '{% trans "Order" %}', + switchable: false, + formatter: function(value, row) { + + var prefix = "{% settings_value 'SALESORDER_REFERENCE_PREFIX' %}"; + + var ref = `${prefix}${row.order_detail.reference}`; + + return renderLink(ref, `/order/sales-order/${row.order}/`); + } + }, + { + field: 'item', + title: '{% trans "Stock Item" %}', + formatter: function(value, row) { + // Render a link to the particular stock item + + var link = `/stock/item/${row.item}/`; + var text = `{% trans "Stock Item" %} ${row.item}`; + + return renderLink(text, link); + } + }, + { + field: 'location', + title: '{% trans "Location" %}', + formatter: function(value, row) { + + if (!value) { + return '{% trans "Location not specified" %}'; + } + + var link = `/stock/location/${value}`; + var text = row.location_detail.description; + + return renderLink(text, link); + } + }, + { + field: 'quantity', + title: '{% trans "Quantity" %}', + } + ] + }); +} \ No newline at end of file
{% trans "Order" %}