mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Merge branch 'sales-order-table-fixes' into sales-order-allocation-fixes
This commit is contained in:
		| @@ -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() | ||||
|  | ||||
|   | ||||
| @@ -179,446 +179,17 @@ $("#new-so-line").click(function() { | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| {% 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 = `<div class='sub-table'><table class='table table-striped table-condensed' id='allocation-table-${row.pk}'></table></div>`; | ||||
|  | ||||
|     element.html(html); | ||||
|  | ||||
|     var lineItem = row; | ||||
|  | ||||
|     var table = $(`#allocation-table-${row.pk}`); | ||||
|  | ||||
|     table.bootstrapTable({ | ||||
|         data: row.allocations, | ||||
|         showHeader: false, | ||||
|         columns: [ | ||||
|     loadSalesOrderLineItemTable( | ||||
|         '#so-lines-table', | ||||
|         { | ||||
|             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 = "<div class='btn-group float-right' role='group'>"; | ||||
|                 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 += "</div>"; | ||||
|  | ||||
|                 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 = `<div class='sub-table'><table class='table table-striped table-condensed' id='${id}'></table></div>`; | ||||
|  | ||||
|     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 `<div>` + po_name + `</div>`; | ||||
|             } | ||||
|         }, | ||||
|         {% if order.status == SalesOrderStatus.PENDING %} | ||||
|         { | ||||
|             field: 'buttons', | ||||
|             formatter: function(value, row, index, field) { | ||||
|  | ||||
|                 var html = `<div class='btn-group float-right' role='group'>`; | ||||
|  | ||||
|                 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 += `</div>`; | ||||
|  | ||||
|                 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}/`, { | ||||
|             fields: { | ||||
|                 quantity: {}, | ||||
|                 reference: {}, | ||||
|                 sale_price: {}, | ||||
|                 sale_price_currency: {}, | ||||
|                 notes: {}, | ||||
|             }, | ||||
|             title: '{% trans "Edit 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,  | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             order: {{ order.pk }}, | ||||
|             status: {{ order.status }}, | ||||
|         } | ||||
|     ); | ||||
|     }); | ||||
|  | ||||
|     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; | ||||
|         } | ||||
|  | ||||
|         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 %} | ||||
| @@ -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, | ||||
|     }); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -24,6 +24,7 @@ | ||||
|     loadPurchaseOrderLineItemTable, | ||||
|     loadPurchaseOrderTable, | ||||
|     loadSalesOrderAllocationTable, | ||||
|     loadSalesOrderLineItemTable, | ||||
|     loadSalesOrderTable, | ||||
|     newPurchaseOrderFromOrderWizard, | ||||
|     newSupplierPartFromOrderWizard, | ||||
| @@ -1126,3 +1127,574 @@ 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 = ` | ||||
|     <div class='sub-table'> | ||||
|         <table class='table table-striped table-condensed' id='allocation-table-${row.pk}'> | ||||
|         </table> | ||||
|     </div>`; | ||||
|  | ||||
|     element.html(html); | ||||
|  | ||||
|     var table = $(`#allocation-table-${row.pk}`); | ||||
|  | ||||
|     // 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: [ | ||||
|             { | ||||
|                 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) { | ||||
|  | ||||
|                     // Location specified | ||||
|                     if (row.location) { | ||||
|                         return renderLink( | ||||
|                             row.location_detail.pathstring || '{% trans "Location" %}', | ||||
|                             `/stock/location/${row.location}/` | ||||
|                         ); | ||||
|                     } else { | ||||
|                         return `<i>{% 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 = `<div class='btn-group float-right' role='group'>`; | ||||
|                     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 += "</div>"; | ||||
|  | ||||
|                     return html; | ||||
|                 }, | ||||
|             }, | ||||
|         ], | ||||
|     }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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 = ` | ||||
|     <div class='sub-table'> | ||||
|         <table class='table table-striped table-condensed' id='${id}'> | ||||
|         </table> | ||||
|     </div>`; | ||||
|  | ||||
|     element.html(html); | ||||
|  | ||||
|     $(`#${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" %}', | ||||
|             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}/`); | ||||
|                 } else { | ||||
|                     return '-'; | ||||
|                 } | ||||
|             }, | ||||
|             footerFormatter:  function() { | ||||
|                 return '{% trans "Total" %}' | ||||
|             }, | ||||
|         }, | ||||
|         { | ||||
|             sortable: true, | ||||
|             field: 'reference', | ||||
|             title: '{% trans "Reference" %}', | ||||
|             switchable: false, | ||||
|         }, | ||||
|         { | ||||
|             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) { | ||||
|                 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" %}', | ||||
|             switchable: false, | ||||
|             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" %}', | ||||
|         }, | ||||
|         // 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 `<div>` + po_name + `</div>`; | ||||
|             } | ||||
|         }, | ||||
|         */ | ||||
|     ]; | ||||
|  | ||||
|     if (pending) { | ||||
|         columns.push({ | ||||
|             field: 'buttons', | ||||
|             formatter: function(value, row, index, field) { | ||||
|  | ||||
|                 var html = `<div class='btn-group float-right' role='group'>`; | ||||
|  | ||||
|                 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 += `</div>`; | ||||
|  | ||||
|                 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; | ||||
|             } | ||||
|  | ||||
|             // Create a new build order | ||||
|             newBuildOrder({ | ||||
|                 part: pk, | ||||
|                 sales_order: options.order, | ||||
|                 quantity: quantity, | ||||
|                 success: reloadTable | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         // 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, | ||||
|     }); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user