mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Merge pull request #1320 from SchrodingersGat/inherited-bom-used-in
Implement filtering which accommodates new inheritable BOM feature
This commit is contained in:
		| @@ -158,6 +158,14 @@ $("#po-create").click(function() { | ||||
|     launchModalForm("{% url 'po-create' %}", | ||||
|         { | ||||
|             follow: true, | ||||
|             secondary: [ | ||||
|                 { | ||||
|                     field: 'supplier', | ||||
|                     label: '{% trans "New Supplier" %}', | ||||
|                     title: '{% trans "Create new Supplier" %}', | ||||
|                     url: '{% url "supplier-create" %}', | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     ); | ||||
| }); | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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,54 @@ 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 +1301,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 +1400,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]) | ||||
|  | ||||
|   | ||||
| @@ -133,11 +133,18 @@ | ||||
|             <td>{% decimal on_order %}</td> | ||||
|         </tr> | ||||
|         {% endif %} | ||||
|         {% if required > 0 %} | ||||
|         {% if required_build_order_quantity > 0 %} | ||||
|         <tr> | ||||
|             <td><span class='fas fa-clipboard-list'></span></td> | ||||
|             <td>{% trans "Required for Orders" %}</td> | ||||
|             <td>{% decimal required %} | ||||
|             <td>{% trans "Required for Build Orders" %}</td> | ||||
|             <td>{% decimal required_build_order_quantity %} | ||||
|         </tr> | ||||
|         {% endif %} | ||||
|         {% if required_sales_order_quantity > 0 %} | ||||
|         <tr> | ||||
|             <td><span class='fas fa-clipboard-list'></span></td> | ||||
|             <td>{% trans "Required for Sales Orders" %}</td> | ||||
|             <td>{% decimal required_sales_order_quantity %} | ||||
|         </tr> | ||||
|         {% endif %} | ||||
|         {% if allocated > 0 %} | ||||
|   | ||||
| @@ -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 %} | ||||
| @@ -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 += "<span class='label label-warning' style='float: right;'>{% trans 'INACTIVE' %}</span>"; | ||||
|                 } | ||||
|  | ||||
|                 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, | ||||
|     }); | ||||
| } | ||||
|   | ||||
| @@ -34,10 +34,12 @@ | ||||
|                   </button> | ||||
|                   <h3 id='modal-title'><i>Form Title Here</i></h3> | ||||
|                 </div> | ||||
|                 <div class='alert alert-block alert-danger' id='form-validation-warning' style="display: none;"> | ||||
|                   {% trans "Form errors exist" %} | ||||
|                 </div> | ||||
|                 <div class='modal-form-content'> | ||||
|                 <div class='modal-form-content-wrapper'> | ||||
|                   <div class='alert alert-block alert-danger' id='form-validation-warning' style="display: none;"> | ||||
|                     {% trans "Form errors exist" %} | ||||
|                   </div> | ||||
|                   <div class='modal-form-content'> | ||||
|                   </div> | ||||
|                 </div> | ||||
|                 <div class='modal-footer'> | ||||
|                     <button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>{% trans "Close" %}</button> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user