Cleanup pricing algorithms

This commit is contained in:
Oliver Walters 2019-05-20 23:53:39 +10:00
parent 8adb4f6c20
commit 6ae48d07c4
3 changed files with 71 additions and 139 deletions

View File

@ -250,7 +250,7 @@ class SupplierPart(models.Model):
- If order multiples are to be observed, then we need to calculate based on that, too - If order multiples are to be observed, then we need to calculate based on that, too
""" """
price_breaks = self.price_breaks.all() price_breaks = self.price_breaks.filter(quantity__lte=quantity)
# No price break information available? # No price break information available?
if len(price_breaks) == 0: if len(price_breaks) == 0:

View File

@ -573,32 +573,16 @@ class Part(models.Model):
""" Return the number of supplier parts available for this part """ """ Return the number of supplier parts available for this part """
return self.supplier_parts.count() return self.supplier_parts.count()
@property
def min_single_price(self):
return self.get_min_supplier_price(1)
@property
def max_single_price(self):
return self.get_max_supplier_price(1)
@property
def min_bom_price(self):
return self.get_min_bom_price(1)
@property
def max_bom_price(self):
return self.get_max_bom_price(1)
@property @property
def has_pricing_info(self): def has_pricing_info(self):
""" Return true if there is pricing information for this part """ """ Return true if there is pricing information for this part """
return self.get_min_price() is not None return self.get_price_range() is not None
@property @property
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(): for item in self.bom_items.all().prefetch_related('sub_part'):
if not item.sub_part.has_pricing_info: if not item.sub_part.has_pricing_info:
return False return False
@ -619,71 +603,44 @@ class Part(models.Model):
bom: Include BOM pricing (default = True) bom: Include BOM pricing (default = True)
""" """
min_price = self.get_min_price(quantity, buy, bom) price_range = self.get_price_range(quantity, buy, bom)
max_price = self.get_max_price(quantity, buy, bom)
if min_price is None: if price_range is None:
return None return None
min_price, max_price = price_range
if min_price == max_price: if min_price == max_price:
return min_price return min_price
return "{a} to {b}".format(a=min_price, b=max_price) return "{a} to {b}".format(a=min_price, b=max_price)
def get_min_supplier_price(self, quantity=1): def get_supplier_price_range(self, quantity=1):
""" Return the minimum price of this part from all available suppliers.
Args:
quantity: Number of units we wish to purchase (default = 1)
Returns:
Numerical price if pricing is available, else None
"""
min_price = None min_price = None
for supplier_part in self.supplier_parts.all():
supplier_price = supplier_part.get_price(quantity)
if supplier_price is None:
continue
if min_price is None or supplier_price < min_price:
min_price = supplier_price
if min_price is None:
return None
else:
return min_price
def get_max_supplier_price(self, quantity=1):
""" Return the maximum price of this part from all available suppliers.
Args:
quantity: Number of units we wish to purchase (default = 1)
Returns:
Numerical price if pricing is available, else None
"""
max_price = None max_price = None
for supplier_part in self.supplier_parts.all(): for supplier in self.supplier_parts.all():
supplier_price = supplier_part.get_price(quantity)
if supplier_price is None: price = supplier.get_price(quantity)
if price is None:
continue continue
if max_price is None or supplier_price > max_price: if min_price is None or price < min_price:
max_price = supplier_price min_price = price
if max_price is None: if max_price is None or price > max_price:
max_price = price
if min_price is None or max_price is None:
return None return None
else:
return max_price
def get_min_bom_price(self, quantity=1): return (min_price, max_price)
""" Return the minimum price of the BOM for this part.
def get_bom_price_range(self, quantity=1):
""" Return the price range of the BOM for this part.
Adds the minimum price for all components in the BOM. Adds the minimum price for all components in the BOM.
Note: If the BOM contains items without pricing information, Note: If the BOM contains items without pricing information,
@ -691,45 +648,33 @@ class Part(models.Model):
""" """
min_price = None min_price = None
max_price = None
for item in self.bom_items.all(): for item in self.bom_items.all().prefetch_related('sub_part'):
price = item.sub_part.get_min_price(quantity * item.quantity) prices = item.sub_part.get_price_range(quantity * item.quantity)
if price is None: if prices is None:
continue continue
low, high = prices
if min_price is None: if min_price is None:
min_price = 0 min_price = 0
min_price += price
return min_price
def get_max_bom_price(self, quantity=1):
""" Return the maximum price of the BOM for this part.
Adds the maximum price for all components in the BOM.
Note: If the BOM contains items without pricing information,
these items cannot be included in the BOM!
"""
max_price = None
for item in self.bom_items.all():
price = item.sub_part.get_max_price(quantity * item.quantity)
if price is None:
continue
if max_price is None: if max_price is None:
max_price = 0 max_price = 0
max_price += price min_price += low
max_price += high
return max_price if min_price is None or max_price is None:
return None
def get_min_price(self, quantity=1, buy=True, bom=True): return (min_price, max_price)
""" Return the minimum price for this part. This price can be either:
def get_price_range(self, quantity=1, buy=True, bom=True):
""" Return the price range for this part. This price can be either:
- Supplier price (if purchased from suppliers) - Supplier price (if purchased from suppliers)
- BOM price (if built from other parts) - BOM price (if built from other parts)
@ -738,37 +683,20 @@ class Part(models.Model):
Minimum of the supplier price or BOM price. If no pricing available, returns None Minimum of the supplier price or BOM price. If no pricing available, returns None
""" """
buy_price = self.get_min_supplier_price(quantity) if buy else None buy_price_range = self.get_supplier_price_range(quantity) if buy else None
bom_price = self.get_min_bom_price(quantity) if bom else None bom_price_range = self.get_bom_price_range(quantity) if bom else None
if buy_price is None: if buy_price_range is None:
return bom_price return bom_price_range
if bom_price is None: elif bom_price_range is None:
return buy_price return buy_price_range
return min(buy_price, bom_price) else:
return (
def get_max_price(self, quantity=1, buy=True, bom=True): min(buy_price_range[0], bom_price_range[0]),
""" Return the maximum price for this part. This price can be either: max(buy_price_range[1], bom_price_range[1])
)
- Supplier price (if purchsed from suppliers)
- BOM price (if built from other parts)
Returns:
Maximum of the supplier price or BOM price. If no pricing available, returns None
"""
buy_price = self.get_max_supplier_price(quantity) if buy else None
bom_price = self.get_max_bom_price(quantity) if bom else None
if buy_price is None:
return bom_price
if bom_price is None:
return buy_price
return max(buy_price, bom_price)
def deepCopy(self, other, **kwargs): def deepCopy(self, other, **kwargs):
""" Duplicates non-field data from another part. """ Duplicates non-field data from another part.

View File

@ -587,30 +587,34 @@ class PartPricing(AjaxView):
# Supplier pricing information # Supplier pricing information
if part.supplier_count > 0: if part.supplier_count > 0:
min_buy_price = part.get_min_supplier_price(quantity) buy_price = part.get_supplier_price_range(quantity)
max_buy_price = part.get_max_supplier_price(quantity)
if min_buy_price: if buy_price is not None:
ctx['min_total_buy_price'] = min_buy_price min_buy_price, max_buy_price = buy_price
ctx['min_unit_buy_price'] = min_buy_price / quantity
if max_buy_price: if min_buy_price:
ctx['max_total_buy_price'] = max_buy_price ctx['min_total_buy_price'] = min_buy_price
ctx['max_unit_buy_price'] = max_buy_price / quantity ctx['min_unit_buy_price'] = min_buy_price / quantity
if max_buy_price:
ctx['max_total_buy_price'] = max_buy_price
ctx['max_unit_buy_price'] = max_buy_price / quantity
# BOM pricing information # BOM pricing information
if part.bom_count > 0: if part.bom_count > 0:
min_bom_price = part.get_min_bom_price(quantity) bom_price = part.get_bom_price_range(quantity)
max_bom_price = part.get_max_bom_price(quantity)
if min_bom_price: if bom_price is not None:
ctx['min_total_bom_price'] = min_bom_price min_bom_price, max_bom_price = bom_price
ctx['min_unit_bom_price'] = min_bom_price / quantity
if max_bom_price: if min_bom_price:
ctx['max_total_bom_price'] = max_bom_price ctx['min_total_bom_price'] = min_bom_price
ctx['max_unit_bom_price'] = max_bom_price / quantity ctx['min_unit_bom_price'] = min_bom_price / quantity
if max_bom_price:
ctx['max_total_bom_price'] = max_bom_price
ctx['max_unit_bom_price'] = max_bom_price / quantity
return ctx return ctx