FPSMS-frontend
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 

126 líneas
4.1 KiB

  1. export type StockQtyRoundMode = "CEILING" | "FLOOR";
  2. export type StockQtyRoundSource = "CREATE" | "QC";
  3. export type StockQtyRoundChoice = {
  4. mode: StockQtyRoundMode;
  5. before: number;
  6. after: number;
  7. };
  8. /** Keep in sync with ItemUomService.isIntegerCountStockUom */
  9. const INTEGER_COUNT_UNIT_TOKENS = new Set([
  10. "包",
  11. "箱",
  12. "件",
  13. "個",
  14. "PCS",
  15. "PC",
  16. "CTN",
  17. "EA",
  18. "PK",
  19. "PKT",
  20. "BOX",
  21. ]);
  22. export type PoStockQtyConvertHint = {
  23. sourceRatioN?: number | null;
  24. sourceRatioD?: number | null;
  25. stockRatioN?: number | null;
  26. stockRatioD?: number | null;
  27. purchaseRatioN?: number | null;
  28. purchaseRatioD?: number | null;
  29. stockUomCode?: string | null;
  30. stockUomShortDesc?: string | null;
  31. stockQtyInteger?: boolean | null;
  32. };
  33. function toRatio(value: number | null | undefined): number | null {
  34. const n = Number(value);
  35. if (!Number.isFinite(n) || n === 0) return null;
  36. return n;
  37. }
  38. function stockQtyMustBeInteger(hint?: PoStockQtyConvertHint): boolean {
  39. if (hint?.stockQtyInteger === true) return true;
  40. if (hint?.stockQtyInteger === false) return false;
  41. const code = hint?.stockUomCode?.trim() ?? "";
  42. if (code.toUpperCase().startsWith("PCS/CTN")) return true;
  43. const short = hint?.stockUomShortDesc?.trim() ?? "";
  44. return INTEGER_COUNT_UNIT_TOKENS.has(short) || INTEGER_COUNT_UNIT_TOKENS.has(code.toUpperCase());
  45. }
  46. /** Java BigDecimal HALF_UP for non-negative qty. */
  47. function roundHalfUp(value: number, scale: number): number {
  48. const factor = 10 ** scale;
  49. return Math.round(value * factor) / factor;
  50. }
  51. function finalizePoStockQty(raw: number, hint?: PoStockQtyConvertHint): number {
  52. if (!Number.isFinite(raw)) return 0;
  53. return stockQtyMustBeInteger(hint) ? roundHalfUp(raw, 0) : roundHalfUp(raw, 2);
  54. }
  55. /**
  56. * Same ratio path as backend convertQtyToStockQtyPrecise:
  57. * base = qty * sourceRatioN / sourceRatioD
  58. * stock = base * stockRatioD / stockRatioN
  59. */
  60. function convertPoBatchToStockQty(
  61. batchM18Qty: number,
  62. hint?: PoStockQtyConvertHint,
  63. ): number | null {
  64. if (!hint || !Number.isFinite(batchM18Qty)) return null;
  65. const sourceN = toRatio(hint.sourceRatioN) ?? toRatio(hint.purchaseRatioN);
  66. const sourceD = toRatio(hint.sourceRatioD) ?? toRatio(hint.purchaseRatioD);
  67. const stockN = toRatio(hint.stockRatioN);
  68. const stockD = toRatio(hint.stockRatioD);
  69. if (sourceN == null || sourceD == null || stockN == null || stockD == null) return null;
  70. const baseQty = (batchM18Qty * sourceN) / sourceD;
  71. const stockQty = (baseQty * stockD) / stockN;
  72. return finalizePoStockQty(stockQty, hint);
  73. }
  74. /**
  75. * Preview converted stock qty for a 來貨數 batch.
  76. * Matches create-stock-in: ratio convert, then integer HALF_UP for 包/箱/PCS, else 2 decimals.
  77. */
  78. export function previewPoBatchStockQty(
  79. orderM18Qty: number,
  80. orderStockQty: number,
  81. batchM18Qty: number,
  82. hint?: PoStockQtyConvertHint,
  83. ): number {
  84. const fromRatios = convertPoBatchToStockQty(batchM18Qty, hint);
  85. if (fromRatios != null) return fromRatios;
  86. if (!Number.isFinite(orderM18Qty) || orderM18Qty === 0) {
  87. return finalizePoStockQty(batchM18Qty, hint);
  88. }
  89. return finalizePoStockQty((batchM18Qty * orderStockQty) / orderM18Qty, hint);
  90. }
  91. export function isNotIntegerQty(qty: number): boolean {
  92. if (!Number.isFinite(qty)) return false;
  93. return Math.abs(qty - Math.round(qty)) > 1e-9;
  94. }
  95. /**
  96. * FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10
  97. * PO 收貨/品檢:pending 或 escalated 且庫存數量非整數時需選擇 CEILING / FLOOR
  98. */
  99. export function needsPoQcStockQtyRound(
  100. purchaseOrderLineId: number | null | undefined,
  101. status: string | null | undefined,
  102. acceptedQty: number | null | undefined,
  103. ): boolean {
  104. if (!purchaseOrderLineId) return false;
  105. const silStatus = (status ?? "").toLowerCase().trim();
  106. if (silStatus !== "pending" && silStatus !== "escalated") return false;
  107. return isNotIntegerQty(Number(acceptedQty ?? 0));
  108. }
  109. /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10 */
  110. export function roundStockQty(before: number, mode: StockQtyRoundMode): number {
  111. if (mode === "CEILING") return Math.ceil(before);
  112. return Math.floor(before);
  113. }