|
- export type StockQtyRoundMode = "CEILING" | "FLOOR";
- export type StockQtyRoundSource = "CREATE" | "QC";
-
- export type StockQtyRoundChoice = {
- mode: StockQtyRoundMode;
- before: number;
- after: number;
- };
-
- /** 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 finalizePoStockQty(batchM18Qty, hint);
- }
- return finalizePoStockQty((batchM18Qty * orderStockQty) / orderM18Qty, hint);
- }
-
- export function isNotIntegerQty(qty: number): boolean {
- if (!Number.isFinite(qty)) return false;
- return Math.abs(qty - Math.round(qty)) > 1e-9;
- }
-
- /**
- * FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10
- * PO 收貨/品檢:pending 或 escalated 且庫存數量非整數時需選擇 CEILING / FLOOR
- */
- export function needsPoQcStockQtyRound(
- purchaseOrderLineId: number | null | undefined,
- status: string | null | undefined,
- acceptedQty: number | null | undefined,
- ): boolean {
- if (!purchaseOrderLineId) return false;
- const silStatus = (status ?? "").toLowerCase().trim();
- if (silStatus !== "pending" && silStatus !== "escalated") return false;
- return isNotIntegerQty(Number(acceptedQty ?? 0));
- }
-
- /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10 */
- export function roundStockQty(before: number, mode: StockQtyRoundMode): number {
- if (mode === "CEILING") return Math.ceil(before);
- return Math.floor(before);
- }
|