|
|
|
@@ -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 { |
|
|
|
|