From 4d2cf233b3b0859ef11f105c9417d9dbf8e57af3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 16:14:39 +1100 Subject: [PATCH 1/5] Move the "loadSalesOrderLineItemTable" code to order.js --- .../templates/order/sales_order_detail.html | 459 +-------------- InvenTree/templates/js/translated/order.js | 527 ++++++++++++++++++ 2 files changed, 542 insertions(+), 444 deletions(-) diff --git a/InvenTree/order/templates/order/sales_order_detail.html b/InvenTree/order/templates/order/sales_order_detail.html index 30799e2296..bd853702c4 100644 --- a/InvenTree/order/templates/order/sales_order_detail.html +++ b/InvenTree/order/templates/order/sales_order_detail.html @@ -158,467 +158,38 @@ $("#so-lines-table").bootstrapTable("refresh"); } -$("#new-so-line").click(function() { + $("#new-so-line").click(function() { - constructForm('{% url "api-so-line-list" %}', { - fields: { - order: { - value: {{ order.pk }}, - hidden: true, - }, - part: {}, - quantity: {}, - reference: {}, - sale_price: {}, - sale_price_currency: {}, - notes: {}, - }, - method: 'POST', - title: '{% trans "Add Line Item" %}', - onSuccess: reloadTable, - }); -}); - -{% if order.status == SalesOrderStatus.PENDING %} -function showAllocationSubTable(index, row, element) { - // Construct a table showing stock items which have been allocated against this line item - - var html = `
`; - - element.html(html); - - var lineItem = row; - - var table = $(`#allocation-table-${row.pk}`); - - table.bootstrapTable({ - data: row.allocations, - showHeader: false, - columns: [ - { - width: '50%', - field: 'allocated', - title: '{% trans "Quantity" %}', - formatter: function(value, row, index, field) { - var text = ''; - - if (row.serial != null && row.quantity == 1) { - text = `{% trans "Serial Number" %}: ${row.serial}`; - } else { - text = `{% trans "Quantity" %}: ${row.quantity}`; - } - - return renderLink(text, `/stock/item/${row.item}/`); - }, - }, - { - field: 'location', - title: 'Location', - formatter: function(value, row, index, field) { - return renderLink(row.location_path, `/stock/location/${row.location}/`); - }, - }, - { - field: 'po' - }, - { - field: 'buttons', - title: '{% trans "Actions" %}', - formatter: function(value, row, index, field) { - - var html = "
"; - var pk = row.pk; - - {% if order.status == SalesOrderStatus.PENDING %} - html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); - html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); - {% endif %} - - html += "
"; - - return html; - }, - }, - ], - }); - - table.find(".button-allocation-edit").click(function() { - - var pk = $(this).attr('pk'); - - launchModalForm(`/order/sales-order/allocation/${pk}/edit/`, { - success: reloadTable, - }); - }); - - table.find(".button-allocation-delete").click(function() { - var pk = $(this).attr('pk'); - - launchModalForm(`/order/sales-order/allocation/${pk}/delete/`, { - success: reloadTable, - }); - }); -} -{% endif %} - -function showFulfilledSubTable(index, row, element) { - // Construct a table showing stock items which have been fulfilled against this line item - - var id = `fulfilled-table-${row.pk}`; - var html = `
`; - - element.html(html); - - var lineItem = row; - - $(`#${id}`).bootstrapTable({ - url: "{% url 'api-stock-list' %}", - queryParams: { - part: row.part, - sales_order: {{ order.id }}, - }, - showHeader: false, - columns: [ - { - field: 'pk', - visible: false, - }, - { - field: 'stock', - formatter: function(value, row) { - var text = ''; - if (row.serial && row.quantity == 1) { - text = `{% trans "Serial Number" %}: ${row.serial}`; - } else { - text = `{% trans "Quantity" %}: ${row.quantity}`; - } - - return renderLink(text, `/stock/item/${row.pk}/`); - }, - }, - { - field: 'po' - }, - ], - }); -} - -$("#so-lines-table").inventreeTable({ - formatNoMatches: function() { return "{% trans 'No matching line items' %}"; }, - queryParams: { - order: {{ order.id }}, - part_detail: true, - allocations: true, - }, - sidePagination: 'server', - uniqueId: 'pk', - url: "{% url 'api-so-line-list' %}", - onPostBody: setupCallbacks, - {% if order.status == SalesOrderStatus.PENDING or order.status == SalesOrderStatus.SHIPPED %} - detailViewByClick: true, - detailView: true, - detailFilter: function(index, row) { - {% if order.status == SalesOrderStatus.PENDING %} - return row.allocated > 0; - {% else %} - return row.fulfilled > 0; - {% endif %} - }, - {% if order.status == SalesOrderStatus.PENDING %} - detailFormatter: showAllocationSubTable, - {% else %} - detailFormatter: showFulfilledSubTable, - {% endif %} - {% endif %} - showFooter: true, - columns: [ - { - field: 'pk', - title: '{% trans "ID" %}', - visible: false, - switchable: false, - }, - { - sortable: true, - sortName: 'part__name', - field: 'part', - title: '{% trans "Part" %}', - formatter: function(value, row, index, field) { - if (row.part) { - return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${value}/`); - } else { - return '-'; - } - }, - footerFormatter: function() { - return '{% trans "Total" %}' - }, - }, - { - sortable: true, - field: 'reference', - title: '{% trans "Reference" %}' - }, - { - 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) - }, - }, - { - sortable: true, - field: 'sale_price', - title: '{% trans "Unit Price" %}', - formatter: function(value, row) { - return row.sale_price_string || row.sale_price; - } - }, - { - sortable: true, - title: '{% trans "Total price" %}', - formatter: function(value, row) { - var total = row.sale_price * row.quantity; - var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.sale_price_currency}); - return formatter.format(total) - }, - 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) - } - }, - - { - field: 'allocated', - {% if order.status == SalesOrderStatus.PENDING %} - title: '{% trans "Allocated" %}', - {% else %} - title: '{% trans "Fulfilled" %}', - {% endif %} - formatter: function(value, row, index, field) { - {% if order.status == SalesOrderStatus.PENDING %} - var quantity = row.allocated; - {% else %} - var quantity = row.fulfilled; - {% endif %} - return makeProgressBar(quantity, row.quantity, { - id: `order-line-progress-${row.pk}`, - }); - }, - sorter: function(valA, valB, rowA, rowB) { - {% if order.status == SalesOrderStatus.PENDING %} - var A = rowA.allocated; - var B = rowB.allocated; - {% else %} - var A = rowA.fulfilled; - var B = rowB.fulfilled; - {% endif %} - - if (A == 0 && B == 0) { - return (rowA.quantity > rowB.quantity) ? 1 : -1; - } - - var progressA = parseFloat(A) / rowA.quantity; - var progressB = parseFloat(B) / rowB.quantity; - - return (progressA < progressB) ? 1 : -1; - } - }, - { - field: 'notes', - title: '{% trans "Notes" %}', - }, - { - field: 'po', - title: '{% trans "PO" %}', - formatter: function(value, row, index, field) { - var po_name = ""; - if (row.allocated) { - row.allocations.forEach(function(allocation) { - if (allocation.po != po_name) { - if (po_name) { - po_name = "-"; - } else { - po_name = allocation.po - } - } - }) - } - return `
` + po_name + `
`; - } - }, - {% if order.status == SalesOrderStatus.PENDING %} - { - field: 'buttons', - formatter: function(value, row, index, field) { - - var html = `
`; - - var pk = row.pk; - - if (row.part) { - var part = row.part_detail; - - if (part.trackable) { - html += makeIconButton('fa-hashtag icon-green', 'button-add-by-sn', pk, '{% trans "Allocate serial numbers" %}'); - } - - html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', pk, '{% trans "Allocate stock" %}'); - - if (part.purchaseable) { - html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}'); - } - - if (part.assembly) { - html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build stock" %}'); - } - - html += makeIconButton('fa-dollar-sign icon-green', 'button-price', pk, '{% trans "Calculate price" %}'); - } - - html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}'); - html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line item " %}'); - - html += `
`; - - return html; - } - }, - {% endif %} - ], -}); - -function setupCallbacks() { - - var table = $("#so-lines-table"); - - // Set up callbacks for the row buttons - table.find(".button-edit").click(function() { - - var pk = $(this).attr('pk'); - - constructForm(`/api/order/so-line/${pk}/`, { + constructForm('{% url "api-so-line-list" %}', { fields: { + order: { + value: {{ order.pk }}, + hidden: true, + }, + part: {}, quantity: {}, reference: {}, sale_price: {}, sale_price_currency: {}, notes: {}, }, - title: '{% trans "Edit Line Item" %}', + method: 'POST', + title: '{% trans "Add Line Item" %}', onSuccess: reloadTable, }); }); - table.find(".button-delete").click(function() { - var pk = $(this).attr('pk'); - - constructForm(`/api/order/so-line/${pk}/`, { - method: 'DELETE', - title: '{% trans "Delete Line Item" %}', - onSuccess: reloadTable, - }); - }); - - table.find(".button-add-by-sn").click(function() { - var pk = $(this).attr('pk'); - - inventreeGet(`/api/order/so-line/${pk}/`, {}, - { - success: function(response) { - launchModalForm('{% url "so-assign-serials" %}', { - success: reloadTable, - data: { - line: pk, - part: response.part, - } - }); - } - } - ); - }); - - table.find(".button-add").click(function() { - var pk = $(this).attr('pk'); - - launchModalForm(`/order/sales-order/allocation/new/`, { - success: reloadTable, - data: { - line: pk, - }, - }); - }); - - table.find(".button-build").click(function() { - - var pk = $(this).attr('pk'); - - // Extract the row data from the table! - var idx = $(this).closest('tr').attr('data-index'); - - var row = table.bootstrapTable('getData')[idx]; - - var quantity = 1; - - if (row.allocated < row.quantity) { - quantity = row.quantity - row.allocated; + loadSalesOrderLineItemTable( + '#so-lines-table', + { + order: {{ order.pk }}, + status: {{ order.status }}, } - - launchModalForm(`/build/new/`, { - follow: true, - data: { - part: pk, - sales_order: {{ order.id }}, - quantity: quantity, - }, - }); - }); - - table.find(".button-buy").click(function() { - var pk = $(this).attr('pk'); - - launchModalForm("{% url 'order-parts' %}", { - data: { - parts: [pk], - }, - }); - }); - - $(".button-price").click(function() { - var pk = $(this).attr('pk'); - var idx = $(this).closest('tr').attr('data-index'); - var row = table.bootstrapTable('getData')[idx]; - - launchModalForm( - "{% url 'line-pricing' %}", - { - submit_text: '{% trans "Calculate price" %}', - data: { - line_item: pk, - quantity: row.quantity, - }, - buttons: [{name: 'update_price', - title: '{% trans "Update Unit Price" %}'},], - success: reloadTable, - } - ); - }); + ); attachNavCallbacks({ name: 'sales-order', default: 'order-items' }); -} {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 43d4b56936..c9a5d92b5b 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -24,6 +24,7 @@ loadPurchaseOrderLineItemTable, loadPurchaseOrderTable, loadSalesOrderAllocationTable, + loadSalesOrderLineItemTable, loadSalesOrderTable, newPurchaseOrderFromOrderWizard, newSupplierPartFromOrderWizard, @@ -1126,3 +1127,529 @@ function loadSalesOrderAllocationTable(table, options={}) { ] }); } + + +/** + * Display an "allocations" sub table, showing stock items allocated againt a sales order + * @param {*} index + * @param {*} row + * @param {*} element + */ +function showAllocationSubTable(index, row, element, options) { + + // Construct a sub-table element + var html = ` +
+ +
+
`; + + element.html(html); + + var lineItem = row; + + var table = $(`#allocation-table-${row.pk}`); + + // Is the parent SalesOrder pending? + var pending = options.status == {{ SalesOrderStatus.PENDING }}; + + table.bootstrapTable({ + data: row.allocations, + showHeader: false, + columns: [ + { + width: '50%', + field: 'allocated', + title: '{% trans "Quantity" %}', + formatter: function(value, row, index, field) { + var text = ''; + + if (row.serial != null && row.quantity == 1) { + text = `{% trans "Serial Number" %}: ${row.serial}`; + } else { + text = `{% trans "Quantity" %}: ${row.quantity}`; + } + + return renderLink(text, `/stock/item/${row.item}/`); + }, + }, + { + field: 'location', + title: '{% trans "Location" %}', + formatter: function(value, row, index, field) { + return renderLink(row.location_path, `/stock/location/${row.location}/`); + }, + }, + { + field: 'po' + }, + { + field: 'buttons', + title: '{% trans "Actions" %}', + formatter: function(value, row, index, field) { + + var html = `
`; + var pk = row.pk; + + if (pending) { + html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); + html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); + } + + html += "
"; + + return html; + }, + }, + ], + }); + + // Add callbacks for 'edit' buttons + table.find(".button-allocation-edit").click(function() { + + var pk = $(this).attr('pk'); + + // TODO: Migrate to API forms + launchModalForm(`/order/sales-order/allocation/${pk}/edit/`, { + success: reloadTable, + }); + }); + + // Add callbacks for 'delete' buttons + table.find(".button-allocation-delete").click(function() { + var pk = $(this).attr('pk'); + + // TODO: Migrate to API forms + launchModalForm(`/order/sales-order/allocation/${pk}/delete/`, { + success: reloadTable, + }); + }); +} + +/** + * Display a "fulfilled" sub table, showing stock items fulfilled against a purchase order + */ +function showFulfilledSubTable(index, row, element, options) { + // Construct a table showing stock items which have been fulfilled against this line item + + if (!options.order) { + return 'ERROR: Order ID not supplied'; + } + + var id = `fulfilled-table-${row.pk}`; + + var html = ` +
+ +
+
`; + + element.html(html); + + var lineItem = row; + + $(`#${id}`).bootstrapTable({ + url: "{% url 'api-stock-list' %}", + queryParams: { + part: row.part, + sales_order: options.order, + }, + showHeader: false, + columns: [ + { + field: 'pk', + visible: false, + }, + { + field: 'stock', + formatter: function(value, row) { + var text = ''; + if (row.serial && row.quantity == 1) { + text = `{% trans "Serial Number" %}: ${row.serial}`; + } else { + text = `{% trans "Quantity" %}: ${row.quantity}`; + } + + return renderLink(text, `/stock/item/${row.pk}/`); + }, + }, + { + field: 'po' + }, + ], + }); +} + + +/** + * 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 loadSalesOrderLineItemTable(table, options={}) { + + 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('salesorderlineitem'); + + for (var key in options.params) { + filters[key] = options.params[key]; + } + + options.url = options.url || '{% url "api-so-line-list" %}'; + + var filter_target = options.filter_target || '#filter-list-sales-order-lines'; + + setupFilterList('salesorderlineitems', $(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, + sortName: 'part__name', + field: 'part', + title: '{% trans "Part" %}', + formatter: function(value, row, index, field) { + if (row.part) { + return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${value}/`); + } else { + return '-'; + } + }, + footerFormatter: function() { + return '{% trans "Total" %}' + }, + }, + { + sortable: true, + field: 'reference', + title: '{% trans "Reference" %}' + }, + { + 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) + }, + }, + { + sortable: true, + field: 'sale_price', + title: '{% trans "Unit Price" %}', + formatter: function(value, row) { + return row.sale_price_string || row.sale_price; + } + }, + { + sortable: true, + title: '{% trans "Total price" %}', + formatter: function(value, row) { + var total = row.sale_price * row.quantity; + var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.sale_price_currency}); + return formatter.format(total) + }, + 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) + } + }, + { + field: 'stock', + title: '{% trans "In Stock" %}', + formatter: function(value, row) { + return row.part_detail.stock; + } + }, + { + field: 'allocated', + title: pending ? '{% trans "Allocated" %}' : '{% trans "Fulfilled" %}', + formatter: function(value, row, index, field) { + + var quantity = pending ? row.allocated : row.fulfilled; + return makeProgressBar(quantity, row.quantity, { + id: `order-line-progress-${row.pk}`, + }); + }, + sorter: function(valA, valB, rowA, rowB) { + + var A = pending ? rowA.allocated : rowA.fulfilled; + var B = pending ? rowB.allocated : rowB.fulfilled; + + if (A == 0 && B == 0) { + return (rowA.quantity > rowB.quantity) ? 1 : -1; + } + + var progressA = parseFloat(A) / rowA.quantity; + var progressB = parseFloat(B) / rowB.quantity; + + return (progressA < progressB) ? 1 : -1; + } + }, + { + field: 'notes', + title: '{% trans "Notes" %}', + }, + { + field: 'po', + title: '{% trans "PO" %}', + formatter: function(value, row, index, field) { + var po_name = ""; + if (row.allocated) { + row.allocations.forEach(function(allocation) { + if (allocation.po != po_name) { + if (po_name) { + po_name = "-"; + } else { + po_name = allocation.po + } + } + }) + } + return `
` + po_name + `
`; + } + }, + ]; + + if (pending) { + columns.push({ + field: 'buttons', + formatter: function(value, row, index, field) { + + var html = `
`; + + var pk = row.pk; + + if (row.part) { + var part = row.part_detail; + + if (part.trackable) { + html += makeIconButton('fa-hashtag icon-green', 'button-add-by-sn', pk, '{% trans "Allocate serial numbers" %}'); + } + + html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', pk, '{% trans "Allocate stock" %}'); + + if (part.purchaseable) { + html += makeIconButton('fa-shopping-cart', 'button-buy', row.part, '{% trans "Purchase stock" %}'); + } + + if (part.assembly) { + html += makeIconButton('fa-tools', 'button-build', row.part, '{% trans "Build stock" %}'); + } + + html += makeIconButton('fa-dollar-sign icon-green', 'button-price', pk, '{% trans "Calculate price" %}'); + } + + html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line item" %}'); + html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line item " %}'); + + html += `
`; + + return html; + } + }); + } else { + // Remove the "in stock" column + delete columns['stock']; + } + + function reloadTable() { + $(table).bootstrapTable('refresh'); + } + + // Configure callback functions once the table is loaded + function setupCallbacks() { + + // Callback for editing line items + $(table).find('.button-edit').click(function() { + var pk = $(this).attr('pk'); + + constructForm(`/api/order/so-line/${pk}/`, { + fields: { + quantity: {}, + reference: {}, + sale_price: {}, + sale_price_currency: {}, + 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-line/${pk}/`, { + method: 'DELETE', + title: '{% trans "Delete Line Item" %}', + onSuccess: reloadTable, + }); + }); + + // Callback for allocating stock items by serial number + $(table).find('.button-add-by-sn').click(function() { + var pk = $(this).attr('pk'); + + // TODO: Migrate this form to the API forms + inventreeGet(`/api/order/so-line/${pk}/`, {}, + { + success: function(response) { + launchModalForm('{% url "so-assign-serials" %}', { + success: reloadTable, + data: { + line: pk, + part: response.part, + } + }); + } + } + ); + }); + + // Callback for allocation stock items to the order + $(table).find('.button-add').click(function() { + var pk = $(this).attr('pk'); + + // TODO: Migrate this form to the API forms + launchModalForm(`/order/sales-order/allocation/new/`, { + success: reloadTable, + data: { + line: pk, + }, + }); + }); + + // Callback for creating a new build + $(table).find('.button-build').click(function() { + var pk = $(this).attr('pk'); + + // Extract the row data from the table! + var idx = $(this).closest('tr').attr('data-index'); + + var row = $(table).bootstrapTable('getData')[idx]; + + var quantity = 1; + + if (row.allocated < row.quantity) { + quantity = row.quantity - row.allocated; + } + + // TODO: Migrate this to the API forms + launchModalForm(`/build/new/`, { + follow: true, + data: { + part: pk, + sales_order: options.order, + quantity: quantity, + }, + }); + }); + + // Callback for purchasing parts + $(table).find('.button-buy').click(function() { + var pk = $(this).attr('pk'); + + launchModalForm("{% url 'order-parts' %}", { + data: { + parts: [pk], + }, + }); + }); + + // Callback for displaying price + $(table).find('.button-price').click(function() { + var pk = $(this).attr('pk'); + var idx = $(this).closest('tr').attr('data-index'); + var row = $(table).bootstrapTable('getData')[idx]; + + launchModalForm( + "{% url 'line-pricing' %}", + { + submit_text: '{% trans "Calculate price" %}', + data: { + line_item: pk, + quantity: row.quantity, + }, + buttons: [{name: 'update_price', + title: '{% trans "Update Unit Price" %}'},], + success: reloadTable, + } + ); + }); + } + + $(table).inventreeTable({ + onPostBody: setupCallbacks, + name: 'salesorderlineitems', + sidePagination: 'server', + formatNoMatches: function() { + return '{% trans "No matching line items" %}'; + }, + queryParams: filters, + original: options.params, + url: options.url, + showFooter: true, + uniqueId: 'pk', + // detailView: show_detail, + // detailViewByClick: show_detail, + // detailFilter: function(index, row) { + // if (pending) { + // // Order is pending + // return row.allocated > 0; + // } else { + // return row.fulfilled > 0; + // } + // }, + // detailFormatter: function(index, row, element) { + // if (pending) { + // return showAllocationSubTable(index, row, element, options); + // } else { + // return showFulfilledSubTable(index, row, element, options); + // } + // }, + columns: columns, + }); +} From f9f8527ae5c7cbd8209828ed80c760b703c7735f Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 16:20:15 +1100 Subject: [PATCH 2/5] Fix build button - Now links into the API forms --- InvenTree/templates/js/translated/build.js | 14 +++++++++++++- InvenTree/templates/js/translated/order.js | 18 +++++++++--------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index aa68b26dd4..7b4d3c035a 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -42,6 +42,9 @@ function buildFormFields() { part_detail: true, } }, + sales_order: { + hidden: true, + }, batch: {}, target_date: {}, take_from: {}, @@ -76,23 +79,32 @@ function newBuildOrder(options={}) { var fields = buildFormFields(); + // Specify the target part if (options.part) { fields.part.value = options.part; } + // Specify the desired quantity if (options.quantity) { fields.quantity.value = options.quantity; } + // Specify the parent build order if (options.parent) { fields.parent.value = options.parent; } + // Specify a parent sales order + if (options.sales_order) { + fields.sales_order.value = options.sales_order; + } + constructForm(`/api/build/`, { fields: fields, follow: true, method: 'POST', - title: '{% trans "Create Build Order" %}' + title: '{% trans "Create Build Order" %}', + onSuccess: options.onSuccess, }); } diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index c9a5d92b5b..69b49d5598 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -580,12 +580,14 @@ function loadPurchaseOrderTable(table, options) { return '{% trans "No purchase orders found" %}'; }, columns: [ + /* { title: '', visible: true, checkbox: true, switchable: false, }, + */ { field: 'reference', title: '{% trans "Purchase Order" %}', @@ -1576,15 +1578,13 @@ function loadSalesOrderLineItemTable(table, options={}) { if (row.allocated < row.quantity) { quantity = row.quantity - row.allocated; } - - // TODO: Migrate this to the API forms - launchModalForm(`/build/new/`, { - follow: true, - data: { - part: pk, - sales_order: options.order, - quantity: quantity, - }, + + // Create a new build order + newBuildOrder({ + part: pk, + sales_order: options.order, + quantity: quantity, + success: reloadTable }); }); From 4d8bec9663018e776eb32798ba1076c985083c4a Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 16:38:13 +1100 Subject: [PATCH 3/5] Fix rendering of row sub tables --- InvenTree/order/serializers.py | 2 +- InvenTree/templates/js/translated/order.js | 64 ++++++++++++++-------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 3886bfd3a5..cc4812d1ca 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -549,7 +549,7 @@ class SOLineItemSerializer(InvenTreeModelSerializer): order_detail = SalesOrderSerializer(source='order', many=False, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) - allocations = SalesOrderAllocationSerializer(many=True, read_only=True) + allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True) quantity = serializers.FloatField() diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 69b49d5598..e596764902 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -580,14 +580,12 @@ function loadPurchaseOrderTable(table, options) { return '{% trans "No purchase orders found" %}'; }, columns: [ - /* { title: '', visible: true, checkbox: true, switchable: false, }, - */ { field: 'reference', title: '{% trans "Purchase Order" %}', @@ -1160,7 +1158,6 @@ function showAllocationSubTable(index, row, element, options) { showHeader: false, columns: [ { - width: '50%', field: 'allocated', title: '{% trans "Quantity" %}', formatter: function(value, row, index, field) { @@ -1179,12 +1176,24 @@ function showAllocationSubTable(index, row, element, options) { field: 'location', title: '{% trans "Location" %}', formatter: function(value, row, index, field) { - return renderLink(row.location_path, `/stock/location/${row.location}/`); + + // Location specified + if (row.location) { + return renderLink( + row.location_detail.pathstring || '{% trans "Location" %}', + `/stock/location/${row.location}/` + ); + } else { + return `{% trans "Stock location not specified" %}`; + } }, }, + // TODO: ?? What is 'po' field all about? + /* { field: 'po' }, + */ { field: 'buttons', title: '{% trans "Actions" %}', @@ -1332,16 +1341,19 @@ function loadSalesOrderLineItemTable(table, options={}) { // Table columns to display var columns = [ + /* { checkbox: true, visible: true, switchable: false, }, + */ { sortable: true, sortName: 'part__name', field: 'part', title: '{% trans "Part" %}', + switchable: false, formatter: function(value, row, index, field) { if (row.part) { return imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${value}/`); @@ -1356,7 +1368,8 @@ function loadSalesOrderLineItemTable(table, options={}) { { sortable: true, field: 'reference', - title: '{% trans "Reference" %}' + title: '{% trans "Reference" %}', + switchable: false, }, { sortable: true, @@ -1369,6 +1382,7 @@ function loadSalesOrderLineItemTable(table, options={}) { return sum + i }, 0) }, + switchable: false, }, { sortable: true, @@ -1402,11 +1416,12 @@ function loadSalesOrderLineItemTable(table, options={}) { title: '{% trans "In Stock" %}', formatter: function(value, row) { return row.part_detail.stock; - } + }, }, { field: 'allocated', title: pending ? '{% trans "Allocated" %}' : '{% trans "Fulfilled" %}', + switchable: false, formatter: function(value, row, index, field) { var quantity = pending ? row.allocated : row.fulfilled; @@ -1433,6 +1448,8 @@ function loadSalesOrderLineItemTable(table, options={}) { field: 'notes', title: '{% trans "Notes" %}', }, + // TODO: Re-introduce the "PO" field, once it is fixed + /* { field: 'po', title: '{% trans "PO" %}', @@ -1452,6 +1469,7 @@ function loadSalesOrderLineItemTable(table, options={}) { return `
` + po_name + `
`; } }, + */ ]; if (pending) { @@ -1633,23 +1651,23 @@ function loadSalesOrderLineItemTable(table, options={}) { url: options.url, showFooter: true, uniqueId: 'pk', - // detailView: show_detail, - // detailViewByClick: show_detail, - // detailFilter: function(index, row) { - // if (pending) { - // // Order is pending - // return row.allocated > 0; - // } else { - // return row.fulfilled > 0; - // } - // }, - // detailFormatter: function(index, row, element) { - // if (pending) { - // return showAllocationSubTable(index, row, element, options); - // } else { - // return showFulfilledSubTable(index, row, element, options); - // } - // }, + detailView: show_detail, + detailViewByClick: show_detail, + detailFilter: function(index, row) { + if (pending) { + // Order is pending + return row.allocated > 0; + } else { + return row.fulfilled > 0; + } + }, + detailFormatter: function(index, row, element) { + if (pending) { + return showAllocationSubTable(index, row, element, options); + } else { + return showFulfilledSubTable(index, row, element, options); + } + }, columns: columns, }); } From ac3a97d4d6b8a919a7ef404c8d276d47c2c61f40 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 16:47:21 +1100 Subject: [PATCH 4/5] Fix button callbacks for the allocation table --- InvenTree/templates/js/translated/order.js | 50 +++++++++++++--------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index e596764902..9d36b4799f 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -1153,7 +1153,36 @@ function showAllocationSubTable(index, row, element, options) { // Is the parent SalesOrder pending? var pending = options.status == {{ SalesOrderStatus.PENDING }}; + // Function to reload the allocation table + function reloadTable() { + table.bootstrapTable('refresh'); + } + + function setupCallbacks() { + // Add callbacks for 'edit' buttons + table.find('.button-allocation-edit').click(function() { + + var pk = $(this).attr('pk'); + + // TODO: Migrate to API forms + launchModalForm(`/order/sales-order/allocation/${pk}/edit/`, { + success: reloadTable, + }); + }); + + // Add callbacks for 'delete' buttons + table.find(".button-allocation-delete").click(function() { + var pk = $(this).attr('pk'); + + // TODO: Migrate to API forms + launchModalForm(`/order/sales-order/allocation/${pk}/delete/`, { + success: reloadTable, + }); + }); + } + table.bootstrapTable({ + onPostBody: setupCallbacks, data: row.allocations, showHeader: false, columns: [ @@ -1214,27 +1243,6 @@ function showAllocationSubTable(index, row, element, options) { }, ], }); - - // Add callbacks for 'edit' buttons - table.find(".button-allocation-edit").click(function() { - - var pk = $(this).attr('pk'); - - // TODO: Migrate to API forms - launchModalForm(`/order/sales-order/allocation/${pk}/edit/`, { - success: reloadTable, - }); - }); - - // Add callbacks for 'delete' buttons - table.find(".button-allocation-delete").click(function() { - var pk = $(this).attr('pk'); - - // TODO: Migrate to API forms - launchModalForm(`/order/sales-order/allocation/${pk}/delete/`, { - success: reloadTable, - }); - }); } /** From 21be5b4a263ffea85d90a2c011ec37b598aa4915 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 16:52:56 +1100 Subject: [PATCH 5/5] JS linting --- InvenTree/templates/js/translated/order.js | 159 ++++++++++++--------- 1 file changed, 89 insertions(+), 70 deletions(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 9d36b4799f..f5d9117cbf 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -1146,8 +1146,6 @@ function showAllocationSubTable(index, row, element, options) { element.html(html); - var lineItem = row; - var table = $(`#allocation-table-${row.pk}`); // Is the parent SalesOrder pending? @@ -1171,7 +1169,7 @@ function showAllocationSubTable(index, row, element, options) { }); // Add callbacks for 'delete' buttons - table.find(".button-allocation-delete").click(function() { + table.find('.button-allocation-delete').click(function() { var pk = $(this).attr('pk'); // TODO: Migrate to API forms @@ -1186,61 +1184,61 @@ function showAllocationSubTable(index, row, element, options) { data: row.allocations, showHeader: false, columns: [ - { - field: 'allocated', - title: '{% trans "Quantity" %}', - formatter: function(value, row, index, field) { - var text = ''; + { + field: 'allocated', + title: '{% trans "Quantity" %}', + formatter: function(value, row, index, field) { + var text = ''; - if (row.serial != null && row.quantity == 1) { - text = `{% trans "Serial Number" %}: ${row.serial}`; - } else { - text = `{% trans "Quantity" %}: ${row.quantity}`; - } + if (row.serial != null && row.quantity == 1) { + text = `{% trans "Serial Number" %}: ${row.serial}`; + } else { + text = `{% trans "Quantity" %}: ${row.quantity}`; + } - return renderLink(text, `/stock/item/${row.item}/`); + return renderLink(text, `/stock/item/${row.item}/`); + }, }, - }, - { - field: 'location', - title: '{% trans "Location" %}', - formatter: function(value, row, index, field) { + { + field: 'location', + title: '{% trans "Location" %}', + formatter: function(value, row, index, field) { - // Location specified - if (row.location) { - return renderLink( - row.location_detail.pathstring || '{% trans "Location" %}', - `/stock/location/${row.location}/` - ); - } else { - return `{% trans "Stock location not specified" %}`; - } + // Location specified + if (row.location) { + return renderLink( + row.location_detail.pathstring || '{% trans "Location" %}', + `/stock/location/${row.location}/` + ); + } else { + return `{% trans "Stock location not specified" %}`; + } + }, }, - }, - // TODO: ?? What is 'po' field all about? - /* - { - field: 'po' - }, - */ - { - field: 'buttons', - title: '{% trans "Actions" %}', - formatter: function(value, row, index, field) { - - var html = `
`; - var pk = row.pk; - - if (pending) { - html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); - html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); - } - - html += "
"; - - return html; + // TODO: ?? What is 'po' field all about? + /* + { + field: 'po' + }, + */ + { + field: 'buttons', + title: '{% trans "Actions" %}', + formatter: function(value, row, index, field) { + + var html = `
`; + var pk = row.pk; + + if (pending) { + html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); + html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); + } + + html += "
"; + + return html; + }, }, - }, ], }); } @@ -1265,10 +1263,8 @@ function showFulfilledSubTable(index, row, element, options) { element.html(html); - var lineItem = row; - $(`#${id}`).bootstrapTable({ - url: "{% url 'api-stock-list' %}", + url: '{% url "api-stock-list" %}', queryParams: { part: row.part, sales_order: options.order, @@ -1292,9 +1288,11 @@ function showFulfilledSubTable(index, row, element, options) { return renderLink(text, `/stock/item/${row.pk}/`); }, }, + /* { field: 'po' }, + */ ], }); } @@ -1313,12 +1311,12 @@ function loadSalesOrderLineItemTable(table, options={}) { options.params = options.params || {}; if (!options.order) { - console.log("ERROR: function called without order ID"); + console.log('ERROR: function called without order ID'); return; } if (!options.status) { - console.log("ERROR: function called without order status"); + console.log('ERROR: function called without order status'); return; } @@ -1405,18 +1403,33 @@ function loadSalesOrderLineItemTable(table, options={}) { title: '{% trans "Total price" %}', formatter: function(value, row) { var total = row.sale_price * row.quantity; - var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.sale_price_currency}); - return formatter.format(total) + var formatter = new Intl.NumberFormat( + 'en-US', + { + style: 'currency', + currency: row.sale_price_currency + } + ); + return formatter.format(total); }, footerFormatter: function(data) { var total = data.map(function (row) { - return +row['sale_price']*row['quantity'] + 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) + 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); } }, { @@ -1618,9 +1631,11 @@ function loadSalesOrderLineItemTable(table, options={}) { $(table).find('.button-buy').click(function() { var pk = $(this).attr('pk'); - launchModalForm("{% url 'order-parts' %}", { + launchModalForm('{% url "order-parts" %}', { data: { - parts: [pk], + parts: [ + pk + ], }, }); }); @@ -1632,15 +1647,19 @@ function loadSalesOrderLineItemTable(table, options={}) { var row = $(table).bootstrapTable('getData')[idx]; launchModalForm( - "{% url 'line-pricing' %}", + '{% url "line-pricing" %}', { submit_text: '{% trans "Calculate price" %}', data: { line_item: pk, quantity: row.quantity, }, - buttons: [{name: 'update_price', - title: '{% trans "Update Unit Price" %}'},], + buttons: [ + { + name: 'update_price', + title: '{% trans "Update Unit Price" %}' + }, + ], success: reloadTable, } );