More complex pricing calculations

- Calculate BOM price for a part
- Calculate total pricing for a part (build or purchase)
- Display pricing information in BOM table
This commit is contained in:
Oliver Walters 2019-05-18 21:22:56 +10:00
parent 4c82714777
commit 2a1fd2b03b
4 changed files with 161 additions and 1 deletions

View File

@ -569,6 +569,49 @@ class Part(models.Model):
def max_single_price(self): def max_single_price(self):
return self.get_max_supplier_price(1) 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
def has_pricing_info(self):
""" Return true if there is pricing information for this part """
return self.get_min_price() is not None
@property
def has_complete_bom_pricing(self):
""" Return true if there is pricing information for each item in the BOM. """
for item in self.bom_items.all():
if not item.sub_part.has_pricing_info:
return False
return True
@property
def single_price_info(self):
""" Return a simplified pricing string for this part at single quantity """
return self.get_price_info()
def get_price_info(self, quantity=1):
""" Return a simplified pricing string for this part """
min_price = self.get_min_price(quantity)
max_price = self.get_max_price(quantity)
if min_price is None:
return None
if min_price == max_price:
return min_price
return "{a} to {b}".format(a=min_price, b=max_price)
def get_min_supplier_price(self, quantity=1): def get_min_supplier_price(self, quantity=1):
""" Return the minimum price of this part from all available suppliers. """ Return the minimum price of this part from all available suppliers.
@ -590,7 +633,10 @@ class Part(models.Model):
if min_price is None or supplier_price < min_price: if min_price is None or supplier_price < min_price:
min_price = supplier_price min_price = supplier_price
return min_price if min_price is None:
return None
else:
return min_price * quantity
def get_max_supplier_price(self, quantity=1): def get_max_supplier_price(self, quantity=1):
""" Return the maximum price of this part from all available suppliers. """ Return the maximum price of this part from all available suppliers.
@ -613,8 +659,99 @@ class Part(models.Model):
if max_price is None or supplier_price > max_price: if max_price is None or supplier_price > max_price:
max_price = supplier_price max_price = supplier_price
if max_price is None:
return None
else:
return max_price * quantity
def get_min_bom_price(self, quantity=1):
""" Return the minimum price of the BOM for this part.
Adds the minimum price for all components in the BOM.
Note: If the BOM contains items without pricing information,
these items cannot be included in the BOM!
"""
min_price = None
for item in self.bom_items.all():
price = item.sub_part.get_min_price(quantity * item.quantity)
if price is None:
continue
if min_price is None:
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:
max_price = 0
max_price += price
return max_price return max_price
def get_min_price(self, quantity=1):
""" Return the minimum price for this part. This price can be either:
- Supplier price (if purchased from suppliers)
- BOM price (if built from other parts)
Returns:
Minimum of the supplier price or BOM price. If no pricing available, returns None
"""
buy_price = self.get_min_supplier_price(quantity)
bom_price = self.get_min_bom_price(quantity)
if buy_price is None:
return bom_price
if bom_price is None:
return buy_price
return min(buy_price, bom_price)
def get_max_price(self, quantity=1):
""" Return the maximum price for this part. This price can be either:
- 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)
bom_price = self.get_max_bom_price(quantity)
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.
Does not alter the normal fields of this part, Does not alter the normal fields of this part,

View File

@ -34,6 +34,7 @@ class PartBriefSerializer(serializers.ModelSerializer):
url = serializers.CharField(source='get_absolute_url', read_only=True) url = serializers.CharField(source='get_absolute_url', read_only=True)
image_url = serializers.CharField(source='get_image_url', read_only=True) image_url = serializers.CharField(source='get_image_url', read_only=True)
single_price_info = serializers.CharField(read_only=True)
class Meta: class Meta:
model = Part model = Part
@ -43,6 +44,7 @@ class PartBriefSerializer(serializers.ModelSerializer):
'full_name', 'full_name',
'description', 'description',
'available_stock', 'available_stock',
'single_price_info',
'image_url', 'image_url',
] ]

View File

@ -11,6 +11,14 @@
<h3>Bill of Materials</h3> <h3>Bill of Materials</h3>
{% if part.has_complete_bom_pricing == False %}
<div class='alert alert-block alert-warning'>
The BOM for <i>{{ part.full_name }}</i> does not have complete pricing information
</div>
{% endif %}
<div class='panel panel-default'>
Single BOM Price: {{ part.min_bom_price }} to {{ part.max_bom_price }}
</div>
{% if part.bom_checked_date %} {% if part.bom_checked_date %}
{% if part.is_bom_valid %} {% if part.is_bom_valid %}
<div class='alert alert-block alert-info'> <div class='alert alert-block alert-info'>

View File

@ -151,6 +151,19 @@ function loadBomTable(table, options) {
} }
} }
); );
cols.push({
field: 'sub_part_detail.single_price_info',
title: 'Price',
sortable: true,
formatter: function(value, row, index, field) {
if (value) {
return value;
} else {
return "<span class='warning-msg'><i>No pricing information</i></span>";
}
},
});
} }
// Part notes // Part notes