From 8caf6bad1057e7a59cdcf7b16ace3cb29e732297 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 18 Feb 2021 00:38:40 +1100 Subject: [PATCH 01/11] Fix for duplicating BOM - Do not duplicate bom items which are "inherited" --- InvenTree/part/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 911a2cdac4..06e0c9f078 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1524,7 +1524,7 @@ class Part(MPTTModel): # Copy existing BOM items from another part # Note: Inherited BOM Items will *not* be duplicated!! - for bom_item in other.bom_items.all(): + for bom_item in other.get_bom_items(include_inherited=False).all(): # If this part already has a BomItem pointing to the same sub-part, # delete that BomItem from this part first! From abe1018abeafb53c37a0e21b6c3b979f1c4bb911 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 18 Feb 2021 00:40:30 +1100 Subject: [PATCH 02/11] Add new fields to BOM item hash --- InvenTree/part/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 06e0c9f078..a1d394ce7b 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2094,6 +2094,8 @@ class BomItem(models.Model): - Quantity - Reference field - Note field + - Optional field + - Inherited field """ @@ -2106,6 +2108,8 @@ class BomItem(models.Model): hash.update(str(self.quantity).encode()) hash.update(str(self.note).encode()) hash.update(str(self.reference).encode()) + hash.update(str(self.optional).encode()) + hash.update(str(self.inherited).encode()) return str(hash.digest()) From 3822b60bb0826ca03e6e1faff9db0da9f8783ff5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 18 Feb 2021 00:49:16 +1100 Subject: [PATCH 03/11] CSS tweaks --- InvenTree/InvenTree/static/css/inventree.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 50a24aa095..61697fafec 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -308,7 +308,7 @@ } .rowinherited { - background-color: #dde; + background-color: #eee; } .dropdown { From 07ee27ad9baf58de66fa5b9ccc0343bf7ef23ab8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 18 Feb 2021 00:49:36 +1100 Subject: [PATCH 04/11] Another CSS tweak --- InvenTree/InvenTree/static/css/inventree.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 61697fafec..ac3d402c3b 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -304,11 +304,11 @@ .rowinvalid { color: #A00; - font-style: italic; } .rowinherited { background-color: #eee; + font-style: italic; } .dropdown { From 7a51e6cf787d9b19bcffb755d0e54be33f8c78ee Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 18 Feb 2021 14:35:21 +1100 Subject: [PATCH 05/11] 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, - }); -} From e75b9d04fe34cb645f798fe78606b9bb5000961d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 18 Feb 2021 14:38:25 +1100 Subject: [PATCH 06/11] PEP fix --- InvenTree/part/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index ef5aac4194..c1dbb454b4 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1228,7 +1228,6 @@ 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. From 1b73f56937a816458cfd58015fb9726010cc0011 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 18 Feb 2021 14:56:35 +1100 Subject: [PATCH 07/11] modal content wrapper for secondary modal --- InvenTree/templates/modals.html | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/modals.html b/InvenTree/templates/modals.html index a162a3f167..11166751f8 100644 --- a/InvenTree/templates/modals.html +++ b/InvenTree/templates/modals.html @@ -34,10 +34,12 @@ - -