{% load i18n %} {% load inventree_extras %} /* globals buildStatusDisplay, constructForm, imageHoverIcon, inventreeGet, launchModalForm, linkButtonsToSelection, loadTableFilters, makeIconBadge, makeIconButton, makePartIcons, makeProgressBar, renderLink, setupFilterList, */ /* exported allocateStockToBuild, autoAllocateStockToBuild, cancelBuildOrder, completeBuildOrder, createBuildOutput, duplicateBuildOrder, editBuildOrder, loadAllocationTable, loadBuildOrderAllocationTable, loadBuildOutputAllocationTable, loadBuildOutputTable, loadBuildTable, */ function buildFormFields() { return { reference: { icon: 'fa-hashtag', }, part: { filters: { assembly: true, virtual: false, } }, title: {}, quantity: {}, priority: {}, parent: { filters: { part_detail: true, } }, sales_order: { icon: 'fa-truck', }, batch: {}, target_date: { icon: 'fa-calendar-alt', }, take_from: { icon: 'fa-sitemap', filters: { structural: false, } }, destination: { icon: 'fa-sitemap', filters: { structural: false, } }, link: { icon: 'fa-link', }, issued_by: { icon: 'fa-user', }, responsible: { icon: 'fa-users', }, }; } /* * Edit an existing BuildOrder via the API */ function editBuildOrder(pk) { var fields = buildFormFields(); constructForm(`{% url "api-build-list" %}${pk}/`, { fields: fields, reload: true, title: '{% trans "Edit Build Order" %}', }); } /* * Create a new build order via an API form */ function newBuildOrder(options={}) { /* Launch modal form to create a new BuildOrder. */ 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; } if (options.data) { delete options.data.pk; } constructForm(`/api/build/`, { fields: fields, data: options.data, follow: true, method: 'POST', title: '{% trans "Create Build Order" %}', onSuccess: options.onSuccess, }); } /* * Duplicate an existing build order. */ function duplicateBuildOrder(build_id, options={}) { inventreeGet(`{% url "api-build-list" %}${build_id}/`, {}, { success: function(data) { // Clear out data we do not want to be duplicated delete data['pk']; delete data['issued_by']; delete data['reference']; options.data = data; newBuildOrder(options); } }); } /* Construct a form to cancel a build order */ function cancelBuildOrder(build_id, options={}) { constructForm( `{% url "api-build-list" %}${build_id}/cancel/`, { method: 'POST', title: '{% trans "Cancel Build Order" %}', confirm: true, fields: { remove_allocated_stock: {}, remove_incomplete_outputs: {}, }, preFormContent: function(opts) { var html = `
{% trans "Are you sure you wish to cancel this build?" %}
`; if (opts.context.has_allocated_stock) { html += `
{% trans "Stock items have been allocated to this build order" %}
`; } if (opts.context.incomplete_outputs) { html += `
{% trans "There are incomplete outputs remaining for this build order" %}
`; } return html; }, onSuccess: function(response) { handleFormSuccess(response, options); } } ); } /* Construct a form to "complete" (finish) a build order */ function completeBuildOrder(build_id, options={}) { constructForm(`{% url "api-build-list" %}${build_id}/finish/`, { fieldsFunction: function(opts) { var ctx = opts.context || {}; var fields = { accept_unallocated: {}, accept_overallocated: {}, accept_incomplete: {}, }; // Hide "accept overallocated" field if the build is *not* overallocated if (!ctx.overallocated) { delete fields.accept_overallocated; } // Hide "accept incomplete" field if the build has been completed if (!ctx.remaining || ctx.remaining == 0) { delete fields.accept_incomplete; } // Hide "accept unallocated" field if the build is fully allocated if (ctx.allocated) { delete fields.accept_unallocated; } return fields; }, preFormContent: function(opts) { var ctx = opts.context || {}; var html = ''; if (ctx.allocated && ctx.remaining == 0 && ctx.incomplete == 0) { html += `
{% trans "Build order is ready to be completed" %}'
`; } else { if (ctx.incomplete > 0) { html += `
{% trans "Build order has incomplete outputs" %}
{% trans "This build order cannot be completed as there are incomplete outputs" %}
`; } else { html += `
{% trans "Build Order is incomplete" %}
`; } if (!ctx.allocated) { html += `
{% trans "Required stock has not been fully allocated" %}
`; } if (ctx.remaining > 0) { html += `
{% trans "Required build quantity has not been completed" %}
`; } } return html; }, reload: true, confirm: true, title: '{% trans "Complete Build Order" %}', method: 'POST', }); } /* * Construct a new build output against the provided build */ function createBuildOutput(build_id, options) { // Request build order information from the server inventreeGet( `{% url "api-build-list" %}${build_id}/`, {}, { success: function(build) { var html = ''; var trackable = build.part_detail.trackable; var remaining = Math.max(0, build.quantity - build.completed); var fields = { quantity: { value: remaining, }, serial_numbers: { hidden: !trackable, required: options.trackable_parts || trackable, }, batch_code: {}, auto_allocate: { hidden: !trackable, }, }; // Work out the next available serial numbers inventreeGet(`{% url "api-part-list" %}${build.part}/serial-numbers/`, {}, { success: function(data) { if (data.next) { fields.serial_numbers.placeholder = `{% trans "Next available serial number" %}: ${data.next}`; } else if (data.latest) { fields.serial_numbers.placeholder = `{% trans "Latest serial number" %}: ${data.latest}`; } }, async: false, }); if (options.trackable_parts) { html += `
{% trans "The Bill of Materials contains trackable parts" %}.
{% trans "Build outputs must be generated individually" %}.
`; } if (trackable) { html += `
{% trans "Trackable parts can have serial numbers specified" %}
{% trans "Enter serial numbers to generate multiple single build outputs" %}
`; } constructForm(`{% url "api-build-list" %}${build_id}/create-output/`, { method: 'POST', title: '{% trans "Create Build Output" %}', confirm: true, fields: fields, preFormContent: html, onSuccess: function(response) { location.reload(); }, }); } } ); } /* * Construct a set of output buttons for a particular build output */ function makeBuildOutputButtons(output_id, build_info, options={}) { var html = ''; // Tracked parts? Must be individually allocated if (options.has_bom_items) { // Add a button to allocate stock against this build output html += makeIconButton( 'fa-sign-in-alt icon-blue', 'button-output-allocate', output_id, '{% trans "Allocate stock items to this build output" %}', { disabled: true, } ); // Add a button to unallocate stock from this build output html += makeIconButton( 'fa-minus-circle icon-red', 'button-output-unallocate', output_id, '{% trans "Unallocate stock from build output" %}', ); } // Add a button to "complete" this build output html += makeIconButton( 'fa-check-circle icon-green', 'button-output-complete', output_id, '{% trans "Complete build output" %}', ); // Add a button to "delete" this build output html += makeDeleteButton( 'button-output-delete', output_id, '{% trans "Delete build output" %}', ); return wrapButtons(html); } /* * Unallocate stock against a particular build order * * Options: * - output: pk value for a stock item "build output" * - bom_item: pk value for a particular BOMItem (build item) */ function unallocateStock(build_id, options={}) { var url = `{% url "api-build-list" %}${build_id}/unallocate/`; var html = `
{% trans "Are you sure you wish to unallocate stock items from this build?" %} `; constructForm(url, { method: 'POST', confirm: true, preFormContent: html, fields: { output: { hidden: true, value: options.output, }, bom_item: { hidden: true, value: options.bom_item, }, }, title: '{% trans "Unallocate Stock Items" %}', onSuccess: function(response, opts) { if (options.onSuccess) { options.onSuccess(response, opts); } else if (options.table) { // Reload the parent table $(options.table).bootstrapTable('refresh'); } } }); } /** * Launch a modal form to complete selected build outputs */ function completeBuildOutputs(build_id, outputs, options={}) { if (outputs.length == 0) { showAlertDialog( '{% trans "Select Build Outputs" %}', '{% trans "At least one build output must be selected" %}', ); return; } // Render a single build output (StockItem) function renderBuildOutput(output, opts={}) { var pk = output.pk; var output_html = imageHoverIcon(output.part_detail.thumbnail); if (output.quantity == 1 && output.serial) { output_html += `{% trans "Serial Number" %}: ${output.serial}`; } else { output_html += `{% trans "Quantity" %}: ${output.quantity}`; if (output.part_detail && output.part_detail.units) { output_html += ` ${output.part_detail.units} `; } } var buttons = `
`; buttons += makeRemoveButton('button-row-remove', pk, '{% trans "Remove row" %}'); buttons += '
'; var field = constructField( `outputs_output_${pk}`, { type: 'raw', html: output_html, }, { hideLabels: true, } ); var html = ` ${field} ${output.part_detail.full_name} ${buttons} `; return html; } // Construct table entries var table_entries = ''; outputs.forEach(function(output) { table_entries += renderBuildOutput(output); }); var html = ` ${table_entries}
{% trans "Output" %}
`; constructForm(`{% url "api-build-list" %}${build_id}/complete/`, { method: 'POST', preFormContent: html, fields: { status: {}, location: { filters: { structural: false, }, }, notes: { icon: 'fa-sticky-note', }, accept_incomplete_allocation: {}, }, confirm: true, title: '{% trans "Complete Build Outputs" %}', afterRender: function(fields, opts) { // Setup callbacks to remove outputs $(opts.modal).find('.button-row-remove').click(function() { var pk = $(this).attr('pk'); $(opts.modal).find(`#output_row_${pk}`).remove(); }); }, onSubmit: function(fields, opts) { // Extract data elements from the form var data = { outputs: [], status: getFormFieldValue('status', {}, opts), location: getFormFieldValue('location', {}, opts), notes: getFormFieldValue('notes', {}, opts), accept_incomplete_allocation: getFormFieldValue('accept_incomplete_allocation', {type: 'boolean'}, opts), }; var output_pk_values = []; outputs.forEach(function(output) { var pk = output.pk; var row = $(opts.modal).find(`#output_row_${pk}`); if (row.exists()) { data.outputs.push({ output: pk, }); output_pk_values.push(pk); } }); // Provide list of nested values opts.nested = { 'outputs': output_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, opts.url); break; } } } ); } }); } /** * Launch a modal form to delete selected build outputs */ function deleteBuildOutputs(build_id, outputs, options={}) { if (outputs.length == 0) { showAlertDialog( '{% trans "Select Build Outputs" %}', '{% trans "At least one build output must be selected" %}', ); return; } // Render a single build output (StockItem) function renderBuildOutput(output, opts={}) { var pk = output.pk; var output_html = imageHoverIcon(output.part_detail.thumbnail); if (output.quantity == 1 && output.serial) { output_html += `{% trans "Serial Number" %}: ${output.serial}`; } else { output_html += `{% trans "Quantity" %}: ${output.quantity}`; if (output.part_detail && output.part_detail.units) { output_html += ` ${output.part_detail.units} `; } } var buttons = `
`; buttons += makeRemoveButton('button-row-remove', pk, '{% trans "Remove row" %}'); buttons += '
'; var field = constructField( `outputs_output_${pk}`, { type: 'raw', html: output_html, }, { hideLabels: true, } ); var html = ` ${field} ${output.part_detail.full_name} ${buttons} `; return html; } // Construct table entries var table_entries = ''; outputs.forEach(function(output) { table_entries += renderBuildOutput(output); }); var html = ` ${table_entries}
{% trans "Output" %}
`; constructForm(`{% url "api-build-list" %}${build_id}/delete-outputs/`, { method: 'POST', preFormContent: html, fields: {}, confirm: true, title: '{% trans "Delete Build Outputs" %}', afterRender: function(fields, opts) { // Setup callbacks to remove outputs $(opts.modal).find('.button-row-remove').click(function() { var pk = $(this).attr('pk'); $(opts.modal).find(`#output_row_${pk}`).remove(); }); }, onSubmit: function(fields, opts) { var data = { outputs: [], }; var output_pk_values = []; outputs.forEach(function(output) { var pk = output.pk; var row = $(opts.modal).find(`#output_row_${pk}`); if (row.exists()) { data.outputs.push({ output: pk }); output_pk_values.push(pk); } }); opts.nested = { 'outputs': output_pk_values, }; inventreePut( opts.url, 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; } } } ); } }); } /** * Load a table showing all the BuildOrder allocations for a given part */ function loadBuildOrderAllocationTable(table, options={}) { options.params['part_detail'] = true; options.params['build_detail'] = true; options.params['location_detail'] = true; options.params['stock_detail'] = true; var filters = loadTableFilters('buildorderallocation', options.params); setupFilterList('buildorderallocation', $(table)); $(table).inventreeTable({ url: '{% url "api-build-item-list" %}', queryParams: filters, name: 'buildorderallocation', groupBy: false, search: false, paginationVAlign: 'bottom', original: options.params, formatNoMatches: function() { return '{% trans "No build order allocations found" %}'; }, columns: [ { field: 'pk', visible: false, switchable: false, }, { field: 'build', switchable: false, title: '{% trans "Build Order" %}', formatter: function(value, row) { var ref = `${row.build_detail.reference}`; return renderLink(ref, `/build/${row.build}/`); } }, { field: 'item', title: '{% trans "Stock Item" %}', formatter: function(value, row) { // Render a link to the particular stock item var link = `/stock/item/${row.stock_item}/`; var text = `{% trans "Stock Item" %} ${row.stock_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, } ] }); } /* Internal helper functions for performing calculations on BOM data */ // Iterate through a list of allocations, returning *only* those which match a particular BOM row function getAllocationsForBomRow(bom_row, allocations) { var part_id = bom_row.sub_part; var matching_allocations = []; allocations.forEach(function(allocation) { if (allocation.bom_part == part_id) { matching_allocations.push(allocation); } }); return matching_allocations; } // Sum the allocation quantity for a given BOM row function sumAllocationsForBomRow(bom_row, allocations) { var quantity = 0; getAllocationsForBomRow(bom_row, allocations).forEach(function(allocation) { quantity += allocation.quantity; }); return formatDecimal(quantity, 10); } /* * Display a "build output" table for a particular build. * * This displays a list of "active" (i.e. "in production") build outputs for a given build * */ function loadBuildOutputTable(build_info, options={}) { var table = options.table || '#build-output-table'; var params = options.params || {}; // Mandatory query filters params.part_detail = true; params.is_building = true; params.build = build_info.pk; var filters = {}; for (var key in params) { filters[key] = params[key]; } setupFilterList('builditems', $(table), options.filterTarget || '#filter-list-incompletebuilditems', { labels: { url: '{% url "api-stockitem-label-list" %}', key: 'item', }, singular_name: '{% trans "build output" %}', plural_name: '{% trans "build outputs" %}', }); function setupBuildOutputButtonCallbacks() { // Callback for the "allocate" button $(table).find('.button-output-allocate').click(function() { var pk = $(this).attr('pk'); // Find the "allocation" sub-table associated with this output var subtable = $(`#output-sub-table-${pk}`); if (subtable.exists()) { var rows = getTableData(`#output-sub-table-${pk}`); allocateStockToBuild( build_info.pk, build_info.part, rows, { output: pk, success: function() { $(table).bootstrapTable('refresh'); } } ); } else { console.warn(`Could not locate sub-table for output ${pk}`); } }); // Callack for the "unallocate" button $(table).find('.button-output-unallocate').click(function() { var pk = $(this).attr('pk'); unallocateStock(build_info.pk, { output: pk, table: table }); }); // Callback for the "complete" button $(table).find('.button-output-complete').click(function() { var pk = $(this).attr('pk'); var output = $(table).bootstrapTable('getRowByUniqueId', pk); completeBuildOutputs( build_info.pk, [ output, ], { success: function() { $(table).bootstrapTable('refresh'); $('#build-stock-table').bootstrapTable('refresh'); } } ); }); // Callback for the "delete" button $(table).find('.button-output-delete').click(function() { var pk = $(this).attr('pk'); var output = $(table).bootstrapTable('getRowByUniqueId', pk); deleteBuildOutputs( build_info.pk, [ output, ], { success: function() { $(table).bootstrapTable('refresh'); $('#build-stock-table').bootstrapTable('refresh'); } } ); }); } // List of "tracked bom items" required for this build order var bom_items = null; // Request list of BOM data for this build order inventreeGet( '{% url "api-bom-list" %}', { part: build_info.part, sub_part_detail: true, sub_part_trackable: true, }, { async: false, success: function(response) { // Save the BOM items bom_items = response; } } ); /* * Construct a "sub table" showing the required BOM items */ function constructBuildOutputSubTable(index, row, element) { var sub_table_id = `output-sub-table-${row.pk}`; var html = `
`; element.html(html); // Pass through the cached BOM items build_info.bom_items = bom_items; loadBuildOutputAllocationTable( build_info, row, { table: `#${sub_table_id}`, parent_table: table, } ); } function updateAllocationData(rows) { // Update stock allocation information for the build outputs // Request updated stock allocation data for this build order inventreeGet( '{% url "api-build-item-list" %}', { build: build_info.pk, part_detail: true, location_detail: true, sub_part_trackable: true, tracked: true, }, { success: function(response) { // Group allocation information by the "install_into" field var allocations = {}; response.forEach(function(allocation) { var target = allocation.install_into; if (target != null) { if (!(target in allocations)) { allocations[target] = []; } allocations[target].push(allocation); } }); // Now that the allocations have been grouped by stock item, // we can update each row in the table, // using the pk value of each row (stock item) var data = []; rows.forEach(function(row) { row.allocations = allocations[row.pk] || []; data.push(row); var n_completed_lines = 0; // Check how many BOM lines have been completely allocated for this build output bom_items.forEach(function(bom_item) { var required_quantity = bom_item.quantity * row.quantity; if (sumAllocationsForBomRow(bom_item, row.allocations) >= required_quantity) { n_completed_lines += 1; } var output_progress_bar = $(`#output-progress-${row.pk}`); if (output_progress_bar.exists()) { output_progress_bar.html( makeProgressBar( n_completed_lines, bom_items.length, { max_width: '150px', } ) ); } }); }); // Reload table with updated data $(table).bootstrapTable('load', data); } } ); } var part_tests = null; function updateTestResultData(rows) { // Update test result information for the build outputs // Request test template data if it has not already been retrieved if (part_tests == null) { inventreeGet( '{% url "api-part-test-template-list" %}', { part: build_info.part, required: true, }, { success: function(response) { // Save the list of part tests part_tests = response; // Callback to this function again updateTestResultData(rows); } } ); return; } // Retrieve stock results for the entire build inventreeGet( '{% url "api-stock-test-result-list" %}', { build: build_info.pk, ordering: '-date', }, { success: function(results) { var data = []; // Iterate through each row and find matching test results rows.forEach(function(row) { var test_results = {}; results.forEach(function(result) { if (result.stock_item == row.pk) { // This test result matches the particular stock item if (!(result.key in test_results)) { test_results[result.key] = result.result; } } }); row.passed_tests = test_results; data.push(row); }); $(table).bootstrapTable('load', data); } } ); } // Return the number of 'passed' tests in a given row function countPassedTests(row) { if (part_tests == null) { return 0; } var results = row.passed_tests || {}; var n = 0; part_tests.forEach(function(test) { if (results[test.key] || false) { n += 1; } }); return n; } // Return the number of 'fully allocated' lines for a given row function countAllocatedLines(row) { var n_completed_lines = 0; bom_items.forEach(function(bom_row) { var required_quantity = bom_row.quantity * row.quantity; if (sumAllocationsForBomRow(bom_row, row.allocations || []) >= required_quantity) { n_completed_lines += 1; } }); return n_completed_lines; } $(table).inventreeTable({ url: '{% url "api-stock-list" %}', queryParams: filters, original: params, showColumns: true, uniqueId: 'pk', name: 'build-outputs', sortable: true, search: true, sidePagination: 'client', detailView: bom_items.length > 0, detailFilter: function(index, row) { return bom_items.length > 0; }, detailFormatter: function(index, row, element) { constructBuildOutputSubTable(index, row, element); }, formatNoMatches: function() { return '{% trans "No active build outputs found" %}'; }, onPostBody: function(rows) { // Add callbacks for the buttons setupBuildOutputButtonCallbacks(); }, onLoadSuccess: function(rows) { updateAllocationData(rows); updateTestResultData(rows); }, buttons: constructExpandCollapseButtons(table), columns: [ { title: '', visible: true, checkbox: true, switchable: false, }, { field: 'part', title: '{% trans "Part" %}', switchable: true, formatter: function(value, row) { var thumb = row.part_detail.thumbnail; return imageHoverIcon(thumb) + row.part_detail.full_name + makePartIcons(row.part_detail); } }, { field: 'quantity', title: '{% trans "Build Output" %}', switchable: false, sortable: true, formatter: function(value, row) { var url = `/stock/item/${row.pk}/`; var text = ''; if (row.serial && row.quantity == 1) { text = `{% trans "Serial Number" %}: ${row.serial}`; } else { text = `{% trans "Quantity" %}: ${row.quantity}`; if (row.part_detail && row.part_detail.units) { text += ` ${row.part_detail.units}`; } } if (row.batch) { text += ` ({% trans "Batch" %}: ${row.batch})`; } text += stockStatusDisplay(row.status, {classes: 'float-right'}); return renderLink(text, url); }, sorter: function(a, b, row_a, row_b) { // Sort first by quantity, and then by serial number if ((row_a.quantity > 1) || (row_b.quantity > 1)) { return row_a.quantity > row_b.quantity ? 1 : -1; } if ((row_a.serial != null) && (row_b.serial != null)) { var sn_a = Number.parseInt(row_a.serial) || 0; var sn_b = Number.parseInt(row_b.serial) || 0; return sn_a > sn_b ? 1 : -1; } return 0; } }, { field: 'allocated', title: '{% trans "Allocated Stock" %}', visible: bom_items.length > 0, switchable: false, sortable: true, formatter: function(value, row) { if (bom_items.length == 0) { return `
{% trans "No tracked BOM items for this build" %}
`; } var progressBar = makeProgressBar( countAllocatedLines(row), bom_items.length, { max_width: '150px', } ); return `
${progressBar}
`; }, sorter: function(value_a, value_b, row_a, row_b) { var q_a = countAllocatedLines(row_a); var q_b = countAllocatedLines(row_b); return q_a > q_b ? 1 : -1; }, }, { field: 'tests', title: '{% trans "Completed Tests" %}', sortable: true, switchable: true, formatter: function(value, row) { if (part_tests == null || part_tests.length == 0) { return `{% trans "No required tests for this build" %}`; } var n_passed = countPassedTests(row); var progress = makeProgressBar( n_passed, part_tests.length, { max_width: '150px', } ); return progress; }, sorter: function(a, b, row_a, row_b) { var n_a = countPassedTests(row_a); var n_b = countPassedTests(row_b); return n_a > n_b ? 1 : -1; } }, { field: 'actions', title: '', switchable: false, formatter: function(value, row) { return makeBuildOutputButtons( row.pk, build_info, { has_bom_items: bom_items.length > 0, } ); } } ] }); // Enable the "allocate" button when the sub-table is exanded $(table).on('expand-row.bs.table', function(detail, index, row) { $(`#button-output-allocate-${row.pk}`).prop('disabled', false); }); // Disable the "allocate" button when the sub-table is collapsed $(table).on('collapse-row.bs.table', function(detail, index, row) { $(`#button-output-allocate-${row.pk}`).prop('disabled', true); }); // Add callbacks for the various table menubar buttons // Complete multiple outputs $('#multi-output-complete').click(function() { var outputs = getTableData(table); completeBuildOutputs( build_info.pk, outputs, { success: function() { // Reload the "in progress" table $('#build-output-table').bootstrapTable('refresh'); // Reload the "completed" table $('#build-stock-table').bootstrapTable('refresh'); } } ); }); // Delete multiple build outputs $('#multi-output-delete').click(function() { var outputs = getTableData(table); deleteBuildOutputs( build_info.pk, outputs, { success: function() { // Reload the "in progress" table $('#build-output-table').bootstrapTable('refresh'); // Reload the "completed" table $('#build-stock-table').bootstrapTable('refresh'); } } ); }); $('#outputs-expand').click(function() { $(table).bootstrapTable('expandAllRows'); }); $('#outputs-collapse').click(function() { $(table).bootstrapTable('collapseAllRows'); }); } /* * Display the "allocation table" for a particular build output. * * This displays a table of required allocations for a particular build output * * Args: * - buildId: The PK of the Build object * - partId: The PK of the Part object * - output: The StockItem object which is the "output" of the build * - options: * -- table: The #id of the table (will be auto-calculated if not provided) */ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var buildId = buildInfo.pk; var partId = buildInfo.part; var outputId = null; if (output) { outputId = output.pk; } else { outputId = 'untracked'; } var bom_items = buildInfo.bom_items || null; // If BOM items have not been provided, load via the API if (bom_items == null) { inventreeGet( '{% url "api-bom-list" %}', { part: partId, sub_part_detail: true, sub_part_trackable: buildInfo.tracked_parts, }, { async: false, success: function(results) { bom_items = results; } } ); } // Apply filters to build table // As the table is constructed locally, we can apply filters directly function filterBuildAllocationTable(filters={}) { $(table).bootstrapTable( 'filterBy', filters, { 'filterAlgorithm': function(row, filters) { let result = true; if (!filters) { return true; } // Filter by 'consumable' flag if ('consumable' in filters) { result &= filters.consumable == '1' ? row.consumable : !row.consumable; } // Filter by 'optional' flag if ('optional' in filters) { result &= filters.optional == '1' ? row.optional : !row.optional; } // Filter by 'allocated' flag if ('allocated' in filters) { let fully_allocated = row.consumable || isRowFullyAllocated(row); result &= filters.allocated == '1' ? fully_allocated : !fully_allocated; } // Filter by 'available' flag if ('available' in filters) { let available = row.available_stock > 0; result &= filters.available == '1' ? available : !available; } return result; } } ); } var table = options.table || `#allocation-table-${outputId}`; // Filters let filters = loadTableFilters('builditems', options.params); setupFilterList('builditems', $(table), options.filterTarget, { callback: function(table, filters, options) { filterBuildAllocationTable(filters); } }); var allocated_items = output == null ? null : output.allocations; function redrawAllocationData() { // Force a refresh of each row in the table // Note we cannot call 'refresh' because we are passing data from memory // How many rows are fully allocated? var allocated_rows = 0; for (var idx = 0; idx < bom_items.length; idx++) { var row = bom_items[idx]; if (isRowFullyAllocated(row)) { allocated_rows++; } } // Reload table data $(table).bootstrapTable('load', bom_items); // Find the top-level progess bar for this build output var output_progress_bar = $(`#output-progress-${outputId}`); if (output_progress_bar.exists()) { if (bom_items.length > 0) { output_progress_bar.html( makeProgressBar( allocated_rows, bom_items.length, { max_width: '150px', } ) ); } } else { console.warn(`Could not find progress bar for output '${outputId}'`); } } function reloadAllocationData(async=true) { // Reload stock allocation data for this particular build output inventreeGet( '{% url "api-build-item-list" %}', { build: buildId, part_detail: true, location_detail: true, output: output == null ? null : output.pk, }, { async: async, success: function(response) { allocated_items = response; redrawAllocationData(); } } ); } if (allocated_items == null) { // No allocation data provided? Request from server (blocking) reloadAllocationData(false); } else { redrawAllocationData(); } function requiredQuantity(row) { // Return the requied quantity for a given row var quantity = 0; if (output) { // "Tracked" parts are calculated against individual build outputs quantity = row.quantity * output.quantity; } else { // "Untracked" parts are specified against the build itself quantity = row.quantity * buildInfo.quantity; } // Store the required quantity in the row data // Prevent weird rounding issues row.required = formatDecimal(quantity, 15); return row.required; } function availableQuantity(row) { // Return the total available stock for a given row // Base stock var available = row.available_stock; // Substitute stock available += (row.available_substitute_stock || 0); // Variant stock if (row.allow_variants) { available += (row.available_variant_stock || 0); } return available; } function allocatedQuantity(row) { row.allocated = sumAllocationsForBomRow(row, allocated_items); return row.allocated; } function isRowFullyAllocated(row) { return allocatedQuantity(row) >= requiredQuantity(row); } function setupCallbacks() { // Register button callbacks once table data are loaded // Callback for 'allocate' button $(table).find('.button-add').click(function() { // Primary key of the 'sub_part' var pk = $(this).attr('pk'); // Extract BomItem information from this row var row = $(table).bootstrapTable('getRowByUniqueId', pk); if (!row) { console.warn('getRowByUniqueId returned null'); return; } allocateStockToBuild( buildId, partId, [ row, ], { source_location: buildInfo.source_location, success: function(data) { // $(table).bootstrapTable('refresh'); reloadAllocationData(); }, output: output == null ? null : output.pk, } ); }); // Callback for 'buy' button $(table).find('.button-buy').click(function() { var pk = $(this).attr('pk'); inventreeGet( `/api/part/${pk}/`, {}, { success: function(part) { orderParts( [part], {} ); } } ); }); // Callback for 'build' button $(table).find('.button-build').click(function() { var pk = $(this).attr('pk'); // Extract row data from the table var idx = $(this).closest('tr').attr('data-index'); var row = $(table).bootstrapTable('getData')[idx]; newBuildOrder({ part: pk, parent: buildId, quantity: requiredQuantity(row) - allocatedQuantity(row), }); }); // Callback for 'unallocate' button $(table).find('.button-unallocate').click(function() { // Extract row data from the table var idx = $(this).closest('tr').attr('data-index'); var row = $(table).bootstrapTable('getData')[idx]; unallocateStock(buildId, { bom_item: row.pk, output: outputId == 'untracked' ? null : outputId, table: table, onSuccess: function(response, opts) { reloadAllocationData(); } }); }); } // Load table of BOM items $(table).inventreeTable({ data: bom_items, disablePagination: true, formatNoMatches: function() { return '{% trans "No BOM items found" %}'; }, name: 'build-allocation', uniqueId: 'sub_part', search: options.search || false, queryParams: filters, original: options.params, onRefresh: function(data) { filterBuildAllocationTable(filters); }, onPostBody: function(data) { // Setup button callbacks setupCallbacks(); }, sortable: true, showColumns: true, detailView: true, detailFilter: function(index, row) { return allocatedQuantity(row) > 0; }, buttons: constructExpandCollapseButtons(table), detailFormatter: function(index, row, element) { // Contruct an 'inner table' which shows which stock items have been allocated var subTableId = `allocation-table-${outputId}-${row.pk}`; var html = `
`; element.html(html); var subTable = $(`#${subTableId}`); subTable.bootstrapTable({ data: getAllocationsForBomRow(row, allocated_items), showHeader: true, columns: [ { field: 'part', title: '{% trans "Part" %}', formatter: function(value, row) { var html = imageHoverIcon(row.part_detail.thumbnail); html += renderLink(row.part_detail.full_name, `/part/${value}/`); return html; } }, { width: '50%', field: 'quantity', title: '{% trans "Assigned Stock" %}', formatter: function(value, row) { var text = ''; var url = ''; var serial = row.serial; if (row.stock_item_detail) { serial = row.stock_item_detail.serial; } if (serial && row.quantity == 1) { text = `{% trans "Serial Number" %}: ${serial}`; } else { text = `{% trans "Quantity" %}: ${row.quantity}`; if (row.part_detail && row.part_detail.units) { text += ` ${row.part_detail.units}`; } } var pk = row.stock_item || row.pk; url = `/stock/item/${pk}/`; return renderLink(text, url); } }, { field: 'location', title: '{% trans "Location" %}', formatter: function(value, row) { if (row.location && row.location_detail) { var text = shortenString(row.location_detail.pathstring); var url = `/stock/location/${row.location}/`; return renderLink(text, url); } else { return '{% trans "No location set" %}'; } } }, { field: 'actions', formatter: function(value, row) { /* Actions available for a particular stock item allocation: * * - Edit the allocation quantity * - Delete the allocation */ var pk = row.pk; var html = ''; html += makeEditButton('button-allocation-edit', pk, '{% trans "Edit stock allocation" %}'); html += makeDeleteButton('button-allocation-delete', pk, '{% trans "Delete stock allocation" %}'); return wrapButtons(html); } } ] }); // Assign button callbacks to the newly created allocation buttons subTable.find('.button-allocation-edit').click(function() { var pk = $(this).attr('pk'); constructForm(`{% url "api-build-item-list" %}${pk}/`, { fields: { quantity: {}, }, title: '{% trans "Edit Allocation" %}', onSuccess: reloadAllocationData, }); }); subTable.find('.button-allocation-delete').click(function() { var pk = $(this).attr('pk'); constructForm(`{% url "api-build-item-list" %}${pk}/`, { method: 'DELETE', title: '{% trans "Remove Allocation" %}', onSuccess: reloadAllocationData, }); }); }, columns: [ { visible: true, switchable: false, checkbox: true, }, { field: 'sub_part_detail.full_name', title: '{% trans "Required Part" %}', sortable: true, switchable: false, formatter: function(value, row) { var url = `/part/${row.sub_part}/`; var thumb = row.sub_part_detail.thumbnail; var name = row.sub_part_detail.full_name; var html = imageHoverIcon(thumb) + renderLink(name, url); html += makePartIcons(row.sub_part_detail); if (row.substitutes && row.substitutes.length > 0) { html += makeIconBadge('fa-exchange-alt', '{% trans "Substitute parts available" %}'); } if (row.allow_variants) { html += makeIconBadge('fa-sitemap', '{% trans "Variant stock allowed" %}'); } return html; } }, { field: 'reference', title: '{% trans "Reference" %}', sortable: true, switchable: true, }, { field: 'consumable', title: '{% trans "Consumable" %}', sortable: true, switchable: true, formatter: function(value) { return yesNoLabel(value); } }, { field: 'optional', title: '{% trans "Optional" %}', sortable: true, switchable: true, formatter: function(value) { return yesNoLabel(value); } }, { field: 'quantity', title: '{% trans "Quantity Per" %}', sortable: true, switchable: false, formatter: function(value, row) { var text = value; if (row.sub_part_detail && row.sub_part_detail.units) { text += ` ${row.sub_part_detail.units}`; } return text; } }, { field: 'available_stock', title: '{% trans "Available" %}', sortable: true, switchable: true, formatter: function(value, row) { var url = `/part/${row.sub_part_detail.pk}/?display=part-stock`; // Calculate total "available" (unallocated) quantity var substitute_stock = row.available_substitute_stock || 0; var variant_stock = row.allow_variants ? (row.available_variant_stock || 0) : 0; var available_stock = availableQuantity(row); var required = requiredQuantity(row); var allocated = allocatedQuantity(row); var text = ''; if (available_stock > 0) { text += `${available_stock}`; if (row.sub_part_detail && row.sub_part_detail.units) { text += ` ${row.sub_part_detail.units}`; } } var icons = ''; if (row.consumable) { icons += ``; } else { if (available_stock < (required - allocated)) { icons += makeIconBadge('fa-times-circle icon-red', '{% trans "Insufficient stock available" %}'); } else { icons += makeIconBadge('fa-check-circle icon-green', '{% trans "Sufficient stock available" %}'); } if (available_stock <= 0) { icons += `{% trans "No Stock Available" %}`; } else { let extra = ''; if ((substitute_stock > 0) && (variant_stock > 0)) { extra = '{% trans "Includes variant and substitute stock" %}'; } else if (variant_stock > 0) { extra = '{% trans "Includes variant stock" %}'; } else if (substitute_stock > 0) { extra = '{% trans "Includes substitute stock" %}'; } if (extra) { icons += makeIconBadge('fa-info-circle icon-blue', extra); } } } if (row.on_order && row.on_order > 0) { makeIconBadge('fa-shopping-cart', '{% trans "On Order" %}', { content: row.on_order, }); } return renderLink(text, url) + icons; }, sorter: function(valA, valB, rowA, rowB) { return availableQuantity(rowA) > availableQuantity(rowB) ? 1 : -1; }, }, { field: 'allocated', title: '{% trans "Allocated" %}', sortable: true, switchable: false, formatter: function(value, row) { var required = requiredQuantity(row); var allocated = row.consumable ? required : allocatedQuantity(row); var progressbar_text = `${allocated} / ${required}`; if (row.sub_part_detail && row.sub_part_detail.units) { progressbar_text += ` ${row.sub_part_detail.units}`; } return makeProgressBar(allocated, required, {text: progressbar_text}); }, sorter: function(valA, valB, rowA, rowB) { // Custom sorting function for progress bars var aA = allocatedQuantity(rowA); var aB = allocatedQuantity(rowB); var qA = requiredQuantity(rowA); var qB = requiredQuantity(rowB); // Handle the case where both numerators are zero if ((aA == 0) && (aB == 0)) { return (qA > qB) ? 1 : -1; } // Handle the case where either denominator is zero if ((qA == 0) || (qB == 0)) { return 1; } var progressA = parseFloat(aA) / qA; var progressB = parseFloat(aB) / qB; // Handle the case where both ratios are equal if (progressA == progressB) { return (qA > qB) ? 1 : -1; } if (progressA == progressB) return 0; return (progressA > progressB) ? 1 : -1; } }, { field: 'actions', title: '{% trans "Actions" %}', switchable: false, sortable: false, formatter: function(value, row) { if (row.consumable) { return `{% trans "Consumable item" %}`; } // Generate action buttons for this build output let html = ''; if (allocatedQuantity(row) < requiredQuantity(row)) { if (row.sub_part_detail.assembly) { html += makeIconButton('fa-tools icon-blue', 'button-build', row.sub_part, '{% trans "Build stock" %}'); } if (row.sub_part_detail.purchaseable) { html += makeIconButton('fa-shopping-cart icon-blue', 'button-buy', row.sub_part, '{% trans "Order stock" %}'); } html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', row.sub_part, '{% trans "Allocate stock" %}'); } html += makeRemoveButton( 'button-unallocate', row.sub_part, '{% trans "Unallocate stock" %}', { disabled: allocatedQuantity(row) == 0, } ); return wrapButtons(html); } }, ] }); filterBuildAllocationTable(filters); } /** * Allocate stock items to a build * * arguments: * - buildId: ID / PK value for the build * - partId: ID / PK value for the part being built * - bom_items: A list of BomItem objects to be allocated * * options: * - output: ID / PK of the associated build output (or null for untracked items) * - source_location: ID / PK of the top-level StockLocation to source stock from (or null) */ function allocateStockToBuild(build_id, part_id, bom_items, options={}) { if (bom_items.length == 0) { showAlertDialog( '{% trans "Select Parts" %}', '{% trans "You must select at least one part to allocate" %}', ); return; } // ID of the associated "build output" (or null) var output_id = options.output || null; var auto_fill_filters = {}; var source_location = options.source_location; if (output_id) { // Request information on the particular build output (stock item) inventreeGet(`{% url "api-stock-list" %}${output_id}/`, {}, { success: function(output) { if (output.quantity == 1 && output.serial != null) { auto_fill_filters.serial = output.serial; } }, async: false, }); } function renderBomItemRow(bom_item, quantity) { var pk = bom_item.pk; var sub_part = bom_item.sub_part_detail; var thumb = thumbnailImage(bom_item.sub_part_detail.thumbnail); var delete_button = `
`; delete_button += makeRemoveButton( 'button-row-remove', pk, '{% trans "Remove row" %}', ); delete_button += `
`; var quantity_input = constructField( `items_quantity_${pk}`, { type: 'decimal', min_value: 0, value: quantity || 0, title: '{% trans "Specify stock allocation quantity" %}', required: true, }, { hideLabels: true, } ); var allocated_display = makeProgressBar( bom_item.allocated, bom_item.required, ); var stock_input = constructField( `items_stock_item_${pk}`, { type: 'related field', required: 'true', }, { hideLabels: true, } ); // var stock_input = constructRelatedFieldInput(`items_stock_item_${pk}`); var html = ` ${thumb} ${sub_part.full_name} ${allocated_display} ${stock_input} ${quantity_input} ${delete_button} `; return html; } var table_entries = ''; for (var idx = 0; idx < bom_items.length; idx++) { var bom_item = bom_items[idx]; // Ignore "consumable" BOM items if (bom_item.consumable) { continue; } var required = bom_item.required || 0; var allocated = bom_item.allocated || 0; var remaining = required - allocated; if (remaining < 0) { remaining = 0; } // Ensure the quantity sent to the form field is correctly formatted remaining = formatDecimal(remaining, 15); // We only care about entries which are not yet fully allocated if (remaining > 0) { table_entries += renderBomItemRow(bom_item, remaining); } } if (table_entries.length == 0) { showAlertDialog( '{% trans "All Parts Allocated" %}', '{% trans "All selected parts have been fully allocated" %}', ); return; } var html = ``; // Render a "source location" input html += constructField( 'take_from', { type: 'related field', label: '{% trans "Source Location" %}', help_text: '{% trans "Select source location (leave blank to take from all locations)" %}', required: false, }, {}, ); // Create table of parts html += ` ${table_entries}
{% trans "Part" %} {% trans "Allocated" %} {% trans "Stock Item" %} {% trans "Quantity" %}
`; constructForm(`{% url "api-build-list" %}${build_id}/allocate/`, { method: 'POST', fields: {}, preFormContent: html, title: '{% trans "Allocate Stock Items to Build Order" %}', afterRender: function(fields, options) { var take_from_field = { name: 'take_from', model: 'stocklocation', api_url: '{% url "api-location-list" %}', required: false, type: 'related field', value: source_location, noResults: function(query) { return '{% trans "No matching stock locations" %}'; }, }; // Initialize "take from" field initializeRelatedField( take_from_field, null, options, ); // Add callback to "clear" button for take_from field addClearCallback( 'take_from', take_from_field, options, ); // Initialize stock item fields bom_items.forEach(function(bom_item) { initializeRelatedField( { name: `items_stock_item_${bom_item.pk}`, api_url: '{% url "api-stock-list" %}', filters: { bom_item: bom_item.pk, in_stock: true, available: true, part_detail: true, location_detail: true, }, model: 'stockitem', required: true, render_part_detail: true, render_location_detail: true, render_pk: false, render_available_quantity: true, auto_fill: true, auto_fill_filters: auto_fill_filters, onSelect: function(data, field, opts) { // Adjust the 'quantity' field based on availability if (!('quantity' in data)) { return; } // Quantity remaining to be allocated var remaining = Math.max((bom_item.required || 0) - (bom_item.allocated || 0), 0); // Calculate the available quantity var available = Math.max((data.quantity || 0) - (data.allocated || 0), 0); // Maximum amount that we need var desired = Math.min(available, remaining); updateFieldValue(`items_quantity_${bom_item.pk}`, desired, {}, opts); }, adjustFilters: function(filters) { // Restrict query to the selected location var location = getFormFieldValue( 'take_from', {}, { modal: options.modal, } ); filters.location = location; filters.cascade = true; return filters; }, noResults: function(query) { return '{% trans "No matching stock items" %}'; } }, null, options, ); }); // Add remove-row button callbacks $(options.modal).find('.button-row-remove').click(function() { var pk = $(this).attr('pk'); $(options.modal).find(`#items_${pk}`).remove(); }); }, onSubmit: function(fields, opts) { // Extract elements from the form var data = { items: [] }; var item_pk_values = []; bom_items.forEach(function(item) { var quantity = getFormFieldValue( `items_quantity_${item.pk}`, {}, { modal: opts.modal, }, ); var stock_item = getFormFieldValue( `items_stock_item_${item.pk}`, {}, { modal: opts.modal, } ); if (quantity != null) { data.items.push({ bom_item: item.pk, stock_item: stock_item, quantity: quantity, output: output_id, }); item_pk_values.push(item.pk); } }); // Provide 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, opts.url); break; } } } ); }, }); } /** * Automatically allocate stock items to a build */ function autoAllocateStockToBuild(build_id, bom_items=[], options={}) { var html = `
{% trans "Automatic Stock Allocation" %}
{% trans "Stock items will be automatically allocated to this build order, according to the provided guidelines" %}:
`; var fields = { location: { value: options.location, filters: { structural: false, } }, exclude_location: {}, interchangeable: { value: true, }, substitutes: { value: true, }, optional_items: { value: false, }, }; constructForm(`{% url "api-build-list" %}${build_id}/auto-allocate/`, { method: 'POST', fields: fields, title: '{% trans "Allocate Stock Items" %}', confirm: true, preFormContent: html, onSuccess: function(response) { if (options.onSuccess) { options.onSuccess(response); } } }); } /* * Display a table of Build orders */ function loadBuildTable(table, options) { // Ensure the table starts in a known state $(table).bootstrapTable('destroy'); var params = options.params || {}; params['part_detail'] = true; var filters = loadTableFilters('build', params); var filterTarget = options.filterTarget || null; setupFilterList('build', table, filterTarget, { download: true, report: { url: '{% url "api-build-report-list" %}', key: 'build', } }); // Which display mode to use for the build table? var display_mode = inventreeLoad('build-table-display-mode', 'list'); var tree_enable = display_mode == 'tree'; var loaded_calendar = false; // Function for rendering BuildOrder calendar display function buildEvents(calendar) { var start = startDate(calendar); var end = endDate(calendar); clearEvents(calendar); // Extract current filters from table var table_options = $(table).bootstrapTable('getOptions'); var filters = table_options.query_params || {}; filters.min_date = start; filters.max_date = end; filters.part_detail = true; // Request build orders from the server within specified date range inventreeGet( '{% url "api-build-list" %}', filters, { success: function(response) { for (var idx = 0; idx < response.length; idx++) { var order = response[idx]; var date = order.creation_date; if (order.completion_date) { date = order.completion_date; } else if (order.target_date) { date = order.target_date; } var title = `${order.reference}`; var color = '#4c68f5'; if (order.completed) { color = '#25c234'; } else if (order.overdue) { color = '#c22525'; } var event = { title: title, start: date, end: date, url: `/build/${order.pk}/`, backgroundColor: color, }; calendar.addEvent(event); } } } ); } $(table).inventreeTable({ method: 'get', formatNoMatches: function() { return '{% trans "No builds matching query" %}'; }, url: '{% url "api-build-list" %}', queryParams: filters, groupBy: false, sidePagination: 'server', name: 'builds', original: params, treeEnable: tree_enable, uniqueId: 'pk', rootParentId: options.parentBuild || null, idField: 'pk', parentIdField: 'parent', treeShowField: tree_enable ? 'reference' : null, showColumns: display_mode == 'list' || display_mode == 'tree', showCustomView: display_mode == 'calendar', showCustomViewButton: false, disablePagination: display_mode == 'calendar', search: display_mode != 'calendar', buttons: constructOrderTableButtons({ prefix: 'build', callback: function() { // Force complete reload of the table loadBuildTable(table, options); } }), columns: [ { field: 'pk', title: 'ID', visible: false, switchable: false, }, { checkbox: true, title: '{% trans "Select" %}', searchable: false, switchable: false, }, { field: 'reference', title: '{% trans "Build" %}', sortable: true, switchable: true, formatter: function(value, row) { var html = renderLink(value, '/build/' + row.pk + '/'); if (row.overdue) { html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Build order is overdue" %}'); } return html; } }, { field: 'title', title: '{% trans "Description" %}', switchable: true, }, { field: 'priority', title: '{% trans "Priority" %}', switchable: true, sortable: true, }, { field: 'part', title: '{% trans "Part" %}', sortable: true, sortName: 'part__name', formatter: function(value, row) { var html = imageHoverIcon(row.part_detail.thumbnail); html += renderLink(row.part_detail.full_name, `/part/${row.part}/`); html += makePartIcons(row.part_detail); return html; } }, { field: 'completed', title: '{% trans "Progress" %}', sortable: true, formatter: function(value, row) { return makeProgressBar( row.completed, row.quantity, { // style: 'max', } ); } }, { field: 'status', title: '{% trans "Status" %}', sortable: true, formatter: function(value) { return buildStatusDisplay(value); }, }, { field: 'creation_date', title: '{% trans "Created" %}', sortable: true, formatter: function(value) { return renderDate(value); } }, { field: 'issued_by', title: '{% trans "Issued by" %}', sortable: true, formatter: function(value, row) { if (value) { return row.issued_by_detail.username; } else { return `{% trans "No user information" %}`; } } }, { field: 'responsible', title: '{% trans "Responsible" %}', sortable: true, formatter: function(value, row) { if (!row.responsible_detail) { return '-'; } var html = row.responsible_detail.name; if (row.responsible_detail.label == '{% trans "group" %}') { html += ``; } else { html += ``; } return html; } }, { field: 'target_date', title: '{% trans "Target Date" %}', sortable: true, formatter: function(value) { return renderDate(value); } }, { field: 'completion_date', title: '{% trans "Completion Date" %}', sortable: true, formatter: function(value) { return renderDate(value); } }, ], customView: function(data) { return `
`; }, onRefresh: function() { loadBuildTable(table, options); }, onLoadSuccess: function() { if (tree_enable) { $(table).treegrid({ treeColumn: 1, }); $(table).treegrid('expandAll'); } else if (display_mode == 'calendar') { if (!loaded_calendar) { loaded_calendar = true; var el = document.getElementById('build-order-calendar'); calendar = new FullCalendar.Calendar(el, { initialView: 'dayGridMonth', nowIndicator: true, aspectRatio: 2.5, locale: options.locale, datesSet: function() { buildEvents(calendar); } }); calendar.render(); } else { calendar.render(); } } } }); linkButtonsToSelection( table, [ '#build-print-options', ] ); } function updateAllocationTotal(id, count, required) { count = parseFloat(count); $('#allocation-total-'+id).html(count); var el = $('#allocation-panel-' + id); el.removeClass('part-allocation-pass part-allocation-underallocated part-allocation-overallocated'); if (count < required) { el.addClass('part-allocation-underallocated'); } else if (count > required) { el.addClass('part-allocation-overallocated'); } else { el.addClass('part-allocation-pass'); } } function loadAllocationTable(table, part_id, part, url, required, button) { // Load the allocation table table.bootstrapTable({ url: url, sortable: false, formatNoMatches: function() { return '{% trans "No parts allocated for" %} ' + part; }, columns: [ { field: 'stock_item_detail', title: '{% trans "Stock Item" %}', formatter: function(value) { return '' + parseFloat(value.quantity) + ' x ' + value.part_name + ' @ ' + value.location_name; } }, { field: 'stock_item_detail.quantity', title: '{% trans "Available" %}', formatter: function(value) { return parseFloat(value); } }, { field: 'quantity', title: '{% trans "Allocated" %}', formatter: function(value, row) { var html = parseFloat(value); var bEdit = ``; var bDel = ``; html += `
${bEdit} ${bDel}
`; return html; } } ], }); // Callback for 'new-item' button button.click(function() { launchModalForm(button.attr('url'), { success: function() { table.bootstrapTable('refresh'); }, }); }); table.on('load-success.bs.table', function() { // Extract table data var results = table.bootstrapTable('getData'); var count = 0; for (var i = 0; i < results.length; i++) { count += parseFloat(results[i].quantity); } updateAllocationTotal(part_id, count, required); }); // Button callbacks for editing and deleting the allocations table.on('click', '.item-edit-button', function() { var button = $(this); launchModalForm(button.attr('url'), { success: function() { table.bootstrapTable('refresh'); } }); }); table.on('click', '.item-del-button', function() { var button = $(this); launchModalForm(button.attr('url'), { success: function() { table.bootstrapTable('refresh'); } }); }); }