fix(po): store the price from the form so POs stop printing £0.00
Every purchase order created from a stock item printed a £0.00 unit
price, even though a cost price was on the record and the form showed
the right number.
Cause: the PO snapshot took costPriceGBP from the stock item's stored
cost_price_gbp instead of from the form. The price the user sees (and
types) into the "Unit Price (£ GBP)" field landed in snapshot.costPrice,
which nothing renders, while the PO PDF renders snapshot.costPriceGBP
(pdf.ts: `£${item.costPriceGBP}`). So a price typed into the form was
discarded and the stock item's cached value was printed instead.
Because the stock item's cost_price_gbp was only written back to the
database AFTER the PO was created, the first PO of an item always showed
£0.00 and only the next one showed the price. Verified in production:
PO-20261023-002 stored costPrice 333.78 / costPriceGBP 0 and printed
£0.00, while the linked stock item's cost_price_gbp was 0 at the time
and was updated to 333.78 by the sync step three minutes later.
Fixes:
- PurchaseOrderForm: snapshot.costPriceGBP is now the price from the
form; snapshot.costPrice is the stock item's ZAR landed cost.
- PurchaseOrderForm: selecting a stock item with no GBP cost price
leaves the price field blank instead of showing £0.00, with a hint
that the supplier's GBP price still has to be entered. Most stock
items only carry a ZAR cost price, so £0.00 looked like a real price.
- EditPurchaseOrderDialog: preserve the ZAR cost price per row instead
of zeroing it when a PO is edited.
This commit is contained in:
parent
2b56053e1c
commit
2682eeb360
2 changed files with 27 additions and 6 deletions
|
|
@ -134,13 +134,18 @@ export function EditPurchaseOrderDialog({ open, onOpenChange, order, refreshData
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Build updated snapshot — keep stable ids + entry mode so the audit
|
// Build updated snapshot — keep stable ids + entry mode so the audit
|
||||||
// trail remains meaningful.
|
// trail remains meaningful. costPrice is the ZAR landed cost, so carry
|
||||||
|
// the existing value over per row instead of zeroing it; costPriceGBP is
|
||||||
|
// the price the user sees in the form and is what the PO PDF renders.
|
||||||
|
const existingSnapshot = order.snapshotItems || []
|
||||||
const updatedSnapshot = normalizedRows.map((r) => ({
|
const updatedSnapshot = normalizedRows.map((r) => ({
|
||||||
stockItemId: r.stockItemId,
|
stockItemId: r.stockItemId,
|
||||||
itemName: r.itemName,
|
itemName: r.itemName,
|
||||||
sku: r.sku,
|
sku: r.sku,
|
||||||
quantity: r.quantity,
|
quantity: r.quantity,
|
||||||
costPrice: 0,
|
costPrice: Number(
|
||||||
|
existingSnapshot.find((s) => s.stockItemId === r.stockItemId)?.costPrice || 0,
|
||||||
|
),
|
||||||
costPriceGBP: r.unitPriceGBP,
|
costPriceGBP: r.unitPriceGBP,
|
||||||
entryMode: r.entryMode,
|
entryMode: r.entryMode,
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
|
|
@ -260,8 +261,12 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro
|
||||||
form.setValue(`items.${index}.stockItemId`, selectedStockItem.id, { shouldDirty: true });
|
form.setValue(`items.${index}.stockItemId`, selectedStockItem.id, { shouldDirty: true });
|
||||||
form.setValue(`items.${index}.itemName`, selectedStockItem.name, { shouldDirty: true });
|
form.setValue(`items.${index}.itemName`, selectedStockItem.name, { shouldDirty: true });
|
||||||
form.setValue(`items.${index}.sku`, selectedStockItem.sku, { shouldDirty: true });
|
form.setValue(`items.${index}.sku`, selectedStockItem.sku, { shouldDirty: true });
|
||||||
// Use GBP price for purchase orders
|
// Use GBP price for purchase orders. Most stock items only carry a ZAR
|
||||||
form.setValue(`items.${index}.costPrice`, selectedStockItem.costPriceGBP, { shouldDirty: true, shouldValidate: true });
|
// cost price (cost_price), so leave the field BLANK instead of silently
|
||||||
|
// showing £0.00 — a blank field tells the user the supplier's GBP price
|
||||||
|
// still has to be entered, whereas "£0.00" looks like a real price.
|
||||||
|
const gbpPrice = Number(selectedStockItem.costPriceGBP || 0);
|
||||||
|
form.setValue(`items.${index}.costPrice`, gbpPrice > 0 ? gbpPrice : ("" as any), { shouldDirty: true, shouldValidate: true });
|
||||||
// Leave quantity blank — user types the amount they want.
|
// Leave quantity blank — user types the amount they want.
|
||||||
form.setValue(`items.${index}.quantity`, "", { shouldDirty: true, shouldValidate: true });
|
form.setValue(`items.${index}.quantity`, "", { shouldDirty: true, shouldValidate: true });
|
||||||
}
|
}
|
||||||
|
|
@ -357,8 +362,14 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro
|
||||||
itemName: item.itemName,
|
itemName: item.itemName,
|
||||||
sku: item.sku,
|
sku: item.sku,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
costPrice: Number(item.costPrice || 0),
|
// ZAR landed cost, for reference only — the PO itself is priced in GBP.
|
||||||
costPriceGBP: Number(stockItem?.costPriceGBP || 0),
|
costPrice: Number(stockItem?.costPrice ?? 0),
|
||||||
|
// The GBP unit price taken from the FORM. This is the only price
|
||||||
|
// field the PO PDF renders (£${item.costPriceGBP}), so it must be the
|
||||||
|
// value the user sees and edits. Reading it from the stock item
|
||||||
|
// instead silently discarded every price typed into the form and
|
||||||
|
// printed £0.00 on the PO.
|
||||||
|
costPriceGBP: Number(item.costPrice || 0),
|
||||||
entryMode: 'stock',
|
entryMode: 'stock',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -755,6 +766,11 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro
|
||||||
onChange={(v) => field.onChange(v)}
|
onChange={(v) => field.onChange(v)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
{!(Number(field.value || 0) > 0) && (
|
||||||
|
<FormDescription className="text-xs text-amber-600 dark:text-amber-400">
|
||||||
|
No GBP cost price on this stock item — enter the supplier's GBP price.
|
||||||
|
</FormDescription>
|
||||||
|
)}
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue