From 7252b299f70334a113fc93a0daebc994cb67b276 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 27 Oct 2021 00:41:12 +1100 Subject: [PATCH] Add modal API form to allocate stock items against a SalesOrder - Added model renderer for SalesOrderShipment - Some refactorin' --- InvenTree/order/serializers.py | 3 + InvenTree/templates/js/translated/build.js | 20 +- InvenTree/templates/js/translated/forms.js | 3 + .../js/translated/model_renderers.js | 17 + InvenTree/templates/js/translated/order.js | 315 ++++++++++++++++++ 5 files changed, 348 insertions(+), 10 deletions(-) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 34032d758b..c872e558a0 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -596,12 +596,15 @@ class SalesOrderShipmentSerializer(InvenTreeModelSerializer): allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True) + order_detail = SalesOrderSerializer(source='order', read_only=True, many=False) + class Meta: model = order.models.SalesOrderShipment fields = [ 'pk', 'order', + 'order_detail', 'allocations', 'shipment_date', 'checked_by', diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index b6c98fc49e..92cdea7452 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1216,7 +1216,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { * * options: * - output: ID / PK of the associated build output (or null for untracked items) - * - source_location: ID / PK of the top-level StockLocation to take parts from (or null) + * - source_location: ID / PK of the top-level StockLocation to source stock from (or null) */ function allocateStockToBuild(build_id, part_id, bom_items, options={}) { @@ -1329,7 +1329,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) { var html = ``; - // Render a "take from" input + // Render a "source location" input html += constructField( 'take_from', { @@ -1387,6 +1387,13 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) { options, ); + // Add callback to "clear" button for take_from field + addClearCallback( + 'take_from', + take_from_field, + options, + ); + // Initialize stock item fields bom_items.forEach(function(bom_item) { initializeRelatedField( @@ -1446,14 +1453,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) { ); }); - // Add callback to "clear" button for take_from field - addClearCallback( - 'take_from', - take_from_field, - options, - ); - - // Add button callbacks + // Add remove-row button callbacks $(options.modal).find('.button-row-remove').click(function() { var pk = $(this).attr('pk'); diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js index db2c8e46cc..2818b00534 100644 --- a/InvenTree/templates/js/translated/forms.js +++ b/InvenTree/templates/js/translated/forms.js @@ -1576,6 +1576,9 @@ function renderModelData(name, model, data, parameters, options) { case 'salesorder': renderer = renderSalesOrder; break; + case 'salesordershipment': + renderer = renderSalesOrderShipment; + break; case 'manufacturerpart': renderer = renderManufacturerPart; break; diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js index bf3628d656..b2495e207f 100644 --- a/InvenTree/templates/js/translated/model_renderers.js +++ b/InvenTree/templates/js/translated/model_renderers.js @@ -241,6 +241,23 @@ function renderSalesOrder(name, data, parameters, options) { } +// Renderer for "SalesOrderShipment" model +// eslint-disable-next-line no-unused-vars +function renderSalesOrderShipment(name, data, parameters, options) { + + var so_prefix = global_settings.SALESORDER_REFERENCE_PREFIX; + + var html = ` + ${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference} + + {% trans "Shipment ID" %}: ${data.pk} + + `; + + return html; +} + + // Renderer for "PartCategory" model // eslint-disable-next-line no-unused-vars function renderPartCategory(name, data, parameters, options) { diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 6e69eb6a02..b02c757df4 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -19,6 +19,7 @@ */ /* exported + allocateStockToSalesOrder, createSalesOrder, editPurchaseOrderLineItem, exportOrder, @@ -1211,6 +1212,306 @@ function loadSalesOrderShipmentTable(table, options={}) { } +/** + * Allocate stock items against a SalesOrder + * + * arguments: + * - order_id: The ID / PK value for the SalesOrder + * - lines: A list of SalesOrderLineItem objects to be allocated + * + * options: + * - source_location: ID / PK of the top-level StockLocation to source stock from (or null) + */ +function allocateStockToSalesOrder(order_id, line_items, options={}) { + + function renderLineItemRow(line_item, quantity) { + // Function to render a single line_item row + + var pk = line_item.pk; + + var part = line_item.part_detail; + + var thumb = thumbnailImage(part.thumbnail || part.image); + + var delete_button = `
`; + + delete_button += makeIconButton( + 'fa-times icon-red', + 'button-row-remove', + pk, + '{% trans "Remove row" %}', + ); + + delete_button += '
'; + + var quantity_input = constructField( + `items_quantity_${pk}`, + { + type: 'decimal', + min_value: 0, + value: quantity || 0, + title: '{% trans "Specify stock allocation quantity" %}', + required: true, + }, + { + hideLabels: true, + } + ); + + var stock_input = constructField( + `items_stock_item_${pk}`, + { + type: 'related field', + required: 'true', + }, + { + hideLabels: true, + } + ); + + var html = ` + + + ${thumb} ${part.full_name} + + + ${stock_input} + + + ${quantity_input} + + + + + {% trans "Part" %} + {% trans "Stock Item" %} + {% trans "Quantity" %} + + + + ${table_entries} + + `; + + constructForm(`/api/order/so/${order_id}/allocate/`, { + method: 'POST', + fields: { + shipment: { + filters: { + order: order_id, + shipped: false, + }, + value: options.shipment || null, + auto_fill: true, + } + }, + preFormContent: html, + confirm: true, + confirmMessage: '{% trans "Confirm stock allocation" %}', + title: '{% trans "Allocate Stock Items to Sales Order" %}', + afterRender: function(fields, opts) { + + // Initialize source location field + var take_from_field = { + name: 'take_from', + model: 'stocklocation', + api_url: '{% url "api-location-list" %}', + required: false, + type: 'related field', + value: options.source_location || null, + noResults: function(query) { + return '{% trans "No matching stock locations" %}'; + }, + }; + + initializeRelatedField( + take_from_field, + null, + opts + ); + + // Add callback to "clear" button for take_from field + addClearCallback( + 'take_from', + take_from_field, + opts, + ); + + // Initialize fields for each line item + line_items.forEach(function(line_item) { + var pk = line_item.pk; + + initializeRelatedField( + { + name: `items_stock_item_${pk}`, + api_url: '{% url "api-stock-list" %}', + filters: { + part: line_item.part, + in_stock: true, + part_detail: true, + location_detail: true, + }, + model: 'stockitem', + required: true, + render_part_detail: true, + render_location_detail: true, + auto_fill: true, + onSelect: function(data, field, opts) { + // Adjust the 'quantity' field based on availability + + var todo = "actually do this"; + }, + adjustFilters: function(filters) { + // Restrict query to the selected location + var location = getFormFieldValue( + 'take_from', + {}, + { + modal: opts.modal, + } + ); + + filters.location = location; + filters.cascade = true; + + return filters; + }, + noResults: function(query) { + return '{% trans "No matching stock items" %}'; + } + }, + null, + opts + ); + }); + + // Add remove-row button callbacks + $(opts.modal).find('.button-row-remove').click(function() { + var pk = $(this).attr('pk'); + + $(opts.modal).find(`#allocation_row_${pk}`).remove(); + }); + }, + onSubmit: function(fields, opts) { + // Extract data elements from the form + var data = { + items: [], + shipment: getFormFieldValue( + 'shipment', + {}, + opts + ) + }; + + var item_pk_values = []; + + line_items.forEach(function(item) { + + var pk = item.pk; + + var quantity = getFormFieldValue( + `items_quantity_${pk}`, + {}, + opts + ); + + var stock_item = getFormFieldValue( + `items_stock_item_${pk}`, + {}, + opts + ); + + if (quantity != null) { + data.items.push({ + line_item: pk, + stock_item: stock_item, + quantity: quantity, + }); + + item_pk_values.push(pk); + } + }); + + // Provide nested values + opts.nested = { + 'items': item_pk_values + }; + + inventreePut( + opts.url, + data, + { + method: 'POST', + success: function(response) { + $(opts.modal).modal('hide'); + + if (options.success) { + options.success(response); + } + }, + error: function(xhr) { + switch (xhr.status) { + case 400: + handleFormErrors(xhr.responseJSON, fields, opts); + break; + default: + $(opts.modal).modal('hide'); + showApiError(xhr); + break; + } + } + } + ); + }, + }); +} + function loadSalesOrderAllocationTable(table, options={}) { /** @@ -1772,6 +2073,20 @@ function loadSalesOrderLineItemTable(table, options={}) { var line_item = $(table).bootstrapTable('getRowByUniqueId', pk); + allocateStockToSalesOrder( + options.order, + [ + line_item + ], + { + success: function() { + $(table).bootstrapTable('refresh'); + } + } + ); + + return; + // Quantity remaining to be allocated var remaining = (line_item.quantity || 0) - (line_item.allocated || 0);