diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index b5622d7ce8..a199a403ce 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -860,9 +860,17 @@ class SOAllocationList(generics.ListAPIView): outstanding = str2bool(outstanding) if outstanding: - queryset = queryset.filter(line__order__status__in=SalesOrderStatus.OPEN) + # Filter only "open" orders + # Filter only allocations which have *not* shipped + queryset = queryset.filter( + line__order__status__in=SalesOrderStatus.OPEN, + shipment__shipment_date=None, + ) else: - queryset = queryset.exclude(line__order__status__in=SalesOrderStatus.OPEN) + queryset = queryset.exclude( + line__order__status__in=SalesOrderStatus.OPEN, + shipment__shipment_date=None + ) return queryset @@ -1002,7 +1010,7 @@ order_api_urls = [ url(r'^.*$', POLineItemList.as_view(), name='api-po-line-list'), ])), - # API endpoints for sales ordesr + # API endpoints for sales orders url(r'^so/', include([ url(r'attachment/', include([ url(r'^(?P\d+)/$', SOAttachmentDetail.as_view(), name='api-so-attachment-detail'), diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 4653e41a61..b3d975513f 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -417,7 +417,7 @@ class Part(MPTTModel): context['allocated_build_order_quantity'] = self.build_order_allocation_count() context['required_sales_order_quantity'] = self.required_sales_order_quantity() - context['allocated_sales_order_quantity'] = self.sales_order_allocation_count() + context['allocated_sales_order_quantity'] = self.sales_order_allocation_count(pending=True) context['available'] = self.available_stock context['on_order'] = self.on_order @@ -1118,7 +1118,9 @@ class Part(MPTTModel): quantity = 0 for line in open_lines: - quantity += line.quantity + # Determine the quantity "remaining" to be shipped out + remaining = max(line.quantity - line.shipped, 0) + quantity += remaining return quantity @@ -1336,19 +1338,36 @@ class Part(MPTTModel): return query['total'] - def sales_order_allocations(self): + def sales_order_allocations(self, **kwargs): """ Return all sales-order-allocation objects which allocate this part to a SalesOrder """ - return OrderModels.SalesOrderAllocation.objects.filter(item__part__id=self.id) + queryset = OrderModels.SalesOrderAllocation.objects.filter(item__part__id=self.id) - def sales_order_allocation_count(self): + pending = kwargs.get('pending', None) + + if pending is True: + # Look only for 'open' orders which have not shipped + queryset = queryset.filter( + line__order__status__in=SalesOrderStatus.OPEN, + shipment__shipment_date=None, + ) + elif pending is False: + # Look only for 'closed' orders or orders which have shipped + queryset = queryset.exclude( + line__order__status__in=SalesOrderStatus.OPEN, + shipment__shipment_date=None, + ) + + return queryset + + def sales_order_allocation_count(self, **kwargs): """ - Return the tutal quantity of this part allocated to sales orders + Return the total quantity of this part allocated to sales orders """ - query = self.sales_order_allocations().aggregate( + query = self.sales_order_allocations(**kwargs).aggregate( total=Coalesce( Sum( 'quantity', diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 67baaf0636..aaa41040f8 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -204,44 +204,60 @@ {% decimal on_order %} {% endif %} + {% if part.component %} {% if required_build_order_quantity > 0 %} {% trans "Required for Build Orders" %} - {% decimal required_build_order_quantity %} + {% decimal required_build_order_quantity %} + + + + {% trans "Allocated to Build Orders" %} + + {% decimal allocated_build_order_quantity %} + {% if allocated_build_order_quantity < required_build_order_quantity %} + + {% else %} + + {% endif %} + {% endif %} + {% endif %} + {% if part.salable %} {% if required_sales_order_quantity > 0 %} {% trans "Required for Sales Orders" %} - {% decimal required_sales_order_quantity %} - - {% endif %} - {% if allocated > 0 %} - - - {% trans "Allocated to Orders" %} - {% decimal allocated %} - - {% endif %} - - {% if not part.is_template %} - {% if part.assembly %} - -
- -
{% trans "Build Status" %}
+ + {% decimal required_sales_order_quantity %} - + + {% trans "Allocated to Sales Orders" %} + + {% decimal allocated_sales_order_quantity %} + {% if allocated_sales_order_quantity < required_sales_order_quantity %} + + {% else %} + + {% endif %} + + + {% endif %} + {% endif %} + {% if not part.is_template %} + {% if part.assembly %} + + {% trans "Can Build" %} {% decimal part.can_build %} {% if quantity_being_built > 0 %} - + {% trans "Building" %} {% decimal quantity_being_built %} diff --git a/InvenTree/part/templates/part/part_sidebar.html b/InvenTree/part/templates/part/part_sidebar.html index 82246da559..5374641bc4 100644 --- a/InvenTree/part/templates/part/part_sidebar.html +++ b/InvenTree/part/templates/part/part_sidebar.html @@ -17,7 +17,9 @@ {% if part.assembly %} {% trans "Bill of Materials" as text %} {% include "sidebar_item.html" with label="bom" text=text icon="fa-list" %} +{% endif %} {% if roles.build.view %} +{% if part.assembly or part.component %} {% trans "Build Orders" as text %} {% include "sidebar_item.html" with label="build-orders" text=text icon="fa-tools" %} {% endif %} @@ -30,10 +32,6 @@ {% trans "Pricing" as text %} {% include "sidebar_item.html" with label="pricing" text=text icon="fa-dollar-sign" %} {% endif %} -{% if part.salable or part.component %} -{% trans "Allocations" as text %} -{% include "sidebar_item.html" with label="allocations" text=text icon="fa-bookmark" %} -{% endif %} {% if part.purchaseable and roles.purchase_order.view %} {% trans "Suppliers" as text %} {% include "sidebar_item.html" with label="suppliers" text=text icon="fa-building" %} diff --git a/InvenTree/stock/templates/stock/item.html b/InvenTree/stock/templates/stock/item.html index 113fefb9b1..648896276d 100644 --- a/InvenTree/stock/templates/stock/item.html +++ b/InvenTree/stock/templates/stock/item.html @@ -37,19 +37,36 @@
+ + {% if item.part.component %}
-

{% trans "Stock Item Allocations" %}

+

{% trans "Build Order Allocations" %}

{% include "spacer.html" %}
-
+
- {% include "filter_list.html" with id="allocations" %} + {% include "filter_list.html" with id="buildorderallocation" %}
-
+
+ {% endif %} + {% if item.part.salable %} +
+

{% trans "Sales Order Allocations" %}

+ {% include "spacer.html" %} +
+
+
+
+ {% include "filter_list.html" with id="salesorderallocation" %} +
+
+
+
+ {% endif %}
@@ -164,14 +181,21 @@ // Load the "allocations" tab onPanelLoad('allocations', function() { - loadStockAllocationTable( - $("#stock-allocation-table"), - { - params: { - stock_item: {{ item.pk }}, - }, + {% if item.part.component %} + loadBuildOrderAllocationTable('#build-order-allocation-table', { + params: { + stock_item: {{ item.pk }}, } - ); + }); + {% endif %} + + {% if item.part.salable %} + loadSalesOrderAllocationTable('#sales-order-allocation-table', { + params: { + stock_item: {{ item.pk }}, + } + }); + {% endif %} }); $('#stock-item-install').click(function() { diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 1dc31cc94f..a9305782a7 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -1843,15 +1843,7 @@ function loadSalesOrderAllocationTable(table, options={}) { 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); + return locationDetail(row.item_detail, true); } }, { diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index 2d84f11e4a..6f12967070 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -48,7 +48,6 @@ findStockItemBySerialNumber, installStockItem, loadInstalledInTable, - loadStockAllocationTable, loadStockLocationTable, loadStockTable, loadStockTestResultsTable, @@ -2328,157 +2327,6 @@ function loadStockTable(table, options) { } -/* - * Display a table of allocated stock, for either a part or stock item - * Allocations are displayed for: - * - * a) Sales Orders - * b) Build Orders - */ -function loadStockAllocationTable(table, options={}) { - - var params = options.params || {}; - - params.build_detail = true; - - var filterListElement = options.filterList || '#filter-list-allocations'; - - var filters = {}; - - var filterKey = options.filterKey || options.name || 'allocations'; - - var original = {}; - - for (var k in params) { - original[k] = params[k]; - filters[k] = params[k]; - } - - setupFilterList(filterKey, table, filterListElement); - - /* - * We have two separate API queries to make here: - * a) Build Order Allocations - * b) Sales Order Allocations - * - * We will let the call to inventreeTable take care of build orders, - * and then load sales orders after that. - */ - table.inventreeTable({ - url: '{% url "api-build-item-list" %}', - name: 'allocations', - original: original, - method: 'get', - queryParams: filters, - sidePagination: 'client', - showColumns: false, - onLoadSuccess: function(tableData) { - - var query_params = params; - - query_params.customer_detail = true; - query_params.order_detail = true; - - delete query_params.build_detail; - - // Load sales order allocation data - inventreeGet('{% url "api-so-allocation-list" %}', query_params, { - success: function(data) { - // Update table to include sales order data - $(table).bootstrapTable('append', data); - } - }); - }, - columns: [ - { - field: 'order', - title: '{% trans "Order" %}', - formatter: function(value, row) { - - var html = ''; - - if (row.build) { - - // Add an icon for the part being built - html += thumbnailImage(row.build_detail.part_detail.thumbnail, { - title: row.build_detail.part_detail.full_name - }); - - html += ' '; - - html += renderLink( - global_settings.BUILDORDER_REFERENCE_PREFIX + row.build_detail.reference, - `/build/${row.build}/` - ); - - html += makeIconBadge('fa-tools', '{% trans "Build Order" %}'); - } else if (row.order) { - - // Add an icon for the customer - html += thumbnailImage(row.customer_detail.thumbnail || row.customer_detail.image, { - title: row.customer_detail.name, - }); - - html += ' '; - - html += renderLink( - global_settings.SALESORDER_REFERENCE_PREFIX + row.order_detail.reference, - `/order/sales-order/${row.order}/` - ); - html += makeIconBadge('fa-truck', '{% trans "Sales Order" %}'); - } else { - return '-'; - } - - return html; - } - }, - { - field: 'description', - title: '{% trans "Description" %}', - formatter: function(value, row) { - if (row.order_detail) { - return row.order_detail.description; - } else if (row.build_detail) { - return row.build_detail.title; - } else { - return '-'; - } - } - }, - { - field: 'status', - title: '{% trans "Order Status" %}', - formatter: function(value, row) { - if (row.build) { - return buildStatusDisplay(row.build_detail.status); - } else if (row.order) { - return salesOrderStatusDisplay(row.order_detail.status); - } else { - return '-'; - } - } - }, - { - field: 'quantity', - title: '{% trans "Allocated Quantity" %}', - formatter: function(value, row) { - var text = value; - var pk = row.stock_item || row.item; - - if (pk) { - var url = `/stock/item/${pk}/`; - return renderLink(text, url); - } else { - return value; - } - } - }, - ] - }); -} - - /* * Display a table of stock locations */ diff --git a/InvenTree/templates/js/translated/table_filters.js b/InvenTree/templates/js/translated/table_filters.js index a4c6a0bbac..81d43d2c3f 100644 --- a/InvenTree/templates/js/translated/table_filters.js +++ b/InvenTree/templates/js/translated/table_filters.js @@ -341,6 +341,15 @@ function getAvailableTableFilters(tableKey) { }; } + if (tableKey == 'salesorderallocation') { + return { + outstanding: { + type: 'bool', + title: '{% trans "Outstanding" %}', + } + }; + } + if (tableKey == 'salesorder') { return { status: {