Refactor bom item filter

- Also updates a number of part functions to make use of inherited BOM items
This commit is contained in:
Oliver Walters 2021-02-17 22:53:56 +11:00
parent 1eb2456e3d
commit bb3440a8a4
5 changed files with 80 additions and 27 deletions

View File

@ -308,8 +308,7 @@
} }
.rowinherited { .rowinherited {
background-color: #efe; background-color: #dde;
opacity: 90%;
} }
.dropdown { .dropdown {

View File

@ -835,16 +835,7 @@ class BomList(generics.ListCreateAPIView):
try: try:
part = Part.objects.get(pk=part) part = Part.objects.get(pk=part)
# Construct a filter for matching the provided part queryset = queryset.filter(part.get_bom_item_filter())
local_part_filter = Q(part=part)
# Construct a filter for matching inherited items from parent parts
parent_parts = part.get_ancestors(include_self=False)
parent_ids = [p.pk for p in parent_parts]
parent_part_filter = Q(part__pk__in=parent_ids, inherited=True)
queryset = queryset.filter(local_part_filter | parent_part_filter)
except (ValueError, Part.DoesNotExist): except (ValueError, Part.DoesNotExist):
pass pass

View File

@ -14,7 +14,7 @@ from django.urls import reverse
from django.db import models, transaction from django.db import models, transaction
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.db.models import Sum, UniqueConstraint from django.db.models import Q, Sum, UniqueConstraint
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
@ -418,8 +418,10 @@ class Part(MPTTModel):
p2=str(parent) p2=str(parent)
))}) ))})
bom_items = self.get_bom_items()
# Ensure that the parent part does not appear under any child BOM item! # Ensure that the parent part does not appear under any child BOM item!
for item in self.bom_items.all(): for item in bom_items.all():
# Check for simple match # Check for simple match
if item.sub_part == parent: if item.sub_part == parent:
@ -1058,8 +1060,10 @@ class Part(MPTTModel):
total = None total = None
bom_items = self.get_bom_items().prefetch_related('sub_part__stock_items')
# Calculate the minimum number of parts that can be built using each sub-part # Calculate the minimum number of parts that can be built using each sub-part
for item in self.bom_items.all().prefetch_related('sub_part__stock_items'): for item in bom_items.all():
stock = item.sub_part.available_stock stock = item.sub_part.available_stock
# If (by some chance) we get here but the BOM item quantity is invalid, # If (by some chance) we get here but the BOM item quantity is invalid,
@ -1189,9 +1193,56 @@ class Part(MPTTModel):
return query['t'] return query['t']
def get_bom_item_filter(self, include_inherited=True):
"""
Returns a query filter for all BOM items associated with this Part.
There are some considerations:
a) BOM items can be defined against *this* part
b) BOM items can be inherited from a *parent* part
We will construct a filter to grab *all* the BOM items!
Note: This does *not* return a queryset, it returns a Q object,
which can be used by some other query operation!
Because we want to keep our code DRY!
"""
bom_filter = Q(part=self)
if include_inherited:
# We wish to include parent parts
parents = self.get_ancestors(include_self=False)
# There are parents available
if parents.count() > 0:
parent_ids = [p.pk for p in parents]
parent_filter = Q(
part__id__in=parent_ids,
inherited=True
)
# OR the filters together
bom_filter |= parent_filter
return bom_filter
def get_bom_items(self, include_inherited=True):
"""
Return a queryset containing all BOM items for this part
By default, will include inherited BOM items
"""
return BomItem.objects.filter(self.get_bom_item_filter(include_inherited=include_inherited))
@property @property
def has_bom(self): def has_bom(self):
return self.bom_count > 0 return self.get_bom_items().count() > 0
@property @property
def has_trackable_parts(self): def has_trackable_parts(self):
@ -1200,7 +1251,7 @@ class Part(MPTTModel):
This is important when building the part. This is important when building the part.
""" """
for bom_item in self.bom_items.all(): for bom_item in self.get_bom_items().all():
if bom_item.sub_part.trackable: if bom_item.sub_part.trackable:
return True return True
@ -1209,7 +1260,7 @@ class Part(MPTTModel):
@property @property
def bom_count(self): def bom_count(self):
""" Return the number of items contained in the BOM for this part """ """ Return the number of items contained in the BOM for this part """
return self.bom_items.count() return self.get_bom_items().count()
@property @property
def used_in_count(self): def used_in_count(self):
@ -1227,7 +1278,10 @@ class Part(MPTTModel):
hash = hashlib.md5(str(self.id).encode()) hash = hashlib.md5(str(self.id).encode())
for item in self.bom_items.all().prefetch_related('sub_part'): # List *all* BOM items (including inherited ones!)
bom_items = self.get_bom_items().all().prefetch_related('sub_part')
for item in bom_items:
hash.update(str(item.get_item_hash()).encode()) hash.update(str(item.get_item_hash()).encode())
return str(hash.digest()) return str(hash.digest())
@ -1246,8 +1300,10 @@ class Part(MPTTModel):
- Saves the current date and the checking user - Saves the current date and the checking user
""" """
# Validate each line item too # Validate each line item, ignoring inherited ones
for item in self.bom_items.all(): bom_items = self.get_bom_items(include_inherited=False)
for item in bom_items.all():
item.validate_hash() item.validate_hash()
self.bom_checksum = self.get_bom_hash() self.bom_checksum = self.get_bom_hash()
@ -1258,7 +1314,10 @@ class Part(MPTTModel):
@transaction.atomic @transaction.atomic
def clear_bom(self): def clear_bom(self):
""" Clear the BOM items for the part (delete all BOM lines). """
Clear the BOM items for the part (delete all BOM lines).
Note: Does *NOT* delete inherited BOM items!
""" """
self.bom_items.all().delete() self.bom_items.all().delete()
@ -1275,9 +1334,9 @@ class Part(MPTTModel):
if parts is None: if parts is None:
parts = set() parts = set()
items = BomItem.objects.filter(part=self.pk) bom_items = self.get_bom_items().all()
for bom_item in items: for bom_item in bom_items:
sub_part = bom_item.sub_part sub_part = bom_item.sub_part
@ -1325,7 +1384,7 @@ class Part(MPTTModel):
def has_complete_bom_pricing(self): def has_complete_bom_pricing(self):
""" Return true if there is pricing information for each item in the BOM. """ """ Return true if there is pricing information for each item in the BOM. """
for item in self.bom_items.all().select_related('sub_part'): for item in self.get_bom_items().all().select_related('sub_part'):
if not item.sub_part.has_pricing_info: if not item.sub_part.has_pricing_info:
return False return False
@ -1392,7 +1451,7 @@ class Part(MPTTModel):
min_price = None min_price = None
max_price = None max_price = None
for item in self.bom_items.all().select_related('sub_part'): for item in self.get_bom_items.all().select_related('sub_part'):
if item.sub_part.pk == self.pk: if item.sub_part.pk == self.pk:
print("Warning: Item contains itself in BOM") print("Warning: Item contains itself in BOM")
@ -1460,8 +1519,11 @@ class Part(MPTTModel):
if clear: if clear:
# Remove existing BOM items # Remove existing BOM items
# Note: Inherited BOM items are *not* deleted!
self.bom_items.all().delete() self.bom_items.all().delete()
# 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.bom_items.all():
# If this part already has a BomItem pointing to the same sub-part, # If this part already has a BomItem pointing to the same sub-part,
# delete that BomItem from this part first! # delete that BomItem from this part first!

View File

@ -372,6 +372,7 @@ class BillOfMaterialsReport(ReportTemplateBase):
return { return {
'part': part, 'part': part,
'category': part.category, 'category': part.category,
'bom_items': part.get_bom_items(),
} }

View File

@ -279,7 +279,7 @@ function loadBomTable(table, options) {
if (!row.inherited) { if (!row.inherited) {
return "-"; return "-";
} else if (row.part == options.parent_id) { } else if (row.part == options.parent_id) {
return '{% trans "Inheritable" %}'; return '{% trans "Inherited" %}';
} else { } else {
// If this BOM item is inherited from a parent part // If this BOM item is inherited from a parent part
return renderLink( return renderLink(