From f2e9f58f1b3916f058e42fcbfa29c3b3e0539b41 Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 13 May 2021 15:47:42 -0400 Subject: [PATCH 1/7] Added purchase price range and average to BOM items/view --- InvenTree/part/api.py | 9 ++++++++- InvenTree/part/serializers.py | 9 +++++++++ InvenTree/templates/js/bom.js | 29 +++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index de6ac5f273..659976a0b0 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals from django_filters.rest_framework import DjangoFilterBackend from django.http import JsonResponse -from django.db.models import Q, F, Count +from django.db.models import Q, F, Count, Min, Max, Avg from django.utils.translation import ugettext_lazy as _ from rest_framework import status @@ -877,6 +877,13 @@ class BomList(generics.ListCreateAPIView): else: queryset = queryset.exclude(pk__in=pks) + # Annotate with purchase prices + queryset = queryset.annotate( + purchase_price_min=Min('sub_part__stock_items__purchase_price'), + purchase_price_max=Max('sub_part__stock_items__purchase_price'), + purchase_price_avg=Avg('sub_part__stock_items__purchase_price'), + ) + return queryset filter_backends = [ diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 7ab385249c..8c2d72f43d 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -367,6 +367,12 @@ class BomItemSerializer(InvenTreeModelSerializer): validated = serializers.BooleanField(read_only=True, source='is_line_valid') + purchase_price_min = serializers.FloatField() + + purchase_price_max = serializers.FloatField() + + purchase_price_avg = serializers.FloatField() + def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. # This saves a bunch of database requests @@ -410,6 +416,9 @@ class BomItemSerializer(InvenTreeModelSerializer): 'sub_part_detail', # 'price_range', 'validated', + 'purchase_price_min', + 'purchase_price_max', + 'purchase_price_avg', ] diff --git a/InvenTree/templates/js/bom.js b/InvenTree/templates/js/bom.js index 462db6eba4..c97d8b30e5 100644 --- a/InvenTree/templates/js/bom.js +++ b/InvenTree/templates/js/bom.js @@ -243,6 +243,35 @@ function loadBomTable(table, options) { } }); + cols.push( + { + field: 'purchase_price_range', + title: '{% trans "Purchase Price Range" %}', + searchable: false, + sortable: true, + formatter: function(value, row, index, field) { + var purchase_price_range = 0; + + if (row.purchase_price_min > 0) { + if (row.purchase_price_min >= row.purchase_price_max) { + purchase_price_range = row.purchase_price_min; + } else { + purchase_price_range = row.purchase_price_min + " - " + row.purchase_price_max; + } + } + + return purchase_price_range; + }, + }); + + cols.push( + { + field: 'purchase_price_avg', + title: '{% trans "Purchase Price Average" %}', + searchable: false, + sortable: true, + }); + /* // TODO - Re-introduce the pricing column at a later stage, From 32d0f3039de910b8e3c90164c5c80da84907e541 Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 13 May 2021 16:17:45 -0400 Subject: [PATCH 2/7] Obviously new float fields should be read-only... --- InvenTree/part/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 8c2d72f43d..2316f2f18b 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -367,11 +367,11 @@ class BomItemSerializer(InvenTreeModelSerializer): validated = serializers.BooleanField(read_only=True, source='is_line_valid') - purchase_price_min = serializers.FloatField() + purchase_price_min = serializers.FloatField(read_only=True) - purchase_price_max = serializers.FloatField() + purchase_price_max = serializers.FloatField(read_only=True) - purchase_price_avg = serializers.FloatField() + purchase_price_avg = serializers.FloatField(read_only=True) def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. From 68f5ec8b6a9f54e8273627e2495edd7fbb169179 Mon Sep 17 00:00:00 2001 From: eeintech Date: Thu, 13 May 2021 17:09:52 -0400 Subject: [PATCH 3/7] Added currency conversion --- InvenTree/part/api.py | 42 +++++++++++++++++++++++++++++++++++ InvenTree/part/serializers.py | 7 +++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 659976a0b0..5b82a168cf 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -15,6 +15,10 @@ from rest_framework.response import Response from rest_framework import filters, serializers from rest_framework import generics +from djmoney.money import Money +from djmoney.contrib.exchange.models import convert_money +from djmoney.contrib.exchange.exceptions import MissingRate + from django.conf.urls import url, include from django.urls import reverse @@ -24,6 +28,7 @@ from .models import PartAttachment, PartTestTemplate from .models import PartSellPriceBreak from .models import PartCategoryParameterTemplate +from common.models import InvenTreeSetting from build.models import Build from . import serializers as part_serializers @@ -882,8 +887,45 @@ class BomList(generics.ListCreateAPIView): purchase_price_min=Min('sub_part__stock_items__purchase_price'), purchase_price_max=Max('sub_part__stock_items__purchase_price'), purchase_price_avg=Avg('sub_part__stock_items__purchase_price'), + purchase_price_currency=F('sub_part__stock_items__purchase_price_currency'), ) + # Convert prices to default currency (using backend conversion rates) + for item in queryset: + # Get default currency from settings + default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + + if default_currency: + if item.purchase_price_min: + # Convert minimum + try: + # Get adjusted price + purchase_price_adjusted = convert_money(Money(item.purchase_price_min, item.purchase_price_currency), default_currency) + # Update queryset + item.purchase_price_min = purchase_price_adjusted + except MissingRate: + pass + + if item.purchase_price_max: + # Convert maximum + try: + # Get adjusted price + purchase_price_adjusted = convert_money(Money(item.purchase_price_max, item.purchase_price_currency), default_currency) + # Update queryset + item.purchase_price_max = purchase_price_adjusted + except MissingRate: + pass + + if item.purchase_price_avg: + # Convert average + try: + # Get adjusted price + purchase_price_adjusted = convert_money(Money(item.purchase_price_avg, item.purchase_price_currency), default_currency) + # Update queryset + item.purchase_price_avg = purchase_price_adjusted + except MissingRate: + pass + return queryset filter_backends = [ diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 2316f2f18b..096d072981 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -12,6 +12,7 @@ from InvenTree.serializers import (InvenTreeAttachmentSerializerField, from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus from rest_framework import serializers from sql_util.utils import SubqueryCount, SubquerySum +from djmoney.contrib.django_rest_framework import MoneyField from stock.models import StockItem from .models import (BomItem, Part, PartAttachment, PartCategory, @@ -367,11 +368,11 @@ class BomItemSerializer(InvenTreeModelSerializer): validated = serializers.BooleanField(read_only=True, source='is_line_valid') - purchase_price_min = serializers.FloatField(read_only=True) + purchase_price_min = MoneyField(max_digits=10, decimal_places=4, read_only=True) - purchase_price_max = serializers.FloatField(read_only=True) + purchase_price_max = MoneyField(max_digits=10, decimal_places=4, read_only=True) - purchase_price_avg = serializers.FloatField(read_only=True) + purchase_price_avg = MoneyField(max_digits=10, decimal_places=4, read_only=True) def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. From 1940fd5199f34e91b6df6b17dc8a8adbd909efbf Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 14 May 2021 16:16:23 -0400 Subject: [PATCH 4/7] Now processing currencies --- InvenTree/part/api.py | 70 ++++++++++++++++++++--------------- InvenTree/part/serializers.py | 43 +++++++++++++++++++-- InvenTree/templates/js/bom.js | 13 ------- 3 files changed, 79 insertions(+), 47 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 5b82a168cf..065eca30c6 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -887,44 +887,54 @@ class BomList(generics.ListCreateAPIView): purchase_price_min=Min('sub_part__stock_items__purchase_price'), purchase_price_max=Max('sub_part__stock_items__purchase_price'), purchase_price_avg=Avg('sub_part__stock_items__purchase_price'), - purchase_price_currency=F('sub_part__stock_items__purchase_price_currency'), ) - # Convert prices to default currency (using backend conversion rates) - for item in queryset: + # Get values for currencies + currencies = queryset.annotate( + purchase_price_currency=F('sub_part__stock_items__purchase_price_currency'), + ).values('pk', 'sub_part', 'purchase_price_currency') + + def convert_price(price, currency, decimal_places=4): + """ Convert price field, returns Money field """ + + price_adjusted = None + # Get default currency from settings default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') - if default_currency: - if item.purchase_price_min: - # Convert minimum + if price: + if currency and default_currency: try: # Get adjusted price - purchase_price_adjusted = convert_money(Money(item.purchase_price_min, item.purchase_price_currency), default_currency) - # Update queryset - item.purchase_price_min = purchase_price_adjusted + price_adjusted = convert_money(Money(price, currency), default_currency) except MissingRate: - pass - - if item.purchase_price_max: - # Convert maximum - try: - # Get adjusted price - purchase_price_adjusted = convert_money(Money(item.purchase_price_max, item.purchase_price_currency), default_currency) - # Update queryset - item.purchase_price_max = purchase_price_adjusted - except MissingRate: - pass - - if item.purchase_price_avg: - # Convert average - try: - # Get adjusted price - purchase_price_adjusted = convert_money(Money(item.purchase_price_avg, item.purchase_price_currency), default_currency) - # Update queryset - item.purchase_price_avg = purchase_price_adjusted - except MissingRate: - pass + # No conversion rate set + price_adjusted = Money(price, currency) + else: + # Currency exists + if currency: + price_adjusted = Money(price, currency) + # Default currency exists + if default_currency: + price_adjusted = Money(price, default_currency) + + if price_adjusted and decimal_places: + price_adjusted.decimal_places = decimal_places + + return price_adjusted + + # Convert prices to default currency (using backend conversion rates) + for bom_item in queryset: + # Find associated currency (select first found) + purchase_price_currency = None + for currency_item in currencies: + if currency_item['pk'] == bom_item.pk and currency_item['sub_part'] == bom_item.sub_part: + purchase_price_currency = currency_item['purchase_price_currency'] + break + # Convert prices + bom_item.purchase_price_min = convert_price(bom_item.purchase_price_min, purchase_price_currency) + bom_item.purchase_price_max = convert_price(bom_item.purchase_price_max, purchase_price_currency) + bom_item.purchase_price_avg = convert_price(bom_item.purchase_price_avg, purchase_price_currency) return queryset diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 096d072981..868997915b 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -368,11 +368,13 @@ class BomItemSerializer(InvenTreeModelSerializer): validated = serializers.BooleanField(read_only=True, source='is_line_valid') - purchase_price_min = MoneyField(max_digits=10, decimal_places=4, read_only=True) + purchase_price_min = MoneyField(max_digits=10, decimal_places=6, read_only=True) - purchase_price_max = MoneyField(max_digits=10, decimal_places=4, read_only=True) - - purchase_price_avg = MoneyField(max_digits=10, decimal_places=4, read_only=True) + purchase_price_max = MoneyField(max_digits=10, decimal_places=6, read_only=True) + + purchase_price_avg = serializers.SerializerMethodField() + + purchase_price_range = serializers.SerializerMethodField() def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. @@ -401,6 +403,38 @@ class BomItemSerializer(InvenTreeModelSerializer): queryset = queryset.prefetch_related('sub_part__supplier_parts__pricebreaks') return queryset + def get_purchase_price_range(self, obj): + """ Return purchase price range """ + + if obj.purchase_price_min and not obj.purchase_price_max: + # Get price range + purchase_price_range = str(obj.purchase_price_max) + elif not obj.purchase_price_min and obj.purchase_price_max: + # Get price range + purchase_price_range = str(obj.purchase_price_max) + elif obj.purchase_price_min and obj.purchase_price_max: + # Get price range + if obj.purchase_price_min >= obj.purchase_price_max: + # If min > max: use min only + purchase_price_range = str(obj.purchase_price_min) + else: + purchase_price_range = str(obj.purchase_price_min) + " - " + str(obj.purchase_price_max) + else: + purchase_price_range = '-' + + return purchase_price_range + + def get_purchase_price_avg(self, obj): + """ Return purchase price average """ + + if obj.purchase_price_avg: + # Get string representation of price average + purchase_price_avg = str(obj.purchase_price_avg) + else: + purchase_price_avg = '-' + + return purchase_price_avg + class Meta: model = BomItem fields = [ @@ -420,6 +454,7 @@ class BomItemSerializer(InvenTreeModelSerializer): 'purchase_price_min', 'purchase_price_max', 'purchase_price_avg', + 'purchase_price_range', ] diff --git a/InvenTree/templates/js/bom.js b/InvenTree/templates/js/bom.js index c97d8b30e5..e35a51d8bd 100644 --- a/InvenTree/templates/js/bom.js +++ b/InvenTree/templates/js/bom.js @@ -249,19 +249,6 @@ function loadBomTable(table, options) { title: '{% trans "Purchase Price Range" %}', searchable: false, sortable: true, - formatter: function(value, row, index, field) { - var purchase_price_range = 0; - - if (row.purchase_price_min > 0) { - if (row.purchase_price_min >= row.purchase_price_max) { - purchase_price_range = row.purchase_price_min; - } else { - purchase_price_range = row.purchase_price_min + " - " + row.purchase_price_max; - } - } - - return purchase_price_range; - }, }); cols.push( From 274eb51e48d7e8e4cea773b3223f33582791c869 Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 14 May 2021 16:29:55 -0400 Subject: [PATCH 5/7] Added read_only args --- InvenTree/part/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 868997915b..b6e24fd199 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -372,9 +372,9 @@ class BomItemSerializer(InvenTreeModelSerializer): purchase_price_max = MoneyField(max_digits=10, decimal_places=6, read_only=True) - purchase_price_avg = serializers.SerializerMethodField() + purchase_price_avg = serializers.SerializerMethodField(read_only=True) - purchase_price_range = serializers.SerializerMethodField() + purchase_price_range = serializers.SerializerMethodField(read_only=True) def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. From e9f41a83576f26b08e3f13b4c7e380184752b224 Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 14 May 2021 16:38:30 -0400 Subject: [PATCH 6/7] Currency finding fix --- InvenTree/part/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 065eca30c6..5bdd572145 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -928,7 +928,7 @@ class BomList(generics.ListCreateAPIView): # Find associated currency (select first found) purchase_price_currency = None for currency_item in currencies: - if currency_item['pk'] == bom_item.pk and currency_item['sub_part'] == bom_item.sub_part: + if currency_item['pk'] == bom_item.pk and currency_item['sub_part'] == bom_item.sub_part.pk: purchase_price_currency = currency_item['purchase_price_currency'] break # Convert prices From 5ce262172d77d133ee591751adab63eb86fd663f Mon Sep 17 00:00:00 2001 From: eeintech Date: Fri, 14 May 2021 16:59:59 -0400 Subject: [PATCH 7/7] Fixed bom_item unit test --- InvenTree/part/serializers.py | 39 ++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index b6e24fd199..04e0b7a119 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -372,9 +372,9 @@ class BomItemSerializer(InvenTreeModelSerializer): purchase_price_max = MoneyField(max_digits=10, decimal_places=6, read_only=True) - purchase_price_avg = serializers.SerializerMethodField(read_only=True) + purchase_price_avg = serializers.SerializerMethodField() - purchase_price_range = serializers.SerializerMethodField(read_only=True) + purchase_price_range = serializers.SerializerMethodField() def __init__(self, *args, **kwargs): # part_detail and sub_part_detail serializers are only included if requested. @@ -406,19 +406,29 @@ class BomItemSerializer(InvenTreeModelSerializer): def get_purchase_price_range(self, obj): """ Return purchase price range """ - if obj.purchase_price_min and not obj.purchase_price_max: + try: + purchase_price_min = obj.purchase_price_min + except AttributeError: + return None + + try: + purchase_price_max = obj.purchase_price_max + except AttributeError: + return None + + if purchase_price_min and not purchase_price_max: # Get price range - purchase_price_range = str(obj.purchase_price_max) - elif not obj.purchase_price_min and obj.purchase_price_max: + purchase_price_range = str(purchase_price_max) + elif not purchase_price_min and purchase_price_max: # Get price range - purchase_price_range = str(obj.purchase_price_max) - elif obj.purchase_price_min and obj.purchase_price_max: + purchase_price_range = str(purchase_price_max) + elif purchase_price_min and purchase_price_max: # Get price range - if obj.purchase_price_min >= obj.purchase_price_max: + if purchase_price_min >= purchase_price_max: # If min > max: use min only - purchase_price_range = str(obj.purchase_price_min) + purchase_price_range = str(purchase_price_min) else: - purchase_price_range = str(obj.purchase_price_min) + " - " + str(obj.purchase_price_max) + purchase_price_range = str(purchase_price_min) + " - " + str(purchase_price_max) else: purchase_price_range = '-' @@ -427,9 +437,14 @@ class BomItemSerializer(InvenTreeModelSerializer): def get_purchase_price_avg(self, obj): """ Return purchase price average """ - if obj.purchase_price_avg: + try: + purchase_price_avg = obj.purchase_price_avg + except AttributeError: + return None + + if purchase_price_avg: # Get string representation of price average - purchase_price_avg = str(obj.purchase_price_avg) + purchase_price_avg = str(purchase_price_avg) else: purchase_price_avg = '-'