@@ -631,6 +648,19 @@
{% endif %}
});
+ // Load the "allocations" tab
+ onPanelLoad('allocations', function() {
+
+ loadStockAllocationTable(
+ $("#part-allocation-table"),
+ {
+ params: {
+ part: {{ part.pk }},
+ },
+ }
+ );
+ });
+
// Load the "related parts" tab
onPanelLoad("related-parts", function() {
diff --git a/InvenTree/part/templates/part/part_sidebar.html b/InvenTree/part/templates/part/part_sidebar.html
index f4e59af865..a8f57aa7b0 100644
--- a/InvenTree/part/templates/part/part_sidebar.html
+++ b/InvenTree/part/templates/part/part_sidebar.html
@@ -27,6 +27,10 @@
{% endif %}
{% trans "Pricing" as text %}
{% include "sidebar_item.html" with label="pricing" text=text icon="fa-dollar-sign" %}
+{% 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 9cc6d85aeb..5c3f94f72f 100644
--- a/InvenTree/stock/templates/stock/item.html
+++ b/InvenTree/stock/templates/stock/item.html
@@ -43,9 +43,26 @@
{% trans "Child Stock Items" %}
+ {% include "spacer.html" %}
{% if item.child_count > 0 %}
@@ -151,6 +168,19 @@
{% block js_ready %}
{{ block.super }}
+ // Load the "allocations" tab
+ onPanelLoad('allocations', function() {
+
+ loadStockAllocationTable(
+ $("#stock-allocation-table"),
+ {
+ params: {
+ stock_item: {{ item.pk }},
+ },
+ }
+ );
+ });
+
$('#stock-item-install').click(function() {
launchModalForm(
diff --git a/InvenTree/stock/templates/stock/stock_sidebar.html b/InvenTree/stock/templates/stock/stock_sidebar.html
index 8161251ad7..3032bab5bf 100644
--- a/InvenTree/stock/templates/stock/stock_sidebar.html
+++ b/InvenTree/stock/templates/stock/stock_sidebar.html
@@ -4,6 +4,10 @@
{% trans "Stock Tracking" as text %}
{% include "sidebar_item.html" with label='history' text=text icon="fa-history" %}
+{% if item.part.salable or item.part.component %}
+{% trans "Allocations" as text %}
+{% include "sidebar_item.html" with label="allocations" text=text icon="fa-bookmark" %}
+{% endif %}
{% if item.part.trackable %}
{% trans "Test Data" as text %}
{% include "sidebar_item.html" with label='test-data' text=text icon="fa-vial" %}
diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js
index ded4c48883..d9fee44297 100644
--- a/InvenTree/templates/js/translated/stock.js
+++ b/InvenTree/templates/js/translated/stock.js
@@ -47,6 +47,7 @@
exportStock,
findStockItemBySerialNumber,
loadInstalledInTable,
+ loadStockAllocationTable,
loadStockLocationTable,
loadStockTable,
loadStockTestResultsTable,
@@ -2203,6 +2204,125 @@ 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.order_detail = true;
+
+ // 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) {
+ 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) {
+ html += renderLink(
+ global_settings.SALESORDER_REFERENCE_PREFIX + row.order,
+ `/order/so/${row.order}/`
+ );
+
+ html += makeIconBadge('fa-truck', '{% trans "Sales Order" %}');
+ } else {
+ return '-';
+ }
+
+ return html;
+ }
+ },
+ {
+ 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
*/
@@ -2252,7 +2372,6 @@ function loadStockLocationTable(table, options) {
method: 'get',
url: options.url || '{% url "api-location-list" %}',
queryParams: filters,
- sidePagination: 'server',
name: 'location',
original: original,
showColumns: true,