From ab4eff19d392ed3322d42a620e2ea66908fa4b9c Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 21 Feb 2025 19:09:57 +1100 Subject: [PATCH] [UI] Pricing chart fixes (#9119) * Fix default values for pricing override * Fix broken calculation for sale pricing - Was previously excluding COMPLETED orders * Fix for PricingOverviewPanel * Fix for InvenTreeMoneySerializer - Numbers should be represented as numbers! * Front-end wrangling too * Fix unit test --- .../InvenTree/InvenTree/serializers.py | 12 ++- src/backend/InvenTree/part/models.py | 6 +- src/backend/InvenTree/stock/test_api.py | 4 +- .../pages/part/pricing/BomPricingPanel.tsx | 2 +- .../part/pricing/PricingOverviewPanel.tsx | 85 +++++++++++++------ .../part/pricing/PurchaseHistoryPanel.tsx | 4 +- .../pages/part/pricing/SaleHistoryPanel.tsx | 2 +- .../part/pricing/SupplierPricingPanel.tsx | 2 +- .../part/pricing/VariantPricingPanel.tsx | 6 +- .../purchasing/SupplierPriceBreakTable.tsx | 2 +- 10 files changed, 86 insertions(+), 39 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/serializers.py b/src/backend/InvenTree/InvenTree/serializers.py index 08b8865475..35c9e0301e 100644 --- a/src/backend/InvenTree/InvenTree/serializers.py +++ b/src/backend/InvenTree/InvenTree/serializers.py @@ -45,6 +45,12 @@ class InvenTreeMoneySerializer(MoneyField): super().__init__(*args, **kwargs) + def to_representation(self, obj): + """Convert the Money object to a decimal value for representation.""" + val = super().to_representation(obj) + + return float(val) + def get_value(self, data): """Test that the returned amount is a valid Decimal.""" amount = super(DecimalField, self).get_value(data) @@ -73,7 +79,11 @@ class InvenTreeMoneySerializer(MoneyField): ): return Money(amount, currency) - return amount + try: + fp_amount = float(amount) + return fp_amount + except Exception: + return amount class InvenTreeCurrencySerializer(serializers.ChoiceField): diff --git a/src/backend/InvenTree/part/models.py b/src/backend/InvenTree/part/models.py index 6a2f94e958..19e59200ab 100644 --- a/src/backend/InvenTree/part/models.py +++ b/src/backend/InvenTree/part/models.py @@ -62,7 +62,6 @@ from order import models as OrderModels from order.status_codes import ( PurchaseOrderStatus, PurchaseOrderStatusGroups, - SalesOrderStatus, SalesOrderStatusGroups, ) from stock import models as StockModels @@ -3088,9 +3087,12 @@ class PartPricing(common.models.MetaMixin): min_sell_history = None max_sell_history = None + # Calculate sale price history too + parts = self.part.get_descendants(include_self=True) + # Find all line items for shipped sales orders which reference this part line_items = OrderModels.SalesOrderLineItem.objects.filter( - order__status=SalesOrderStatus.SHIPPED, part=self.part + order__status__in=SalesOrderStatusGroups.COMPLETE, part__in=parts ) # Exclude line items which do not have associated pricing data diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index 8a4f5dec2a..487cc5614d 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -1515,13 +1515,13 @@ class StockItemTest(StockAPITestCase): data = self.get(url, expected_code=200).data # Check fixture values - self.assertEqual(data['purchase_price'], '123.000000') + self.assertAlmostEqual(data['purchase_price'], 123, 3) self.assertEqual(data['purchase_price_currency'], 'AUD') # Update just the amount data = self.patch(url, {'purchase_price': 456}, expected_code=200).data - self.assertEqual(data['purchase_price'], '456.000000') + self.assertAlmostEqual(data['purchase_price'], 456, 3) self.assertEqual(data['purchase_price_currency'], 'AUD') # Update the currency diff --git a/src/frontend/src/pages/part/pricing/BomPricingPanel.tsx b/src/frontend/src/pages/part/pricing/BomPricingPanel.tsx index e628b838fd..f7aa623254 100644 --- a/src/frontend/src/pages/part/pricing/BomPricingPanel.tsx +++ b/src/frontend/src/pages/part/pricing/BomPricingPanel.tsx @@ -68,7 +68,7 @@ function BomPieChart({ return { // Note: Replace '.' in name to avoid issues with tooltip name: entry?.name?.replace('.', '') ?? '', - value: entry?.total_price_max, + value: Number.parseFloat(entry?.total_price_max), color: `${CHART_COLORS[index % CHART_COLORS.length]}.5` }; }) ?? [] diff --git a/src/frontend/src/pages/part/pricing/PricingOverviewPanel.tsx b/src/frontend/src/pages/part/pricing/PricingOverviewPanel.tsx index db41175b4a..6fcd515a84 100644 --- a/src/frontend/src/pages/part/pricing/PricingOverviewPanel.tsx +++ b/src/frontend/src/pages/part/pricing/PricingOverviewPanel.tsx @@ -26,6 +26,7 @@ import { type ReactNode, useCallback, useMemo } from 'react'; import { api } from '../../../App'; import { tooltipFormatter } from '../../../components/charts/tooltipFormatter'; +import type { ApiFormFieldSet } from '../../../components/forms/fields/ApiFormField'; import { EditItemAction, OptionsActionDropdown @@ -35,12 +36,14 @@ import { ApiEndpoints } from '../../../enums/ApiEndpoints'; import { InvenTreeIcon } from '../../../functions/icons'; import { useEditApiFormModal } from '../../../hooks/UseForm'; import { apiUrl } from '../../../states/ApiState'; +import { useGlobalSettingsState } from '../../../states/SettingsState'; import { panelOptions } from '../PartPricingPanel'; interface PricingOverviewEntry { icon: ReactNode; name: panelOptions; title: string; + valid: boolean; min_value: number | null | undefined; max_value: number | null | undefined; visible?: boolean; @@ -58,6 +61,8 @@ export default function PricingOverviewPanel({ pricingQuery: UseQueryResult; doNavigation: (panel: panelOptions) => void; }>): ReactNode { + const globalSettings = useGlobalSettingsState(); + const refreshPricing = useCallback(() => { const url = apiUrl(ApiEndpoints.part_pricing, part.pk); @@ -99,19 +104,29 @@ export default function PricingOverviewPanel({ }); }, [part]); - const editPricing = useEditApiFormModal({ - title: t`Edit Pricing`, - url: apiUrl(ApiEndpoints.part_pricing, part.pk), - fields: { + const pricingFields: ApiFormFieldSet = useMemo(() => { + return { override_min: {}, - override_min_currency: {}, + override_min_currency: { + default: + globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY') ?? 'USD' + }, override_max: {}, - override_max_currency: {}, + override_max_currency: { + default: + globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY') ?? 'USD' + }, update: { hidden: true, value: true } - }, + }; + }, [globalSettings]); + + const editPricing = useEditApiFormModal({ + title: t`Edit Pricing`, + url: apiUrl(ApiEndpoints.part_pricing, part.pk), + fields: pricingFields, onFormSuccess: () => { pricingQuery.refetch(); } @@ -172,67 +187,85 @@ export default function PricingOverviewPanel({ name: panelOptions.override, title: t`Override Pricing`, icon: , - min_value: pricing?.override_min, - max_value: pricing?.override_max + min_value: Number.parseFloat(pricing?.override_min), + max_value: Number.parseFloat(pricing?.override_max), + valid: pricing?.override_min != null && pricing?.override_max != null }, { name: panelOptions.overall, title: t`Overall Pricing`, icon: , - min_value: pricing?.overall_min, - max_value: pricing?.overall_max + min_value: Number.parseFloat(pricing?.overall_min), + max_value: Number.parseFloat(pricing?.overall_max), + valid: pricing?.overall_min != null && pricing?.overall_max != null }, { name: panelOptions.internal, title: t`Internal Pricing`, icon: , - min_value: pricing?.internal_cost_min, - max_value: pricing?.internal_cost_max + min_value: Number.parseFloat(pricing?.internal_cost_min), + max_value: Number.parseFloat(pricing?.internal_cost_max), + valid: + pricing?.internal_cost_min != null && + pricing?.internal_cost_max != null }, { name: panelOptions.bom, title: t`BOM Pricing`, icon: , - min_value: pricing?.bom_cost_min, - max_value: pricing?.bom_cost_max + min_value: Number.parseFloat(pricing?.bom_cost_min), + max_value: Number.parseFloat(pricing?.bom_cost_max), + valid: pricing?.bom_cost_min != null && pricing?.bom_cost_max != null }, { name: panelOptions.purchase, title: t`Purchase Pricing`, icon: , - min_value: pricing?.purchase_cost_min, - max_value: pricing?.purchase_cost_max + min_value: Number.parseFloat(pricing?.purchase_cost_min), + max_value: Number.parseFloat(pricing?.purchase_cost_max), + valid: + pricing?.purchase_cost_min != null && + pricing?.purchase_cost_max != null }, { name: panelOptions.supplier, title: t`Supplier Pricing`, icon: , - min_value: pricing?.supplier_price_min, - max_value: pricing?.supplier_price_max + min_value: Number.parseFloat(pricing?.supplier_price_min), + max_value: Number.parseFloat(pricing?.supplier_price_max), + valid: + pricing?.supplier_price_min != null && + pricing?.supplier_price_max != null }, { name: panelOptions.variant, title: t`Variant Pricing`, icon: , - min_value: pricing?.variant_cost_min, - max_value: pricing?.variant_cost_max + min_value: Number.parseFloat(pricing?.variant_cost_min), + max_value: Number.parseFloat(pricing?.variant_cost_max), + valid: + pricing?.variant_cost_min != null && pricing?.variant_cost_max != null }, { name: panelOptions.sale_pricing, title: t`Sale Pricing`, icon: , - min_value: pricing?.sale_price_min, - max_value: pricing?.sale_price_max + min_value: Number.parseFloat(pricing?.sale_price_min), + max_value: Number.parseFloat(pricing?.sale_price_max), + valid: + pricing?.sale_price_min != null && pricing?.sale_price_max != null }, { name: panelOptions.sale_history, title: t`Sale History`, icon: , - min_value: pricing?.sale_history_min, - max_value: pricing?.sale_history_max + min_value: Number.parseFloat(pricing?.sale_history_min), + max_value: Number.parseFloat(pricing?.sale_history_max), + valid: + pricing?.sale_history_min != null && pricing?.sale_history_max != null } ].filter((entry) => { - return !(entry.min_value == null || entry.max_value == null); + return entry.valid; }); }, [part, pricing]); diff --git a/src/frontend/src/pages/part/pricing/PurchaseHistoryPanel.tsx b/src/frontend/src/pages/part/pricing/PurchaseHistoryPanel.tsx index aeec576b76..4dab1bf33d 100644 --- a/src/frontend/src/pages/part/pricing/PurchaseHistoryPanel.tsx +++ b/src/frontend/src/pages/part/pricing/PurchaseHistoryPanel.tsx @@ -21,7 +21,7 @@ export default function PurchaseHistoryPanel({ const calculateUnitPrice = useCallback((record: any) => { const pack_quantity = record?.supplier_part_detail?.pack_quantity_native ?? 1; - const unit_price = record.purchase_price / pack_quantity; + const unit_price = Number.parseFloat(record.purchase_price) / pack_quantity; return unit_price; }, []); @@ -95,7 +95,7 @@ export default function PurchaseHistoryPanel({ return table.records.map((record: any) => { return { quantity: record.quantity, - purchase_price: record.purchase_price, + purchase_price: Number.parseFloat(record.purchase_price), unit_price: calculateUnitPrice(record), name: record.order_detail.reference }; diff --git a/src/frontend/src/pages/part/pricing/SaleHistoryPanel.tsx b/src/frontend/src/pages/part/pricing/SaleHistoryPanel.tsx index 442686fb44..7412cc271f 100644 --- a/src/frontend/src/pages/part/pricing/SaleHistoryPanel.tsx +++ b/src/frontend/src/pages/part/pricing/SaleHistoryPanel.tsx @@ -57,7 +57,7 @@ export default function SaleHistoryPanel({ return table.records.map((record: any) => { return { name: record.order_detail.reference, - sale_price: record.sale_price + sale_price: Number.parseFloat(record.sale_price) }; }); }, [table.records]); diff --git a/src/frontend/src/pages/part/pricing/SupplierPricingPanel.tsx b/src/frontend/src/pages/part/pricing/SupplierPricingPanel.tsx index 4a3b7a2718..63ad4d3feb 100644 --- a/src/frontend/src/pages/part/pricing/SupplierPricingPanel.tsx +++ b/src/frontend/src/pages/part/pricing/SupplierPricingPanel.tsx @@ -36,7 +36,7 @@ export default function SupplierPricingPanel({ table.records?.map((record: any) => { return { quantity: record.quantity, - supplier_price: record.price, + supplier_price: Number.parseFloat(record.price), unit_price: calculateSupplierPartUnitPrice(record), name: record.part_detail?.SKU }; diff --git a/src/frontend/src/pages/part/pricing/VariantPricingPanel.tsx b/src/frontend/src/pages/part/pricing/VariantPricingPanel.tsx index 69ed90d60e..ce9ef6ae73 100644 --- a/src/frontend/src/pages/part/pricing/VariantPricingPanel.tsx +++ b/src/frontend/src/pages/part/pricing/VariantPricingPanel.tsx @@ -63,8 +63,10 @@ export default function VariantPricingPanel({ return { part: variant, name: variant.full_name, - pmin: variant.pricing_min ?? variant.pricing_max ?? 0, - pmax: variant.pricing_max ?? variant.pricing_min ?? 0 + pmin: Number.parseFloat( + variant.pricing_min ?? variant.pricing_max ?? 0 + ), + pmax: Number.parseFloat(variant.pricing_max ?? variant.pricing_min ?? 0) }; }); diff --git a/src/frontend/src/tables/purchasing/SupplierPriceBreakTable.tsx b/src/frontend/src/tables/purchasing/SupplierPriceBreakTable.tsx index b0085a04a1..e3f195bb0f 100644 --- a/src/frontend/src/tables/purchasing/SupplierPriceBreakTable.tsx +++ b/src/frontend/src/tables/purchasing/SupplierPriceBreakTable.tsx @@ -24,7 +24,7 @@ import { type RowAction, RowDeleteAction, RowEditAction } from '../RowActions'; export function calculateSupplierPartUnitPrice(record: any) { const pack_quantity = record?.part_detail?.pack_quantity_native ?? 1; - const unit_price = record.price / pack_quantity; + const unit_price = Number.parseFloat(record.price) / pack_quantity; return unit_price; }