diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 9797c8dedf..cbdcd26d45 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -35,6 +35,29 @@
+ +
+
+

{% trans "Sales Order Lines" %}

+ {% include "spacer.html" %} +
+ {% if roles.sales_order.change and order.is_pending %} + + {% endif %} +
+
+
+
+
+
+ {% include "filter_list.html" with id="sales-order-additional-lines" %} +
+
+ +
+
{% if order.is_pending %} @@ -245,6 +268,30 @@ } ); + $("#new-so-additional-line").click(function() { + + var fields = soAdditionalLineItemFields({ + order: {{ order.pk }}, + }); + + constructForm('{% url "api-so-additional-line-list" %}', { + fields: fields, + method: 'POST', + title: '{% trans "Add Order Line" %}', + onSuccess: function() { + $("#so-additional-lines-table").bootstrapTable("refresh"); + }, + }); + }); + + loadSalesOrderAdditionalLineItemTable( + '#so-additional-lines-table', + { + order: {{ order.pk }}, + status: {{ order.status }}, + } + ); + enableSidebar('salesorder'); {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 6ea4e9ebb6..94fcbf2655 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -30,6 +30,7 @@ loadSalesOrderAllocationTable, loadSalesOrderLineItemTable, loadSalesOrderShipmentTable, + loadSalesOrderAdditionalLineItemTable loadSalesOrderTable, newPurchaseOrderFromOrderWizard, newSupplierPartFromOrderWizard, @@ -305,6 +306,28 @@ function soLineItemFields(options={}) { } +/* Construct a set of fields for the SalesOrderAdditionalLineItem form */ +function SOAdditionalLineItemFields(options={}) { + + var fields = { + order: { + hidden: true, + }, + quantity: {}, + reference: {}, + sale_price: {}, + sale_price_currency: {}, + notes: {}, + }; + + if (options.order) { + fields.order.value = options.order; + } + + return fields; +} + + /* Construct a set of fields for the PurchaseOrderLineItem form */ function poLineItemFields(options={}) { @@ -2773,3 +2796,241 @@ function loadSalesOrderLineItemTable(table, options={}) { columns: columns, }); } + + +/** + * Load a table displaying line items for a particular SalesOrder + * + * @param {String} table : HTML ID tag e.g. '#table' + * @param {Object} options : object which contains: + * - order {integer} : pk of the SalesOrder + * - status: {integer} : status code for the order + */ + function loadSalesOrderAdditionalLineItemTable(table, options={}) { + + options.table = table; + + options.params = options.params || {}; + + if (!options.order) { + console.log('ERROR: function called without order ID'); + return; + } + + if (!options.status) { + console.log('ERROR: function called without order status'); + return; + } + + options.params.order = options.order; + options.params.part_detail = true; + options.params.allocations = true; + + var filters = loadTableFilters('salesorderadditionallineitem'); + + for (var key in options.params) { + filters[key] = options.params[key]; + } + + options.url = options.url || '{% url "api-so-additional-line-list" %}'; + + var filter_target = options.filter_target || '#filter-list-sales-order-additional-lines'; + + setupFilterList('salesorderadditionallineitem', $(table), filter_target); + + // Is the order pending? + var pending = options.status == {{ SalesOrderStatus.PENDING }}; + + // Has the order shipped? + var shipped = options.status == {{ SalesOrderStatus.SHIPPED }}; + + // Show detail view if the PurchaseOrder is PENDING or SHIPPED + var show_detail = pending || shipped; + + // Table columns to display + var columns = [ + /* + { + checkbox: true, + visible: true, + switchable: false, + }, + */ + { + sortable: true, + field: 'reference', + title: '{% trans "Reference" %}', + switchable: true, + }, + { + sortable: true, + field: 'quantity', + title: '{% trans "Quantity" %}', + footerFormatter: function(data) { + return data.map(function(row) { + return +row['quantity']; + }).reduce(function(sum, i) { + return sum + i; + }, 0); + }, + switchable: false, + }, + { + sortable: true, + field: 'sale_price', + title: '{% trans "Unit Price" %}', + formatter: function(value, row) { + var formatter = new Intl.NumberFormat( + 'en-US', + { + style: 'currency', + currency: row.sale_price_currency + } + ); + + return formatter.format(row.sale_price); + } + }, + { + field: 'total_price', + sortable: true, + title: '{% trans "Total Price" %}', + formatter: function(value, row) { + var formatter = new Intl.NumberFormat( + 'en-US', + { + style: 'currency', + currency: row.sale_price_currency + } + ); + + return formatter.format(row.sale_price * row.quantity); + }, + footerFormatter: function(data) { + var total = data.map(function(row) { + return +row['sale_price'] * row['quantity']; + }).reduce(function(sum, i) { + return sum + i; + }, 0); + + var currency = (data.slice(-1)[0] && data.slice(-1)[0].sale_price_currency) || 'USD'; + + var formatter = new Intl.NumberFormat( + 'en-US', + { + style: 'currency', + currency: currency + } + ); + + return formatter.format(total); + } + } + ]; + + columns.push({ + field: 'notes', + title: '{% trans "Notes" %}', + }); + + if (pending) { + columns.push({ + field: 'buttons', + switchable: false, + formatter: function(value, row, index, field) { + + var html = `
`; + + var pk = row.pk; + + html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line item" %}'); + html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}'); + + var title = '{% trans "Delete line item" %}'; + + // Prevent deletion of the line item if items have been allocated or shipped! + html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, title, ); + + html += `
`; + + return html; + } + }); + } + + function reloadTable() { + $(table).bootstrapTable('refresh'); + } + + // Configure callback functions once the table is loaded + function setupCallbacks() { + + // Callback for duplicating line items + $(table).find('.button-duplicate').click(function() { + var pk = $(this).attr('pk'); + + inventreeGet(`/api/order/so-additional-line/${pk}/`, {}, { + success: function(data) { + + var fields = soLineItemFields(); + + constructForm('{% url "api-so-additional-line-list" %}', { + method: 'POST', + fields: fields, + data: data, + title: '{% trans "Duplicate Line Item" %}', + onSuccess: function(response) { + $(table).bootstrapTable('refresh'); + } + }); + } + }); + }); + + // Callback for editing line items + $(table).find('.button-edit').click(function() { + var pk = $(this).attr('pk'); + + constructForm(`/api/order/so-additional-line/${pk}/`, { + fields: { + quantity: {}, + reference: {}, + sale_price: {}, + sale_price_currency: {}, + target_date: {}, + notes: {}, + }, + title: '{% trans "Edit Line Item" %}', + onSuccess: reloadTable, + }); + }); + + // Callback for deleting line items + $(table).find('.button-delete').click(function() { + var pk = $(this).attr('pk'); + + constructForm(`/api/order/so-additional-line/${pk}/`, { + method: 'DELETE', + title: '{% trans "Delete Line Item" %}', + onSuccess: reloadTable, + }); + }); + } + + $(table).inventreeTable({ + onPostBody: setupCallbacks, + name: 'salesorderadditionallineitems', + sidePagination: 'client', + formatNoMatches: function() { + return '{% trans "No matching line items" %}'; + }, + queryParams: filters, + original: options.params, + url: options.url, + showFooter: true, + uniqueId: 'pk', + detailView: show_detail, + detailViewByClick: false, + columns: columns, + }); +}