Merge pull request #1581 from eeintech/bom_items_purchase_price

Added purchase price range and average to BOM items/view
This commit is contained in:
Oliver 2021-05-28 12:50:31 +10:00 committed by GitHub
commit 70e13e4cae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 136 additions and 1 deletions

View File

@ -7,7 +7,7 @@ from __future__ import unicode_literals
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from django.http import JsonResponse 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 django.utils.translation import ugettext_lazy as _
from rest_framework import status 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 filters, serializers
from rest_framework import generics 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.conf.urls import url, include
from django.urls import reverse from django.urls import reverse
@ -24,6 +28,7 @@ from .models import PartAttachment, PartTestTemplate
from .models import PartSellPriceBreak from .models import PartSellPriceBreak
from .models import PartCategoryParameterTemplate from .models import PartCategoryParameterTemplate
from common.models import InvenTreeSetting
from build.models import Build from build.models import Build
from . import serializers as part_serializers from . import serializers as part_serializers
@ -877,6 +882,60 @@ class BomList(generics.ListCreateAPIView):
else: else:
queryset = queryset.exclude(pk__in=pks) 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 return queryset
filter_backends = [ filter_backends = [

View File

@ -12,6 +12,7 @@ from InvenTree.serializers import (InvenTreeAttachmentSerializerField,
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus
from rest_framework import serializers from rest_framework import serializers
from sql_util.utils import SubqueryCount, SubquerySum from sql_util.utils import SubqueryCount, SubquerySum
from djmoney.contrib.django_rest_framework import MoneyField
from stock.models import StockItem from stock.models import StockItem
from .models import (BomItem, Part, PartAttachment, PartCategory, from .models import (BomItem, Part, PartAttachment, PartCategory,
@ -367,6 +368,14 @@ class BomItemSerializer(InvenTreeModelSerializer):
validated = serializers.BooleanField(read_only=True, source='is_line_valid') 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): def __init__(self, *args, **kwargs):
# part_detail and sub_part_detail serializers are only included if requested. # part_detail and sub_part_detail serializers are only included if requested.
# This saves a bunch of database requests # This saves a bunch of database requests
@ -394,6 +403,53 @@ class BomItemSerializer(InvenTreeModelSerializer):
queryset = queryset.prefetch_related('sub_part__supplier_parts__pricebreaks') queryset = queryset.prefetch_related('sub_part__supplier_parts__pricebreaks')
return queryset 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: class Meta:
model = BomItem model = BomItem
fields = [ fields = [
@ -410,6 +466,10 @@ class BomItemSerializer(InvenTreeModelSerializer):
'sub_part_detail', 'sub_part_detail',
# 'price_range', # 'price_range',
'validated', 'validated',
'purchase_price_min',
'purchase_price_max',
'purchase_price_avg',
'purchase_price_range',
] ]

View File

@ -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, // TODO - Re-introduce the pricing column at a later stage,