From 87b38ec992394ad0ec48ac59db6ae94bfbe2966d Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 29 Apr 2021 14:22:07 -0400 Subject: [PATCH 1/2] Added support for fractional part/bom quantity and price --- InvenTree/company/models.py | 23 +++++++++++++++++++-- InvenTree/company/tests.py | 8 +++++--- InvenTree/part/views.py | 40 ++++++++++++++++++++++++------------- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index c743bf4434..89a3f6c9bf 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -6,7 +6,7 @@ Company database model definitions from __future__ import unicode_literals import os - +import decimal import math from django.utils.translation import ugettext_lazy as _ @@ -566,12 +566,15 @@ class SupplierPart(models.Model): - If order multiples are to be observed, then we need to calculate based on that, too """ - price_breaks = self.price_breaks.filter(quantity__lte=quantity) + price_breaks = self.price_breaks.all() # No price break information available? if len(price_breaks) == 0: return None + # Check if quantity is fraction and disable multiples + multiples = (quantity % 1 == 0) + # Order multiples if multiples: quantity = int(math.ceil(quantity / self.multiple) * self.multiple) @@ -584,7 +587,12 @@ class SupplierPart(models.Model): # Default currency selection currency = common.models.InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + pb_min = None for pb in self.price_breaks.all(): + # Store smallest price break + if not pb_min: + pb_min = pb + # Ignore this pricebreak (quantity is too high) if pb.quantity > quantity: continue @@ -598,6 +606,17 @@ class SupplierPart(models.Model): # Convert everything to the selected currency pb_cost = pb.convert_to(currency) + # Use smallest price break + if not pb_found and pb_min: + # Update price break information + pb_quantity = pb_min.quantity + pb_cost = pb_min.convert_to(currency) + # Trigger cost calculation using smallest price break + pb_found = True + + # Convert quantity to decimal.Decimal format + quantity = decimal.Decimal(f'{quantity}') + if pb_found: cost = pb_cost * quantity return normalize(cost + self.base_cost) diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py index cb366e8a6d..fa6ce5ed50 100644 --- a/InvenTree/company/tests.py +++ b/InvenTree/company/tests.py @@ -5,6 +5,7 @@ from django.test import TestCase from django.core.exceptions import ValidationError import os +from decimal import Decimal from .models import Company, Contact, ManufacturerPart, SupplierPart from .models import rename_company_image @@ -103,8 +104,8 @@ class CompanySimpleTest(TestCase): self.assertEqual(p(100), 350) p = self.acme0002.get_price - self.assertEqual(p(1), None) - self.assertEqual(p(2), None) + self.assertEqual(p(1), 7) + self.assertEqual(p(2), 14) self.assertEqual(p(5), 35) self.assertEqual(p(45), 315) self.assertEqual(p(55), 68.75) @@ -121,7 +122,8 @@ class CompanySimpleTest(TestCase): m3x12 = Part.objects.get(name='M3x12 SHCS') - self.assertIsNone(m3x12.get_price_info(3)) + self.assertEqual(m3x12.get_price_info(0.3), Decimal('2.4')) + self.assertEqual(m3x12.get_price_info(3), Decimal('24')) self.assertIsNotNone(m3x12.get_price_info(50)) def test_currency_validation(self): diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 229ae06109..d7c68dd6a3 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -1957,6 +1957,11 @@ class PartPricing(AjaxView): role_required = ['sales_order.view', 'part.view'] + def get_quantity(self): + """ Return set quantity in decimal format """ + + return Decimal(self.request.POST.get('quantity', 1)) + def get_part(self): try: return Part.objects.get(id=self.kwargs['pk']) @@ -1965,12 +1970,12 @@ class PartPricing(AjaxView): def get_pricing(self, quantity=1, currency=None): - try: - quantity = int(quantity) - except ValueError: - quantity = 1 + # try: + # quantity = int(quantity) + # except ValueError: + # quantity = 1 - if quantity < 1: + if quantity <= 0: quantity = 1 # TODO - Capacity for price comparison in different currencies @@ -2000,16 +2005,19 @@ class PartPricing(AjaxView): min_buy_price /= scaler max_buy_price /= scaler + min_unit_buy_price = round(min_buy_price / quantity, 3) + max_unit_buy_price = round(max_buy_price / quantity, 3) + min_buy_price = round(min_buy_price, 3) max_buy_price = round(max_buy_price, 3) if min_buy_price: ctx['min_total_buy_price'] = min_buy_price - ctx['min_unit_buy_price'] = min_buy_price / quantity + ctx['min_unit_buy_price'] = min_unit_buy_price if max_buy_price: ctx['max_total_buy_price'] = max_buy_price - ctx['max_unit_buy_price'] = max_buy_price / quantity + ctx['max_unit_buy_price'] = max_unit_buy_price # BOM pricing information if part.bom_count > 0: @@ -2022,16 +2030,19 @@ class PartPricing(AjaxView): min_bom_price /= scaler max_bom_price /= scaler + min_unit_bom_price = round(min_bom_price / quantity, 3) + max_unit_bom_price = round(max_bom_price / quantity, 3) + min_bom_price = round(min_bom_price, 3) max_bom_price = round(max_bom_price, 3) if min_bom_price: ctx['min_total_bom_price'] = min_bom_price - ctx['min_unit_bom_price'] = min_bom_price / quantity + ctx['min_unit_bom_price'] = min_unit_bom_price if max_bom_price: ctx['max_total_bom_price'] = max_bom_price - ctx['max_unit_bom_price'] = max_bom_price / quantity + ctx['max_unit_bom_price'] = max_unit_bom_price return ctx @@ -2043,10 +2054,11 @@ class PartPricing(AjaxView): currency = None - try: - quantity = int(self.request.POST.get('quantity', 1)) - except ValueError: - quantity = 1 + quantity = self.get_quantity() + + # Retain quantity value set by user + form = self.form_class() + form.fields['quantity'].initial = quantity # TODO - How to handle pricing in different currencies? currency = None @@ -2056,7 +2068,7 @@ class PartPricing(AjaxView): 'form_valid': False, } - return self.renderJsonResponse(request, self.form_class(), data=data, context=self.get_pricing(quantity, currency)) + return self.renderJsonResponse(request, form, data=data, context=self.get_pricing(quantity, currency)) class PartParameterTemplateCreate(AjaxCreateView): From ad8d95f2f09456758414180717ec8a6c96b5dfa1 Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 29 Apr 2021 14:35:23 -0400 Subject: [PATCH 2/2] Added two more tests with fractional quantities --- InvenTree/company/tests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py index fa6ce5ed50..ec56503815 100644 --- a/InvenTree/company/tests.py +++ b/InvenTree/company/tests.py @@ -104,6 +104,7 @@ class CompanySimpleTest(TestCase): self.assertEqual(p(100), 350) p = self.acme0002.get_price + self.assertEqual(p(0.5), 3.5) self.assertEqual(p(1), 7) self.assertEqual(p(2), 14) self.assertEqual(p(5), 35) @@ -113,6 +114,7 @@ class CompanySimpleTest(TestCase): def test_part_pricing(self): m2x4 = Part.objects.get(name='M2x4 LPHS') + self.assertEqual(m2x4.get_price_info(5.5), "38.5 - 41.25") self.assertEqual(m2x4.get_price_info(10), "70 - 75") self.assertEqual(m2x4.get_price_info(100), "125 - 350")