From 4d2cf233b3b0859ef11f105c9417d9dbf8e57af3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 16:14:39 +1100 Subject: [PATCH 01/29] 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 02/29] 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 03/29] 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 04/29] 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 05/29] 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, } ); From 0b7811b7e053bd106505039f806fb4ba6be44c9d Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 16:54:40 +1100 Subject: [PATCH 06/29] Add API endpoint for SalesOrderAllocation model - Increment API version number --- InvenTree/InvenTree/version.py | 8 ++++++-- InvenTree/order/api.py | 9 +++++++++ InvenTree/order/serializers.py | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 03ee877cb2..39d071e182 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -10,12 +10,16 @@ import common.models INVENTREE_SW_VERSION = "0.6.0 dev" -INVENTREE_API_VERSION = 14 +INVENTREE_API_VERSION = 15 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about -v14 -> 2021-20-05 +v15 -> 2021-10-06 + - Adds detail endpoint for SalesOrderAllocation model + - Allows use of the API forms interface for adjusting SalesOrderAllocation objects + +v14 -> 2021-10-05 - Stock adjustment actions API is improved, using native DRF serializer support - However adjustment actions now only support 'pk' as a lookup field diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 26e6ed3546..af52fd0dec 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -631,6 +631,15 @@ class SOLineItemDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = SOLineItemSerializer +class SOAllocationDetail(generics.RetrieveUpdateDestroyAPIView): + """ + API endpoint for detali view of a SalesOrderAllocation object + """ + + queryset = SalesOrderAllocation.objects.all() + serializer_class = SalesOrderAllocationSerializer + + class SOAllocationList(generics.ListCreateAPIView): """ API endpoint for listing SalesOrderAllocation objects diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index cc4812d1ca..40cd2def58 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -478,7 +478,7 @@ class SalesOrderAllocationSerializer(InvenTreeModelSerializer): part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True) order = serializers.PrimaryKeyRelatedField(source='line.order', many=False, read_only=True) serial = serializers.CharField(source='get_serial', read_only=True) - quantity = serializers.FloatField(read_only=True) + quantity = serializers.FloatField(read_only=False) location = serializers.PrimaryKeyRelatedField(source='item.location', many=False, read_only=True) # Extra detail fields From e055902f3e398c731e6a4c0a8726d6a402bc8773 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 16:58:00 +1100 Subject: [PATCH 07/29] More JS linting --- InvenTree/templates/js/translated/order.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index f5d9117cbf..c6e5ebf94b 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -1234,7 +1234,7 @@ function showAllocationSubTable(index, row, element, options) { html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); } - html += ""; + html += ''; return html; }, @@ -1367,8 +1367,8 @@ function loadSalesOrderLineItemTable(table, options={}) { return '-'; } }, - footerFormatter: function() { - return '{% trans "Total" %}' + footerFormatter: function() { + return '{% trans "Total" %}'; }, }, { @@ -1382,11 +1382,11 @@ function loadSalesOrderLineItemTable(table, options={}) { field: 'quantity', title: '{% trans "Quantity" %}', footerFormatter: function(data) { - return data.map(function (row) { - return +row['quantity'] - }).reduce(function (sum, i) { - return sum + i - }, 0) + return data.map(function(row) { + return +row['quantity']; + }).reduce(function(sum, i) { + return sum + i; + }, 0); }, switchable: false, }, @@ -1410,12 +1410,13 @@ function loadSalesOrderLineItemTable(table, options={}) { currency: row.sale_price_currency } ); + return formatter.format(total); }, footerFormatter: function(data) { - var total = data.map(function (row) { + var total = data.map(function(row) { return +row['sale_price'] * row['quantity']; - }).reduce(function (sum, i) { + }).reduce(function(sum, i) { return sum + i; }, 0); From b8b4c60c4359fb20ef18219c88cfb284c28804b0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 17:02:03 +1100 Subject: [PATCH 08/29] Fix API endpoints for so-allocation detail --- InvenTree/order/api.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index af52fd0dec..af30a3a5c5 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -752,8 +752,10 @@ order_api_urls = [ ])), # API endpoints for purchase order line items - url(r'^po-line/(?P\d+)/$', POLineItemDetail.as_view(), name='api-po-line-detail'), - url(r'^po-line/$', POLineItemList.as_view(), name='api-po-line-list'), + url(r'^po-line/', include([ + url(r'^(?P\d+)/$', POLineItemDetail.as_view(), name='api-po-line-detail'), + url(r'^.*$', POLineItemList.as_view(), name='api-po-line-list'), + ])), # API endpoints for sales ordesr url(r'^so/', include([ @@ -773,9 +775,8 @@ order_api_urls = [ ])), # API endpoints for sales order allocations - url(r'^so-allocation', include([ - - # List all sales order allocations + url(r'^so-allocation/', include([ + url(r'^(?P\d+)/$', SOAllocationDetail.as_view(), name='api-so-allocation-detail'), url(r'^.*$', SOAllocationList.as_view(), name='api-so-allocation-list'), ])), ] From 87775ae16b42a353ebb7046cb9db8e3f90ed5a46 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 17:02:27 +1100 Subject: [PATCH 09/29] Brace yourseflf --- InvenTree/templates/js/translated/order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index c6e5ebf94b..38554f8fcb 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -1431,7 +1431,7 @@ function loadSalesOrderLineItemTable(table, options={}) { ); return formatter.format(total); - } + } }, { field: 'stock', From 4d1367445250cf540e3fdfacdb78e383bc7515b4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 17:15:24 +1100 Subject: [PATCH 10/29] Migrate views to use the API forms architecture: - SalesOrderAllocationEdit - SalesOrderAllocationDelete --- .../templates/order/so_allocation_delete.html | 14 ------- InvenTree/order/urls.py | 4 -- InvenTree/order/views.py | 24 ----------- InvenTree/templates/js/translated/order.js | 40 ++++++++++++++----- 4 files changed, 31 insertions(+), 51 deletions(-) delete mode 100644 InvenTree/order/templates/order/so_allocation_delete.html diff --git a/InvenTree/order/templates/order/so_allocation_delete.html b/InvenTree/order/templates/order/so_allocation_delete.html deleted file mode 100644 index 34cf20083b..0000000000 --- a/InvenTree/order/templates/order/so_allocation_delete.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "modal_delete_form.html" %} -{% load i18n %} -{% load inventree_extras %} - -{% block pre_form_content %} -
- {% trans "This action will unallocate the following stock from the Sales Order" %}: -
- - {% decimal allocation.get_allocated %} x {{ allocation.line.part.full_name }} - {% if allocation.item.location %} ({{ allocation.get_location }}){% endif %} - -
-{% endblock %} \ No newline at end of file diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 5ea9a56867..5cdcd3f18d 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -45,10 +45,6 @@ sales_order_urls = [ url(r'^allocation/', include([ url(r'^new/', views.SalesOrderAllocationCreate.as_view(), name='so-allocation-create'), url(r'^assign-serials/', views.SalesOrderAssignSerials.as_view(), name='so-assign-serials'), - url(r'(?P\d+)/', include([ - url(r'^edit/', views.SalesOrderAllocationEdit.as_view(), name='so-allocation-edit'), - url(r'^delete/', views.SalesOrderAllocationDelete.as_view(), name='so-allocation-delete'), - ])), ])), # Display detail view for a single SalesOrder diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 8a5e709926..5d1a4aed76 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -1051,30 +1051,6 @@ class SalesOrderAllocationCreate(AjaxCreateView): return form -class SalesOrderAllocationEdit(AjaxUpdateView): - - model = SalesOrderAllocation - form_class = order_forms.EditSalesOrderAllocationForm - ajax_form_title = _('Edit Allocation Quantity') - - def get_form(self): - form = super().get_form() - - # Prevent the user from editing particular fields - form.fields.pop('item') - form.fields.pop('line') - - return form - - -class SalesOrderAllocationDelete(AjaxDeleteView): - - model = SalesOrderAllocation - ajax_form_title = _("Remove allocation") - context_object_name = 'allocation' - ajax_template_name = "order/so_allocation_delete.html" - - class LineItemPricing(PartPricing): """ View for inspecting part pricing information """ diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index f5d9117cbf..771490b134 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -532,6 +532,7 @@ function editPurchaseOrderLineItem(e) { var url = $(src).attr('url'); + // TODO: Migrate this to the API forms launchModalForm(url, { reload: true, }); @@ -547,7 +548,8 @@ function removePurchaseOrderLineItem(e) { var src = e.target || e.srcElement; var url = $(src).attr('url'); - + + // TODO: Migrate this to the API forms launchModalForm(url, { reload: true, }); @@ -1162,20 +1164,38 @@ function showAllocationSubTable(index, row, element, options) { var pk = $(this).attr('pk'); - // TODO: Migrate to API forms - launchModalForm(`/order/sales-order/allocation/${pk}/edit/`, { - success: reloadTable, - }); + // Edit the sales order alloction + constructForm( + `/api/order/so-allocation/${pk}/`, + { + fields: { + quantity: {}, + }, + title: '{% trans "Edit Stock Allocation" %}', + onSuccess: function() { + // Refresh the parent table + $(options.table).bootstrapTable('refresh'); + }, + }, + ); }); // 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, - }); + constructForm( + `/api/order/so-allocation/${pk}/`, + { + method: 'DELETE', + confirmMessage: '{% trans "Confirm Delete Operation" %}', + title: '{% trans "Delete Stock Allocation" %}', + onSuccess: function() { + // Refresh the parent table + $(options.table).bootstrapTable('refresh'); + } + } + ); }); } @@ -1308,6 +1328,8 @@ function showFulfilledSubTable(index, row, element, options) { */ function loadSalesOrderLineItemTable(table, options={}) { + options.table = table; + options.params = options.params || {}; if (!options.order) { From b5c5842e7d77498788cc7aa54e97c6aceffdc7b4 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 17:26:48 +1100 Subject: [PATCH 11/29] Add ability to create a new sales order allocation using the API forms framework --- InvenTree/templates/js/translated/order.js | 40 ++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 771490b134..62f68f6f40 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -1616,13 +1616,41 @@ function loadSalesOrderLineItemTable(table, options={}) { $(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, + var line_item = $(table).bootstrapTable('getRowByUniqueId', pk); + + var fields = { + // SalesOrderLineItem reference + line: { + hidden: true, + value: pk, }, - }); + item: { + filters: { + part_detail: true, + location_detail: true, + in_stock: true, + part: line_item.part, + // TODO: Exclude items already allocated to this sales order + } + }, + quantity: { + }, + }; + + // Exclude expired stock? + if (global_settings.STOCK_ENABLE_EXPIRY && !global_settings.STOCK_ALLOW_EXPIRED_SALE) { + fields.item.filters.expired = false; + } + + constructForm( + `/api/order/so-allocation/`, + { + method: 'POST', + fields: fields, + title: '{% trans "Allocate Stock Item" %}', + onSuccess: reloadTable, + } + ); }); // Callback for creating a new build From 336b05aa4eff2141274b5d08a94ebe537b6923f6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 17:42:00 +1100 Subject: [PATCH 12/29] Exclude sales order allocations when searching stock --- InvenTree/order/models.py | 14 +++++++++-- InvenTree/stock/api.py | 28 ++++++++++++++++++++++ InvenTree/templates/js/translated/order.js | 2 +- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 495ea2d333..4ac8925259 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -840,7 +840,13 @@ class SalesOrderLineItem(OrderLineItem): def get_api_url(): return reverse('api-so-line-list') - order = models.ForeignKey(SalesOrder, on_delete=models.CASCADE, related_name='lines', verbose_name=_('Order'), help_text=_('Sales Order')) + order = models.ForeignKey( + SalesOrder, + on_delete=models.CASCADE, + related_name='lines', + verbose_name=_('Order'), + help_text=_('Sales Order') + ) part = models.ForeignKey('part.Part', on_delete=models.SET_NULL, related_name='sales_order_line_items', null=True, verbose_name=_('Part'), help_text=_('Part'), limit_choices_to={'salable': True}) @@ -954,7 +960,11 @@ class SalesOrderAllocation(models.Model): if len(errors) > 0: raise ValidationError(errors) - line = models.ForeignKey(SalesOrderLineItem, on_delete=models.CASCADE, verbose_name=_('Line'), related_name='allocations') + line = models.ForeignKey( + SalesOrderLineItem, + on_delete=models.CASCADE, + verbose_name=_('Line'), + related_name='allocations') item = models.ForeignKey( 'stock.StockItem', diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index ad487c7a5a..34045bda43 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -6,6 +6,8 @@ JSON API for the Stock app from __future__ import unicode_literals from datetime import datetime, timedelta +from django.db.models import query +from django.db.models.query import QuerySet from django.utils.translation import ugettext_lazy as _ @@ -34,6 +36,7 @@ from company.models import Company, SupplierPart from company.serializers import CompanySerializer, SupplierPartSerializer from order.models import PurchaseOrder +from order.models import SalesOrder, SalesOrderAllocation from order.serializers import POSerializer import common.settings @@ -645,6 +648,31 @@ class StockList(generics.ListCreateAPIView): # Filter StockItem without build allocations or sales order allocations queryset = queryset.filter(Q(sales_order_allocations__isnull=True) & Q(allocations__isnull=True)) + # Exclude StockItems which are already allocated to a particular SalesOrder + exclude_so_allocation = params.get('exclude_so_allocation', None) + + if exclude_so_allocation is not None: + + try: + order = SalesOrder.objects.get(pk=exclude_so_allocation) + + # Grab all the active SalesOrderAllocations for this order + allocations = SalesOrderAllocation.objects.filter( + line__pk__in=[ + line.pk for line in order.lines.all() + ] + ) + + # Exclude any stock item which is already allocated to the sales order + queryset = queryset.exclude( + pk__in=[ + a.item.pk for a in allocations + ] + ) + + except (ValueError, SalesOrder.DoesNotExist): + pass + # Does the client wish to filter by the Part ID? part_id = params.get('part', None) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 7bee488b75..870ac9150d 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -1631,7 +1631,7 @@ function loadSalesOrderLineItemTable(table, options={}) { location_detail: true, in_stock: true, part: line_item.part, - // TODO: Exclude items already allocated to this sales order + exclude_so_allocation: options.order, } }, quantity: { From 5cdf5a26700d3c9d2d3d289dbc9af0b92d1837d2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 20:16:59 +1100 Subject: [PATCH 13/29] Only include "in stocK" field if the sales order is pending --- InvenTree/templates/js/translated/order.js | 28 +++++++++++++--------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index 870ac9150d..a595a61d7c 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -1455,13 +1455,21 @@ function loadSalesOrderLineItemTable(table, options={}) { return formatter.format(total); } }, - { - field: 'stock', - title: '{% trans "In Stock" %}', - formatter: function(value, row) { - return row.part_detail.stock; + ]; + + if (pending) { + columns.push( + { + field: 'stock', + title: '{% trans "In Stock" %}', + formatter: function(value, row) { + return row.part_detail.stock; + }, }, - }, + ); + } + + columns.push( { field: 'allocated', title: pending ? '{% trans "Allocated" %}' : '{% trans "Fulfilled" %}', @@ -1492,7 +1500,9 @@ function loadSalesOrderLineItemTable(table, options={}) { field: 'notes', title: '{% trans "Notes" %}', }, - // TODO: Re-introduce the "PO" field, once it is fixed + ); + + // TODO: Re-introduce the "PO" field, once it is fixed /* { field: 'po', @@ -1514,7 +1524,6 @@ function loadSalesOrderLineItemTable(table, options={}) { } }, */ - ]; if (pending) { columns.push({ @@ -1553,9 +1562,6 @@ function loadSalesOrderLineItemTable(table, options={}) { return html; } }); - } else { - // Remove the "in stock" column - delete columns['stock']; } function reloadTable() { From 535d45bce4997e9ec480a8c32d0725da774a9b9a Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 20:19:29 +1100 Subject: [PATCH 14/29] Remove outdated Views / Forms --- InvenTree/order/forms.py | 17 --------- InvenTree/order/urls.py | 1 - InvenTree/order/views.py | 78 +--------------------------------------- InvenTree/stock/api.py | 2 -- 4 files changed, 1 insertion(+), 97 deletions(-) diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index 87e042f4f3..227109c46c 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -115,23 +115,6 @@ class AllocateSerialsToSalesOrderForm(forms.Form): ] -class CreateSalesOrderAllocationForm(HelperForm): - """ - Form for creating a SalesOrderAllocation item. - """ - - quantity = RoundingDecimalFormField(max_digits=10, decimal_places=5, label=_('Quantity')) - - class Meta: - model = SalesOrderAllocation - - fields = [ - 'line', - 'item', - 'quantity', - ] - - class EditSalesOrderAllocationForm(HelperForm): """ Form for editing a SalesOrderAllocation item diff --git a/InvenTree/order/urls.py b/InvenTree/order/urls.py index 5cdcd3f18d..37433e02f0 100644 --- a/InvenTree/order/urls.py +++ b/InvenTree/order/urls.py @@ -43,7 +43,6 @@ sales_order_detail_urls = [ sales_order_urls = [ # URLs for sales order allocations url(r'^allocation/', include([ - url(r'^new/', views.SalesOrderAllocationCreate.as_view(), name='so-allocation-create'), url(r'^assign-serials/', views.SalesOrderAssignSerials.as_view(), name='so-assign-serials'), ])), diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index 5d1a4aed76..5bb6d161b6 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -29,7 +29,6 @@ from company.models import Company, SupplierPart # ManufacturerPart from stock.models import StockItem from part.models import Part -from common.models import InvenTreeSetting from common.forms import UploadFileForm, MatchFieldForm from common.views import FileManagementFormView from common.files import FileManager @@ -37,7 +36,7 @@ from common.files import FileManager from . import forms as order_forms from part.views import PartPricing -from InvenTree.views import AjaxView, AjaxCreateView, AjaxUpdateView, AjaxDeleteView +from InvenTree.views import AjaxView, AjaxUpdateView from InvenTree.helpers import DownloadFile, str2bool from InvenTree.helpers import extract_serial_numbers from InvenTree.views import InvenTreeRoleMixin @@ -976,81 +975,6 @@ class SalesOrderAssignSerials(AjaxView, FormMixin): ) -class SalesOrderAllocationCreate(AjaxCreateView): - """ View for creating a new SalesOrderAllocation """ - - model = SalesOrderAllocation - form_class = order_forms.CreateSalesOrderAllocationForm - ajax_form_title = _('Allocate Stock to Order') - - def get_initial(self): - initials = super().get_initial().copy() - - line_id = self.request.GET.get('line', None) - - if line_id is not None: - line = SalesOrderLineItem.objects.get(pk=line_id) - - initials['line'] = line - - # Search for matching stock items, pre-fill if there is only one - items = StockItem.objects.filter(part=line.part) - - quantity = line.quantity - line.allocated_quantity() - - if quantity < 0: - quantity = 0 - - if items.count() == 1: - item = items.first() - initials['item'] = item - - # Reduce the quantity IF there is not enough stock - qmax = item.quantity - item.allocation_count() - - if qmax < quantity: - quantity = qmax - - initials['quantity'] = quantity - - return initials - - def get_form(self): - - form = super().get_form() - - line_id = form['line'].value() - - # If a line item has been specified, reduce the queryset for the stockitem accordingly - try: - line = SalesOrderLineItem.objects.get(pk=line_id) - - # Construct a queryset for allowable stock items - queryset = StockItem.objects.filter(StockItem.IN_STOCK_FILTER) - - # Ensure the part reference matches - queryset = queryset.filter(part=line.part) - - # Exclude StockItem which are already allocated to this order - allocated = [allocation.item.pk for allocation in line.allocations.all()] - - queryset = queryset.exclude(pk__in=allocated) - - # Exclude stock items which have expired - if not InvenTreeSetting.get_setting('STOCK_ALLOW_EXPIRED_SALE'): - queryset = queryset.exclude(StockItem.EXPIRED_FILTER) - - form.fields['item'].queryset = queryset - - # Hide the 'line' field - form.fields['line'].widget = HiddenInput() - - except (ValueError, SalesOrderLineItem.DoesNotExist): - pass - - return form - - class LineItemPricing(PartPricing): """ View for inspecting part pricing information """ diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 34045bda43..d848f0e6b9 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -6,8 +6,6 @@ JSON API for the Stock app from __future__ import unicode_literals from datetime import datetime, timedelta -from django.db.models import query -from django.db.models.query import QuerySet from django.utils.translation import ugettext_lazy as _ From 5b94e24af14da32779e3507b799f3b984f228363 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 20:23:42 +1100 Subject: [PATCH 15/29] Fix docs link for release versions - Use full x.y.z format - Requires a docs release for every code relesase --- InvenTree/InvenTree/version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 03ee877cb2..3ad28ab11b 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -104,7 +104,7 @@ def inventreeDocsVersion(): Return the version string matching the latest documentation. Development -> "latest" - Release -> "major.minor" + Release -> "major.minor.sub" e.g. "0.5.2" """ @@ -113,7 +113,7 @@ def inventreeDocsVersion(): else: major, minor, patch = inventreeVersionTuple() - return f"{major}.{minor}" + return f"{major}.{minor}.{patch}" def isInvenTreeUpToDate(): From a8388b09991953192c658c10322518f86ae61cd9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 20:35:51 +1100 Subject: [PATCH 16/29] JS linting --- InvenTree/templates/js/translated/order.js | 28 ---------------------- 1 file changed, 28 deletions(-) diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index a595a61d7c..f4b1d0b997 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -1153,11 +1153,6 @@ 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() { @@ -1502,29 +1497,6 @@ function loadSalesOrderLineItemTable(table, options={}) { }, ); - // TODO: Re-introduce the "PO" field, once it is fixed - /* - { - 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', From bc3be635aee75517e2f275eec7c401b7cd09acfb Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 21:03:40 +1100 Subject: [PATCH 17/29] Reset modal image when a new image is uploaded --- InvenTree/company/templates/company/company_base.html | 6 ++++++ InvenTree/part/templates/part/part_base.html | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/InvenTree/company/templates/company/company_base.html b/InvenTree/company/templates/company/company_base.html index e4ca64b32e..b05a21304d 100644 --- a/InvenTree/company/templates/company/company_base.html +++ b/InvenTree/company/templates/company/company_base.html @@ -158,6 +158,12 @@ function reloadImage(data) { if (data.image) { $('#company-image').attr('src', data.image); + + // Reset the "modal image" view + $('#company-image').click(function() { + showModalImage(data.image); + }); + } else { location.reload(); } diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index a81f918013..c1542ac13b 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -328,6 +328,12 @@ // If image / thumbnail data present, live update if (data.image) { $('#part-image').attr('src', data.image); + + // Reset the "modal image" view + $('#part-thumb').click(function() { + showModalImage(data.image); + }); + } else { // Otherwise, reload the page location.reload(); From 80d8645a0c79ce1265ea3aec09594af8ad3e5d39 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 21:20:40 +1100 Subject: [PATCH 18/29] Adds a new task to periodically remove old error messages --- InvenTree/InvenTree/apps.py | 6 ++++++ InvenTree/InvenTree/tasks.py | 37 +++++++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py index 6456c5994f..31a887d736 100644 --- a/InvenTree/InvenTree/apps.py +++ b/InvenTree/InvenTree/apps.py @@ -63,6 +63,12 @@ class InvenTreeConfig(AppConfig): schedule_type=Schedule.DAILY, ) + # Delete old error messages + InvenTree.tasks.schedule_task( + 'InvenTree.tasks.delete_old_error_logs', + schedule_type=Schedule.DAILY, + ) + # Delete "old" stock items InvenTree.tasks.schedule_task( 'stock.tasks.delete_old_stock_items', diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 5fb6960601..3889f108af 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -156,7 +156,34 @@ def delete_successful_tasks(): started__lte=threshold ) - results.delete() + if results.count() > 0: + logger.info(f"Deleting {results.count()} successful task records") + results.delete() + + +def delete_old_error_logs(): + """ + Delete old error logs from the server + """ + + try: + from error_report.models import Error + + # Delete any error logs more than 30 days old + threshold = timezone.now() - timedelta(days=30) + + errors = Error.objects.filter( + when__lte=threshold, + ) + + if errors.count() > 0: + logger.info(f"Deleting {errors.count()} old error logs") + errors.delete() + + except AppRegistryNotReady: + # Apps not yet loaded + logger.info("Could not perform 'delete_old_error_logs' - App registry not ready") + return def check_for_updates(): @@ -215,7 +242,7 @@ def delete_expired_sessions(): # Delete any sessions that expired more than a day ago expired = Session.objects.filter(expire_date__lt=timezone.now() - timedelta(days=1)) - if True or expired.count() > 0: + if expired.count() > 0: logger.info(f"Deleting {expired.count()} expired sessions.") expired.delete() @@ -247,15 +274,15 @@ def update_exchange_rates(): pass except: # Some other error - print("Database not ready") + logger.warning("update_exchange_rates: Database not ready") return backend = InvenTreeExchange() - print(f"Updating exchange rates from {backend.url}") + logger.info(f"Updating exchange rates from {backend.url}") base = currency_code_default() - print(f"Using base currency '{base}'") + logger.info(f"Using base currency '{base}'") backend.update_rates(base_currency=base) From 166af3592d9969460a65fd5445930df99b705880 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 22:12:59 +1100 Subject: [PATCH 19/29] Add optional display of part stock quantity in forms --- InvenTree/InvenTree/static/css/inventree.css | 7 +++++++ InvenTree/common/models.py | 1 - .../js/translated/model_renderers.js | 19 ++++++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 585c0b3825..d3f17074fa 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -242,6 +242,13 @@ border-color: var(--label-red); } +.label-form { + margin: 2px; + padding-left: 4px; + padding-right: 4px; + border-radius: 5px; +} + .label-red { background: var(--label-red); } diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index aed6f2bf14..fd64e2a042 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -648,7 +648,6 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'validator': bool, }, - # TODO: Remove this setting in future, new API forms make this not useful 'PART_SHOW_QUANTITY_IN_FORMS': { 'name': _('Show Quantity in Forms'), 'description': _('Display available part quantity in some forms'), diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js index 0c3dabc27e..37d236f41e 100644 --- a/InvenTree/templates/js/translated/model_renderers.js +++ b/InvenTree/templates/js/translated/model_renderers.js @@ -159,7 +159,24 @@ function renderPart(name, data, parameters, options) { html += ` - ${data.description}`; } - html += `{% trans "Part ID" %}: ${data.pk}`; + var stock = ''; + + // Display available part quantity + if (global_settings.PART_SHOW_QUANTITY_IN_FORMS) { + if (data.in_stock == 0) { + stock = `{% trans "No Stock" %}`; + } else { + stock = `{% trans "In Stock" %}: ${data.in_stock}`; + } + } + + html += ` + + + ${stock} + {% trans "Part ID" %}: ${data.pk} + + `; return html; } From d9fddf64f1f2261ee78d6bbdc2f2ff82617eb125 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 6 Oct 2021 22:22:20 +1100 Subject: [PATCH 20/29] Make this setting a per-user setting --- InvenTree/InvenTree/static/css/inventree.css | 5 +++-- InvenTree/common/models.py | 14 ++++++------ .../templates/InvenTree/settings/navbar.html | 6 +++++ .../templates/InvenTree/settings/part.html | 1 - .../InvenTree/settings/settings.html | 1 + .../InvenTree/settings/user_forms.html | 22 +++++++++++++++++++ .../js/translated/model_renderers.js | 2 +- 7 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 InvenTree/templates/InvenTree/settings/user_forms.html diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index d3f17074fa..71e518560b 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -244,8 +244,9 @@ .label-form { margin: 2px; - padding-left: 4px; - padding-right: 4px; + padding: 3px; + padding-left: 10px; + padding-right: 10px; border-radius: 5px; } diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index fd64e2a042..41bfa596ed 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -648,13 +648,6 @@ class InvenTreeSetting(BaseInvenTreeSetting): 'validator': bool, }, - 'PART_SHOW_QUANTITY_IN_FORMS': { - 'name': _('Show Quantity in Forms'), - 'description': _('Display available part quantity in some forms'), - 'default': True, - 'validator': bool, - }, - 'PART_SHOW_IMPORT': { 'name': _('Show Import in Views'), 'description': _('Display the import wizard in some part views'), @@ -969,6 +962,13 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): 'default': 10, 'validator': [int, MinValueValidator(1)] }, + + 'PART_SHOW_QUANTITY_IN_FORMS': { + 'name': _('Show Quantity in Forms'), + 'description': _('Display available part quantity in some forms'), + 'default': True, + 'validator': bool, + }, } class Meta: diff --git a/InvenTree/templates/InvenTree/settings/navbar.html b/InvenTree/templates/InvenTree/settings/navbar.html index ebf24bffb1..095e616f5d 100644 --- a/InvenTree/templates/InvenTree/settings/navbar.html +++ b/InvenTree/templates/InvenTree/settings/navbar.html @@ -42,6 +42,12 @@ +
  • + + {% trans "Forms" %} + +
  • + {% endblock %} @@ -196,4 +201,8 @@ $('#print-order-report').click(function() { printSalesOrderReports([{{ order.pk }}]); }); +$('#export-order').click(function() { + location.href = '{% url "so-export" order.id %}'; +}); + {% endblock %} \ No newline at end of file From 64cf916c5080a2f751b7603b97215c195a599815 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 7 Oct 2021 13:33:10 +1100 Subject: [PATCH 25/29] Add a form to select export format --- .../order/templates/order/order_base.html | 8 ++-- .../templates/order/sales_order_base.html | 2 +- InvenTree/templates/js/dynamic/inventree.js | 26 +++++++++++ InvenTree/templates/js/translated/order.js | 44 +++++++++++++++++++ InvenTree/templates/js/translated/stock.js | 19 +------- 5 files changed, 76 insertions(+), 23 deletions(-) diff --git a/InvenTree/order/templates/order/order_base.html b/InvenTree/order/templates/order/order_base.html index 6a50497a94..8b98755900 100644 --- a/InvenTree/order/templates/order/order_base.html +++ b/InvenTree/order/templates/order/order_base.html @@ -39,6 +39,9 @@ src="{% static 'img/blank_image.png' %}" + {% if roles.purchase_order.change %} {% endif %} {% endif %} - {% endblock %} @@ -224,7 +224,7 @@ $("#cancel-order").click(function() { }); $("#export-order").click(function() { - location.href = '{% url "po-export" order.id %}'; + exportOrder('{% url "po-export" order.id %}'); }); diff --git a/InvenTree/order/templates/order/sales_order_base.html b/InvenTree/order/templates/order/sales_order_base.html index 63f3b037ed..3fd34e42b9 100644 --- a/InvenTree/order/templates/order/sales_order_base.html +++ b/InvenTree/order/templates/order/sales_order_base.html @@ -202,7 +202,7 @@ $('#print-order-report').click(function() { }); $('#export-order').click(function() { - location.href = '{% url "so-export" order.id %}'; + exportOrder('{% url "so-export" order.id %}'); }); {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/js/dynamic/inventree.js b/InvenTree/templates/js/dynamic/inventree.js index 5cd7bfcb80..1821c5ef70 100644 --- a/InvenTree/templates/js/dynamic/inventree.js +++ b/InvenTree/templates/js/dynamic/inventree.js @@ -10,6 +10,7 @@ /* exported attachClipboard, enableDragAndDrop, + exportFormatOptions, inventreeDocReady, inventreeLoad, inventreeSave, @@ -46,6 +47,31 @@ function attachClipboard(selector, containerselector, textElement) { } +/** + * Return a standard list of export format options * + */ +function exportFormatOptions() { + return [ + { + value: 'csv', + display_name: 'CSV', + }, + { + value: 'tsv', + display_name: 'TSV', + }, + { + value: 'xls', + display_name: 'XLS', + }, + { + value: 'xlsx', + display_name: 'XLSX', + }, + ]; +} + + function inventreeDocReady() { /* Run this function when the HTML document is loaded. * This will be called for every page that extends "base.html" diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js index f4b1d0b997..7cadfe453d 100644 --- a/InvenTree/templates/js/translated/order.js +++ b/InvenTree/templates/js/translated/order.js @@ -21,6 +21,7 @@ /* exported createSalesOrder, editPurchaseOrderLineItem, + exportOrder, loadPurchaseOrderLineItemTable, loadPurchaseOrderTable, loadSalesOrderAllocationTable, @@ -187,6 +188,49 @@ function newSupplierPartFromOrderWizard(e) { }); } +/** + * Export an order (PurchaseOrder or SalesOrder) + * + * - Display a simple form which presents the user with export options + * + */ +function exportOrder(redirect_url, options={}) { + + var format = options.format; + + // If default format is not provided, lookup + if (!format) { + format = inventreeLoad('order-export-format', 'csv'); + } + + constructFormBody({}, { + title: '{% trans "Export Order" %}', + fields: { + format: { + label: '{% trans "Format" %}', + help_text: '{% trans "Select file format" %}', + required: true, + type: 'choice', + value: format, + choices: exportFormatOptions(), + } + }, + onSubmit: function(fields, opts) { + + var format = getFormFieldValue('format', fields['format'], opts); + + // Save the format for next time + inventreeSave('order-export-format', format); + + // Hide the modal + $(opts.modal).modal('hide'); + + // Download the file! + location.href = `${redirect_url}?format=${format}`; + } + }); +} + function newPurchaseOrderFromOrderWizard(e) { /* Create a new purchase order directly from an order form. * Launches a secondary modal and (if successful), diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index b88f5f1862..67c50bffef 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -98,24 +98,7 @@ function exportStock(params={}) { required: true, type: 'choice', value: 'csv', - choices: [ - { - value: 'csv', - display_name: 'CSV', - }, - { - value: 'tsv', - display_name: 'TSV', - }, - { - value: 'xls', - display_name: 'XLS', - }, - { - value: 'xlsx', - display_name: 'XLSX', - }, - ], + choices: exportFormatOptions(), }, sublocations: { label: '{% trans "Include Sublocations" %}', From 59866a692cefde50d7ba58ddc11c4ede90ef7c48 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 7 Oct 2021 22:00:03 +1100 Subject: [PATCH 26/29] Add workflow to build and test docker image --- .github/workflows/docker_stable.yaml | 3 +- .github/workflows/docker_tag.yaml | 3 +- .github/workflows/docker_test.yaml | 41 ++++++++++++++++++++++++++++ ci/check_api_endpoint.py | 40 +++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/docker_test.yaml create mode 100644 ci/check_api_endpoint.py diff --git a/.github/workflows/docker_stable.yaml b/.github/workflows/docker_stable.yaml index 3d435e40da..1f3c7749c7 100644 --- a/.github/workflows/docker_stable.yaml +++ b/.github/workflows/docker_stable.yaml @@ -1,4 +1,5 @@ -# Build and push latest docker image on push to master branch +# Build and push docker image on push to 'stable' branch +# Docker build will be uploaded to dockerhub with the 'inventree:stable' tag name: Docker Build diff --git a/.github/workflows/docker_tag.yaml b/.github/workflows/docker_tag.yaml index b3b0c53d12..90b8f72505 100644 --- a/.github/workflows/docker_tag.yaml +++ b/.github/workflows/docker_tag.yaml @@ -1,4 +1,5 @@ -# Publish docker images to dockerhub +# Publish docker images to dockerhub on a tagged release +# Docker build will be uploaded to dockerhub with the 'invetree:' tag name: Docker Publish diff --git a/.github/workflows/docker_test.yaml b/.github/workflows/docker_test.yaml new file mode 100644 index 0000000000..36638784f2 --- /dev/null +++ b/.github/workflows/docker_test.yaml @@ -0,0 +1,41 @@ +# Test that the InvenTree docker image compiles correctly + +# This CI action runs on pushes to either the master or stable branches + +# 1. Build the development docker image (as per the documentation) +# 2. Install requied python libs into the docker container +# 3. Launch the container +# 4. Check that the API endpoint is available + +name: Docker Test + +on: + push: + branches: + - 'master' + - 'stable' + + pull_request: + branches-ignore: + - l10* + +jobs: + + docker: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + - name: Build Docker Image + run: | + cd docker + docker-compose -f docker-compose.dev.yml build + docker-compose -f docker-compose.dev.yml run inventree-dev-server invoke update + docker-compose -f docker-compose.dev.yml up -d + - name: Sleepy Time + run: sleep 60 + - name: Test API + run: | + pip install requests + python3 ci/check_api_endpoint.py diff --git a/ci/check_api_endpoint.py b/ci/check_api_endpoint.py new file mode 100644 index 0000000000..2969c64792 --- /dev/null +++ b/ci/check_api_endpoint.py @@ -0,0 +1,40 @@ +""" +Test that the root API endpoint is available. +""" + +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import json +import requests + +# We expect the server to be running on the local host +url = "http://localhost:8000/api/" + +print("Testing InvenTree API endpoint") + +response = requests.get(url) + +assert(response.status_code == 200) + +print("- Response 200 OK") + +data = json.loads(response.text) + +required_keys = [ + 'server', + 'version', + 'apiVersion', + 'worker_running', +] + +for key in required_keys: + assert(key in data) + print(f"- Found key '{key}'") + +# Check that the worker is running +assert(data['worker_running']) + +print("- Background worker is operational") + +print("API Endpoint Tests Passed OK") From 53f18368ba4b6dc2916c14f66269616dcc7b95ee Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 7 Oct 2021 22:04:42 +1100 Subject: [PATCH 27/29] This should break - Trying to install a fake package --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index e4ebbc1b4b..ef055e9ca7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -68,7 +68,8 @@ RUN apk add --no-cache git make bash \ # PostgreSQL support postgresql postgresql-contrib postgresql-dev libpq \ # MySQL/MariaDB support - mariadb-connector-c mariadb-dev mariadb-client + mariadb-connector-c mariadb-dev mariadb-client \ + some-made-up-package # Install required base-level python packages COPY requirements.txt requirements.txt From 4c3b5554809c04938506e9f2e8d94bdae298edf5 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 7 Oct 2021 22:25:55 +1100 Subject: [PATCH 28/29] Revert "This should break" This reverts commit 53f18368ba4b6dc2916c14f66269616dcc7b95ee. --- docker/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ef055e9ca7..e4ebbc1b4b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -68,8 +68,7 @@ RUN apk add --no-cache git make bash \ # PostgreSQL support postgresql postgresql-contrib postgresql-dev libpq \ # MySQL/MariaDB support - mariadb-connector-c mariadb-dev mariadb-client \ - some-made-up-package + mariadb-connector-c mariadb-dev mariadb-client # Install required base-level python packages COPY requirements.txt requirements.txt From b4ff4c4018ba09a7b0fb98a87ea814dededb597c Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 7 Oct 2021 22:26:39 +1100 Subject: [PATCH 29/29] Only run on pushes --- .github/workflows/docker_test.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/docker_test.yaml b/.github/workflows/docker_test.yaml index 36638784f2..0067f337e5 100644 --- a/.github/workflows/docker_test.yaml +++ b/.github/workflows/docker_test.yaml @@ -15,10 +15,6 @@ on: - 'master' - 'stable' - pull_request: - branches-ignore: - - l10* - jobs: docker: