{% load i18n %} {% load inventree_extras %} {% load status_codes %} /* globals attachSelect, closeModal, constructField, constructFormBody, getFormFieldValue, global_settings, handleFormErrors, imageHoverIcon, inventreeGet, inventreePut, launchModalForm, linkButtonsToSelection, loadTableFilters, makeIconBadge, makeIconButton, makeOptionsList, modalEnable, modalSetContent, modalSetTitle, modalSubmit, openModal, renderLink, scanItemsIntoLocation, showAlertDialog, setupFilterList, showApiError, stockStatusDisplay, */ /* exported assignStockToCustomer, createNewStockItem, createStockLocation, deleteStockItem, deleteStockLocation, duplicateStockItem, editStockItem, editStockLocation, findStockItemBySerialNumber, installStockItem, loadInstalledInTable, loadStockLocationTable, loadStockTable, loadStockTestResultsTable, loadStockTrackingTable, loadTableFilters, mergeStockItems, removeStockRow, serializeStockItem, stockItemFields, stockLocationFields, stockStatusCodes, uninstallStockItem, */ /* * Launches a modal form to serialize a particular StockItem */ function serializeStockItem(pk, options={}) { var url = `/api/stock/${pk}/serialize/`; options.method = 'POST'; options.title = '{% trans "Serialize Stock Item" %}'; options.fields = { quantity: {}, serial_numbers: { icon: 'fa-hashtag', }, destination: { icon: 'fa-sitemap', filters: { structural: false, } }, notes: {}, }; if (options.part) { // Work out the next available serial number inventreeGet(`{% url "api-part-list" %}${options.part}/serial-numbers/`, {}, { success: function(data) { if (data.next) { options.fields.serial_numbers.placeholder = `{% trans "Next available serial number" %}: ${data.next}`; } else if (data.latest) { options.fields.serial_numbers.placeholder = `{% trans "Latest serial number" %}: ${data.latest}`; } }, async: false, }); } options.confirm = true; options.confirmMessage = '{% trans "Confirm Stock Serialization" %}'; constructForm(url, options); } function stockLocationFields(options={}) { var fields = { parent: { help_text: '{% trans "Parent stock location" %}', required: false, }, name: {}, description: {}, owner: {}, structural: {}, external: {}, icon: { help_text: `{% trans "Icon (optional) - Explore all available icons on" %} Font Awesome.`, placeholder: 'fas fa-box', }, }; if (options.parent) { fields.parent.value = options.parent; } if (!global_settings.STOCK_OWNERSHIP_CONTROL) { delete fields['owner']; } return fields; } /* * Launch an API form to edit a stock location */ function editStockLocation(pk, options={}) { var url = `/api/stock/location/${pk}/`; options.fields = stockLocationFields(options); options.title = '{% trans "Edit Stock Location" %}'; constructForm(url, options); } /* * Launch an API form to create a new stock location */ function createStockLocation(options={}) { var url = '{% url "api-location-list" %}'; options.method = 'POST'; options.fields = stockLocationFields(options); options.title = '{% trans "New Stock Location" %}'; options.persist = true; options.persistMessage = '{% trans "Create another location after this one" %}'; options.successMessage = '{% trans "Stock location created" %}'; constructForm(url, options); } /* * Launch an API form to delete a StockLocation */ function deleteStockLocation(pk, options={}) { var url = `/api/stock/location/${pk}/`; var html = `
{% trans "Are you sure you want to delete this stock location?" %}
`; var subChoices = [ { value: 0, display_name: '{% trans "Move to parent stock location" %}', }, { value: 1, display_name: '{% trans "Delete" %}', } ]; constructForm(url, { title: '{% trans "Delete Stock Location" %}', method: 'DELETE', fields: { 'delete_stock_items': { label: '{% trans "Action for stock items in this stock location" %}', choices: subChoices, type: 'choice' }, 'delete_sub_locations': { label: '{% trans "Action for sub-locations" %}', choices: subChoices, type: 'choice' }, }, preFormContent: html, onSuccess: function(response) { handleFormSuccess(response, options); } }); } function stockItemFields(options={}) { var fields = { part: { // Hide the part field unless we are "creating" a new stock item hidden: !options.create, onSelect: function(data, field, opts) { // Callback when a new "part" is selected // If we are "creating" a new stock item, // change the available fields based on the part properties if (options.create) { // If a "trackable" part is selected, enable serial number field if (data.trackable) { enableFormInput('serial_numbers', opts); // Request part serial number information from the server inventreeGet(`{% url "api-part-list" %}${data.pk}/serial-numbers/`, {}, { success: function(data) { var placeholder = ''; if (data.next) { placeholder = `{% trans "Next available serial number" %}: ${data.next}`; } else if (data.latest) { placeholder = `{% trans "Latest serial number" %}: ${data.latest}`; } setFormInputPlaceholder('serial_numbers', placeholder, opts); if (global_settings.SERIAL_NUMBER_AUTOFILL) { if (data.next) { updateFieldValue('serial_numbers', `${data.next}+`, {}, opts); } } } }); } else { clearFormInput('serial_numbers', opts); disableFormInput('serial_numbers', opts); setFormInputPlaceholder('serial_numbers', '{% trans "This part cannot be serialized" %}', opts); } // Enable / disable fields based on purchaseable status if (data.purchaseable) { enableFormInput('supplier_part', opts); enableFormInput('purchase_price', opts); enableFormInput('purchase_price_currency', opts); } else { clearFormInput('supplier_part', opts); clearFormInput('purchase_price', opts); disableFormInput('supplier_part', opts); disableFormInput('purchase_price', opts); disableFormInput('purchase_price_currency', opts); } } } }, supplier_part: { icon: 'fa-building', filters: { part_detail: true, supplier_detail: true, }, adjustFilters: function(query, opts) { var part = getFormFieldValue('part', {}, opts); if (part) { query.part = part; } return query; } }, location: { icon: 'fa-sitemap', filters: { structural: false, }, }, quantity: { help_text: '{% trans "Enter initial quantity for this stock item" %}', }, serial_numbers: { icon: 'fa-hashtag', type: 'string', label: '{% trans "Serial Numbers" %}', help_text: '{% trans "Enter serial numbers for new stock (or leave blank)" %}', required: false, }, serial: { icon: 'fa-hashtag', }, batch: { icon: 'fa-layer-group', }, status: {}, expiry_date: { icon: 'fa-calendar-alt', }, purchase_price: { icon: 'fa-dollar-sign', }, purchase_price_currency: { icon: 'fa-coins', }, packaging: { icon: 'fa-box', }, link: { icon: 'fa-link', }, owner: { icon: 'fa-user', }, delete_on_deplete: {}, }; if (options.create) { // Use special "serial numbers" field when creating a new stock item delete fields['serial']; } else { // These fields cannot be edited once the stock item has been created delete fields['serial_numbers']; delete fields['quantity']; delete fields['location']; } // Remove stock expiry fields if feature is not enabled if (!global_settings.STOCK_ENABLE_EXPIRY) { delete fields['expiry_date']; } // Remove ownership field if feature is not enanbled if (!global_settings.STOCK_OWNERSHIP_CONTROL) { delete fields['owner']; } return fields; } function stockItemGroups(options={}) { return { }; } /* * Launch a modal form to duplicate a given StockItem */ function duplicateStockItem(pk, options) { // If no "success" function provided, add a default if (!options.onSuccess) { options.onSuccess = function(response) { showAlertOrCache('{% trans "Stock item duplicated" %}', true, {style: 'success'}); window.location.href = `/stock/item/${response.pk}/`; }; } // First, we need the StockItem information inventreeGet(`{% url "api-stock-list" %}${pk}/`, {}, { success: function(data) { // Do not duplicate the serial number delete data['serial']; options.data = data; options.create = true; options.fields = stockItemFields(options); options.groups = stockItemGroups(options); options.method = 'POST'; options.title = '{% trans "Duplicate Stock Item" %}'; constructForm('{% url "api-stock-list" %}', options); } }); } /* * Launch a modal form to delete a given StockItem */ function deleteStockItem(pk, options={}) { var url = `/api/stock/${pk}/`; var html = `
{% trans "Are you sure you want to delete this stock item?" %}
`; constructForm(url, { method: 'DELETE', title: '{% trans "Delete Stock Item" %}', preFormContent: html, onSuccess: function(response) { handleFormSuccess(response, options); } }); } /* * Launch a modal form to edit a given StockItem */ function editStockItem(pk, options={}) { var url = `/api/stock/${pk}/`; options.create = false; options.fields = stockItemFields(options); options.groups = stockItemGroups(options); options.title = '{% trans "Edit Stock Item" %}'; // Query parameters for retrieving stock item data options.params = { part_detail: true, supplier_part_detail: true, }; // Augment the rendered form when we receive information about the StockItem options.processResults = function(data, fields, options) { if (data.part_detail.trackable) { delete options.fields.delete_on_deplete; } else { // Remove serial number field if part is not trackable delete options.fields.serial; } // Remove pricing fields if part is not purchaseable if (!data.part_detail.purchaseable) { delete options.fields.supplier_part; delete options.fields.purchase_price; delete options.fields.purchase_price_currency; } }; constructForm(url, options); } /* * Launch an API form to contsruct a new stock item */ function createNewStockItem(options={}) { var url = '{% url "api-stock-list" %}'; options.title = '{% trans "New Stock Item" %}'; options.method = 'POST'; options.create = true; options.persist = true; options.persistMessage = '{% trans "Create another item after this one" %}'; options.successMessage = '{% trans "Stock item created" %}'; options.fields = stockItemFields(options); options.groups = stockItemGroups(options); if (!options.onSuccess) { options.onSuccess = function(response) { // If a single stock item has been created, follow it! if (response.pk) { var url = `/stock/item/${response.pk}/`; addCachedAlert('{% trans "Created new stock item" %}', { icon: 'fas fa-boxes', }); window.location.href = url; } else { // Multiple stock items have been created (i.e. serialized stock) var details = `
{% trans "Quantity" %}: ${response.quantity}
{% trans "Serial Numbers" %}: ${response.serial_numbers} `; showMessage('{% trans "Created multiple stock items" %}', { icon: 'fas fa-boxes', details: details, }); var table = options.table || '#stock-table'; // Reload the table $(table).bootstrapTable('refresh'); } }; } constructForm(url, options); } /* * Launch a modal form to find a particular stock item by serial number. * Arguments: * - part: ID (PK) of the part in question */ function findStockItemBySerialNumber(part_id) { constructFormBody({}, { title: '{% trans "Find Serial Number" %}', fields: { serial: { label: '{% trans "Serial Number" %}', help_text: '{% trans "Enter serial number" %}', placeholder: '{% trans "Enter serial number" %}', required: true, type: 'string', value: '', } }, onSubmit: function(fields, opts) { var serial = getFormFieldValue('serial', fields['serial'], opts); serial = serial.toString().trim(); if (!serial) { handleFormErrors( { 'serial': [ '{% trans "Enter a serial number" %}', ] }, fields, opts ); return; } inventreeGet( '{% url "api-stock-list" %}', { part_tree: part_id, serial: serial, }, { success: function(response) { if (response.length == 0) { // No results! handleFormErrors( { 'serial': [ '{% trans "No matching serial number" %}', ] }, fields, opts ); } else if (response.length > 1) { // Too many results! handleFormErrors( { 'serial': [ '{% trans "More than one matching result found" %}', ] }, fields, opts ); } else { $(opts.modal).modal('hide'); // Redirect var pk = response[0].pk; location.href = `/stock/item/${pk}/`; } }, error: function(xhr) { showApiError(xhr, opts.url); $(opts.modal).modal('hide'); } } ); } }); } /* 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 %} ]; } /** * Assign multiple stock items to a customer */ function assignStockToCustomer(items, options={}) { // Generate HTML content for the form var html = ` `; for (var idx = 0; idx < items.length; idx++) { var item = items[idx]; var pk = item.pk; var part = item.part_detail; var thumbnail = thumbnailImage(part.thumbnail || part.image); var status = stockStatusDisplay(item.status, {classes: 'float-right'}); var quantity = ''; if (item.serial && item.quantity == 1) { quantity = `{% trans "Serial" %}: ${item.serial}`; } else { quantity = `{% trans "Quantity" %}: ${item.quantity}`; } quantity += status; var location = locationDetail(item, false); var buttons = `
`; buttons += makeRemoveButton( 'button-stock-item-remove', pk, '{% trans "Remove row" %}', ); buttons += '
'; html += ` `; } html += `
{% trans "Part" %} {% trans "Stock Item" %} {% trans "Location" %}
${thumbnail} ${part.full_name}
${quantity}
${location} ${buttons}
`; constructForm('{% url "api-stock-assign" %}', { method: 'POST', preFormContent: html, fields: { customer: { value: options.customer, filters: { is_customer: true, }, }, notes: { icon: 'fa-sticky-note', }, }, confirm: true, confirmMessage: '{% trans "Confirm stock assignment" %}', title: '{% trans "Assign Stock to Customer" %}', 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(); }); }, onSubmit: function(fields, opts) { // Extract data elements from the form var data = { customer: getFormFieldValue('customer', {}, opts), notes: getFormFieldValue('notes', {}, opts), items: [], }; 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.exists()) { item_pk_values.push(pk); data.items.push({ item: pk, }); } }); opts.nested = { 'items': item_pk_values, }; inventreePut( '{% url "api-stock-assign" %}', data, { method: 'POST', success: function(response) { $(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, opts.url); break; } } } ); } }); } /** * Merge multiple stock items together */ function mergeStockItems(items, options={}) { // Generate HTML content for the form var html = `
{% trans "Warning: Merge operation cannot be reversed" %}
{% trans "Some information will be lost when merging stock items" %}:
`; html += ` `; // Keep track of how many "locations" there are var locations = []; for (var idx = 0; idx < items.length; idx++) { var item = items[idx]; var pk = item.pk; if (item.location && !locations.includes(item.location)) { locations.push(item.location); } var part = item.part_detail; var location = locationDetail(item, false); var thumbnail = thumbnailImage(part.thumbnail || part.image); var quantity = ''; if (item.serial && item.quantity == 1) { quantity = `{% trans "Serial" %}: ${item.serial}`; } else { quantity = `{% trans "Quantity" %}: ${item.quantity}`; } quantity += stockStatusDisplay(item.status, {classes: 'float-right'}); let buttons = wrapButtons( makeIconButton( 'fa-times icon-red', 'button-stock-item-remove', pk, '{% trans "Remove row" %}', ) ); html += ` `; } html += '
{% trans "Part" %} {% trans "Stock Item" %} {% trans "Location" %}
${thumbnail} ${part.full_name}
${quantity}
${location} ${buttons}
'; var location = locations.length == 1 ? locations[0] : null; constructForm('{% url "api-stock-merge" %}', { method: 'POST', preFormContent: html, fields: { location: { value: location, icon: 'fa-sitemap', filters: { structural: false, } }, notes: { icon: 'fa-sticky-note', }, allow_mismatched_suppliers: {}, allow_mismatched_status: {}, }, confirm: true, confirmMessage: '{% trans "Confirm stock item merge" %}', title: '{% trans "Merge Stock Items" %}', 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(); }); }, onSubmit: function(fields, opts) { // Extract data elements from the form var data = { items: [], }; var item_pk_values = []; items.forEach(function(item) { var pk = item.pk; // Does the row still exist in the form? var row = $(opts.modal).find(`#stock_item_${pk}`); if (row.exists()) { item_pk_values.push(pk); data.items.push({ item: pk, }); } }); var extra_fields = [ 'location', 'notes', 'allow_mismatched_suppliers', 'allow_mismatched_status', ]; extra_fields.forEach(function(field) { data[field] = getFormFieldValue(field, fields[field], opts); }); opts.nested = { 'items': item_pk_values }; // Submit the form data inventreePut( '{% url "api-stock-merge" %}', data, { method: 'POST', success: function(response) { $(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, opts.url); break; } } } ); } }); } /** * 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) && (item.serial != '') && !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; if (item.part_detail.units != null) { quantity += ` ${item.part_detail.units}`; } var location = locationDetail(item, false); if (item.location_detail) { location = item.location_detail.pathstring; } if (item.serial != null) { quantity = `#${item.serial}`; } if (item.batch) { quantity += ` - {% trans "Batch" %}: ${item.batch}`; } 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, } ); } let buttons = wrapButtons(makeRemoveButton( 'button-stock-item-remove', pk, '{% trans "Remove stock item" %}', )); 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 = { filters: { structural: false, }, }; } 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.exists()) { 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 ids = []; items.forEach(function(item) { ids.push(item.pk); }); showModalSpinner(opts.modal, true); inventreeDelete( '{% url "api-stock-list" %}', { data: { items: ids, }, success: function(response) { $(opts.modal).modal('hide'); options.success(response); } } ); 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, opts.url); 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 = renderDate(row.date); if (row.user_detail) { html += `${row.user_detail.username}`; } return html; } /* Construct set of default fields for a StockItemTestResult */ function stockItemTestResultFields(options={}) { let fields = { test: {}, result: {}, value: {}, attachment: {}, notes: { icon: 'fa-sticky-note', }, stock_item: { hidden: true, }, }; if (options.stock_item) { fields.stock_item.value = options.stock_item; } return fields; } /* * Load StockItemTestResult table */ function loadStockTestResultsTable(table, options) { // Setup filters for the table var filterTarget = options.filterTarget || '#filter-list-stocktests'; var filterKey = options.filterKey || options.name || 'stocktests'; let params = { part: options.part, }; var filters = loadTableFilters(filterKey, params); setupFilterList(filterKey, table, filterTarget); function makeButtons(row, grouped) { // Helper function for rendering buttons let html = ''; if (row.requires_attachment == false && row.requires_value == false && !row.result) { // Enable a "quick tick" option for this test result html += makeIconButton('fa-check-circle icon-green', 'button-test-tick', row.test_name, '{% trans "Pass test" %}'); } 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 += makeEditButton('button-test-edit', pk, '{% trans "Edit test result" %}'); html += makeDeleteButton('button-test-delete', pk, '{% trans "Delete test result" %}'); } return wrapButtons(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: 'pk', treeShowField: 'test_name', formatNoMatches: function() { return '{% trans "No test results found" %}'; }, queryParams: filters, original: params, onPostBody: function() { table.treegrid({ treeColumn: 0, }); table.treegrid('collapseAll'); }, columns: [ { field: 'pk', title: 'ID', visible: false, switchable: false, }, { field: 'test_name', title: '{% trans "Test" %}', 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: 'description', title: '{% trans "Description" %}', formatter: function(value, row) { return row.description || row.test_description; } }, { field: 'value', title: '{% trans "Value" %}', formatter: function(value, row) { var html = value; if (row.attachment) { let text = makeIconBadge('fa-file-alt', ''); html += renderLink(text, row.attachment, {download: true}); } return html; } }, { 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 var filters = loadTableFilters(filterKey); var query_params = { stock_item: options.stock_item, user_detail: true, attachment_detail: true, ordering: '-date', }; if ('result' in filters) { query_params.result = filters.result; } if ('include_installed' in filters) { query_params.include_installed = filters.include_installed; } inventreeGet( '{% url "api-stock-test-result-list" %}', query_params, { 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.test_description = row.description; 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); } } ); } }); /* Register button callbacks */ function reloadTestTable(response) { $(table).bootstrapTable('refresh'); } // "tick" a test result $(table).on('click', '.button-test-tick', function() { var button = $(this); var test_name = button.attr('pk'); inventreePut( '{% url "api-stock-test-result-list" %}', { test: test_name, result: true, stock_item: options.stock_item, }, { method: 'POST', success: reloadTestTable, } ); }); // Add a test result $(table).on('click', '.button-test-add', function() { var button = $(this); var test_name = button.attr('pk'); constructForm('{% url "api-stock-test-result-list" %}', { method: 'POST', fields: { test: { value: test_name, }, result: {}, value: {}, attachment: {}, notes: { icon: 'fa-sticky-note', }, stock_item: { value: options.stock_item, hidden: true, } }, title: '{% trans "Add Test Result" %}', onSuccess: reloadTestTable, }); }); // Edit a test result $(table).on('click', '.button-test-edit', function() { var button = $(this); var pk = button.attr('pk'); var url = `/api/stock/test/${pk}/`; constructForm(url, { fields: stockItemTestResultFields(), title: '{% trans "Edit Test Result" %}', onSuccess: reloadTestTable, }); }); // Delete a test result $(table).on('click', '.button-test-delete', function() { var button = $(this); var pk = button.attr('pk'); var url = `/api/stock/test/${pk}/`; var row = $(table).bootstrapTable('getRowByUniqueId', pk); var html = `
{% trans "Delete test result" %}: ${row.test_name || row.test || row.key}
`; constructForm(url, { method: 'DELETE', title: '{% trans "Delete Test Result" %}', onSuccess: reloadTestTable, preFormContent: html, }); }); } 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}/?display=installed-items`; } else if (row.customer) { // StockItem has been assigned to a customer text = '{% trans "Shipped to customer" %}'; url = `/company/${row.customer}/?display=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 && row.location_detail) { text = shortenString(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; } } /* 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 * buttons - Which buttons to link to stock selection callbacks * filterList -