diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index de6ac5f273..5bdd572145 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 @@ -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 @@ -877,6 +882,60 @@ 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'), + ) + + # 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 price: + if currency and default_currency: + try: + # Get adjusted price + price_adjusted = convert_money(Money(price, currency), default_currency) + except MissingRate: + # 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.pk: + 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 filter_backends = [ diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 7ab385249c..04e0b7a119 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,6 +368,14 @@ class BomItemSerializer(InvenTreeModelSerializer): validated = serializers.BooleanField(read_only=True, source='is_line_valid') + purchase_price_min = MoneyField(max_digits=10, decimal_places=6, 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. # This saves a bunch of database requests @@ -394,6 +403,53 @@ 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 """ + + 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(purchase_price_max) + elif not purchase_price_min and purchase_price_max: + # Get price range + purchase_price_range = str(purchase_price_max) + elif purchase_price_min and purchase_price_max: + # Get price range + if purchase_price_min >= purchase_price_max: + # If min > max: use min only + purchase_price_range = str(purchase_price_min) + else: + purchase_price_range = str(purchase_price_min) + " - " + str(purchase_price_max) + else: + purchase_price_range = '-' + + return purchase_price_range + + def get_purchase_price_avg(self, obj): + """ Return purchase price average """ + + 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(purchase_price_avg) + else: + purchase_price_avg = '-' + + return purchase_price_avg + class Meta: model = BomItem fields = [ @@ -410,6 +466,10 @@ class BomItemSerializer(InvenTreeModelSerializer): 'sub_part_detail', # 'price_range', 'validated', + '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 462db6eba4..e35a51d8bd 100644 --- a/InvenTree/templates/js/bom.js +++ b/InvenTree/templates/js/bom.js @@ -243,6 +243,22 @@ function loadBomTable(table, options) { } }); + cols.push( + { + field: 'purchase_price_range', + title: '{% trans "Purchase Price Range" %}', + searchable: false, + sortable: true, + }); + + 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,