{% load i18n %} {% load inventree_extras %} {% load status_codes %} /* globals attachSelect, enableField, clearField, clearFieldOptions, closeModal, constructField, constructFormBody, getFormFieldValue, global_settings, handleFormErrors, imageHoverIcon, inventreeDelete, inventreeGet, inventreePut, launchModalForm, linkButtonsToSelection, loadTableFilters, makeIconBadge, makeIconButton, makeOptionsList, makePartIcons, modalEnable, modalSetContent, modalSetTitle, modalSubmit, moment, openModal, printStockItemLabels, printTestReports, renderLink, reloadFieldOptions, scanItemsIntoLocation, showAlertDialog, setFieldValue, setupFilterList, showApiError, stockStatusDisplay, */ /* exported createNewStockItem, exportStock, loadInstalledInTable, loadStockLocationTable, loadStockTable, loadStockTestResultsTable, loadStockTrackingTable, loadTableFilters, locationFields, removeStockRow, stockStatusCodes, */ function locationFields() { return { parent: { help_text: '{% trans "Parent stock location" %}', }, name: {}, description: {}, }; } /* Stock API functions * Requires api.js to be loaded first */ function stockStatusCodes() { return [ {% for code in StockStatus.list %} { key: {{ code.key }}, text: '{{ code.value }}', }, {% endfor %} ]; } /* * Export stock table */ function exportStock(params={}) { constructFormBody({}, { title: '{% trans "Export Stock" %}', fields: { format: { label: '{% trans "Format" %}', help_text: '{% trans "Select file format" %}', required: true, type: 'choice', value: 'csv', choices: exportFormatOptions(), }, sublocations: { label: '{% trans "Include Sublocations" %}', help_text: '{% trans "Include stock items in sublocations" %}', type: 'boolean', value: 'true', } }, onSubmit: function(fields, form_options) { var format = getFormFieldValue('format', fields['format'], form_options); var cascade = getFormFieldValue('sublocations', fields['sublocations'], form_options); // Hide the modal $(form_options.modal).modal('hide'); var url = `{% url "stock-export" %}?format=${format}&cascade=${cascade}`; for (var key in params) { url += `&${key}=${params[key]}`; } console.log(url); location.href = url; } }); } /** * Perform stock adjustments */ function adjustStock(action, items, options={}) { var formTitle = 'Form Title Here'; var actionTitle = null; // API url var url = null; var specifyLocation = false; var allowSerializedStock = false; switch (action) { case 'move': formTitle = '{% trans "Transfer Stock" %}'; actionTitle = '{% trans "Move" %}'; specifyLocation = true; allowSerializedStock = true; url = '{% url "api-stock-transfer" %}'; break; case 'count': formTitle = '{% trans "Count Stock" %}'; actionTitle = '{% trans "Count" %}'; url = '{% url "api-stock-count" %}'; break; case 'take': formTitle = '{% trans "Remove Stock" %}'; actionTitle = '{% trans "Take" %}'; url = '{% url "api-stock-remove" %}'; break; case 'add': formTitle = '{% trans "Add Stock" %}'; actionTitle = '{% trans "Add" %}'; url = '{% url "api-stock-add" %}'; break; case 'delete': formTitle = '{% trans "Delete Stock" %}'; allowSerializedStock = true; break; default: break; } // Generate modal HTML content var html = ` `; var itemCount = 0; for (var idx = 0; idx < items.length; idx++) { var item = items[idx]; if ((item.serial != null) && !allowSerializedStock) { continue; } var pk = item.pk; var readonly = (item.serial != null); var minValue = null; var maxValue = null; var value = null; switch (action) { case 'move': minValue = 0; maxValue = item.quantity; value = item.quantity; break; case 'add': minValue = 0; value = 0; break; case 'take': minValue = 0; value = 0; break; case 'count': minValue = 0; value = item.quantity; break; default: break; } var thumb = thumbnailImage(item.part_detail.thumbnail || item.part_detail.image); var status = stockStatusDisplay(item.status, { classes: 'float-right' }); var quantity = item.quantity; var location = locationDetail(item, false); if (item.location_detail) { location = item.location_detail.pathstring; } if (item.serial != null) { quantity = `#${item.serial}`; } var actionInput = ''; if (actionTitle != null) { actionInput = constructField( `items_quantity_${pk}`, { type: 'decimal', min_value: minValue, max_value: maxValue, value: value, title: readonly ? '{% trans "Quantity cannot be adjusted for serialized stock" %}' : '{% trans "Specify stock quantity" %}', required: true, }, { hideLabels: true, } ); } var buttons = `
`; buttons += makeIconButton( 'fa-times icon-red', 'button-stock-item-remove', pk, '{% trans "Remove stock item" %}', ); buttons += `
`; html += ` `; itemCount += 1; } if (itemCount == 0) { showAlertDialog( '{% trans "Select Stock Items" %}', '{% trans "You must select at least one available stock item" %}', ); return; } html += `
{% trans "Part" %} {% trans "Stock" %} {% trans "Location" %} ${actionTitle || ''}
${thumb} ${item.part_detail.full_name} ${quantity}${status} ${location}
${actionInput}
${buttons}
`; var extraFields = {}; if (specifyLocation) { extraFields.location = {}; } if (action != 'delete') { extraFields.notes = {}; } constructForm(url, { method: 'POST', fields: extraFields, preFormContent: html, confirm: true, confirmMessage: '{% trans "Confirm stock adjustment" %}', title: formTitle, afterRender: function(fields, opts) { // Add button callbacks to remove rows $(opts.modal).find('.button-stock-item-remove').click(function() { var pk = $(this).attr('pk'); $(opts.modal).find(`#stock_item_${pk}`).remove(); }); // Initialize "location" field if (specifyLocation) { initializeRelatedField( { name: 'location', type: 'related field', model: 'stocklocation', required: true, }, null, opts ); } }, onSubmit: function(fields, opts) { // Extract data elements from the form var data = { items: [], }; if (action != 'delete') { data.notes = getFormFieldValue('notes', {}, opts); } if (specifyLocation) { data.location = getFormFieldValue('location', {}, opts); } var item_pk_values = []; items.forEach(function(item) { var pk = item.pk; // Does the row exist in the form? var row = $(opts.modal).find(`#stock_item_${pk}`); if (row) { item_pk_values.push(pk); var quantity = getFormFieldValue(`items_quantity_${pk}`, {}, opts); data.items.push({ pk: pk, quantity: quantity, }); } }); // Delete action is handled differently if (action == 'delete') { var requests = []; item_pk_values.forEach(function(pk) { requests.push( inventreeDelete( `/api/stock/${pk}/`, ) ); }); // Wait for *all* the requests to complete $.when.apply($, requests).done(function() { // Destroy the modal window $(opts.modal).modal('hide'); if (options.success) { options.success(); } }); return; } opts.nested = { 'items': item_pk_values, }; inventreePut( 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 removeStockRow(e) { // Remove a selected row from a stock modal form e = e || window.event; var src = e.target || e.srcElement; var row = $(src).attr('row'); $('#' + row).remove(); } function passFailBadge(result) { if (result) { return `{% trans "PASS" %}`; } else { return `{% trans "FAIL" %}`; } } function noResultBadge() { return `{% trans "NO RESULT" %}`; } function formatDate(row) { // Function for formatting date field var html = row.date; if (row.user_detail) { html += `${row.user_detail.username}`; } if (row.attachment) { html += ``; } return html; } function loadStockTestResultsTable(table, options) { /* * Load StockItemTestResult table */ function makeButtons(row, grouped) { var html = `
`; html += makeIconButton('fa-plus icon-green', 'button-test-add', row.test_name, '{% trans "Add test result" %}'); if (!grouped && row.result != null) { var pk = row.pk; html += makeIconButton('fa-edit icon-blue', 'button-test-edit', pk, '{% trans "Edit test result" %}'); html += makeIconButton('fa-trash-alt icon-red', 'button-test-delete', pk, '{% trans "Delete test result" %}'); } html += '
'; return html; } var parent_node = 'parent node'; table.inventreeTable({ url: '{% url "api-part-test-template-list" %}', method: 'get', name: 'testresult', treeEnable: true, rootParentId: parent_node, parentIdField: 'parent', idField: 'pk', uniqueId: 'key', treeShowField: 'test_name', formatNoMatches: function() { return '{% trans "No test results found" %}'; }, queryParams: { part: options.part, }, onPostBody: function() { table.treegrid({ treeColumn: 0, }); table.treegrid('collapseAll'); }, columns: [ { field: 'pk', title: 'ID', visible: false, switchable: false, }, { field: 'test_name', title: '{% trans "Test Name" %}', sortable: true, formatter: function(value, row) { var html = value; if (row.required) { html = `${value}`; } if (row.result == null) { html += noResultBadge(); } else { html += passFailBadge(row.result); } return html; } }, { field: 'value', title: '{% trans "Value" %}', }, { field: 'notes', title: '{% trans "Notes" %}', }, { field: 'date', title: '{% trans "Test Date" %}', sortable: true, formatter: function(value, row) { return formatDate(row); }, }, { field: 'buttons', formatter: function(value, row) { return makeButtons(row, false); } } ], onLoadSuccess: function(tableData) { // Set "parent" for each existing row tableData.forEach(function(item, idx) { tableData[idx].parent = parent_node; }); // Once the test template data are loaded, query for test results inventreeGet( '{% url "api-stock-test-result-list" %}', { stock_item: options.stock_item, user_detail: true, attachment_detail: true, ordering: '-date', }, { success: function(data) { // Iterate through the returned test data data.forEach(function(item) { var match = false; var override = false; // Extract the simplified test key var key = item.key; // Attempt to associate this result with an existing test for (var idx = 0; idx < tableData.length; idx++) { var row = tableData[idx]; if (key == row.key) { item.test_name = row.test_name; item.required = row.required; if (row.result == null) { item.parent = parent_node; tableData[idx] = item; override = true; } else { item.parent = row.pk; } match = true; break; } } // No match could be found if (!match) { item.test_name = item.test; item.parent = parent_node; } if (!override) { tableData.push(item); } }); // Push data back into the table table.bootstrapTable('load', tableData); } } ); } }); } function locationDetail(row, showLink=true) { /* * Function to display a "location" of a StockItem. * * Complicating factors: A StockItem may not actually *be* in a location! * - Could be at a customer * - Could be installed in another stock item * - Could be assigned to a sales order * - Could be currently in production! * * So, instead of being naive, we'll check! */ // Display text var text = ''; // URL (optional) var url = ''; if (row.is_building && row.build) { // StockItem is currently being built! text = '{% trans "In production" %}'; url = `/build/${row.build}/`; } else if (row.belongs_to) { // StockItem is installed inside a different StockItem text = `{% trans "Installed in Stock Item" %} ${row.belongs_to}`; url = `/stock/item/${row.belongs_to}/installed/`; } else if (row.customer) { // StockItem has been assigned to a customer text = '{% trans "Shipped to customer" %}'; url = `/company/${row.customer}/assigned-stock/`; } else if (row.sales_order) { // StockItem has been assigned to a sales order text = '{% trans "Assigned to Sales Order" %}'; url = `/order/sales-order/${row.sales_order}/`; } else if (row.location) { text = row.location_detail.pathstring; url = `/stock/location/${row.location}/`; } else { text = '{% trans "No stock location set" %}'; url = ''; } if (showLink && url) { return renderLink(text, url); } else { return text; } } function loadStockTable(table, options) { /* Load data into a stock table with adjustable options. * Fetches data (via AJAX) and loads into a bootstrap table. * Also links in default button callbacks. * * Options: * url - URL for the stock query * params - query params for augmenting stock data request * groupByField - Column for grouping stock items * buttons - Which buttons to link to stock selection callbacks * filterList -