2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 11:36:44 +00:00

[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
This commit is contained in:
Oliver 2025-02-21 19:09:57 +11:00 committed by GitHub
parent 15ad62494f
commit ab4eff19d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 86 additions and 39 deletions

View File

@ -45,6 +45,12 @@ class InvenTreeMoneySerializer(MoneyField):
super().__init__(*args, **kwargs) 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): def get_value(self, data):
"""Test that the returned amount is a valid Decimal.""" """Test that the returned amount is a valid Decimal."""
amount = super(DecimalField, self).get_value(data) amount = super(DecimalField, self).get_value(data)
@ -73,7 +79,11 @@ class InvenTreeMoneySerializer(MoneyField):
): ):
return Money(amount, currency) return Money(amount, currency)
return amount try:
fp_amount = float(amount)
return fp_amount
except Exception:
return amount
class InvenTreeCurrencySerializer(serializers.ChoiceField): class InvenTreeCurrencySerializer(serializers.ChoiceField):

View File

@ -62,7 +62,6 @@ from order import models as OrderModels
from order.status_codes import ( from order.status_codes import (
PurchaseOrderStatus, PurchaseOrderStatus,
PurchaseOrderStatusGroups, PurchaseOrderStatusGroups,
SalesOrderStatus,
SalesOrderStatusGroups, SalesOrderStatusGroups,
) )
from stock import models as StockModels from stock import models as StockModels
@ -3088,9 +3087,12 @@ class PartPricing(common.models.MetaMixin):
min_sell_history = None min_sell_history = None
max_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 # Find all line items for shipped sales orders which reference this part
line_items = OrderModels.SalesOrderLineItem.objects.filter( 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 # Exclude line items which do not have associated pricing data

View File

@ -1515,13 +1515,13 @@ class StockItemTest(StockAPITestCase):
data = self.get(url, expected_code=200).data data = self.get(url, expected_code=200).data
# Check fixture values # Check fixture values
self.assertEqual(data['purchase_price'], '123.000000') self.assertAlmostEqual(data['purchase_price'], 123, 3)
self.assertEqual(data['purchase_price_currency'], 'AUD') self.assertEqual(data['purchase_price_currency'], 'AUD')
# Update just the amount # Update just the amount
data = self.patch(url, {'purchase_price': 456}, expected_code=200).data 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') self.assertEqual(data['purchase_price_currency'], 'AUD')
# Update the currency # Update the currency

View File

@ -68,7 +68,7 @@ function BomPieChart({
return { return {
// Note: Replace '.' in name to avoid issues with tooltip // Note: Replace '.' in name to avoid issues with tooltip
name: entry?.name?.replace('.', '') ?? '', name: entry?.name?.replace('.', '') ?? '',
value: entry?.total_price_max, value: Number.parseFloat(entry?.total_price_max),
color: `${CHART_COLORS[index % CHART_COLORS.length]}.5` color: `${CHART_COLORS[index % CHART_COLORS.length]}.5`
}; };
}) ?? [] }) ?? []

View File

@ -26,6 +26,7 @@ import { type ReactNode, useCallback, useMemo } from 'react';
import { api } from '../../../App'; import { api } from '../../../App';
import { tooltipFormatter } from '../../../components/charts/tooltipFormatter'; import { tooltipFormatter } from '../../../components/charts/tooltipFormatter';
import type { ApiFormFieldSet } from '../../../components/forms/fields/ApiFormField';
import { import {
EditItemAction, EditItemAction,
OptionsActionDropdown OptionsActionDropdown
@ -35,12 +36,14 @@ import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { InvenTreeIcon } from '../../../functions/icons'; import { InvenTreeIcon } from '../../../functions/icons';
import { useEditApiFormModal } from '../../../hooks/UseForm'; import { useEditApiFormModal } from '../../../hooks/UseForm';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useGlobalSettingsState } from '../../../states/SettingsState';
import { panelOptions } from '../PartPricingPanel'; import { panelOptions } from '../PartPricingPanel';
interface PricingOverviewEntry { interface PricingOverviewEntry {
icon: ReactNode; icon: ReactNode;
name: panelOptions; name: panelOptions;
title: string; title: string;
valid: boolean;
min_value: number | null | undefined; min_value: number | null | undefined;
max_value: number | null | undefined; max_value: number | null | undefined;
visible?: boolean; visible?: boolean;
@ -58,6 +61,8 @@ export default function PricingOverviewPanel({
pricingQuery: UseQueryResult; pricingQuery: UseQueryResult;
doNavigation: (panel: panelOptions) => void; doNavigation: (panel: panelOptions) => void;
}>): ReactNode { }>): ReactNode {
const globalSettings = useGlobalSettingsState();
const refreshPricing = useCallback(() => { const refreshPricing = useCallback(() => {
const url = apiUrl(ApiEndpoints.part_pricing, part.pk); const url = apiUrl(ApiEndpoints.part_pricing, part.pk);
@ -99,19 +104,29 @@ export default function PricingOverviewPanel({
}); });
}, [part]); }, [part]);
const editPricing = useEditApiFormModal({ const pricingFields: ApiFormFieldSet = useMemo(() => {
title: t`Edit Pricing`, return {
url: apiUrl(ApiEndpoints.part_pricing, part.pk),
fields: {
override_min: {}, override_min: {},
override_min_currency: {}, override_min_currency: {
default:
globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY') ?? 'USD'
},
override_max: {}, override_max: {},
override_max_currency: {}, override_max_currency: {
default:
globalSettings.getSetting('INVENTREE_DEFAULT_CURRENCY') ?? 'USD'
},
update: { update: {
hidden: true, hidden: true,
value: true value: true
} }
}, };
}, [globalSettings]);
const editPricing = useEditApiFormModal({
title: t`Edit Pricing`,
url: apiUrl(ApiEndpoints.part_pricing, part.pk),
fields: pricingFields,
onFormSuccess: () => { onFormSuccess: () => {
pricingQuery.refetch(); pricingQuery.refetch();
} }
@ -172,67 +187,85 @@ export default function PricingOverviewPanel({
name: panelOptions.override, name: panelOptions.override,
title: t`Override Pricing`, title: t`Override Pricing`,
icon: <IconExclamationCircle />, icon: <IconExclamationCircle />,
min_value: pricing?.override_min, min_value: Number.parseFloat(pricing?.override_min),
max_value: pricing?.override_max max_value: Number.parseFloat(pricing?.override_max),
valid: pricing?.override_min != null && pricing?.override_max != null
}, },
{ {
name: panelOptions.overall, name: panelOptions.overall,
title: t`Overall Pricing`, title: t`Overall Pricing`,
icon: <IconReportAnalytics />, icon: <IconReportAnalytics />,
min_value: pricing?.overall_min, min_value: Number.parseFloat(pricing?.overall_min),
max_value: pricing?.overall_max max_value: Number.parseFloat(pricing?.overall_max),
valid: pricing?.overall_min != null && pricing?.overall_max != null
}, },
{ {
name: panelOptions.internal, name: panelOptions.internal,
title: t`Internal Pricing`, title: t`Internal Pricing`,
icon: <IconList />, icon: <IconList />,
min_value: pricing?.internal_cost_min, min_value: Number.parseFloat(pricing?.internal_cost_min),
max_value: pricing?.internal_cost_max max_value: Number.parseFloat(pricing?.internal_cost_max),
valid:
pricing?.internal_cost_min != null &&
pricing?.internal_cost_max != null
}, },
{ {
name: panelOptions.bom, name: panelOptions.bom,
title: t`BOM Pricing`, title: t`BOM Pricing`,
icon: <IconChartDonut />, icon: <IconChartDonut />,
min_value: pricing?.bom_cost_min, min_value: Number.parseFloat(pricing?.bom_cost_min),
max_value: pricing?.bom_cost_max max_value: Number.parseFloat(pricing?.bom_cost_max),
valid: pricing?.bom_cost_min != null && pricing?.bom_cost_max != null
}, },
{ {
name: panelOptions.purchase, name: panelOptions.purchase,
title: t`Purchase Pricing`, title: t`Purchase Pricing`,
icon: <IconShoppingCart />, icon: <IconShoppingCart />,
min_value: pricing?.purchase_cost_min, min_value: Number.parseFloat(pricing?.purchase_cost_min),
max_value: pricing?.purchase_cost_max max_value: Number.parseFloat(pricing?.purchase_cost_max),
valid:
pricing?.purchase_cost_min != null &&
pricing?.purchase_cost_max != null
}, },
{ {
name: panelOptions.supplier, name: panelOptions.supplier,
title: t`Supplier Pricing`, title: t`Supplier Pricing`,
icon: <IconBuildingWarehouse />, icon: <IconBuildingWarehouse />,
min_value: pricing?.supplier_price_min, min_value: Number.parseFloat(pricing?.supplier_price_min),
max_value: pricing?.supplier_price_max max_value: Number.parseFloat(pricing?.supplier_price_max),
valid:
pricing?.supplier_price_min != null &&
pricing?.supplier_price_max != null
}, },
{ {
name: panelOptions.variant, name: panelOptions.variant,
title: t`Variant Pricing`, title: t`Variant Pricing`,
icon: <IconTriangleSquareCircle />, icon: <IconTriangleSquareCircle />,
min_value: pricing?.variant_cost_min, min_value: Number.parseFloat(pricing?.variant_cost_min),
max_value: pricing?.variant_cost_max max_value: Number.parseFloat(pricing?.variant_cost_max),
valid:
pricing?.variant_cost_min != null && pricing?.variant_cost_max != null
}, },
{ {
name: panelOptions.sale_pricing, name: panelOptions.sale_pricing,
title: t`Sale Pricing`, title: t`Sale Pricing`,
icon: <IconTriangleSquareCircle />, icon: <IconTriangleSquareCircle />,
min_value: pricing?.sale_price_min, min_value: Number.parseFloat(pricing?.sale_price_min),
max_value: pricing?.sale_price_max max_value: Number.parseFloat(pricing?.sale_price_max),
valid:
pricing?.sale_price_min != null && pricing?.sale_price_max != null
}, },
{ {
name: panelOptions.sale_history, name: panelOptions.sale_history,
title: t`Sale History`, title: t`Sale History`,
icon: <IconTriangleSquareCircle />, icon: <IconTriangleSquareCircle />,
min_value: pricing?.sale_history_min, min_value: Number.parseFloat(pricing?.sale_history_min),
max_value: pricing?.sale_history_max max_value: Number.parseFloat(pricing?.sale_history_max),
valid:
pricing?.sale_history_min != null && pricing?.sale_history_max != null
} }
].filter((entry) => { ].filter((entry) => {
return !(entry.min_value == null || entry.max_value == null); return entry.valid;
}); });
}, [part, pricing]); }, [part, pricing]);

View File

@ -21,7 +21,7 @@ export default function PurchaseHistoryPanel({
const calculateUnitPrice = useCallback((record: any) => { const calculateUnitPrice = useCallback((record: any) => {
const pack_quantity = const pack_quantity =
record?.supplier_part_detail?.pack_quantity_native ?? 1; 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; return unit_price;
}, []); }, []);
@ -95,7 +95,7 @@ export default function PurchaseHistoryPanel({
return table.records.map((record: any) => { return table.records.map((record: any) => {
return { return {
quantity: record.quantity, quantity: record.quantity,
purchase_price: record.purchase_price, purchase_price: Number.parseFloat(record.purchase_price),
unit_price: calculateUnitPrice(record), unit_price: calculateUnitPrice(record),
name: record.order_detail.reference name: record.order_detail.reference
}; };

View File

@ -57,7 +57,7 @@ export default function SaleHistoryPanel({
return table.records.map((record: any) => { return table.records.map((record: any) => {
return { return {
name: record.order_detail.reference, name: record.order_detail.reference,
sale_price: record.sale_price sale_price: Number.parseFloat(record.sale_price)
}; };
}); });
}, [table.records]); }, [table.records]);

View File

@ -36,7 +36,7 @@ export default function SupplierPricingPanel({
table.records?.map((record: any) => { table.records?.map((record: any) => {
return { return {
quantity: record.quantity, quantity: record.quantity,
supplier_price: record.price, supplier_price: Number.parseFloat(record.price),
unit_price: calculateSupplierPartUnitPrice(record), unit_price: calculateSupplierPartUnitPrice(record),
name: record.part_detail?.SKU name: record.part_detail?.SKU
}; };

View File

@ -63,8 +63,10 @@ export default function VariantPricingPanel({
return { return {
part: variant, part: variant,
name: variant.full_name, name: variant.full_name,
pmin: variant.pricing_min ?? variant.pricing_max ?? 0, pmin: Number.parseFloat(
pmax: variant.pricing_max ?? variant.pricing_min ?? 0 variant.pricing_min ?? variant.pricing_max ?? 0
),
pmax: Number.parseFloat(variant.pricing_max ?? variant.pricing_min ?? 0)
}; };
}); });

View File

@ -24,7 +24,7 @@ import { type RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
export function calculateSupplierPartUnitPrice(record: any) { export function calculateSupplierPartUnitPrice(record: any) {
const pack_quantity = record?.part_detail?.pack_quantity_native ?? 1; 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; return unit_price;
} }