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)
This commit is contained in:
Oliver Walters 2021-02-18 14:35:21 +11:00
parent 25ada20a19
commit 7a51e6cf78
5 changed files with 94 additions and 122 deletions

View File

@ -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)
@ -840,12 +852,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)

View File

@ -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,16 +909,8 @@ 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
@ -926,19 +918,15 @@ class Part(MPTTModel):
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])

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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,
});
}