From 43ee4e390ad1a5c46c13ea2fab8aeb97aefaa734 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 3 Dec 2021 20:53:59 +1100 Subject: [PATCH 1/4] API updates - Allow filtering of POLineItem list endpoint by base part instance - Include "order detail" in POLineItem serializer --- InvenTree/InvenTree/version.py | 6 +++++- InvenTree/order/api.py | 24 +++++++++++++++++++++++- InvenTree/order/serializers.py | 8 ++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index bbf0174453..15049a5456 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -12,11 +12,15 @@ import common.models INVENTREE_SW_VERSION = "0.6.0 dev" # InvenTree API version -INVENTREE_API_VERSION = 19 +INVENTREE_API_VERSION = 20 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v20 -> 2021-12-03 + - Adds ability to filter POLineItem endpoint by "base_part" + - Adds optional "order_detail" to POLineItem list endpoint + v19 -> 2021-12-02 - Adds the ability to filter the StockItem API by "part_tree" - Returns only stock items which match a particular part.tree_id field diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index dc5f5b87f4..bdeb8ee4f6 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -13,7 +13,6 @@ from rest_framework import generics from rest_framework import filters, status from rest_framework.response import Response - from InvenTree.filters import InvenTreeOrderingFilter from InvenTree.helpers import str2bool from InvenTree.api import AttachmentMixin @@ -300,6 +299,7 @@ class POLineItemList(generics.ListCreateAPIView): try: kwargs['part_detail'] = str2bool(self.request.query_params.get('part_detail', False)) + kwargs['order_detail'] = str2bool(self.request.query_params.get('order_detail', False)) except AttributeError: pass @@ -307,6 +307,28 @@ class POLineItemList(generics.ListCreateAPIView): return self.serializer_class(*args, **kwargs) + def filter_queryset(self, queryset): + """ + Additional filtering options + """ + + params = self.request.query_params + + queryset = super().filter_queryset(queryset) + + base_part = params.get('base_part', None) + + if base_part: + try: + base_part = Part.objects.get(pk=base_part) + + queryset = queryset.filter(part__part=base_part) + + except (ValueError, Part.DoesNotExist): + pass + + return queryset + filter_backends = [ rest_filters.DjangoFilterBackend, filters.SearchFilter, diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 38a058ae6d..060fd9ff1f 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -143,12 +143,17 @@ class POLineItemSerializer(InvenTreeModelSerializer): part_detail = kwargs.pop('part_detail', False) + order_detail = kwargs.pop('order_detail', False) + super().__init__(*args, **kwargs) if part_detail is not True: self.fields.pop('part_detail') self.fields.pop('supplier_part_detail') + if order_detail is not True: + self.fields.pop('order_detail') + quantity = serializers.FloatField(default=1) received = serializers.FloatField(default=0) @@ -170,6 +175,8 @@ class POLineItemSerializer(InvenTreeModelSerializer): help_text=_('Purchase price currency'), ) + order_detail = POSerializer(source='order', read_only=True, many=False) + class Meta: model = PurchaseOrderLineItem @@ -179,6 +186,7 @@ class POLineItemSerializer(InvenTreeModelSerializer): 'reference', 'notes', 'order', + 'order_detail', 'part', 'part_detail', 'supplier_part_detail', From 6d90ded27f7984df1c183a43b4af6f7e09358166 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 3 Dec 2021 20:54:21 +1100 Subject: [PATCH 2/4] First pass at a part-purchase-order table --- InvenTree/part/templates/part/detail.html | 12 +- InvenTree/templates/js/translated/order.js | 15 ++- InvenTree/templates/js/translated/part.js | 130 +++++++++++++++++++++ 3 files changed, 142 insertions(+), 15 deletions(-) diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index ce78e445e1..de1d46596a 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -73,7 +73,7 @@
- {% include "filter_list.html" with id="purchaseorder" %} + {% include "filter_list.html" with id="partpurchaseorders" %}
@@ -703,12 +703,10 @@ }); onPanelLoad("purchase-orders", function() { - loadPurchaseOrderTable($("#purchase-order-table"), { - url: "{% url 'api-po-list' %}", - params: { - part: {{ part.id }}, - }, - }); + loadPartPurchaseOrderTable( + "#purchase-order-table", + {{ part.pk }}, + ); }); onPanelLoad("sales-orders", function() { diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index a471182399..b75ad8e42d 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -648,6 +648,13 @@ function loadPurchaseOrderTable(table, options) { var html = renderLink(value, `/order/purchase-order/${row.pk}/`); + html += purchaseOrderStatusDisplay( + row.status, + { + classes: 'float-right', + } + ); + if (row.overdue) { html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Order is overdue" %}'); } @@ -672,14 +679,6 @@ function loadPurchaseOrderTable(table, options) { field: 'description', title: '{% trans "Description" %}', }, - { - field: 'status', - title: '{% trans "Status" %}', - sortable: true, - formatter: function(value, row) { - return purchaseOrderStatusDisplay(row.status, row.status_text); - } - }, { field: 'creation_date', title: '{% trans "Date" %}', diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 0a27f2ba2f..011cde0def 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -29,6 +29,7 @@ loadParametricPartTable, loadPartCategoryTable, loadPartParameterTable, + loadPartPurchaseOrderTable, loadPartTable, loadPartTestTemplateTable, loadPartVariantTable, @@ -712,6 +713,135 @@ function loadPartParameterTable(table, url, options) { } +/* + * Construct a table showing a list of purchase orders for a given part. + * + * This requests API data from the PurchaseOrderLineItem endpoint + */ +function loadPartPurchaseOrderTable(table, part_id, options={}) { + + options.params = options.params || {}; + + // Construct API filterset + options.params.base_part = part_id; + options.params.part_detail = true; + options.params.order_detail = true; + + var filters = loadTableFilters('partpurchaseorders'); + + for (var key in options.params) { + filters[key] = options.params[key]; + } + + setupFilterList('partpurchaseorders', $(table)); + + $(table).inventreeTable({ + url: '{% url "api-po-line-list" %}', + queryParams: filters, + name: 'partpurchaseorders', + original: options.params, + showColumns: true, + formatNoMatches: function() { + return '{% trans "No purchase orders found" %}'; + }, + columns: [ + { + field: 'order', + title: '{% trans "Purchase Order" %}', + switchable: false, + formatter: function(value, row) { + var order = row.order_detail; + + if (!order) { + return '-'; + } + + var ref = global_settings.PURCHASEORDER_REFERENCE_PREFIX + order.reference; + + var html = renderLink(ref, `/order/po/${order.pk}/`); + + html += purchaseOrderStatusDisplay( + order.status, + { + classes: 'float-right', + } + ); + + return html; + }, + }, + { + field: 'supplier', + title: '{% trans "Supplier" %}', + switchable: true, + formatter: function(value, row) { + + if (row.supplier_part_detail && row.supplier_part_detail.supplier_detail) { + var supp = row.supplier_part_detail.supplier_detail; + var html = imageHoverIcon(supp.thumbnail || supp.image); + + html += ' ' + renderLink(supp.name, `/company/${supp.pk}/`); + + return html; + } else { + return '-'; + } + } + }, + { + field: 'sku', + title: '{% trans "SKU" %}', + switchable: true, + formatter: function(value, row) { + if (row.supplier_part_detail) { + var supp = row.supplier_part_detail; + + return renderLink(supp.SKU, `/supplier-part/${supp.pk}/`); + } else { + return '-'; + } + }, + }, + { + field: 'mpn', + title: '{% trans "MPN" %}', + switchable: true, + formatter: function(value, row) { + if (row.supplier_part_detail && row.supplier_part_detail.manufacturer_part_detail) { + var manu = row.supplier_part_detail.manufacturer_part_detail; + return renderLink(manu.MPN, `/manufacturer-part/${manu.pk}/`); + } + } + }, + { + field: 'quantity', + title: '{% trans "Quantity" %}', + }, + { + field: 'received', + title: '{% trans "Received" %}', + switchable: true, + }, + { + field: 'purchase_price', + title: '{% trans "Price" %}', + formatter: function(value, row) { + var formatter = new Intl.NumberFormat( + 'en-US', + { + style: 'currency', + currency: row.purchase_price_currency, + } + ); + + return formatter.format(row.purchase_price); + } + } + ] + }); +} + + function loadRelatedPartsTable(table, part_id, options={}) { /* * Load table of "related" parts From acfafe22c5988aa2de96ca230b3274a878416aca Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 3 Dec 2021 21:07:09 +1100 Subject: [PATCH 3/4] order lines can be received directly from the new table --- InvenTree/templates/js/translated/order.js | 2 +- InvenTree/templates/js/translated/part.js | 51 ++++++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index b75ad8e42d..d565a6f1c3 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -988,7 +988,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) { field: 'buttons', title: '', formatter: function(value, row, index, field) { - var html = `
`; + var html = `
`; var pk = row.pk; diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index 011cde0def..d6410f57aa 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -733,7 +733,7 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) { filters[key] = options.params[key]; } - setupFilterList('partpurchaseorders', $(table)); + setupFilterList('purchaseorderlineitem', $(table), '#filter-list-partpurchaseorders'); $(table).inventreeTable({ url: '{% url "api-po-line-list" %}', @@ -741,9 +741,34 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) { name: 'partpurchaseorders', original: options.params, showColumns: true, + uniqueId: 'pk', formatNoMatches: function() { return '{% trans "No purchase orders found" %}'; }, + onPostBody: function() { + $(table).find('.button-line-receive').click(function() { + var pk = $(this).attr('pk'); + + var line_item = $(table).bootstrapTable('getRowByUniqueId', pk); + + if (!line_item) { + console.log('WARNING: getRowByUniqueId returned null'); + return; + } + + receivePurchaseOrderItems( + line_item.order, + [ + line_item, + ], + { + success: function() { + $(table).bootstrapTable('refresh'); + } + } + ); + }) + }, columns: [ { field: 'order', @@ -758,7 +783,7 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) { var ref = global_settings.PURCHASEORDER_REFERENCE_PREFIX + order.reference; - var html = renderLink(ref, `/order/po/${order.pk}/`); + var html = renderLink(ref, `/order/purchase-order/${order.pk}/`); html += purchaseOrderStatusDisplay( order.status, @@ -825,6 +850,7 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) { { field: 'purchase_price', title: '{% trans "Price" %}', + switchable: true, formatter: function(value, row) { var formatter = new Intl.NumberFormat( 'en-US', @@ -836,8 +862,27 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) { return formatter.format(row.purchase_price); } + }, + { + field: 'actions', + title: '', + formatter: function(value, row) { + + if (row.received >= row.quantity) { + // Already recevied + return `{% trans "Received" %}`; + } else { + var html = `
`; + var pk = row.pk; + + html += makeIconButton('fa-sign-in-alt', 'button-line-receive', pk, '{% trans "Receive line item" %}'); + + html += `
`; + return html; + } + } } - ] + ], }); } From f6e1de42f55df98c56f5e7338ac8b4618881e5a6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 3 Dec 2021 21:16:58 +1100 Subject: [PATCH 4/4] JS linting --- InvenTree/templates/js/translated/part.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index d6410f57aa..c2e4bb15a2 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -767,7 +767,7 @@ function loadPartPurchaseOrderTable(table, part_id, options={}) { } } ); - }) + }); }, columns: [ {