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:
parent
15ad62494f
commit
ab4eff19d3
@ -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):
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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`
|
||||||
};
|
};
|
||||||
}) ?? []
|
}) ?? []
|
||||||
|
@ -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]);
|
||||||
|
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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]);
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user