mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-20 13:56:30 +00:00
Merge branch 'inventree:master' into price-history
This commit is contained in:
@ -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 = [
|
||||
|
@ -1861,6 +1861,59 @@ class Part(MPTTModel):
|
||||
|
||||
return self.get_descendants(include_self=False)
|
||||
|
||||
@property
|
||||
def can_convert(self):
|
||||
"""
|
||||
Check if this Part can be "converted" to a different variant:
|
||||
|
||||
It can be converted if:
|
||||
|
||||
a) It has non-virtual variant parts underneath it
|
||||
b) It has non-virtual template parts above it
|
||||
c) It has non-virtual sibling variants
|
||||
|
||||
"""
|
||||
|
||||
return self.get_conversion_options().count() > 0
|
||||
|
||||
def get_conversion_options(self):
|
||||
"""
|
||||
Return options for converting this part to a "variant" within the same tree
|
||||
|
||||
a) Variants underneath this one
|
||||
b) Immediate parent
|
||||
c) Siblings
|
||||
"""
|
||||
|
||||
parts = []
|
||||
|
||||
# Child parts
|
||||
children = self.get_descendants(include_self=False)
|
||||
|
||||
for child in children:
|
||||
parts.append(child)
|
||||
|
||||
# Immediate parent
|
||||
if self.variant_of:
|
||||
parts.append(self.variant_of)
|
||||
|
||||
siblings = self.get_siblings(include_self=False)
|
||||
|
||||
for sib in siblings:
|
||||
parts.append(sib)
|
||||
|
||||
filtered_parts = Part.objects.filter(pk__in=[part.pk for part in parts])
|
||||
|
||||
# Ensure this part is not in the queryset, somehow
|
||||
filtered_parts = filtered_parts.exclude(pk=self.pk)
|
||||
|
||||
filtered_parts = filtered_parts.filter(
|
||||
active=True,
|
||||
virtual=False,
|
||||
)
|
||||
|
||||
return filtered_parts
|
||||
|
||||
def get_related_parts(self):
|
||||
""" Return list of tuples for all related parts:
|
||||
- first value is PartRelated object
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -102,6 +102,11 @@
|
||||
</div>
|
||||
|
||||
<div class='info-messages'>
|
||||
{% if part.virtual %}
|
||||
<div class='alert alert-warning alert-block'>
|
||||
{% trans "This is a virtual part" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if part.variant_of %}
|
||||
<div class='alert alert-info alert-block'>
|
||||
{% object_link 'part-variants' part.variant_of.id part.variant_of.full_name as link %}
|
||||
|
@ -2764,7 +2764,7 @@ class PartSalePriceBreakCreate(AjaxCreateView):
|
||||
|
||||
initials['part'] = self.get_part()
|
||||
|
||||
default_currency = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY')
|
||||
default_currency = settings.BASE_CURRENCY
|
||||
currency = CURRENCIES.get(default_currency, None)
|
||||
|
||||
if currency is not None:
|
||||
|
Reference in New Issue
Block a user