mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	Merge branch 'inventree:master' into matmair/issue2694
This commit is contained in:
		@@ -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>
 | 
			
		||||
		Reference in New Issue
	
	Block a user