mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 04:55:44 +00:00
Merge remote-tracking branch 'inventree/master' into order-parts-wizard
# Conflicts: # InvenTree/order/serializers.py # InvenTree/templates/js/translated/model_renderers.js
This commit is contained in:
@ -1,8 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Permission Denied" %}
|
||||
{% inventree_title %} | {% trans "Permission Denied" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -1,8 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Page Not Found" %}
|
||||
{% inventree_title %} | {% trans "Page Not Found" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -1,8 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block page_title %}
|
||||
InvenTree | {% trans "Internal Server Error" %}
|
||||
{% inventree_title %} | {% trans "Internal Server Error" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -11,7 +12,7 @@ InvenTree | {% trans "Internal Server Error" %}
|
||||
<h3>{% trans "Internal Server Error" %}</h3>
|
||||
|
||||
<div class='alert alert-danger alert-block'>
|
||||
{% trans "The InvenTree server raised an internal error" %}<br>
|
||||
{% blocktrans %}The {{ inventree_title }} server raised an internal error{% endblocktrans %}<br>
|
||||
{% trans "Refer to the error log in the admin interface for further details" %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
{% extends "skeleton.html" %}
|
||||
{% load static %}
|
||||
{% load inventree_extras %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
@ -30,7 +31,7 @@
|
||||
<div class='container-fluid'>
|
||||
|
||||
<div class='clearfix content-heading login-header d-flex flex-wrap'>
|
||||
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
|
||||
<img class="pull-left" src="{% inventree_logo %}" width="60" height="60"/>
|
||||
{% include "spacer.html" %}
|
||||
<span class='float-right'><h3>{% block body_title %}{% trans 'Site is in Maintenance' %}{% endblock %}</h3></span>
|
||||
</div>
|
||||
|
@ -15,6 +15,7 @@
|
||||
<tbody>
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE" icon="fa-info-circle" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_INSTANCE_TITLE" icon="fa-info-circle" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_RESTRICT_ABOUT" icon="fa-info-circle" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_BASE_URL" icon="fa-globe" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_COMPANY_NAME" icon="fa-building" %}
|
||||
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DOWNLOAD_FROM_URL" icon="fa-cloud-download-alt" %}
|
||||
|
@ -13,7 +13,7 @@
|
||||
{% block content %}
|
||||
|
||||
<div class='alert alert-block alert-danger'>
|
||||
{% trans "Changing the settings below require you to immediatly restart InvenTree. Do not change this while under active usage." %}
|
||||
{% trans "Changing the settings below require you to immediatly restart the server. Do not change this while under active usage." %}
|
||||
</div>
|
||||
|
||||
<div class='table-responsive'>
|
||||
|
@ -85,7 +85,7 @@
|
||||
{% if plugin.is_package %}
|
||||
{% trans "This plugin was installed as a package" %}
|
||||
{% else %}
|
||||
{% trans "This plugin was found in a local InvenTree path" %}
|
||||
{% trans "This plugin was found in a local server path" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -13,15 +13,15 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
{% inventree_demo_mode as demo %}
|
||||
{% if not demo %}
|
||||
{% inventree_customize 'hide_password_reset' as hide_password_reset %}
|
||||
{% if not hide_password_reset %}
|
||||
<div class='btn btn-outline-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'>
|
||||
<span class='fas fa-key'></span> {% trans "Set Password" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'>
|
||||
<span class='fas fa-user-cog'></span> {% trans "Edit" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -101,7 +101,7 @@
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<h4>{% trans "Help the translation efforts!" %}</h4>
|
||||
<p>{% blocktrans with link="https://crowdin.com/project/inventree" %}Native language translation of the InvenTree web application is <a href="{{link}}">community contributed via crowdin</a>. Contributions are welcomed and encouraged.{% endblocktrans %}</p>
|
||||
<p>{% blocktrans with link="https://crowdin.com/project/inventree" %}Native language translation of the web application is <a href="{{link}}">community contributed via crowdin</a>. Contributions are welcomed and encouraged.{% endblocktrans %}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -67,7 +67,7 @@
|
||||
<div class='container-fluid'>
|
||||
|
||||
<div class='clearfix content-heading login-header d-flex flex-wrap'>
|
||||
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
|
||||
<img class="pull-left" src="{% inventree_logo %}" width="60" height="60"/>
|
||||
{% include "spacer.html" %}
|
||||
<span class='float-right'><h3>{% inventree_title %}</h3></span>
|
||||
</div>
|
||||
@ -89,7 +89,7 @@
|
||||
<script type='text/javascript' src="{% static 'script/jquery-ui/jquery-ui.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'bootstrap/js/bootstrap.bundle.min.js' %}"></script>
|
||||
|
||||
<!-- general InvenTree -->
|
||||
<!-- general JS -->
|
||||
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'notification.js' %}"></script>
|
||||
|
||||
|
@ -10,8 +10,8 @@
|
||||
{% settings_value 'LOGIN_ENABLE_REG' as enable_reg %}
|
||||
{% settings_value 'LOGIN_ENABLE_PWD_FORGOT' as enable_pwd_forgot %}
|
||||
{% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %}
|
||||
{% inventree_customize 'login_message' as login_message %}
|
||||
{% mail_configured as mail_conf %}
|
||||
{% inventree_demo_mode as demo %}
|
||||
|
||||
<h1>{% trans "Sign In" %}</h1>
|
||||
|
||||
@ -35,19 +35,15 @@ for a account and sign in below:{% endblocktrans %}</p>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
{% if login_message %}
|
||||
<div>{{ login_message | safe }}<hr></div>
|
||||
{% endif %}
|
||||
<div class="btn-group float-right" role="group">
|
||||
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
|
||||
</div>
|
||||
{% if mail_conf and enable_pwd_forgot and not demo %}
|
||||
{% if mail_conf and enable_pwd_forgot %}
|
||||
<a class="" href="{% url 'account_reset_password' %}"><small>{% trans "Forgot Password?" %}</small></a>
|
||||
{% endif %}
|
||||
{% if demo %}
|
||||
<p>
|
||||
<h6>
|
||||
{% trans "InvenTree demo instance" %} - <a href='https://inventree.readthedocs.io/en/latest/demo/'>{% trans "Click here for login details" %}</a>
|
||||
</h6>
|
||||
</p>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% if enable_sso %}
|
||||
|
@ -7,7 +7,7 @@
|
||||
{% settings_value "REPORT_ENABLE" as report_enabled %}
|
||||
{% settings_value "SERVER_RESTART_REQUIRED" as server_restart_required %}
|
||||
{% settings_value "LABEL_ENABLE" with user=user as labels_enabled %}
|
||||
{% inventree_demo_mode as demo_mode %}
|
||||
{% inventree_show_about user as show_about %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@ -93,7 +93,7 @@
|
||||
{% block alerts %}
|
||||
<div class='notification-area' id='alerts'>
|
||||
<!-- Div for displayed alerts -->
|
||||
{% if server_restart_required and not demo_mode %}
|
||||
{% if server_restart_required %}
|
||||
<div id='alert-restart-server' class='alert alert-danger' role='alert'>
|
||||
<span class='fas fa-server'></span>
|
||||
<strong>{% trans "Server Restart Required" %}</strong>
|
||||
@ -130,7 +130,7 @@
|
||||
</div>
|
||||
|
||||
{% include 'modals.html' %}
|
||||
{% include 'about.html' %}
|
||||
{% if show_about %}{% include 'about.html' %}{% endif %}
|
||||
{% include "notifications.html" %}
|
||||
{% include "search.html" %}
|
||||
</div>
|
||||
@ -165,8 +165,9 @@
|
||||
|
||||
<script type='text/javascript' src="{% static 'script/clipboard.min.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/qr-scanner.umd.min.js' %}"></script>
|
||||
|
||||
<!-- general InvenTree -->
|
||||
<!-- general JS -->
|
||||
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
|
||||
|
||||
<!-- dynamic javascript templates -->
|
||||
|
@ -32,7 +32,7 @@
|
||||
{% block footer_prefix %}
|
||||
<!-- Custom footer information goes here -->
|
||||
{% endblock %}
|
||||
<p><em><small>{% trans "InvenTree version" %}: {% inventree_version %} - <a href='https://inventree.readthedocs.io'>inventree.readthedocs.io</a></small></em></p>
|
||||
<p><em><small>{% inventree_version shortstring=True %} - <a href='https://inventree.readthedocs.io'>readthedocs.io</a></small></em></p>
|
||||
{% block footer_suffix %}
|
||||
<!-- Custom footer information goes here -->
|
||||
{% endblock %}
|
||||
|
@ -19,6 +19,7 @@
|
||||
linkBarcodeDialog,
|
||||
scanItemsIntoLocation,
|
||||
unlinkBarcode,
|
||||
onBarcodeScanClicked,
|
||||
*/
|
||||
|
||||
function makeBarcodeInput(placeholderText='', hintText='') {
|
||||
@ -31,6 +32,9 @@ function makeBarcodeInput(placeholderText='', hintText='') {
|
||||
hintText = hintText || '{% trans "Enter barcode data" %}';
|
||||
|
||||
var html = `
|
||||
<div id='barcode_scan_video_container' class='text-center' style='height: 240px; display: none;'>
|
||||
<video id='barcode_scan_video' disablepictureinpicture playsinline height='240' style='object-fit: fill;'></video>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<label class='control-label' for='barcode'>{% trans "Barcode" %}</label>
|
||||
<div class='controls'>
|
||||
@ -39,6 +43,7 @@ function makeBarcodeInput(placeholderText='', hintText='') {
|
||||
<span class='fas fa-qrcode'></span>
|
||||
</span>
|
||||
<input id='barcode' class='textinput textInput form-control' type='text' name='barcode' placeholder='${placeholderText}'>
|
||||
<button id='barcode_scan_btn' class='btn btn-secondary' onclick='onBarcodeScanClicked()' style='display: none;'><span class='fas fa-camera'></span></button>
|
||||
</div>
|
||||
<div id='hint_barcode_data' class='help-block'>${hintText}</div>
|
||||
</div>
|
||||
@ -48,6 +53,44 @@ function makeBarcodeInput(placeholderText='', hintText='') {
|
||||
return html;
|
||||
}
|
||||
|
||||
qrScanner = null;
|
||||
|
||||
function startQrScanner() {
|
||||
$('#barcode_scan_video_container').show();
|
||||
qrScanner.start();
|
||||
}
|
||||
|
||||
function stopQrScanner() {
|
||||
if (qrScanner != null) qrScanner.stop();
|
||||
$('#barcode_scan_video_container').hide();
|
||||
}
|
||||
|
||||
function onBarcodeScanClicked(e) {
|
||||
if ($('#barcode_scan_video_container').is(':visible') == false) startQrScanner(); else stopQrScanner();
|
||||
}
|
||||
|
||||
function onCameraAvailable(hasCamera, options) {
|
||||
if ( hasCamera == true ) {
|
||||
// Camera is only acccessible if page is served over secure connection
|
||||
if ( window.isSecureContext == true ) {
|
||||
qrScanner = new QrScanner(document.getElementById('barcode_scan_video'), (result) => {
|
||||
onBarcodeScanCompleted(result, options);
|
||||
}, {
|
||||
highlightScanRegion: true,
|
||||
highlightCodeOutline: true,
|
||||
});
|
||||
$('#barcode_scan_btn').show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onBarcodeScanCompleted(result, options) {
|
||||
if (result.data == '') return;
|
||||
console.log('decoded qr code:', result.data);
|
||||
stopQrScanner();
|
||||
postBarcodeData(result.data, options);
|
||||
}
|
||||
|
||||
function makeNotesField(options={}) {
|
||||
|
||||
var tooltip = options.tooltip || '{% trans "Enter optional notes for stock transfer" %}';
|
||||
@ -186,6 +229,11 @@ function barcodeDialog(title, options={}) {
|
||||
$(modal).on('shown.bs.modal', function() {
|
||||
$(modal + ' .modal-form-content').scrollTop(0);
|
||||
|
||||
// Check for qr-scanner camera
|
||||
QrScanner.hasCamera().then( (hasCamera) => {
|
||||
onCameraAvailable(hasCamera, options);
|
||||
});
|
||||
|
||||
var barcode = $(modal + ' #barcode');
|
||||
|
||||
// Handle 'enter' key on barcode
|
||||
@ -220,6 +268,12 @@ function barcodeDialog(title, options={}) {
|
||||
|
||||
});
|
||||
|
||||
$(modal).on('hidden.bs.modal', function() {
|
||||
stopQrScanner();
|
||||
if (qrScanner != null) qrScanner.destroy();
|
||||
qrScanner = null;
|
||||
});
|
||||
|
||||
modalSetTitle(modal, title);
|
||||
|
||||
if (options.onSubmit) {
|
||||
|
@ -743,11 +743,29 @@ function loadBomTable(table, options={}) {
|
||||
field: 'sub_part',
|
||||
title: '{% trans "Part" %}',
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
formatter: function(value, row) {
|
||||
var url = `/part/${row.sub_part}/`;
|
||||
var html = imageHoverIcon(row.sub_part_detail.thumbnail) + renderLink(row.sub_part_detail.full_name, url);
|
||||
var html = '';
|
||||
|
||||
var sub_part = row.sub_part_detail;
|
||||
|
||||
// Display an extra icon if this part is an assembly
|
||||
if (sub_part.assembly) {
|
||||
|
||||
if (row.sub_assembly_received) {
|
||||
// Data received, ignore
|
||||
} else if (row.sub_assembly_requested) {
|
||||
html += `<a href='#'><span class='fas fa-sync fa-spin'></span></a>`;
|
||||
} else {
|
||||
html += `
|
||||
<a href='#' pk='${row.pk}' class='load-sub-assembly' id='load-sub-assembly-${row.pk}'>
|
||||
<span class='fas fa-sync-alt' title='{% trans "Load BOM for subassembly" %}'></span>
|
||||
</a> `;
|
||||
}
|
||||
}
|
||||
|
||||
html += imageHoverIcon(sub_part.thumbnail) + renderLink(row.sub_part_detail.full_name, url);
|
||||
|
||||
html += makePartIcons(sub_part);
|
||||
|
||||
@ -759,13 +777,6 @@ function loadBomTable(table, options={}) {
|
||||
html += makeIconBadge('fa-sitemap', '{% trans "Variant stock allowed" %}');
|
||||
}
|
||||
|
||||
// Display an extra icon if this part is an assembly
|
||||
if (sub_part.assembly) {
|
||||
var text = `<span title='{% trans "Open subassembly" %}' class='fas fa-stream float-right'></span>`;
|
||||
|
||||
html += renderLink(text, `/part/${row.sub_part}/bom/`);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
}
|
||||
@ -1027,14 +1038,6 @@ function loadBomTable(table, options={}) {
|
||||
// This function may be called recursively for multi-level BOMs
|
||||
function requestSubItems(bom_pk, part_pk, depth=0) {
|
||||
|
||||
// Prevent multi-level recursion
|
||||
const MAX_BOM_DEPTH = 25;
|
||||
|
||||
if (depth >= MAX_BOM_DEPTH) {
|
||||
console.log(`Maximum BOM depth (${MAX_BOM_DEPTH}) reached!`);
|
||||
return;
|
||||
}
|
||||
|
||||
inventreeGet(
|
||||
options.bom_url,
|
||||
{
|
||||
@ -1049,17 +1052,13 @@ function loadBomTable(table, options={}) {
|
||||
for (var idx = 0; idx < response.length; idx++) {
|
||||
response[idx].parentId = bom_pk;
|
||||
}
|
||||
|
||||
var row = $(table).bootstrapTable('getRowByUniqueId', bom_pk);
|
||||
row.sub_assembly_received = true;
|
||||
|
||||
$(table).bootstrapTable('updateByUniqueId', bom_pk, row, true);
|
||||
|
||||
table.bootstrapTable('append', response);
|
||||
|
||||
// Next, re-iterate and check if the new items also have sub items
|
||||
response.forEach(function(bom_item) {
|
||||
if (bom_item.sub_part_detail.assembly) {
|
||||
requestSubItems(bom_item.pk, bom_item.sub_part, depth + 1);
|
||||
}
|
||||
});
|
||||
|
||||
table.treegrid('collapseAll');
|
||||
},
|
||||
error: function(xhr) {
|
||||
console.log('Error requesting BOM for part=' + part_pk);
|
||||
@ -1103,7 +1102,6 @@ function loadBomTable(table, options={}) {
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No BOM items found" %}';
|
||||
},
|
||||
clickToSelect: true,
|
||||
queryParams: filters,
|
||||
original: params,
|
||||
columns: cols,
|
||||
@ -1117,32 +1115,26 @@ function loadBomTable(table, options={}) {
|
||||
});
|
||||
|
||||
table.treegrid('collapseAll');
|
||||
|
||||
// Callback for 'load sub assembly' button
|
||||
$(table).find('.load-sub-assembly').click(function(event) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
var pk = $(this).attr('pk');
|
||||
var row = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||
|
||||
// Request BOM data for this subassembly
|
||||
requestSubItems(row.pk, row.sub_part);
|
||||
|
||||
row.sub_assembly_requested = true;
|
||||
$(table).bootstrapTable('updateByUniqueId', pk, row, true);
|
||||
});
|
||||
},
|
||||
onLoadSuccess: function() {
|
||||
|
||||
if (options.editable) {
|
||||
table.bootstrapTable('uncheckAll');
|
||||
}
|
||||
|
||||
var data = table.bootstrapTable('getData');
|
||||
|
||||
for (var idx = 0; idx < data.length; idx++) {
|
||||
var row = data[idx];
|
||||
|
||||
// If a row already has a parent ID set, it's already been updated!
|
||||
if (row.parentId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set the parent ID of the top-level rows
|
||||
row.parentId = parent_id;
|
||||
|
||||
table.bootstrapTable('updateRow', idx, row, true);
|
||||
|
||||
if (row.sub_part_detail.assembly) {
|
||||
requestSubItems(row.pk, row.sub_part);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -264,7 +264,7 @@ function makeBuildOutputButtons(output_id, build_info, options={}) {
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
// Tracked parts? Must be individually allocated
|
||||
if (build_info.tracked_parts) {
|
||||
if (options.has_bom_items) {
|
||||
|
||||
// Add a button to allocate stock against this build output
|
||||
html += makeIconButton(
|
||||
@ -342,7 +342,9 @@ function unallocateStock(build_id, options={}) {
|
||||
},
|
||||
title: '{% trans "Unallocate Stock Items" %}',
|
||||
onSuccess: function(response, opts) {
|
||||
if (options.table) {
|
||||
if (options.onSuccess) {
|
||||
options.onSuccess(response, opts);
|
||||
} else if (options.table) {
|
||||
// Reload the parent table
|
||||
$(options.table).bootstrapTable('refresh');
|
||||
}
|
||||
@ -427,6 +429,8 @@ function completeBuildOutputs(build_id, outputs, options={}) {
|
||||
fields: {
|
||||
status: {},
|
||||
location: {},
|
||||
notes: {},
|
||||
accept_incomplete_allocation: {},
|
||||
},
|
||||
confirm: true,
|
||||
title: '{% trans "Complete Build Outputs" %}',
|
||||
@ -445,6 +449,8 @@ function completeBuildOutputs(build_id, outputs, options={}) {
|
||||
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 = [];
|
||||
@ -720,6 +726,35 @@ function loadBuildOrderAllocationTable(table, options={}) {
|
||||
}
|
||||
|
||||
|
||||
/* 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 parseFloat(quantity).toFixed(15);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Display a "build output" table for a particular build.
|
||||
*
|
||||
@ -737,18 +772,6 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
params.is_building = true;
|
||||
params.build = build_info.pk;
|
||||
|
||||
// Construct a list of "tracked" BOM items
|
||||
var tracked_bom_items = [];
|
||||
|
||||
var has_tracked_items = false;
|
||||
|
||||
build_info.bom_items.forEach(function(bom_item) {
|
||||
if (bom_item.sub_part_detail.trackable) {
|
||||
tracked_bom_items.push(bom_item);
|
||||
has_tracked_items = true;
|
||||
};
|
||||
});
|
||||
|
||||
var filters = {};
|
||||
|
||||
for (var key in params) {
|
||||
@ -786,7 +809,7 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log(`WARNING: Could not locate sub-table for output ${pk}`);
|
||||
console.warn(`Could not locate sub-table for output ${pk}`);
|
||||
}
|
||||
});
|
||||
|
||||
@ -841,6 +864,26 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
*/
|
||||
@ -855,6 +898,9 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
|
||||
element.html(html);
|
||||
|
||||
// Pass through the cached BOM items
|
||||
build_info.bom_items = bom_items;
|
||||
|
||||
loadBuildOutputAllocationTable(
|
||||
build_info,
|
||||
row,
|
||||
@ -865,19 +911,180 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
);
|
||||
}
|
||||
|
||||
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)
|
||||
rows.forEach(function(row) {
|
||||
row.allocations = allocations[row.pk] || [];
|
||||
$(table).bootstrapTable('updateByUniqueId', row.pk, row, true);
|
||||
|
||||
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',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
{
|
||||
success: function(results) {
|
||||
|
||||
// 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;
|
||||
|
||||
$(table).bootstrapTable('updateByUniqueId', row.pk, row, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 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: false,
|
||||
showColumns: true,
|
||||
uniqueId: 'pk',
|
||||
name: 'build-outputs',
|
||||
sortable: true,
|
||||
search: false,
|
||||
sidePagination: 'server',
|
||||
detailView: has_tracked_items,
|
||||
sidePagination: 'client',
|
||||
detailView: bom_items.length > 0,
|
||||
detailFilter: function(index, row) {
|
||||
return true;
|
||||
return bom_items.length > 0;
|
||||
},
|
||||
detailFormatter: function(index, row, element) {
|
||||
constructBuildOutputSubTable(index, row, element);
|
||||
@ -885,11 +1092,14 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No active build outputs found" %}';
|
||||
},
|
||||
onPostBody: function() {
|
||||
onPostBody: function(rows) {
|
||||
// Add callbacks for the buttons
|
||||
setupBuildOutputButtonCallbacks();
|
||||
},
|
||||
onLoadSuccess: function(rows) {
|
||||
|
||||
$(table).bootstrapTable('expandAllRows');
|
||||
updateAllocationData(rows);
|
||||
updateTestResultData(rows);
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
@ -901,6 +1111,7 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
{
|
||||
field: 'part',
|
||||
title: '{% trans "Part" %}',
|
||||
switchable: true,
|
||||
formatter: function(value, row) {
|
||||
var thumb = row.part_detail.thumbnail;
|
||||
|
||||
@ -909,7 +1120,9 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
},
|
||||
{
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
title: '{% trans "Build Output" %}',
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
|
||||
var url = `/stock/item/${row.pk}/`;
|
||||
@ -922,15 +1135,84 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
text = `{% trans "Quantity" %}: ${row.quantity}`;
|
||||
}
|
||||
|
||||
if (row.batch) {
|
||||
text += ` <small>({% trans "Batch" %}: ${row.batch})</small>`;
|
||||
}
|
||||
|
||||
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 Parts" %}',
|
||||
visible: has_tracked_items,
|
||||
title: '{% trans "Allocated Stock" %}',
|
||||
visible: bom_items.length > 0,
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
return `<div id='output-progress-${row.pk}'><span class='fas fa-spin fa-spinner'></span></div>`;
|
||||
|
||||
if (bom_items.length == 0) {
|
||||
return `<div id='output-progress-${row.pk}'><em><small>{% trans "No tracked BOM items for this build" %}</small></em></div>`;
|
||||
}
|
||||
|
||||
var progressBar = makeProgressBar(
|
||||
countAllocatedLines(row),
|
||||
bom_items.length,
|
||||
{
|
||||
max_width: '150px',
|
||||
}
|
||||
);
|
||||
|
||||
return `<div id='output-progress-${row.pk}'>${progressBar}</div>`;
|
||||
},
|
||||
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 `<em><small>{% trans "No required tests for this build" %}</small></em>`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -941,6 +1223,9 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
return makeBuildOutputButtons(
|
||||
row.pk,
|
||||
build_info,
|
||||
{
|
||||
has_bom_items: bom_items.length > 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -956,6 +1241,79 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
$(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 = $(table).bootstrapTable('getSelections');
|
||||
|
||||
if (outputs.length == 0) {
|
||||
outputs = $(table).bootstrapTable('getData');
|
||||
}
|
||||
|
||||
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 = $(table).bootstrapTable('getSelections');
|
||||
|
||||
if (outputs.length == 0) {
|
||||
outputs = $(table).bootstrapTable('getData');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Print stock item labels
|
||||
$('#incomplete-output-print-label').click(function() {
|
||||
var outputs = $(table).bootstrapTable('getSelections');
|
||||
|
||||
if (outputs.length == 0) {
|
||||
outputs = $(table).bootstrapTable('getData');
|
||||
}
|
||||
|
||||
var stock_id_values = [];
|
||||
|
||||
outputs.forEach(function(output) {
|
||||
stock_id_values.push(output.pk);
|
||||
});
|
||||
|
||||
printStockItemLabels(stock_id_values);
|
||||
});
|
||||
|
||||
$('#outputs-expand').click(function() {
|
||||
$(table).bootstrapTable('expandAllRows');
|
||||
});
|
||||
|
||||
$('#outputs-collapse').click(function() {
|
||||
$(table).bootstrapTable('collapseAllRows');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -973,7 +1331,6 @@ function loadBuildOutputTable(build_info, options={}) {
|
||||
*/
|
||||
function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
|
||||
|
||||
var buildId = buildInfo.pk;
|
||||
var partId = buildInfo.part;
|
||||
|
||||
@ -985,6 +1342,26 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
var table = options.table;
|
||||
|
||||
if (options.table == null) {
|
||||
@ -1002,13 +1379,72 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
|
||||
setupFilterList('builditems', $(table), options.filterTarget);
|
||||
|
||||
// If an "output" is specified, then only "trackable" parts are allocated
|
||||
// Otherwise, only "untrackable" parts are allowed
|
||||
var trackable = ! !output;
|
||||
var allocated_items = output == null ? null : output.allocations;
|
||||
|
||||
function reloadTable() {
|
||||
// Reload the entire build allocation table
|
||||
$(table).bootstrapTable('refresh');
|
||||
function redrawAllocationData() {
|
||||
// Force a refresh of each row in the table
|
||||
// Note we cannot call 'refresh' because we are passing data from memory
|
||||
// var rows = $(table).bootstrapTable('getData');
|
||||
|
||||
// How many rows are fully allocated?
|
||||
var allocated_rows = 0;
|
||||
|
||||
bom_items.forEach(function(row) {
|
||||
$(table).bootstrapTable('updateByUniqueId', row.pk, row, true);
|
||||
|
||||
if (isRowFullyAllocated(row)) {
|
||||
allocated_rows += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// 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) {
|
||||
@ -1032,6 +1468,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
}
|
||||
|
||||
function availableQuantity(row) {
|
||||
// Return the total available stock for a given row
|
||||
|
||||
// Base stock
|
||||
var available = row.available_stock;
|
||||
@ -1045,27 +1482,17 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
}
|
||||
|
||||
return available;
|
||||
|
||||
}
|
||||
|
||||
function sumAllocations(row) {
|
||||
// Calculat total allocations for a given row
|
||||
if (!row.allocations) {
|
||||
row.allocated = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
var quantity = 0;
|
||||
|
||||
row.allocations.forEach(function(item) {
|
||||
quantity += item.quantity;
|
||||
});
|
||||
|
||||
row.allocated = parseFloat(quantity.toFixed(15));
|
||||
|
||||
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
|
||||
|
||||
@ -1079,7 +1506,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
var row = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||
|
||||
if (!row) {
|
||||
console.log('WARNING: getRowByUniqueId returned null');
|
||||
console.warn('getRowByUniqueId returned null');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1092,7 +1519,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
{
|
||||
source_location: buildInfo.source_location,
|
||||
success: function(data) {
|
||||
$(table).bootstrapTable('refresh');
|
||||
// $(table).bootstrapTable('refresh');
|
||||
reloadAllocationData();
|
||||
},
|
||||
output: output == null ? null : output.pk,
|
||||
}
|
||||
@ -1124,7 +1552,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
newBuildOrder({
|
||||
part: pk,
|
||||
parent: buildId,
|
||||
quantity: requiredQuantity(row) - sumAllocations(row),
|
||||
quantity: requiredQuantity(row) - allocatedQuantity(row),
|
||||
});
|
||||
});
|
||||
|
||||
@ -1139,18 +1567,16 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
bom_item: row.pk,
|
||||
output: outputId == 'untracked' ? null : outputId,
|
||||
table: table,
|
||||
onSuccess: function(response, opts) {
|
||||
reloadAllocationData();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Load table of BOM items
|
||||
$(table).inventreeTable({
|
||||
url: '{% url "api-bom-list" %}',
|
||||
queryParams: {
|
||||
part: partId,
|
||||
sub_part_detail: true,
|
||||
sub_part_trackable: trackable,
|
||||
},
|
||||
data: bom_items,
|
||||
disablePagination: true,
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No BOM items found" %}';
|
||||
@ -1162,124 +1588,11 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
// Setup button callbacks
|
||||
setupCallbacks();
|
||||
},
|
||||
onLoadSuccess: function(tableData) {
|
||||
// Once the BOM data are loaded, request allocation data for this build output
|
||||
|
||||
var params = {
|
||||
build: buildId,
|
||||
part_detail: true,
|
||||
location_detail: true,
|
||||
};
|
||||
|
||||
if (output) {
|
||||
params.sub_part_trackable = true;
|
||||
params.output = outputId;
|
||||
} else {
|
||||
params.sub_part_trackable = false;
|
||||
}
|
||||
|
||||
inventreeGet('/api/build/item/',
|
||||
params,
|
||||
{
|
||||
success: function(data) {
|
||||
// Iterate through the returned data, and group by the part they point to
|
||||
var allocations = {};
|
||||
|
||||
// Total number of line items
|
||||
var totalLines = tableData.length;
|
||||
|
||||
// Total number of "completely allocated" lines
|
||||
var allocatedLines = 0;
|
||||
|
||||
data.forEach(function(item) {
|
||||
|
||||
// Group BuildItem objects by part
|
||||
var part = item.bom_part || item.part;
|
||||
var key = parseInt(part);
|
||||
|
||||
if (!(key in allocations)) {
|
||||
allocations[key] = [];
|
||||
}
|
||||
|
||||
allocations[key].push(item);
|
||||
});
|
||||
|
||||
// Now update the allocations for each row in the table
|
||||
for (var key in allocations) {
|
||||
|
||||
// Select the associated row in the table
|
||||
var tableRow = $(table).bootstrapTable('getRowByUniqueId', key);
|
||||
|
||||
if (!tableRow) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set the allocation list for that row
|
||||
tableRow.allocations = allocations[key];
|
||||
|
||||
// Calculate the total allocated quantity
|
||||
var allocatedQuantity = sumAllocations(tableRow);
|
||||
|
||||
var requiredQuantity = 0;
|
||||
|
||||
if (output) {
|
||||
requiredQuantity = tableRow.quantity * output.quantity;
|
||||
} else {
|
||||
requiredQuantity = tableRow.quantity * buildInfo.quantity;
|
||||
}
|
||||
|
||||
// Is this line item fully allocated?
|
||||
if (allocatedQuantity >= requiredQuantity) {
|
||||
allocatedLines += 1;
|
||||
}
|
||||
|
||||
// Push the updated row back into the main table
|
||||
$(table).bootstrapTable('updateByUniqueId', key, tableRow, true);
|
||||
}
|
||||
|
||||
// Update any rows which we did not receive allocation information for
|
||||
var td = $(table).bootstrapTable('getData');
|
||||
|
||||
td.forEach(function(tableRow) {
|
||||
if (tableRow.allocations == null) {
|
||||
|
||||
tableRow.allocations = [];
|
||||
|
||||
$(table).bootstrapTable('updateByUniqueId', tableRow.pk, tableRow, true);
|
||||
}
|
||||
});
|
||||
|
||||
// Update the progress bar for this build output
|
||||
var build_progress = $(`#output-progress-${outputId}`);
|
||||
|
||||
if (build_progress.exists()) {
|
||||
if (totalLines > 0) {
|
||||
|
||||
var progress = makeProgressBar(
|
||||
allocatedLines,
|
||||
totalLines,
|
||||
{
|
||||
max_width: '150px',
|
||||
}
|
||||
);
|
||||
|
||||
build_progress.html(progress);
|
||||
} else {
|
||||
build_progress.html('');
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log(`WARNING: Could not find progress bar for output ${outputId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
sortable: true,
|
||||
showColumns: false,
|
||||
detailView: true,
|
||||
detailFilter: function(index, row) {
|
||||
return row.allocations != null;
|
||||
return allocatedQuantity(row) > 0;
|
||||
},
|
||||
detailFormatter: function(index, row, element) {
|
||||
// Contruct an 'inner table' which shows which stock items have been allocated
|
||||
@ -1293,7 +1606,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
var subTable = $(`#${subTableId}`);
|
||||
|
||||
subTable.bootstrapTable({
|
||||
data: row.allocations,
|
||||
data: getAllocationsForBomRow(row, allocated_items),
|
||||
showHeader: true,
|
||||
columns: [
|
||||
{
|
||||
@ -1315,7 +1628,6 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
|
||||
var url = '';
|
||||
|
||||
|
||||
var serial = row.serial;
|
||||
|
||||
if (row.stock_item_detail) {
|
||||
@ -1383,7 +1695,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
quantity: {},
|
||||
},
|
||||
title: '{% trans "Edit Allocation" %}',
|
||||
onSuccess: reloadTable,
|
||||
onSuccess: reloadAllocationData,
|
||||
});
|
||||
});
|
||||
|
||||
@ -1393,7 +1705,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
constructForm(`/api/build/item/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
title: '{% trans "Remove Allocation" %}',
|
||||
onSuccess: reloadTable,
|
||||
onSuccess: reloadAllocationData,
|
||||
});
|
||||
});
|
||||
},
|
||||
@ -1494,25 +1806,15 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
title: '{% trans "Allocated" %}',
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
var allocated = 0;
|
||||
|
||||
if (row.allocations != null) {
|
||||
row.allocations.forEach(function(item) {
|
||||
allocated += item.quantity;
|
||||
});
|
||||
|
||||
var required = requiredQuantity(row);
|
||||
|
||||
return makeProgressBar(allocated, required);
|
||||
} else {
|
||||
return `<em>{% trans "loading" %}...</em><span class='fas fa-spinner fa-spin float-right'></span>`;
|
||||
}
|
||||
var allocated = allocatedQuantity(row);
|
||||
var required = requiredQuantity(row);
|
||||
return makeProgressBar(allocated, required);
|
||||
},
|
||||
sorter: function(valA, valB, rowA, rowB) {
|
||||
// Custom sorting function for progress bars
|
||||
|
||||
var aA = sumAllocations(rowA);
|
||||
var aB = sumAllocations(rowB);
|
||||
var aA = allocatedQuantity(rowA);
|
||||
var aB = allocatedQuantity(rowB);
|
||||
|
||||
var qA = requiredQuantity(rowA);
|
||||
var qB = requiredQuantity(rowB);
|
||||
@ -1532,12 +1834,12 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
|
||||
// Handle the case where both ratios are equal
|
||||
if (progressA == progressB) {
|
||||
return (qA < qB) ? 1 : -1;
|
||||
return (qA > qB) ? 1 : -1;
|
||||
}
|
||||
|
||||
if (progressA == progressB) return 0;
|
||||
|
||||
return (progressA < progressB) ? 1 : -1;
|
||||
return (progressA > progressB) ? 1 : -1;
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -1547,7 +1849,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
// Generate action buttons for this build output
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
if (sumAllocations(row) < requiredQuantity(row)) {
|
||||
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" %}');
|
||||
}
|
||||
@ -1563,7 +1865,7 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
'fa-minus-circle icon-red', 'button-unallocate', row.sub_part,
|
||||
'{% trans "Unallocate stock" %}',
|
||||
{
|
||||
disabled: row.allocations == null
|
||||
disabled: allocatedQuantity(row) == 0,
|
||||
}
|
||||
);
|
||||
|
||||
@ -1672,7 +1974,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
||||
// var stock_input = constructRelatedFieldInput(`items_stock_item_${pk}`);
|
||||
|
||||
var html = `
|
||||
<tr id='allocation_row_${pk}' class='part-allocation-row'>
|
||||
<tr id='items_${pk}' class='part-allocation-row'>
|
||||
<td id='part_${pk}'>
|
||||
${thumb} ${sub_part.full_name}
|
||||
</td>
|
||||
@ -1762,8 +2064,6 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
||||
method: 'POST',
|
||||
fields: {},
|
||||
preFormContent: html,
|
||||
confirm: true,
|
||||
confirmMessage: '{% trans "Confirm stock allocation" %}',
|
||||
title: '{% trans "Allocate Stock Items to Build Order" %}',
|
||||
afterRender: function(fields, options) {
|
||||
|
||||
@ -1859,7 +2159,7 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
||||
$(options.modal).find('.button-row-remove').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
$(options.modal).find(`#allocation_row_${pk}`).remove();
|
||||
$(options.modal).find(`#items_${pk}`).remove();
|
||||
});
|
||||
},
|
||||
onSubmit: function(fields, opts) {
|
||||
@ -1974,7 +2274,9 @@ function autoAllocateStockToBuild(build_id, bom_items=[], options={}) {
|
||||
confirm: true,
|
||||
preFormContent: html,
|
||||
onSuccess: function(response) {
|
||||
$('#allocation-table-untracked').bootstrapTable('refresh');
|
||||
if (options.onSuccess) {
|
||||
options.onSuccess(response);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -2072,8 +2374,8 @@ function loadBuildTable(table, options) {
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'quantity',
|
||||
title: '{% trans "Completed" %}',
|
||||
field: 'completed',
|
||||
title: '{% trans "Progress" %}',
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
return makeProgressBar(
|
||||
|
@ -163,27 +163,29 @@ function makeProgressBar(value, maximum, opts={}) {
|
||||
|
||||
var style = options.style || '';
|
||||
|
||||
var text = '';
|
||||
var text = options.text;
|
||||
|
||||
if (!text) {
|
||||
if (style == 'percent') {
|
||||
// Display e.g. "50%"
|
||||
|
||||
if (style == 'percent') {
|
||||
// Display e.g. "50%"
|
||||
text = `${percent}%`;
|
||||
} else if (style == 'max') {
|
||||
// Display just the maximum value
|
||||
text = `${maximum}`;
|
||||
} else if (style == 'value') {
|
||||
// Display just the current value
|
||||
text = `${value}`;
|
||||
} else if (style == 'blank') {
|
||||
// No display!
|
||||
text = '';
|
||||
} else {
|
||||
/* Default style
|
||||
* Display e.g. "5 / 10"
|
||||
*/
|
||||
|
||||
text = `${percent}%`;
|
||||
} else if (style == 'max') {
|
||||
// Display just the maximum value
|
||||
text = `${maximum}`;
|
||||
} else if (style == 'value') {
|
||||
// Display just the current value
|
||||
text = `${value}`;
|
||||
} else if (style == 'blank') {
|
||||
// No display!
|
||||
text = '';
|
||||
} else {
|
||||
/* Default style
|
||||
* Display e.g. "5 / 10"
|
||||
*/
|
||||
|
||||
text = `${value} / ${maximum}`;
|
||||
text = `${value} / ${maximum}`;
|
||||
}
|
||||
}
|
||||
|
||||
var id = options.id || 'progress-bar';
|
||||
|
@ -113,8 +113,6 @@ function renderStockItem(name, data, parameters={}, options={}) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var html = `
|
||||
<span>
|
||||
${part_detail}
|
||||
@ -162,12 +160,10 @@ function renderBuild(name, data, parameters={}, options={}) {
|
||||
|
||||
var html = select2Thumbnail(image);
|
||||
|
||||
html += `<span><b>${data.reference}</b></span> - ${data.quantity} x ${data.part_detail.full_name}`;
|
||||
html += `<span><b>${data.reference}</b> - ${data.quantity} x ${data.part_detail.full_name}</span>`;
|
||||
|
||||
html += renderId('{% trans "Build ID" %}', data.pk, parameters);
|
||||
|
||||
html += `<p><i>${data.title}</i></p>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@ -302,8 +298,9 @@ function renderSalesOrderShipment(name, data, parameters={}, options={}) {
|
||||
var so_prefix = global_settings.SALESORDER_REFERENCE_PREFIX;
|
||||
|
||||
var html = `
|
||||
<span>
|
||||
${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}
|
||||
<span>${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}</span>
|
||||
<span class='float-right'>
|
||||
<small>{% trans "Shipment ID" %}: ${data.pk}</small>
|
||||
</span>
|
||||
`;
|
||||
|
||||
@ -406,5 +403,4 @@ function renderSupplierPart(name, data, parameters={}, options={}) {
|
||||
html += renderId('{% trans "Supplier Part ID" %}', data.pk, parameters);
|
||||
|
||||
return html;
|
||||
|
||||
}
|
||||
|
@ -26,9 +26,11 @@
|
||||
editPurchaseOrderLineItem,
|
||||
exportOrder,
|
||||
loadPurchaseOrderLineItemTable,
|
||||
loadPurchaseOrderExtraLineTable
|
||||
loadPurchaseOrderTable,
|
||||
loadSalesOrderAllocationTable,
|
||||
loadSalesOrderLineItemTable,
|
||||
loadSalesOrderExtraLineTable
|
||||
loadSalesOrderShipmentTable,
|
||||
loadSalesOrderTable,
|
||||
newPurchaseOrderFromOrderWizard,
|
||||
@ -36,6 +38,8 @@
|
||||
orderParts,
|
||||
removeOrderRowFromOrderWizard,
|
||||
removePurchaseOrderLineItem,
|
||||
loadOrderTotal,
|
||||
extraLineFields,
|
||||
*/
|
||||
|
||||
|
||||
@ -273,7 +277,7 @@ function createPurchaseOrder(options={}) {
|
||||
if (options.onSuccess) {
|
||||
options.onSuccess(data);
|
||||
} else {
|
||||
// Default action is to redirect browser to the new PO
|
||||
// Default action is to redirect browser to the new PurchaseOrder
|
||||
location.href = `/order/purchase-order/${data.pk}/`;
|
||||
}
|
||||
},
|
||||
@ -306,6 +310,28 @@ function soLineItemFields(options={}) {
|
||||
}
|
||||
|
||||
|
||||
/* Construct a set of fields for a OrderExtraLine form */
|
||||
function extraLineFields(options={}) {
|
||||
|
||||
var fields = {
|
||||
order: {
|
||||
hidden: true,
|
||||
},
|
||||
quantity: {},
|
||||
reference: {},
|
||||
price: {},
|
||||
price_currency: {},
|
||||
notes: {},
|
||||
};
|
||||
|
||||
if (options.order) {
|
||||
fields.order.value = options.order;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
|
||||
/* Construct a set of fields for the PurchaseOrderLineItem form */
|
||||
function poLineItemFields(options={}) {
|
||||
|
||||
@ -758,7 +784,7 @@ function newPurchaseOrderFromOrderWizard(e) {
|
||||
|
||||
/**
|
||||
* Receive stock items against a PurchaseOrder
|
||||
* Uses the POReceive API endpoint
|
||||
* Uses the PurchaseOrderReceive API endpoint
|
||||
*
|
||||
* arguments:
|
||||
* - order_id, ID / PK for the PurchaseOrder instance
|
||||
@ -1631,6 +1657,226 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load a table displaying lines for a particular PurchaseOrder
|
||||
*
|
||||
* @param {String} table : HTML ID tag e.g. '#table'
|
||||
* @param {Object} options : object which contains:
|
||||
* - order {integer} : pk of the PurchaseOrder
|
||||
* - status: {integer} : status code for the order
|
||||
*/
|
||||
function loadPurchaseOrderExtraLineTable(table, options={}) {
|
||||
|
||||
options.table = table;
|
||||
|
||||
options.params = options.params || {};
|
||||
|
||||
if (!options.order) {
|
||||
console.log('ERROR: function called without order ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.status) {
|
||||
console.log('ERROR: function called without order status');
|
||||
return;
|
||||
}
|
||||
|
||||
options.params.order = options.order;
|
||||
options.params.part_detail = true;
|
||||
options.params.allocations = true;
|
||||
|
||||
var filters = loadTableFilters('purchaseorderextraline');
|
||||
|
||||
for (var key in options.params) {
|
||||
filters[key] = options.params[key];
|
||||
}
|
||||
|
||||
options.url = options.url || '{% url "api-po-extra-line-list" %}';
|
||||
|
||||
var filter_target = options.filter_target || '#filter-list-purchase-order-extra-lines';
|
||||
|
||||
setupFilterList('purchaseorderextraline', $(table), filter_target);
|
||||
|
||||
// Is the order pending?
|
||||
var pending = options.status == {{ SalesOrderStatus.PENDING }};
|
||||
|
||||
// Table columns to display
|
||||
var columns = [
|
||||
{
|
||||
sortable: true,
|
||||
field: 'reference',
|
||||
title: '{% trans "Reference" %}',
|
||||
switchable: true,
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
footerFormatter: function(data) {
|
||||
return data.map(function(row) {
|
||||
return +row['quantity'];
|
||||
}).reduce(function(sum, i) {
|
||||
return sum + i;
|
||||
}, 0);
|
||||
},
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'price',
|
||||
title: '{% trans "Unit Price" %}',
|
||||
formatter: function(value, row) {
|
||||
var formatter = new Intl.NumberFormat(
|
||||
'en-US',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: row.price_currency
|
||||
}
|
||||
);
|
||||
|
||||
return formatter.format(row.price);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'total_price',
|
||||
sortable: true,
|
||||
title: '{% trans "Total Price" %}',
|
||||
formatter: function(value, row) {
|
||||
var formatter = new Intl.NumberFormat(
|
||||
'en-US',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: row.price_currency
|
||||
}
|
||||
);
|
||||
|
||||
return formatter.format(row.price * row.quantity);
|
||||
},
|
||||
footerFormatter: function(data) {
|
||||
var total = data.map(function(row) {
|
||||
return +row['price'] * row['quantity'];
|
||||
}).reduce(function(sum, i) {
|
||||
return sum + i;
|
||||
}, 0);
|
||||
|
||||
var currency = (data.slice(-1)[0] && data.slice(-1)[0].price_currency) || 'USD';
|
||||
|
||||
var formatter = new Intl.NumberFormat(
|
||||
'en-US',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}
|
||||
);
|
||||
|
||||
return formatter.format(total);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
columns.push({
|
||||
field: 'notes',
|
||||
title: '{% trans "Notes" %}',
|
||||
});
|
||||
|
||||
if (pending) {
|
||||
columns.push({
|
||||
field: 'buttons',
|
||||
switchable: false,
|
||||
formatter: function(value, row, index, field) {
|
||||
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
var pk = row.pk;
|
||||
|
||||
html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line" %}');
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line" %}', );
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
reloadTotal();
|
||||
}
|
||||
|
||||
// Configure callback functions once the table is loaded
|
||||
function setupCallbacks() {
|
||||
|
||||
// Callback for duplicating lines
|
||||
$(table).find('.button-duplicate').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
inventreeGet(`/api/order/po-extra-line/${pk}/`, {}, {
|
||||
success: function(data) {
|
||||
|
||||
var fields = extraLineFields();
|
||||
|
||||
constructForm('{% url "api-po-extra-line-list" %}', {
|
||||
method: 'POST',
|
||||
fields: fields,
|
||||
data: data,
|
||||
title: '{% trans "Duplicate Line" %}',
|
||||
onSuccess: function(response) {
|
||||
$(table).bootstrapTable('refresh');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Callback for editing lines
|
||||
$(table).find('.button-edit').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`/api/order/po-extra-line/${pk}/`, {
|
||||
fields: {
|
||||
quantity: {},
|
||||
reference: {},
|
||||
price: {},
|
||||
price_currency: {},
|
||||
notes: {},
|
||||
},
|
||||
title: '{% trans "Edit Line" %}',
|
||||
onSuccess: reloadTable,
|
||||
});
|
||||
});
|
||||
|
||||
// Callback for deleting lines
|
||||
$(table).find('.button-delete').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`/api/order/po-extra-line/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
title: '{% trans "Delete Line" %}',
|
||||
onSuccess: reloadTable,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(table).inventreeTable({
|
||||
onPostBody: setupCallbacks,
|
||||
name: 'purchaseorderextraline',
|
||||
sidePagination: 'client',
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No matching line" %}';
|
||||
},
|
||||
queryParams: filters,
|
||||
original: options.params,
|
||||
url: options.url,
|
||||
showFooter: true,
|
||||
uniqueId: 'pk',
|
||||
detailViewByClick: false,
|
||||
columns: columns,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Load table displaying list of sales orders
|
||||
*/
|
||||
@ -2425,7 +2671,7 @@ function showAllocationSubTable(index, row, element, options) {
|
||||
},
|
||||
{
|
||||
field: 'buttons',
|
||||
title: '{% trans "" %}',
|
||||
title: '',
|
||||
formatter: function(value, row, index, field) {
|
||||
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
@ -2517,6 +2763,26 @@ function showFulfilledSubTable(index, row, element, options) {
|
||||
});
|
||||
}
|
||||
|
||||
var TotalPriceRef = ''; // reference to total price field
|
||||
var TotalPriceOptions = {}; // options to reload the price
|
||||
|
||||
function loadOrderTotal(reference, options={}) {
|
||||
TotalPriceRef = reference;
|
||||
TotalPriceOptions = options;
|
||||
}
|
||||
|
||||
function reloadTotal() {
|
||||
inventreeGet(
|
||||
TotalPriceOptions.url,
|
||||
{},
|
||||
{
|
||||
success: function(data) {
|
||||
$(TotalPriceRef).html(data.total_price_string);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Load a table displaying line items for a particular SalesOrder
|
||||
@ -2814,6 +3080,7 @@ function loadSalesOrderLineItemTable(table, options={}) {
|
||||
|
||||
function reloadTable() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
reloadTotal();
|
||||
}
|
||||
|
||||
// Configure callback functions once the table is loaded
|
||||
@ -3023,3 +3290,223 @@ function loadSalesOrderLineItemTable(table, options={}) {
|
||||
columns: columns,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load a table displaying lines for a particular SalesOrder
|
||||
*
|
||||
* @param {String} table : HTML ID tag e.g. '#table'
|
||||
* @param {Object} options : object which contains:
|
||||
* - order {integer} : pk of the SalesOrder
|
||||
* - status: {integer} : status code for the order
|
||||
*/
|
||||
function loadSalesOrderExtraLineTable(table, options={}) {
|
||||
|
||||
options.table = table;
|
||||
|
||||
options.params = options.params || {};
|
||||
|
||||
if (!options.order) {
|
||||
console.log('ERROR: function called without order ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.status) {
|
||||
console.log('ERROR: function called without order status');
|
||||
return;
|
||||
}
|
||||
|
||||
options.params.order = options.order;
|
||||
options.params.part_detail = true;
|
||||
options.params.allocations = true;
|
||||
|
||||
var filters = loadTableFilters('salesorderextraline');
|
||||
|
||||
for (var key in options.params) {
|
||||
filters[key] = options.params[key];
|
||||
}
|
||||
|
||||
options.url = options.url || '{% url "api-so-extra-line-list" %}';
|
||||
|
||||
var filter_target = options.filter_target || '#filter-list-sales-order-extra-lines';
|
||||
|
||||
setupFilterList('salesorderextraline', $(table), filter_target);
|
||||
|
||||
// Is the order pending?
|
||||
var pending = options.status == {{ SalesOrderStatus.PENDING }};
|
||||
|
||||
// Table columns to display
|
||||
var columns = [
|
||||
{
|
||||
sortable: true,
|
||||
field: 'reference',
|
||||
title: '{% trans "Reference" %}',
|
||||
switchable: true,
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'quantity',
|
||||
title: '{% trans "Quantity" %}',
|
||||
footerFormatter: function(data) {
|
||||
return data.map(function(row) {
|
||||
return +row['quantity'];
|
||||
}).reduce(function(sum, i) {
|
||||
return sum + i;
|
||||
}, 0);
|
||||
},
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
sortable: true,
|
||||
field: 'price',
|
||||
title: '{% trans "Unit Price" %}',
|
||||
formatter: function(value, row) {
|
||||
var formatter = new Intl.NumberFormat(
|
||||
'en-US',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: row.price_currency
|
||||
}
|
||||
);
|
||||
|
||||
return formatter.format(row.price);
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'total_price',
|
||||
sortable: true,
|
||||
title: '{% trans "Total Price" %}',
|
||||
formatter: function(value, row) {
|
||||
var formatter = new Intl.NumberFormat(
|
||||
'en-US',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: row.price_currency
|
||||
}
|
||||
);
|
||||
|
||||
return formatter.format(row.price * row.quantity);
|
||||
},
|
||||
footerFormatter: function(data) {
|
||||
var total = data.map(function(row) {
|
||||
return +row['price'] * row['quantity'];
|
||||
}).reduce(function(sum, i) {
|
||||
return sum + i;
|
||||
}, 0);
|
||||
|
||||
var currency = (data.slice(-1)[0] && data.slice(-1)[0].price_currency) || 'USD';
|
||||
|
||||
var formatter = new Intl.NumberFormat(
|
||||
'en-US',
|
||||
{
|
||||
style: 'currency',
|
||||
currency: currency
|
||||
}
|
||||
);
|
||||
|
||||
return formatter.format(total);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
columns.push({
|
||||
field: 'notes',
|
||||
title: '{% trans "Notes" %}',
|
||||
});
|
||||
|
||||
if (pending) {
|
||||
columns.push({
|
||||
field: 'buttons',
|
||||
switchable: false,
|
||||
formatter: function(value, row, index, field) {
|
||||
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
var pk = row.pk;
|
||||
|
||||
html += makeIconButton('fa-clone', 'button-duplicate', pk, '{% trans "Duplicate line" %}');
|
||||
html += makeIconButton('fa-edit icon-blue', 'button-edit', pk, '{% trans "Edit line" %}');
|
||||
html += makeIconButton('fa-trash-alt icon-red', 'button-delete', pk, '{% trans "Delete line" %}', );
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function reloadTable() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
reloadTotal();
|
||||
}
|
||||
|
||||
// Configure callback functions once the table is loaded
|
||||
function setupCallbacks() {
|
||||
|
||||
// Callback for duplicating lines
|
||||
$(table).find('.button-duplicate').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
inventreeGet(`/api/order/so-extra-line/${pk}/`, {}, {
|
||||
success: function(data) {
|
||||
|
||||
var fields = extraLineFields();
|
||||
|
||||
constructForm('{% url "api-so-extra-line-list" %}', {
|
||||
method: 'POST',
|
||||
fields: fields,
|
||||
data: data,
|
||||
title: '{% trans "Duplicate Line" %}',
|
||||
onSuccess: function(response) {
|
||||
$(table).bootstrapTable('refresh');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Callback for editing lines
|
||||
$(table).find('.button-edit').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`/api/order/so-extra-line/${pk}/`, {
|
||||
fields: {
|
||||
quantity: {},
|
||||
reference: {},
|
||||
price: {},
|
||||
price_currency: {},
|
||||
notes: {},
|
||||
},
|
||||
title: '{% trans "Edit Line" %}',
|
||||
onSuccess: reloadTable,
|
||||
});
|
||||
});
|
||||
|
||||
// Callback for deleting lines
|
||||
$(table).find('.button-delete').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
constructForm(`/api/order/so-extra-line/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
title: '{% trans "Delete Line" %}',
|
||||
onSuccess: reloadTable,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(table).inventreeTable({
|
||||
onPostBody: setupCallbacks,
|
||||
name: 'salesorderextraline',
|
||||
sidePagination: 'client',
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No matching lines" %}';
|
||||
},
|
||||
queryParams: filters,
|
||||
original: options.params,
|
||||
url: options.url,
|
||||
showFooter: true,
|
||||
uniqueId: 'pk',
|
||||
detailViewByClick: false,
|
||||
columns: columns,
|
||||
});
|
||||
}
|
||||
|
@ -500,6 +500,11 @@ function duplicateBom(part_id, options={}) {
|
||||
*/
|
||||
function partStockLabel(part, options={}) {
|
||||
|
||||
// Prevent literal string 'null' from being displayed
|
||||
if (part.units == null) {
|
||||
part.units = '';
|
||||
}
|
||||
|
||||
if (part.in_stock) {
|
||||
// There IS stock available for this part
|
||||
|
||||
|
@ -271,7 +271,7 @@ function printBomReports(parts) {
|
||||
|
||||
function printPurchaseOrderReports(orders) {
|
||||
/**
|
||||
* Print PO reports for the provided purchase order(s)
|
||||
* Print PurchaseOrder reports for the provided purchase order(s)
|
||||
*/
|
||||
|
||||
if (orders.length == 0) {
|
||||
@ -325,7 +325,7 @@ function printPurchaseOrderReports(orders) {
|
||||
|
||||
function printSalesOrderReports(orders) {
|
||||
/**
|
||||
* Print SO reports for the provided purchase order(s)
|
||||
* Print SalesOrder reports for the provided purchase order(s)
|
||||
*/
|
||||
|
||||
if (orders.length == 0) {
|
||||
|
@ -6,12 +6,15 @@
|
||||
{% settings_value 'BARCODE_ENABLE' as barcodes %}
|
||||
{% settings_value 'STICKY_HEADER' user=request.user as sticky %}
|
||||
{% navigation_enabled as plugin_nav %}
|
||||
{% inventree_demo_mode as demo %}
|
||||
|
||||
{% inventree_show_about user as show_about %}
|
||||
{% inventree_customize 'navbar_message' as navbar_message %}
|
||||
{% inventree_customize 'hide_admin_link' as hide_admin_link %}
|
||||
|
||||
<nav class="navbar {% if sticky %}fixed-top{% endif %} navbar-expand-lg navbar-light">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header clearfix content-heading">
|
||||
<a class="navbar-brand" id='logo' href="{% url 'index' %}" style="padding-top: 7px; padding-bottom: 5px;"><img src="{% static 'img/inventree.png' %}" width="32" height="32" style="display:block; margin: auto;"/></a>
|
||||
<a class="navbar-brand" id='logo' href="{% url 'index' %}" style="padding-top: 7px; padding-bottom: 5px;"><img src="{% inventree_logo %}" width="32" height="32" style="display:block; margin: auto;"/></a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse" id="navbar-objects">
|
||||
<ul class="navbar-nav">
|
||||
@ -84,8 +87,13 @@
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
{% if demo %}
|
||||
{% include "navbar_demo.html" %}
|
||||
{% if navbar_message %}
|
||||
{% include "spacer.html" %}
|
||||
<div class='flex justify-content-center'>
|
||||
{{ navbar_message | safe }}
|
||||
</div>
|
||||
{% include "spacer.html" %}
|
||||
{% include "spacer.html" %}
|
||||
{% endif %}
|
||||
|
||||
<ul class='navbar-nav flex-row'>
|
||||
@ -125,7 +133,7 @@
|
||||
</a>
|
||||
<ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'>
|
||||
{% if user.is_authenticated %}
|
||||
{% if user.is_staff and not demo %}
|
||||
{% if user.is_staff and not hide_admin_link %}
|
||||
<li><a class='dropdown-item' href="{% url 'admin:index' %}"><span class="fas fa-user-shield"></span> {% trans "Admin" %}</a></li>
|
||||
{% endif %}
|
||||
<li><a class='dropdown-item' href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li>
|
||||
@ -144,6 +152,7 @@
|
||||
{% trans "System Information" %}
|
||||
</a>
|
||||
</li>
|
||||
{% if show_about %}
|
||||
<li id='launch-about'>
|
||||
<a class='dropdown-item' href='#'>
|
||||
{% if up_to_date %}
|
||||
@ -154,6 +163,7 @@
|
||||
{% trans "About InvenTree" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -1,12 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% include "spacer.html" %}
|
||||
<div class='flex'>
|
||||
<h6>
|
||||
{% trans "InvenTree demo mode" %}
|
||||
<a href='https://inventree.readthedocs.io/en/latest/demo/'>
|
||||
<span class='fas fa-info-circle'></span>
|
||||
</a>
|
||||
</h6>
|
||||
</div>
|
||||
{% include "spacer.html" %}
|
||||
{% include "spacer.html" %}
|
@ -75,7 +75,7 @@
|
||||
<script type='text/javascript' src="{% static 'script/clipboard.min.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
|
||||
|
||||
<!-- general InvenTree -->
|
||||
<!-- general JS -->
|
||||
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
|
||||
<script type='text/javascript' src="{% i18n_static 'notification.js' %}"></script>
|
||||
{% block body_scripts_inventree %}
|
||||
|
@ -87,31 +87,4 @@
|
||||
<!-- TODO - Enumerate system issues here! -->
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<tr>
|
||||
<td colspan='3'><strong>{% trans "Parts" %}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-sitemap'></span></td>
|
||||
<td>{% trans "Part Categories" %}</td>
|
||||
<td>{{ part_cat_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-shapes'></span></td>
|
||||
<td>{% trans "Parts" %}</td>
|
||||
<td>{{ part_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3"><strong>{% trans "Stock Items" %}</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-map-marker-alt'></span></td>
|
||||
<td>{% trans "Stock Locations" %}</td>
|
||||
<td>{{ stock_loc_count }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class='fas fa-boxes'></span></td>
|
||||
<td>{% trans "Stock Items" %}</td>
|
||||
<td>{{ stock_item_count }}</td>
|
||||
</tr>
|
||||
</table>
|
Reference in New Issue
Block a user