Merge branch 'inventree:master' into matmair/issue2788

This commit is contained in:
Matthias Mair 2022-04-30 19:53:10 +02:00 committed by GitHub
commit 71a0d8159e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 762 additions and 492 deletions

View File

@ -150,13 +150,13 @@ class DeleteForm(forms.Form):
class EditUserForm(HelperForm):
""" Form for editing user information
"""
Form for editing user information
"""
class Meta:
model = User
fields = [
'username',
'first_name',
'last_name',
]

View File

@ -427,8 +427,9 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
serials = serials.strip()
# fill in the next serial number into the serial
if '~' in serials:
serials = serials.replace('~', str(next_number))
while '~' in serials:
serials = serials.replace('~', str(next_number), 1)
next_number += 1
# Split input string by whitespace or comma (,) characters
groups = re.split("[\s,]+", serials)
@ -438,6 +439,12 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
# Helper function to check for duplicated numbers
def add_sn(sn):
# Attempt integer conversion first, so numerical strings are never stored
try:
sn = int(sn)
except ValueError:
pass
if sn in numbers:
errors.append(_('Duplicate serial: {sn}').format(sn=sn))
else:
@ -451,15 +458,25 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
if len(serials) == 0:
raise ValidationError([_("Empty serial number string")])
for group in groups:
# If the user has supplied the correct number of serials, don't process them for groups
# just add them so any duplicates (or future validations) are checked
if len(groups) == expected_quantity:
for group in groups:
add_sn(group)
if len(errors) > 0:
raise ValidationError(errors)
return numbers
for group in groups:
group = group.strip()
# Hyphen indicates a range of numbers
if '-' in group:
items = group.split('-')
if len(items) == 2:
if len(items) == 2 and all([i.isnumeric() for i in items]):
a = items[0].strip()
b = items[1].strip()
@ -471,13 +488,14 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
for n in range(a, b + 1):
add_sn(n)
else:
errors.append(_("Invalid group: {g}").format(g=group))
errors.append(_("Invalid group range: {g}").format(g=group))
except ValueError:
errors.append(_("Invalid group: {g}").format(g=group))
continue
else:
errors.append(_("Invalid group: {g}").format(g=group))
# More than 2 hyphens or non-numeric group so add without interpolating
add_sn(group)
# plus signals either
# 1: 'start+': expected number of serials, starting at start
@ -495,23 +513,17 @@ def extract_serial_numbers(serials, expected_quantity, next_number: int):
# case 1
else:
end = start + expected_quantity
end = start + (expected_quantity - len(numbers))
for n in range(start, end):
add_sn(n)
# no case
else:
errors.append(_("Invalid group: {g}").format(g=group))
errors.append(_("Invalid group sequence: {g}").format(g=group))
# At this point, we assume that the "group" is just a single serial value
elif group:
try:
# First attempt to add as an integer value
add_sn(int(group))
except (ValueError):
# As a backup, add as a string value
add_sn(group)
add_sn(group)
# No valid input group detected
else:

View File

@ -62,12 +62,6 @@ DEBUG = _is_true(get_setting(
CONFIG.get('debug', True)
))
# Determine if we are running in "demo mode"
DEMO_MODE = _is_true(get_setting(
'INVENTREE_DEMO',
CONFIG.get('demo', False)
))
DOCKER = _is_true(get_setting(
'INVENTREE_DOCKER',
False
@ -217,9 +211,6 @@ MEDIA_URL = '/media/'
if DEBUG:
logger.info("InvenTree running with DEBUG enabled")
if DEMO_MODE:
logger.warning("InvenTree running in DEMO mode") # pragma: no cover
logger.debug(f"MEDIA_ROOT: '{MEDIA_ROOT}'")
logger.debug(f"STATIC_ROOT: '{STATIC_ROOT}'")
@ -921,7 +912,7 @@ PLUGIN_TESTING_SETUP = get_setting('PLUGIN_TESTING_SETUP', False) # load plugin
PLUGIN_RETRY = get_setting('PLUGIN_RETRY', 5) # how often should plugin loading be tried?
PLUGIN_FILE_CHECKED = False # Was the plugin file checked?
# user interface customization values
# User interface customization values
CUSTOMIZE = get_setting(
'INVENTREE_CUSTOMIZE',
CONFIG.get('customize', {}),

View File

@ -252,6 +252,31 @@ class TestSerialNumberExtraction(TestCase):
sn = e("1, 2, 3, 4, 5", 5, 1)
self.assertEqual(len(sn), 5)
# Test partially specifying serials
sn = e("1, 2, 4+", 5, 1)
self.assertEqual(len(sn), 5)
self.assertEqual(sn, [1, 2, 4, 5, 6])
# Test groups are not interpolated if enough serials are supplied
sn = e("1, 2, 3, AF5-69H, 5", 5, 1)
self.assertEqual(len(sn), 5)
self.assertEqual(sn, [1, 2, 3, "AF5-69H", 5])
# Test groups are not interpolated with more than one hyphen in a word
sn = e("1, 2, TG-4SR-92, 4+", 5, 1)
self.assertEqual(len(sn), 5)
self.assertEqual(sn, [1, 2, "TG-4SR-92", 4, 5])
# Test groups are not interpolated with alpha characters
sn = e("1, A-2, 3+", 5, 1)
self.assertEqual(len(sn), 5)
self.assertEqual(sn, [1, "A-2", 3, 4, 5])
# Test multiple placeholders
sn = e("1 2 ~ ~ ~", 5, 3)
self.assertEqual(len(sn), 5)
self.assertEqual(sn, [1, 2, 3, 4, 5])
sn = e("1-5, 10-15", 11, 1)
self.assertIn(3, sn)
self.assertIn(13, sn)
@ -307,6 +332,10 @@ class TestSerialNumberExtraction(TestCase):
with self.assertRaises(ValidationError):
e("10, a, 7-70j", 4, 1)
# Test groups are not interpolated with word characters
with self.assertRaises(ValidationError):
e("1, 2, 3, E-5", 5, 1)
def test_combinations(self):
e = helpers.extract_serial_numbers

View File

@ -34,8 +34,7 @@ from user_sessions.views import SessionDeleteView, SessionDeleteOtherView
from common.settings import currency_code_default, currency_codes
from part.models import Part, PartCategory
from stock.models import StockLocation, StockItem
from part.models import PartCategory
from common.models import InvenTreeSetting, ColorTheme
from users.models import check_user_role, RuleSet
@ -882,29 +881,6 @@ class DatabaseStatsView(AjaxView):
ajax_template_name = "stats.html"
ajax_form_title = _("System Information")
def get_context_data(self, **kwargs):
ctx = {}
# Part stats
ctx['part_count'] = Part.objects.count()
ctx['part_cat_count'] = PartCategory.objects.count()
# Stock stats
ctx['stock_item_count'] = StockItem.objects.count()
ctx['stock_loc_count'] = StockLocation.objects.count()
"""
TODO: Other ideas for database metrics
- "Popular" parts (used to make other parts?)
- Most ordered part
- Most sold part
- etc etc etc
"""
return ctx
class NotificationsView(TemplateView):
""" View for showing notifications

View File

@ -96,6 +96,7 @@ class BuildList(generics.ListCreateAPIView):
'target_date',
'completion_date',
'quantity',
'completed',
'issued_by',
'responsible',
]
@ -442,6 +443,18 @@ class BuildItemList(generics.ListCreateAPIView):
if part_pk:
queryset = queryset.filter(stock_item__part=part_pk)
# Filter by "tracked" status
# Tracked means that the item is "installed" into a build output (stock item)
tracked = params.get('tracked', None)
if tracked is not None:
tracked = str2bool(tracked)
if tracked:
queryset = queryset.exclude(install_into=None)
else:
queryset = queryset.filter(install_into=None)
# Filter by output target
output = params.get('output', None)

View File

@ -1260,7 +1260,7 @@ class BuildItem(models.Model):
})
@transaction.atomic
def complete_allocation(self, user):
def complete_allocation(self, user, notes=''):
"""
Complete the allocation of this BuildItem into the output stock item.
@ -1286,8 +1286,13 @@ class BuildItem(models.Model):
self.save()
# Install the stock item into the output
item.belongs_to = self.install_into
item.save()
self.install_into.installStockItem(
item,
self.quantity,
user,
notes
)
else:
# Simply remove the items from stock
item.take_stock(

View File

@ -161,7 +161,12 @@ class BuildOutputSerializer(serializers.Serializer):
# The build output must have all tracked parts allocated
if not build.is_fully_allocated(output):
raise ValidationError(_("This build output is not fully allocated"))
# Check if the user has specified that incomplete allocations are ok
accept_incomplete = InvenTree.helpers.str2bool(self.context['request'].data.get('accept_incomplete_allocation', False))
if not accept_incomplete:
raise ValidationError(_("This build output is not fully allocated"))
return output
@ -355,6 +360,7 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
'outputs',
'location',
'status',
'accept_incomplete_allocation',
'notes',
]
@ -377,6 +383,13 @@ class BuildOutputCompleteSerializer(serializers.Serializer):
label=_("Status"),
)
accept_incomplete_allocation = serializers.BooleanField(
default=False,
required=False,
label=_('Accept Incomplete Allocation'),
help_text=_('Complete ouputs if stock has not been fully allocated'),
)
notes = serializers.CharField(
label=_("Notes"),
required=False,
@ -617,6 +630,7 @@ class BuildAllocationItemSerializer(serializers.Serializer):
super().validate(data)
build = self.context['build']
bom_item = data['bom_item']
stock_item = data['stock_item']
quantity = data['quantity']
@ -641,16 +655,20 @@ class BuildAllocationItemSerializer(serializers.Serializer):
# Output *must* be set for trackable parts
if output is None and bom_item.sub_part.trackable:
raise ValidationError({
'output': _('Build output must be specified for allocation of tracked parts')
'output': _('Build output must be specified for allocation of tracked parts'),
})
# Output *cannot* be set for un-tracked parts
if output is not None and not bom_item.sub_part.trackable:
raise ValidationError({
'output': _('Build output cannot be specified for allocation of untracked parts')
'output': _('Build output cannot be specified for allocation of untracked parts'),
})
# Check if this allocation would be unique
if BuildItem.objects.filter(build=build, stock_item=stock_item, install_into=output).exists():
raise ValidationError(_('This stock item has already been allocated to this build output'))
return data

View File

@ -270,6 +270,16 @@
</ul>
</div>
{% if build.has_tracked_bom_items %}
<button id='outputs-expand' class='btn btn-outline-secondary' type='button' title='{% trans "Expand all build output rows" %}'>
<span class='fas fa-expand'></span>
</button>
<button id='outputs-collapse' class='btn btn-outline-secondary' type='button' title='{% trans "Collapse all build output rows" %}'>
<span class='fas fa-compress'></span>
</button>
{% endif %}
{% include "filter_list.html" with id='incompletebuilditems' %}
</div>
{% endif %}
@ -401,110 +411,53 @@ function reloadTable() {
$('#allocation-table-untracked').bootstrapTable('refresh');
}
// Get the list of BOM items required for this build
inventreeGet(
'{% url "api-bom-list" %}',
{
onPanelLoad('outputs', function() {
{% if build.active %}
var build_info = {
pk: {{ build.pk }},
part: {{ build.part.pk }},
sub_part_detail: true,
},
{
success: function(response) {
quantity: {{ build.quantity }},
{% if build.take_from %}
source_location: {{ build.take_from.pk }},
{% endif %}
tracked_parts: true,
};
var build_info = {
pk: {{ build.pk }},
part: {{ build.part.pk }},
quantity: {{ build.quantity }},
bom_items: response,
{% if build.take_from %}
source_location: {{ build.take_from.pk }},
{% endif %}
{% if build.has_tracked_bom_items %}
tracked_parts: true,
{% else %}
tracked_parts: false,
{% endif %}
};
loadBuildOutputTable(build_info);
{% if build.active %}
loadBuildOutputTable(build_info);
linkButtonsToSelection(
'#build-output-table',
[
'#output-options',
'#multi-output-complete',
'#multi-output-delete',
]
);
{% endif %}
});
$('#multi-output-complete').click(function() {
var outputs = $('#build-output-table').bootstrapTable('getSelections');
{% if build.active and build.has_untracked_bom_items %}
completeBuildOutputs(
build_info.pk,
outputs,
{
success: function() {
// Reload the "in progress" table
$('#build-output-table').bootstrapTable('refresh');
function loadUntrackedStockTable() {
// Reload the "completed" table
$('#build-stock-table').bootstrapTable('refresh');
}
}
);
});
var build_info = {
pk: {{ build.pk }},
part: {{ build.part.pk }},
quantity: {{ build.quantity }},
{% if build.take_from %}
source_location: {{ build.take_from.pk }},
{% endif %}
tracked_parts: false,
};
$('#allocation-table-untracked').bootstrapTable('destroy');
$('#multi-output-delete').click(function() {
var outputs = $('#build-output-table').bootstrapTable('getSelections');
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');
}
}
)
});
$('#incomplete-output-print-label').click(function() {
var outputs = $('#build-output-table').bootstrapTable('getSelections');
if (outputs.length == 0) {
outputs = $('#build-output-table').bootstrapTable('getData');
}
var stock_id_values = [];
outputs.forEach(function(output) {
stock_id_values.push(output.pk);
});
printStockItemLabels(stock_id_values);
});
{% endif %}
{% if build.active and build.has_untracked_bom_items %}
// Load allocation table for un-tracked parts
loadBuildOutputAllocationTable(
build_info,
null,
{
search: true,
}
);
{% endif %}
// Load allocation table for un-tracked parts
loadBuildOutputAllocationTable(
build_info,
null,
{
search: true,
}
}
);
);
}
loadUntrackedStockTable();
{% endif %}
$('#btn-create-output').click(function() {
@ -527,6 +480,7 @@ $("#btn-auto-allocate").on('click', function() {
{% if build.take_from %}
location: {{ build.take_from.pk }},
{% endif %}
onSuccess: loadUntrackedStockTable,
}
);
});
@ -558,9 +512,7 @@ $("#btn-allocate").on('click', function() {
{% if build.take_from %}
source_location: {{ build.take_from.pk }},
{% endif %}
success: function(data) {
$('#allocation-table-untracked').bootstrapTable('refresh');
}
success: loadUntrackedStockTable,
}
);
}
@ -569,6 +521,7 @@ $("#btn-allocate").on('click', function() {
$('#btn-unallocate').on('click', function() {
unallocateStock({{ build.id }}, {
table: '#allocation-table-untracked',
onSuccess: loadUntrackedStockTable,
});
});
@ -588,9 +541,7 @@ $('#allocate-selected-items').click(function() {
{% if build.take_from %}
source_location: {{ build.take_from.pk }},
{% endif %}
success: function(data) {
$('#allocation-table-untracked').bootstrapTable('refresh');
}
success: loadUntrackedStockTable,
}
);
});

View File

@ -201,3 +201,5 @@ static_root: '/home/inventree/data/static'
# login_message: InvenTree demo instance - <a href='https://inventree.readthedocs.io/en/latest/demo/'> Click here for login details</a>
# navbar_message: <h6>InvenTree demo mode <a href='https://inventree.readthedocs.io/en/latest/demo/'><span class='fas fa-info-circle'></span></a></h6>
# logo: logo.png
# hide_admin_link: true
# hide_password_reset: true

View File

@ -383,7 +383,7 @@ class PartTestTemplateList(generics.ListCreateAPIView):
required = params.get('required', None)
if required is not None:
queryset = queryset.filter(required=required)
queryset = queryset.filter(required=str2bool(required))
return queryset

View File

@ -2740,8 +2740,8 @@ class BomItem(models.Model, DataImportMixin):
if not p.active:
continue
# Trackable parts cannot be 'auto allocated'
if p.trackable:
# Trackable status must be the same as the sub_part
if p.trackable != self.sub_part.trackable:
continue
valid_parts.append(p)

View File

@ -160,13 +160,6 @@ def inventree_in_debug_mode(*args, **kwargs):
return djangosettings.DEBUG
@register.simple_tag()
def inventree_demo_mode(*args, **kwargs):
""" Return True if the server is running in DEMO mode """
return djangosettings.DEMO_MODE
@register.simple_tag()
def inventree_show_about(user, *args, **kwargs):
""" Return True if the about modal should be shown """

View File

@ -23,6 +23,8 @@ from rest_framework.serializers import ValidationError
from rest_framework.response import Response
from rest_framework import generics, filters
from build.models import Build
import common.settings
import common.models
@ -1159,6 +1161,19 @@ class StockItemTestResultList(generics.ListCreateAPIView):
queryset = super().filter_queryset(queryset)
# Filter by 'build'
build = params.get('build', None)
if build is not None:
try:
build = Build.objects.get(pk=build)
queryset = queryset.filter(stock_item__build=build)
except (ValueError, Build.DoesNotExist):
pass
# Filter by stock item
item = params.get('stock_item', None)

View File

@ -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 %}

View File

@ -12,7 +12,6 @@
{% 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>
@ -37,12 +36,12 @@ for a account and sign in below:{% endblocktrans %}</p>
<hr>
{% if login_message %}
<div>{{ login_message }}<hr></div>
<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 %}
</form>

View File

@ -8,7 +8,6 @@
{% settings_value "SERVER_RESTART_REQUIRED" as server_restart_required %}
{% settings_value "LABEL_ENABLE" with user=user as labels_enabled %}
{% inventree_show_about user as show_about %}
{% inventree_demo_mode as demo_mode %}
<!DOCTYPE html>
<html lang="en">
@ -94,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>

View File

@ -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);
}
}
},
});

View File

@ -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(

View File

@ -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';

View File

@ -113,8 +113,6 @@ function renderStockItem(name, data, parameters={}, options={}) {
}
}
var html = `
<span>
${part_detail}
@ -146,7 +144,7 @@ function renderStockLocation(name, data, parameters={}, options={}) {
html += ` - <i>${data.description}</i>`;
}
html += `<span class='float-right'><small>{% trans "Location ID" %}: ${data.pk}</small></span>`;
html += renderId('{% trans "Location ID" %}', data.pk, parameters);
return html;
}
@ -162,10 +160,9 @@ 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 class='float-right'><small>{% trans "Build ID" %}: ${data.pk}</span></span>`;
html += `<span><b>${data.reference}</b> - ${data.quantity} x ${data.part_detail.full_name}</span>`;
html += `<p><i>${data.title}</i></p>`;
html += renderId('{% trans "Build ID" %}', data.pk, parameters);
return html;
}
@ -300,12 +297,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>
<span class='float-right'>
<small>{% trans "Shipment ID" %}: ${data.pk}</small>
</span>
`;
var html = `<span>${so_prefix}${data.order_detail.reference} - {% trans "Shipment" %} ${data.reference}</span>`;
html += renderId('{% trans "Shipment ID" %}', data.pk, parameters);
return html;
}
@ -323,7 +317,7 @@ function renderPartCategory(name, data, parameters={}, options={}) {
html += ` - <i>${data.description}</i>`;
}
html += `<span class='float-right'><small>{% trans "Category ID" %}: ${data.pk}</small></span>`;
html += renderId('{% trans "Category ID" %}', data.pk, parameters);
return html;
}
@ -366,7 +360,7 @@ function renderManufacturerPart(name, data, parameters={}, options={}) {
html += ` <span><b>${data.manufacturer_detail.name}</b> - ${data.MPN}</span>`;
html += ` - <i>${data.part_detail.full_name}</i>`;
html += `<span class='float-right'><small>{% trans "Manufacturer Part ID" %}: ${data.pk}</small></span>`;
html += renderId('{% trans "Manufacturer Part ID" %}', data.pk, parameters);
return html;
}
@ -395,9 +389,7 @@ function renderSupplierPart(name, data, parameters={}, options={}) {
html += ` <span><b>${data.supplier_detail.name}</b> - ${data.SKU}</span>`;
html += ` - <i>${data.part_detail.full_name}</i>`;
html += `<span class='float-right'><small>{% trans "Supplier Part ID" %}: ${data.pk}</small></span>`;
html += renderId('{% trans "Supplier Part ID" %}', data.pk, parameters);
return html;
}

View File

@ -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

View File

@ -6,9 +6,10 @@
{% 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">
@ -89,7 +90,7 @@
{% if navbar_message %}
{% include "spacer.html" %}
<div class='flex justify-content-center'>
{{ navbar_message }}
{{ navbar_message | safe }}
</div>
{% include "spacer.html" %}
{% include "spacer.html" %}
@ -132,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>

View File

@ -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>