From 7a51e6cf787d9b19bcffb755d0e54be33f8c78ee Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 18 Feb 2021 14:35:21 +1100 Subject: [PATCH] Implement filtering which accommodates new inheritable BOM feature - Can no longer filter bom_items by sub_part - Adds get_used_in_filter() and get_used_in() for part model (returns a query of other part objects) --- InvenTree/part/api.py | 18 ++-- InvenTree/part/models.py | 91 ++++++++++++++------ InvenTree/part/templates/part/part_base.html | 13 ++- InvenTree/part/templates/part/used_in.html | 12 ++- InvenTree/templates/js/bom.js | 82 ------------------ 5 files changed, 94 insertions(+), 122 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 1fd5092f14..04a37e5fff 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -465,6 +465,18 @@ class PartList(generics.ListCreateAPIView): queryset = super().filter_queryset(queryset) + # Filter by "uses" query - Limit to parts which use the provided part + uses = params.get('uses', None) + + if uses: + try: + uses = Part.objects.get(pk=uses) + + queryset = queryset.filter(uses.get_used_in_filter()) + + except (ValueError, Part.DoesNotExist): + pass + # Filter by 'ancestor'? ancestor = params.get('ancestor', None) @@ -839,12 +851,6 @@ class BomList(generics.ListCreateAPIView): except (ValueError, Part.DoesNotExist): pass - - # Filter by sub-part? - sub_part = params.get('sub_part', None) - - if sub_part is not None: - queryset = queryset.filter(sub_part=sub_part) # Filter by "active" status of the part part_active = params.get('part_active', None) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index a1d394ce7b..ef5aac4194 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -643,7 +643,7 @@ class Part(MPTTModel): super().clean() if self.trackable: - for item in self.used_in.all(): + for item in self.get_used_in().all(): parent_part = item.part if not parent_part.trackable: parent_part.trackable = True @@ -891,10 +891,10 @@ class Part(MPTTModel): Return list of outstanding build orders which require this part """ - # List of BOM that this part is required for - boms = BomItem.objects.filter(sub_part=self) + # List parts that this part is required for + parts = self.get_used_in().all() - part_ids = [bom.part.pk for bom in boms] + part_ids = [part.pk for part in parts] # Now, get a list of outstanding build orders which require this part builds = BuildModels.Build.objects.filter( @@ -909,36 +909,24 @@ class Part(MPTTModel): Return the quantity of this part required for active build orders """ - # List of BOM that this part is required for - boms = BomItem.objects.filter(sub_part=self) - - part_ids = [bom.part.pk for bom in boms] - - # Now, get a list of outstanding build orders which require this part - builds = BuildModels.Build.objects.filter( - part__in=part_ids, - status__in=BuildStatus.ACTIVE_CODES - ) + # List active build orders which reference this part + builds = self.requiring_build_orders() quantity = 0 for build in builds: - + bom_item = None + # List the bom lines required to make the build (including inherited ones!) + bom_items = build.part.get_bom_items().filter(sub_part=self) + # Match BOM item to build - for bom in boms: - if bom.part == build.part: - bom_item = bom - break + for bom_item in bom_items: - if bom_item is None: - logger.warning("Found null BomItem when calculating required quantity") - continue + build_quantity = build.quantity * bom_item.quantity - build_quantity = build.quantity * bom_item.quantity - - quantity += build_quantity + quantity += build_quantity return quantity @@ -1240,6 +1228,55 @@ class Part(MPTTModel): return BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited)) + + def get_used_in_filter(self, include_inherited=True): + """ + Return a query filter for all parts that this part is used in. + + There are some considerations: + + a) This part may be directly specified against a BOM for a part + b) This part may be specifed in a BOM which is then inherited by another part + + Note: This function returns a Q object, not an actual queryset. + The Q object is used to filter against a list of Part objects + """ + + # This is pretty expensive - we need to traverse multiple variant lists! + # TODO - In the future, could this be improved somehow? + + # Keep a set of Part ID values + parts = set() + + # First, grab a list of all BomItem objects which "require" this part + bom_items = BomItem.objects.filter(sub_part=self) + + for bom_item in bom_items: + + # Add the directly referenced part + parts.add(bom_item.part) + + # Traverse down the variant tree? + if include_inherited and bom_item.inherited: + + part_variants = bom_item.part.get_descendants(include_self=False) + + for variant in part_variants: + parts.add(variant) + + # Turn into a list of valid IDs (for matching against a Part query) + part_ids = [part.pk for part in parts] + + return Q(id__in=part_ids) + + def get_used_in(self, include_inherited=True): + """ + Return a queryset containing all parts this part is used in. + + Includes consideration of inherited BOMs + """ + return Part.objects.filter(self.get_used_in_filter(include_inherited=include_inherited)) + @property def has_bom(self): return self.get_bom_items().count() > 0 @@ -1265,7 +1302,7 @@ class Part(MPTTModel): @property def used_in_count(self): """ Return the number of part BOMs that this part appears in """ - return self.used_in.count() + return self.get_used_in().count() def get_bom_hash(self): """ Return a checksum hash for the BOM for this part. @@ -1364,7 +1401,7 @@ class Part(MPTTModel): parts = parts.exclude(id=self.id) # Exclude any parts that this part is used *in* (to prevent recursive BOMs) - used_in = self.used_in.all() + used_in = self.get_used_in().all() parts = parts.exclude(id__in=[item.part.id for item in used_in]) diff --git a/InvenTree/part/templates/part/part_base.html b/InvenTree/part/templates/part/part_base.html index 1221c271b6..6b0163089d 100644 --- a/InvenTree/part/templates/part/part_base.html +++ b/InvenTree/part/templates/part/part_base.html @@ -133,11 +133,18 @@ {% decimal on_order %} {% endif %} - {% if required > 0 %} + {% if required_build_order_quantity > 0 %} - {% trans "Required for Orders" %} - {% decimal required %} + {% trans "Required for Build Orders" %} + {% decimal required_build_order_quantity %} + + {% endif %} + {% if required_sales_order_quantity > 0 %} + + + {% trans "Required for Sales Orders" %} + {% decimal required_sales_order_quantity %} {% endif %} {% if allocated > 0 %} diff --git a/InvenTree/part/templates/part/used_in.html b/InvenTree/part/templates/part/used_in.html index 25e858b5f0..686578a93a 100644 --- a/InvenTree/part/templates/part/used_in.html +++ b/InvenTree/part/templates/part/used_in.html @@ -22,10 +22,14 @@ {% block js_ready %} {{ block.super }} - loadUsedInTable('#used-table', { - part_detail: true, - part_id: {{ part.pk }} - }); + loadSimplePartTable('#used-table', + '{% url "api-part-list" %}', + { + params: { + uses: {{ part.pk }}, + } + } + ); {% endblock %} \ No newline at end of file diff --git a/InvenTree/templates/js/bom.js b/InvenTree/templates/js/bom.js index 76c65d44dd..beb4110df6 100644 --- a/InvenTree/templates/js/bom.js +++ b/InvenTree/templates/js/bom.js @@ -539,85 +539,3 @@ function loadBomTable(table, options) { }); } } - -function loadUsedInTable(table, options) { - /* Load a table which displays all the parts that the given part is used in. - */ - - var params = { - sub_part: options.part_id, - ordering: 'name', - } - - if (options.part_detail) { - params.part_detail = true; - } - - if (options.sub_part_detail) { - params.sub_part_detail = true; - } - - var filters = {}; - - if (!options.disableFilters) { - filters = loadTableFilters("usedin"); - } - - for (var key in params) { - filters[key] = params[key]; - } - - setupFilterList("usedin", $(table)); - - // Columns to display in the table - var cols = [ - { - field: 'pk', - title: 'ID', - visible: false, - switchable: false, - }, - { - field: 'part_detail.full_name', - title: '{% trans "Part" %}', - sortable: true, - formatter: function(value, row, index, field) { - var link = `/part/${row.part}/bom/`; - var html = imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, link); - - if (!row.part_detail.active) { - html += "{% trans 'INACTIVE' %}"; - } - - return html; - } - }, - { - field: 'part_detail.description', - title: '{% trans "Description" %}', - sortable: true, - }, - { - sortable: true, - field: 'quantity', - title: '{% trans "Uses" %}', - formatter: function(value, row, index, field) { - return parseFloat(value); - }, - } - ]; - - // Load the table - $(table).inventreeTable({ - url: "{% url 'api-bom-list' %}", - formatNoMatches: function() { - return '{% trans "No matching parts found" %}'; - }, - columns: cols, - showColumns: true, - sortable: true, - serach: true, - queryParams: filters, - original: params, - }); -}