mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05: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' %}", |     launchModalForm("{% url 'po-create' %}", | ||||||
|         { |         { | ||||||
|             follow: true, |             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) |         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'? |         # Filter by 'ancestor'? | ||||||
|         ancestor = params.get('ancestor', None) |         ancestor = params.get('ancestor', None) | ||||||
|  |  | ||||||
| @@ -840,12 +852,6 @@ class BomList(generics.ListCreateAPIView): | |||||||
|             except (ValueError, Part.DoesNotExist): |             except (ValueError, Part.DoesNotExist): | ||||||
|                 pass |                 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 |         # Filter by "active" status of the part | ||||||
|         part_active = params.get('part_active', None) |         part_active = params.get('part_active', None) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -643,7 +643,7 @@ class Part(MPTTModel): | |||||||
|         super().clean() |         super().clean() | ||||||
|  |  | ||||||
|         if self.trackable: |         if self.trackable: | ||||||
|             for item in self.used_in.all(): |             for item in self.get_used_in().all(): | ||||||
|                 parent_part = item.part |                 parent_part = item.part | ||||||
|                 if not parent_part.trackable: |                 if not parent_part.trackable: | ||||||
|                     parent_part.trackable = True |                     parent_part.trackable = True | ||||||
| @@ -891,10 +891,10 @@ class Part(MPTTModel): | |||||||
|         Return list of outstanding build orders which require this part |         Return list of outstanding build orders which require this part | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         # List of BOM that this part is required for |         # List parts that this part is required for | ||||||
|         boms = BomItem.objects.filter(sub_part=self) |         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 |         # Now, get a list of outstanding build orders which require this part | ||||||
|         builds = BuildModels.Build.objects.filter( |         builds = BuildModels.Build.objects.filter( | ||||||
| @@ -909,16 +909,8 @@ class Part(MPTTModel): | |||||||
|         Return the quantity of this part required for active build orders |         Return the quantity of this part required for active build orders | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|         # List of BOM that this part is required for |         # List active build orders which reference this part | ||||||
|         boms = BomItem.objects.filter(sub_part=self) |         builds = self.requiring_build_orders() | ||||||
|  |  | ||||||
|         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 |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         quantity = 0 |         quantity = 0 | ||||||
|  |  | ||||||
| @@ -926,15 +918,11 @@ class Part(MPTTModel): | |||||||
|      |      | ||||||
|             bom_item = None |             bom_item = None | ||||||
|  |  | ||||||
|             # Match BOM item to build |             # List the bom lines required to make the build (including inherited ones!) | ||||||
|             for bom in boms: |             bom_items = build.part.get_bom_items().filter(sub_part=self) | ||||||
|                 if bom.part == build.part: |  | ||||||
|                     bom_item = bom |  | ||||||
|                     break |  | ||||||
|  |  | ||||||
|             if bom_item is None: |             # Match BOM item to build | ||||||
|                 logger.warning("Found null BomItem when calculating required quantity") |             for bom_item in bom_items: | ||||||
|                 continue |  | ||||||
|  |  | ||||||
|                 build_quantity = build.quantity * bom_item.quantity |                 build_quantity = build.quantity * bom_item.quantity | ||||||
|  |  | ||||||
| @@ -1240,6 +1228,54 @@ class Part(MPTTModel): | |||||||
|  |  | ||||||
|         return BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited)) |         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 |     @property | ||||||
|     def has_bom(self): |     def has_bom(self): | ||||||
|         return self.get_bom_items().count() > 0 |         return self.get_bom_items().count() > 0 | ||||||
| @@ -1265,7 +1301,7 @@ class Part(MPTTModel): | |||||||
|     @property |     @property | ||||||
|     def used_in_count(self): |     def used_in_count(self): | ||||||
|         """ Return the number of part BOMs that this part appears in """ |         """ 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): |     def get_bom_hash(self): | ||||||
|         """ Return a checksum hash for the BOM for this part. |         """ Return a checksum hash for the BOM for this part. | ||||||
| @@ -1364,7 +1400,7 @@ class Part(MPTTModel): | |||||||
|         parts = parts.exclude(id=self.id) |         parts = parts.exclude(id=self.id) | ||||||
|  |  | ||||||
|         # Exclude any parts that this part is used *in* (to prevent recursive BOMs) |         # 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]) |         parts = parts.exclude(id__in=[item.part.id for item in used_in]) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -133,11 +133,18 @@ | |||||||
|             <td>{% decimal on_order %}</td> |             <td>{% decimal on_order %}</td> | ||||||
|         </tr> |         </tr> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|         {% if required > 0 %} |         {% if required_build_order_quantity > 0 %} | ||||||
|         <tr> |         <tr> | ||||||
|             <td><span class='fas fa-clipboard-list'></span></td> |             <td><span class='fas fa-clipboard-list'></span></td> | ||||||
|             <td>{% trans "Required for Orders" %}</td> |             <td>{% trans "Required for Build Orders" %}</td> | ||||||
|             <td>{% decimal required %} |             <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> |         </tr> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|         {% if allocated > 0 %} |         {% if allocated > 0 %} | ||||||
|   | |||||||
| @@ -22,10 +22,14 @@ | |||||||
| {% block js_ready %} | {% block js_ready %} | ||||||
| {{ block.super }} | {{ block.super }} | ||||||
|  |  | ||||||
|     loadUsedInTable('#used-table', { |     loadSimplePartTable('#used-table', | ||||||
|         part_detail: true, |         '{% url "api-part-list" %}', | ||||||
|         part_id: {{ part.pk }} |         { | ||||||
|     }); |             params: { | ||||||
|  |                 uses: {{ part.pk }}, | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |  | ||||||
| {% endblock %} | {% 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,11 +34,13 @@ | |||||||
|                   </button> |                   </button> | ||||||
|                   <h3 id='modal-title'><i>Form Title Here</i></h3> |                   <h3 id='modal-title'><i>Form Title Here</i></h3> | ||||||
|                 </div> |                 </div> | ||||||
|  |                 <div class='modal-form-content-wrapper'> | ||||||
|                   <div class='alert alert-block alert-danger' id='form-validation-warning' style="display: none;"> |                   <div class='alert alert-block alert-danger' id='form-validation-warning' style="display: none;"> | ||||||
|                     {% trans "Form errors exist" %} |                     {% trans "Form errors exist" %} | ||||||
|                   </div> |                   </div> | ||||||
|                   <div class='modal-form-content'> |                   <div class='modal-form-content'> | ||||||
|                   </div> |                   </div> | ||||||
|  |                 </div> | ||||||
|                 <div class='modal-footer'> |                 <div class='modal-footer'> | ||||||
|                     <button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>{% trans "Close" %}</button> |                     <button type='button' class='btn btn-default' id='modal-form-close' data-dismiss='modal'>{% trans "Close" %}</button> | ||||||
|                     <button type='button' class='btn btn-primary' id='modal-form-submit'>{% trans "Submit" %}</button> |                     <button type='button' class='btn btn-primary' id='modal-form-submit'>{% trans "Submit" %}</button> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user