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