{% load i18n %} {% load inventree_extras %} /* globals companyFormFields, constructForm, createSupplierPart, global_settings, imageHoverIcon, inventreeGet, launchModalForm, loadTableFilters, makeIconBadge, purchaseOrderStatusDisplay, receivePurchaseOrderItems, renderLink, salesOrderStatusDisplay, setupFilterList, */ /* exported createSalesOrder, editPurchaseOrderLineItem, loadPurchaseOrderLineItemTable, loadPurchaseOrderTable, loadSalesOrderAllocationTable, loadSalesOrderTable, newPurchaseOrderFromOrderWizard, newSupplierPartFromOrderWizard, removeOrderRowFromOrderWizard, removePurchaseOrderLineItem, */ // Create a new SalesOrder function createSalesOrder(options={}) { constructForm('{% url "api-so-list" %}', { method: 'POST', fields: { reference: { prefix: global_settings.SALESORDER_REFERENCE_PREFIX, }, customer: { value: options.customer, secondary: { title: '{% trans "Add Customer" %}', fields: function() { var fields = companyFormFields(); fields.is_customer.value = true; return fields; } } }, customer_reference: {}, description: {}, target_date: { icon: 'fa-calendar-alt', }, link: { icon: 'fa-link', }, responsible: { icon: 'fa-user', } }, onSuccess: function(data) { location.href = `/order/sales-order/${data.pk}/`; }, title: '{% trans "Create Sales Order" %}', }); } // Create a new PurchaseOrder function createPurchaseOrder(options={}) { constructForm('{% url "api-po-list" %}', { method: 'POST', fields: { reference: { prefix: global_settings.PURCHASEORDER_REFERENCE_PREFIX, }, supplier: { value: options.supplier, secondary: { title: '{% trans "Add Supplier" %}', fields: function() { var fields = companyFormFields(); fields.is_supplier.value = true; return fields; } } }, supplier_reference: {}, description: {}, target_date: { icon: 'fa-calendar-alt', }, link: { icon: 'fa-link', }, responsible: { icon: 'fa-user', } }, onSuccess: function(data) { if (options.onSuccess) { options.onSuccess(data); } else { // Default action is to redirect browser to the new PO location.href = `/order/purchase-order/${data.pk}/`; } }, title: '{% trans "Create Purchase Order" %}', }); } function removeOrderRowFromOrderWizard(e) { /* Remove a part selection from an order form. */ e = e || window.event; var src = e.target || e.srcElement; var row = $(src).attr('row'); $('#' + row).remove(); } function newSupplierPartFromOrderWizard(e) { /* Create a new supplier part directly from an order form. * Launches a secondary modal and (if successful), * back-populates the selected row. */ e = e || window.event; var src = e.srcElement || e.target; var part = $(src).attr('part'); if (!part) { part = $(src).closest('button').attr('part'); } createSupplierPart({ part: part, onSuccess: function(data) { // TODO: 2021-08-23 - This whole form wizard needs to be refactored. // In the future, use the API forms functionality to add the new item // For now, this hack will have to do... var dropdown = `#id_supplier_part_${part}`; var pk = data.pk; inventreeGet( `/api/company/part/${pk}/`, { supplier_detail: true, }, { success: function(response) { var text = ''; if (response.supplier_detail) { text += response.supplier_detail.name; text += ' | '; } text += response.SKU; var option = new Option(text, pk, true, true); $('#modal-form').find(dropdown).append(option).trigger('change'); } } ); } }); } function newPurchaseOrderFromOrderWizard(e) { /* Create a new purchase order directly from an order form. * Launches a secondary modal and (if successful), * back-fills the newly created purchase order. */ e = e || window.event; var src = e.target || e.srcElement; var supplier = $(src).attr('supplierid'); createPurchaseOrder({ supplier: supplier, onSuccess: function(data) { // TODO: 2021-08-23 - The whole form wizard needs to be refactored // In the future, the drop-down should be using a dynamic AJAX request // to fill out the select2 options! var pk = data.pk; inventreeGet( `/api/order/po/${pk}/`, { supplier_detail: true, }, { success: function(response) { var text = global_settings.PURCHASEORDER_REFERENCE_PREFIX || ''; text += response.reference; if (response.supplier_detail) { text += ` ${response.supplier_detail.name}`; } var dropdown = `#id-purchase-order-${supplier}`; var option = new Option(text, pk, true, true); $('#modal-form').find(dropdown).append(option).trigger('change'); } } ); } }); } /** * Receive stock items against a PurchaseOrder * Uses the POReceive API endpoint * * arguments: * - order_id, ID / PK for the PurchaseOrder instance * - line_items: A list of PurchaseOrderLineItems objects to be allocated * * options: * - */ function receivePurchaseOrderItems(order_id, line_items, options={}) { if (line_items.length == 0) { showAlertDialog( '{% trans "Select Line Items" %}', '{% trans "At least one line item must be selected" %}', ); return; } function renderLineItem(line_item, opts={}) { var pk = line_item.pk; // Part thumbnail + description var thumb = thumbnailImage(line_item.part_detail.thumbnail); var quantity = (line_item.quantity || 0) - (line_item.received || 0); if (quantity < 0) { quantity = 0; } // Quantity to Receive var quantity_input = constructField( `items_quantity_${pk}`, { type: 'decimal', min_value: 0, value: quantity, title: '{% trans "Quantity to receive" %}', required: true, }, { hideLabels: true, } ); // Construct list of StockItem status codes var choices = []; for (var key in stockCodes) { choices.push({ value: key, display_name: stockCodes[key].value, }); } var destination_input = constructField( `items_location_${pk}`, { type: 'related field', label: '{% trans "Location" %}', required: false, }, { hideLabels: true, } ); var status_input = constructField( `items_status_${pk}`, { type: 'choice', label: '{% trans "Stock Status" %}', required: true, choices: choices, value: 10, // OK }, { hideLabels: true, } ); // Button to remove the row var delete_button = `
`; delete_button += makeIconButton( 'fa-times icon-red', 'button-row-remove', pk, '{% trans "Remove row" %}', ); delete_button += '
'; var html = ` ${thumb} ${line_item.part_detail.full_name} ${line_item.supplier_part_detail.SKU} ${line_item.quantity} ${line_item.received} ${quantity_input} ${status_input} ${destination_input} ${delete_button} `; return html; } var table_entries = ''; line_items.forEach(function(item) { table_entries += renderLineItem(item); }); var html = ``; // Add table html += ` ${table_entries}
{% trans "Part" %} {% trans "Order Code" %} {% trans "Ordered" %} {% trans "Received" %} {% trans "Receive" %} {% trans "Status" %} {% trans "Destination" %}
`; constructForm(`/api/order/po/${order_id}/receive/`, { method: 'POST', fields: { location: {}, }, preFormContent: html, confirm: true, confirmMessage: '{% trans "Confirm receipt of items" %}', title: '{% trans "Receive Purchase Order Items" %}', afterRender: function(fields, opts) { // Initialize the "destination" field for each item line_items.forEach(function(item) { var pk = item.pk; var name = `items_location_${pk}`; var field_details = { name: name, api_url: '{% url "api-location-list" %}', filters: { }, type: 'related field', model: 'stocklocation', required: false, auto_fill: false, value: item.destination || item.part_detail.default_location, render_description: false, }; initializeRelatedField( field_details, null, opts, ); addClearCallback( name, field_details, opts ); initializeChoiceField( { name: `items_status_${pk}`, }, null, opts ); }); // Add callbacks to remove rows $(opts.modal).find('.button-row-remove').click(function() { var pk = $(this).attr('pk'); $(opts.modal).find(`#receive_row_${pk}`).remove(); }); }, onSubmit: function(fields, opts) { // Extract data elements from the form var data = { items: [], location: getFormFieldValue('location', {}, opts), }; var item_pk_values = []; line_items.forEach(function(item) { var pk = item.pk; var quantity = getFormFieldValue(`items_quantity_${pk}`, {}, opts); var status = getFormFieldValue(`items_status_${pk}`, {}, opts); var location = getFormFieldValue(`items_location_${pk}`, {}, opts); if (quantity != null) { data.items.push({ line_item: pk, quantity: quantity, status: status, location: location, }); item_pk_values.push(pk); } }); // Provide list of nested values opts.nested = { 'items': item_pk_values, }; inventreePut( opts.url, data, { method: 'POST', success: function(response) { // Hide the modal $(opts.modal).modal('hide'); if (options.success) { options.success(response); } }, error: function(xhr) { switch (xhr.status) { case 400: handleFormErrors(xhr.responseJSON, fields, opts); break; default: $(opts.modal).modal('hide'); showApiError(xhr); break; } } } ); } }); } function editPurchaseOrderLineItem(e) { /* Edit a purchase order line item in a modal form. */ e = e || window.event; var src = e.target || e.srcElement; var url = $(src).attr('url'); launchModalForm(url, { reload: true, }); } function removePurchaseOrderLineItem(e) { /* Delete a purchase order line item in a modal form */ e = e || window.event; var src = e.target || e.srcElement; var url = $(src).attr('url'); launchModalForm(url, { reload: true, }); } function loadPurchaseOrderTable(table, options) { /* Create a purchase-order table */ options.params = options.params || {}; options.params['supplier_detail'] = true; var filters = loadTableFilters('purchaseorder'); for (var key in options.params) { filters[key] = options.params[key]; } options.url = options.url || '{% url "api-po-list" %}'; setupFilterList('purchaseorder', $(table)); $(table).inventreeTable({ url: options.url, queryParams: filters, name: 'purchaseorder', groupBy: false, sidePagination: 'server', original: options.params, formatNoMatches: function() { return '{% trans "No purchase orders found" %}'; }, columns: [ { title: '', visible: true, checkbox: true, switchable: false, }, { field: 'reference', title: '{% trans "Purchase Order" %}', sortable: true, switchable: false, formatter: function(value, row) { var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX; if (prefix) { value = `${prefix}${value}`; } var html = renderLink(value, `/order/purchase-order/${row.pk}/`); if (row.overdue) { html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Order is overdue" %}'); } return html; } }, { field: 'supplier_detail', title: '{% trans "Supplier" %}', sortable: true, sortName: 'supplier__name', formatter: function(value, row) { return imageHoverIcon(row.supplier_detail.image) + renderLink(row.supplier_detail.name, `/company/${row.supplier}/purchase-orders/`); } }, { field: 'supplier_reference', title: '{% trans "Supplier Reference" %}', }, { field: 'description', title: '{% trans "Description" %}', }, { field: 'status', title: '{% trans "Status" %}', sortable: true, formatter: function(value, row) { return purchaseOrderStatusDisplay(row.status, row.status_text); } }, { field: 'creation_date', title: '{% trans "Date" %}', sortable: true, }, { field: 'target_date', title: '{% trans "Target Date" %}', sortable: true, }, { field: 'line_items', title: '{% trans "Items" %}', sortable: true, }, ], }); } /** * Load a table displaying line items for a particular PurchasesOrder * @param {String} table - HTML ID tag e.g. '#table' * @param {Object} options - options which must provide: * - order (integer PK) * - supplier (integer PK) * - allow_edit (boolean) * - allow_receive (boolean) */ function loadPurchaseOrderLineItemTable(table, options={}) { function setupCallbacks() { if (options.allow_edit) { $(table).find('.button-line-edit').click(function() { var pk = $(this).attr('pk'); constructForm(`/api/order/po-line/${pk}/`, { fields: { part: { filters: { part_detail: true, supplier_detail: true, supplier: options.supplier, } }, quantity: {}, reference: {}, purchase_price: {}, purchase_price_currency: {}, destination: {}, notes: {}, }, title: '{% trans "Edit Line Item" %}', onSuccess: function() { $(table).bootstrapTable('refresh'); } }); }); $(table).find('.button-line-delete').click(function() { var pk = $(this).attr('pk'); constructForm(`/api/order/po-line/${pk}/`, { method: 'DELETE', title: '{% trans "Delete Line Item" %}', onSuccess: function() { $(table).bootstrapTable('refresh'); } }); }); } if (options.allow_receive) { $(table).find('.button-line-receive').click(function() { var pk = $(this).attr('pk'); var line_item = $(table).bootstrapTable('getRowByUniqueId', pk); if (!line_item) { console.log('WARNING: getRowByUniqueId returned null'); return; } receivePurchaseOrderItems( options.order, [ line_item, ], { success: function() { $(table).bootstrapTable('refresh'); } } ); }); } } $(table).inventreeTable({ onPostBody: setupCallbacks, name: 'purchaseorderlines', sidePagination: 'server', formatNoMatches: function() { return '{% trans "No line items found" %}'; }, queryParams: { order: options.order, part_detail: true }, url: '{% url "api-po-line-list" %}', showFooter: true, uniqueId: 'pk', columns: [ { checkbox: true, visible: true, switchable: false, }, { field: 'part', sortable: true, sortName: 'part_name', 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/${row.part_detail.pk}/`); } else { return '-'; } }, footerFormatter: function() { return '{% trans "Total" %}'; } }, { field: 'part_detail.description', title: '{% trans "Description" %}', }, { sortable: true, sortName: 'SKU', field: 'supplier_part_detail.SKU', title: '{% trans "SKU" %}', formatter: function(value, row, index, field) { if (value) { return renderLink(value, `/supplier-part/${row.part}/`); } else { return '-'; } }, }, { sortable: true, sortName: 'MPN', field: 'supplier_part_detail.manufacturer_part_detail.MPN', title: '{% trans "MPN" %}', formatter: function(value, row, index, field) { if (row.supplier_part_detail && row.supplier_part_detail.manufacturer_part) { return renderLink(value, `/manufacturer-part/${row.supplier_part_detail.manufacturer_part}/`); } else { return '-'; } }, }, { 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: 'purchase_price', title: '{% trans "Unit Price" %}', formatter: function(value, row) { return row.purchase_price_string || row.purchase_price; } }, { field: 'total_price', sortable: true, field: 'total_price', title: '{% trans "Total price" %}', formatter: function(value, row) { var total = row.purchase_price * row.quantity; var formatter = new Intl.NumberFormat('en-US', {style: 'currency', currency: row.purchase_price_currency}); return formatter.format(total); }, footerFormatter: function(data) { var total = data.map(function(row) { return +row['purchase_price']*row['quantity']; }).reduce(function(sum, i) { return sum + i; }, 0); var currency = (data.slice(-1)[0] && data.slice(-1)[0].purchase_price_currency) || 'USD'; var formatter = new Intl.NumberFormat( 'en-US', { style: 'currency', currency: currency } ); return formatter.format(total); } }, { sortable: false, field: 'received', switchable: false, title: '{% trans "Received" %}', formatter: function(value, row, index, field) { return makeProgressBar(row.received, row.quantity, { id: `order-line-progress-${row.pk}`, }); }, sorter: function(valA, valB, rowA, rowB) { if (rowA.received == 0 && rowB.received == 0) { return (rowA.quantity > rowB.quantity) ? 1 : -1; } var progressA = parseFloat(rowA.received) / rowA.quantity; var progressB = parseFloat(rowB.received) / rowB.quantity; return (progressA < progressB) ? 1 : -1; } }, { field: 'destination', title: '{% trans "Destination" %}', formatter: function(value, row) { if (value) { return renderLink(row.destination_detail.pathstring, `/stock/location/${value}/`); } else { return '-'; } } }, { field: 'notes', title: '{% trans "Notes" %}', }, { switchable: false, field: 'buttons', title: '', formatter: function(value, row, index, field) { var html = `
`; var pk = row.pk; if (options.allow_edit) { html += makeIconButton('fa-edit icon-blue', 'button-line-edit', pk, '{% trans "Edit line item" %}'); html += makeIconButton('fa-trash-alt icon-red', 'button-line-delete', pk, '{% trans "Delete line item" %}'); } if (options.allow_receive && row.received < row.quantity) { html += makeIconButton('fa-sign-in-alt', 'button-line-receive', pk, '{% trans "Receive line item" %}'); } html += `
`; return html; }, } ] }); } function loadSalesOrderTable(table, options) { options.params = options.params || {}; options.params['customer_detail'] = true; var filters = loadTableFilters('salesorder'); for (var key in options.params) { filters[key] = options.params[key]; } options.url = options.url || '{% url "api-so-list" %}'; setupFilterList('salesorder', $(table)); $(table).inventreeTable({ url: options.url, queryParams: filters, name: 'salesorder', groupBy: false, sidePagination: 'server', original: options.params, formatNoMatches: function() { return '{% trans "No sales orders found" %}'; }, columns: [ { title: '', checkbox: true, visible: true, switchable: false, }, { sortable: true, field: 'reference', title: '{% trans "Sales Order" %}', formatter: function(value, row) { var prefix = global_settings.SALESORDER_REFERENCE_PREFIX; if (prefix) { value = `${prefix}${value}`; } var html = renderLink(value, `/order/sales-order/${row.pk}/`); if (row.overdue) { html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Order is overdue" %}'); } return html; }, }, { sortable: true, sortName: 'customer__name', field: 'customer_detail', title: '{% trans "Customer" %}', formatter: function(value, row) { if (!row.customer_detail) { return '{% trans "Invalid Customer" %}'; } return imageHoverIcon(row.customer_detail.image) + renderLink(row.customer_detail.name, `/company/${row.customer}/sales-orders/`); } }, { sortable: true, field: 'customer_reference', title: '{% trans "Customer Reference" %}', }, { sortable: false, field: 'description', title: '{% trans "Description" %}', }, { sortable: true, field: 'status', title: '{% trans "Status" %}', formatter: function(value, row) { return salesOrderStatusDisplay(row.status, row.status_text); } }, { sortable: true, field: 'creation_date', title: '{% trans "Creation Date" %}', }, { sortable: true, field: 'target_date', title: '{% trans "Target Date" %}', }, { sortable: true, field: 'shipment_date', title: '{% trans "Shipment Date" %}', }, { sortable: true, field: 'line_items', title: '{% trans "Items" %}' }, ], }); } function loadSalesOrderAllocationTable(table, options={}) { /** * Load a table with SalesOrderAllocation items */ options.params = options.params || {}; options.params['location_detail'] = true; options.params['part_detail'] = true; options.params['item_detail'] = true; options.params['order_detail'] = true; var filters = loadTableFilters('salesorderallocation'); for (var key in options.params) { filters[key] = options.params[key]; } setupFilterList('salesorderallocation', $(table)); $(table).inventreeTable({ url: '{% url "api-so-allocation-list" %}', queryParams: filters, name: 'salesorderallocation', groupBy: false, search: false, paginationVAlign: 'bottom', original: options.params, formatNoMatches: function() { return '{% trans "No sales order allocations found" %}'; }, columns: [ { field: 'pk', visible: false, switchable: false, }, { field: 'order', switchable: false, title: '{% trans "Order" %}', formatter: function(value, row) { var prefix = global_settings.SALESORDER_REFERENCE_PREFIX; var ref = `${prefix}${row.order_detail.reference}`; return renderLink(ref, `/order/sales-order/${row.order}/`); } }, { field: 'item', title: '{% trans "Stock Item" %}', formatter: function(value, row) { // Render a link to the particular stock item var link = `/stock/item/${row.item}/`; var text = `{% trans "Stock Item" %} ${row.item}`; return renderLink(text, link); } }, { field: 'location', title: '{% trans "Location" %}', formatter: function(value, row) { if (!value) { return '{% trans "Location not specified" %}'; } var link = `/stock/location/${value}`; var text = row.location_detail.description; return renderLink(text, link); } }, { field: 'quantity', title: '{% trans "Quantity" %}', sortable: true, } ] }); }