[UI] Suggested pricing (#10867)

* Refactor NumberField into separate component

* Add helper func to ensure a number is a number

* Use placeholder value for suggested sale price

* Fix for auto-fill

* Tweak price calculation

* Add UI testing for sales order price breaks

* Fix aria label name

* Annotate price breaks to supplier part

* Fetch price break data

* Support price breaks for purchase order pricing

* Fix required to prevent circular imports

* Add playwright tests for purchase order price breaks

* Bump API version

* Re-add output options for SupplierPriceBreakList

* Revert change

* Simplify unit test
This commit is contained in:
Oliver
2025-11-20 22:51:27 +11:00
committed by GitHub
parent 835c7784f9
commit a76ec0a7b8
16 changed files with 465 additions and 201 deletions
+57 -5
View File
@@ -34,6 +34,7 @@ import { StandaloneField } from '../components/forms/StandaloneField';
import { ProgressBar } from '@lib/components/ProgressBar';
import { apiUrl } from '@lib/functions/Api';
import { toNumber } from '@lib/functions/Conversion';
import type {
ApiFormAdjustFilterType,
ApiFormFieldSet
@@ -58,19 +59,59 @@ import { useGlobalSettingsState } from '../states/SettingsStates';
export function usePurchaseOrderLineItemFields({
supplierId,
orderId,
currency,
create
}: {
supplierId?: number;
orderId?: number;
currency?: string;
create?: boolean;
}) {
const globalSettings = useGlobalSettingsState();
const [purchasePrice, setPurchasePrice] = useState<string>('');
const [autoPricing, setAutoPricing] = useState(true);
const [purchasePriceCurrency, setPurchasePriceCurrency] = useState<string>(
currency ?? ''
);
const [autoPricing, setAutoPricing] = useState(false);
const [quantity, setQuantity] = useState<string>('1');
// Internal part information
const [part, setPart] = useState<any>({});
const [priceBreaks, setPriceBreaks] = useState<any[]>([]);
const [suggestedPurchasePrice, setSuggestedPurchasePrice] = useState<
string | undefined
>(undefined);
// Update suggested purchase price when part, quantity, or price breaks change
useEffect(() => {
// Only attempt to set purchase price for new line items
if (!create) return;
const qty = toNumber(quantity, null);
if (qty == null || qty <= 0) {
setSuggestedPurchasePrice(undefined);
return;
}
if (!part || !priceBreaks || priceBreaks.length === 0) {
setSuggestedPurchasePrice(undefined);
return;
}
const applicablePriceBreaks = priceBreaks
.filter((pb: any) => qty >= pb.quantity)
.sort((a: any, b: any) => b.quantity - a.quantity);
if (applicablePriceBreaks.length) {
setSuggestedPurchasePrice(applicablePriceBreaks[0].price);
} else {
setSuggestedPurchasePrice(undefined);
}
}, [create, part, quantity, priceBreaks]);
useEffect(() => {
if (autoPricing) {
@@ -95,9 +136,11 @@ export function usePurchaseOrderLineItemFields({
part_detail: true,
supplier_detail: true,
active: true,
part_active: true
part_active: true,
price_breaks: true
},
onValueChange: (value, record) => {
setPriceBreaks(record?.price_breaks ?? []);
setPart(record?.part_detail ?? {});
},
adjustFilters: (adjust: ApiFormAdjustFilterType) => {
@@ -108,14 +151,22 @@ export function usePurchaseOrderLineItemFields({
}
},
reference: {},
quantity: {},
quantity: {
onValueChange: (value) => {
setQuantity(value);
}
},
purchase_price: {
icon: <IconCurrencyDollar />,
value: purchasePrice,
placeholder: suggestedPurchasePrice,
placeholderAutofill: true,
onValueChange: setPurchasePrice
},
purchase_price_currency: {
icon: <IconCoins />
icon: <IconCoins />,
value: purchasePriceCurrency,
onValueChange: setPurchasePriceCurrency
},
auto_pricing: {
value: autoPricing,
@@ -162,7 +213,8 @@ export function usePurchaseOrderLineItemFields({
globalSettings,
supplierId,
autoPricing,
purchasePrice
purchasePrice,
suggestedPurchasePrice
]);
return fields;