mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'inventree:master' into matmair/issue2788
This commit is contained in:
commit
71a0d8159e
@ -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',
|
||||
]
|
||||
|
@ -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:
|
||||
|
@ -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', {}),
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 """
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user