From 2a1fd2b03b8efc190995fe87ef73c64a8fc998a5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 18 May 2019 21:22:56 +1000 Subject: [PATCH] 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 --- InvenTree/part/models.py | 139 ++++++++++++++++++++++- InvenTree/part/serializers.py | 2 + InvenTree/part/templates/part/bom.html | 8 ++ InvenTree/static/script/inventree/bom.js | 13 +++ 4 files changed, 161 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 4313228498..7025ae20ce 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -569,6 +569,49 @@ class Part(models.Model): 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 + 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): """ 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: 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): """ 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: 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 + 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): """ Duplicates non-field data from another part. Does not alter the normal fields of this part, diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 947fa9904b..9415cdbfb0 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -34,6 +34,7 @@ class PartBriefSerializer(serializers.ModelSerializer): url = serializers.CharField(source='get_absolute_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: model = Part @@ -43,6 +44,7 @@ class PartBriefSerializer(serializers.ModelSerializer): 'full_name', 'description', 'available_stock', + 'single_price_info', 'image_url', ] diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 3990bfab6c..bff082b6ae 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -11,6 +11,14 @@

Bill of Materials

+{% if part.has_complete_bom_pricing == False %} +
+ The BOM for {{ part.full_name }} does not have complete pricing information +
+{% endif %} +
+ Single BOM Price: {{ part.min_bom_price }} to {{ part.max_bom_price }} +
{% if part.bom_checked_date %} {% if part.is_bom_valid %}
diff --git a/InvenTree/static/script/inventree/bom.js b/InvenTree/static/script/inventree/bom.js index 6ff81de4fc..f71353fce7 100644 --- a/InvenTree/static/script/inventree/bom.js +++ b/InvenTree/static/script/inventree/bom.js @@ -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 "No pricing information"; + } + }, + }); } // Part notes