From 49d5ae2fa89b130e5ec585246a94587d18902fbf Mon Sep 17 00:00:00 2001 From: "PC-20260115JRSN\\Administrator" Date: Fri, 18 Sep 2026 10:55:31 +0800 Subject: [PATCH] align the calculation of unit with stock in line data create calculation --- src/app/api/po/index.ts | 3 + src/components/PoDetail/PoDetailRow.tsx | 2 + src/components/PoDetail/stockQtyRound.ts | 85 +++++++++++++++++++++++- 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/app/api/po/index.ts b/src/app/api/po/index.ts index ac6dc583..e20cb25f 100644 --- a/src/app/api/po/index.ts +++ b/src/app/api/po/index.ts @@ -60,6 +60,9 @@ export interface StockUomForPoLine { stockRatioD: number; purchaseRatioN: number; purchaseRatioD: number; + sourceRatioN?: number | null; + sourceRatioD?: number | null; + stockQtyInteger?: boolean | null; } // DEPRECIATED diff --git a/src/components/PoDetail/PoDetailRow.tsx b/src/components/PoDetail/PoDetailRow.tsx index 5bfdfb3e..d16c5259 100644 --- a/src/components/PoDetail/PoDetailRow.tsx +++ b/src/components/PoDetail/PoDetailRow.tsx @@ -117,6 +117,7 @@ export const PoDetailRow = memo(function PoDetailRow({ orderQty, Number(row.stockUom?.stockQty ?? 0), acceptedQty, + row.stockUom, ); const doSubmit = () => { @@ -211,6 +212,7 @@ export const PoDetailRow = memo(function PoDetailRow({ Number(row.qty ?? 0), Number(row.stockUom?.stockQty ?? 0), enteredBatchQty, + row.stockUom, ); const convertedStockQtyWithUnit = formatQtyWithStockUom( convertedStockQty, diff --git a/src/components/PoDetail/stockQtyRound.ts b/src/components/PoDetail/stockQtyRound.ts index f3dcab3a..c2d066b6 100644 --- a/src/components/PoDetail/stockQtyRound.ts +++ b/src/components/PoDetail/stockQtyRound.ts @@ -7,16 +7,95 @@ export type StockQtyRoundChoice = { after: number; }; -/** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10 */ +/** Keep in sync with ItemUomService.isIntegerCountStockUom */ +const INTEGER_COUNT_UNIT_TOKENS = new Set([ + "包", + "箱", + "件", + "個", + "PCS", + "PC", + "CTN", + "EA", + "PK", + "PKT", + "BOX", +]); + +export type PoStockQtyConvertHint = { + sourceRatioN?: number | null; + sourceRatioD?: number | null; + stockRatioN?: number | null; + stockRatioD?: number | null; + purchaseRatioN?: number | null; + purchaseRatioD?: number | null; + stockUomCode?: string | null; + stockUomShortDesc?: string | null; + stockQtyInteger?: boolean | null; +}; + +function toRatio(value: number | null | undefined): number | null { + const n = Number(value); + if (!Number.isFinite(n) || n === 0) return null; + return n; +} + +function stockQtyMustBeInteger(hint?: PoStockQtyConvertHint): boolean { + if (hint?.stockQtyInteger === true) return true; + if (hint?.stockQtyInteger === false) return false; + const code = hint?.stockUomCode?.trim() ?? ""; + if (code.toUpperCase().startsWith("PCS/CTN")) return true; + const short = hint?.stockUomShortDesc?.trim() ?? ""; + return INTEGER_COUNT_UNIT_TOKENS.has(short) || INTEGER_COUNT_UNIT_TOKENS.has(code.toUpperCase()); +} + +/** Java BigDecimal HALF_UP for non-negative qty. */ +function roundHalfUp(value: number, scale: number): number { + const factor = 10 ** scale; + return Math.round(value * factor) / factor; +} + +function finalizePoStockQty(raw: number, hint?: PoStockQtyConvertHint): number { + if (!Number.isFinite(raw)) return 0; + return stockQtyMustBeInteger(hint) ? roundHalfUp(raw, 0) : roundHalfUp(raw, 2); +} + +/** + * Same ratio path as backend convertQtyToStockQtyPrecise: + * base = qty * sourceRatioN / sourceRatioD + * stock = base * stockRatioD / stockRatioN + */ +function convertPoBatchToStockQty( + batchM18Qty: number, + hint?: PoStockQtyConvertHint, +): number | null { + if (!hint || !Number.isFinite(batchM18Qty)) return null; + const sourceN = toRatio(hint.sourceRatioN) ?? toRatio(hint.purchaseRatioN); + const sourceD = toRatio(hint.sourceRatioD) ?? toRatio(hint.purchaseRatioD); + const stockN = toRatio(hint.stockRatioN); + const stockD = toRatio(hint.stockRatioD); + if (sourceN == null || sourceD == null || stockN == null || stockD == null) return null; + const baseQty = (batchM18Qty * sourceN) / sourceD; + const stockQty = (baseQty * stockD) / stockN; + return finalizePoStockQty(stockQty, hint); +} + +/** + * Preview converted stock qty for a 來貨數 batch. + * Matches create-stock-in: ratio convert, then integer HALF_UP for 包/箱/PCS, else 2 decimals. + */ export function previewPoBatchStockQty( orderM18Qty: number, orderStockQty: number, batchM18Qty: number, + hint?: PoStockQtyConvertHint, ): number { + const fromRatios = convertPoBatchToStockQty(batchM18Qty, hint); + if (fromRatios != null) return fromRatios; if (!Number.isFinite(orderM18Qty) || orderM18Qty === 0) { - return Number(batchM18Qty.toFixed(2)); + return finalizePoStockQty(batchM18Qty, hint); } - return Number(((batchM18Qty * orderStockQty) / orderM18Qty).toFixed(2)); + return finalizePoStockQty((batchM18Qty * orderStockQty) / orderM18Qty, hint); } export function isNotIntegerQty(qty: number): boolean {