From 6e912bd5dcf7ee3cc932965abdf9ba480abf80e5 Mon Sep 17 00:00:00 2001 From: "PC-20260115JRSN\\Administrator" Date: Fri, 18 Sep 2026 01:11:50 +0800 Subject: [PATCH] added unit description for the number before submit in PO stock in --- src/app/api/po/index.ts | 1 + src/app/api/settings/uom/index.ts | 2 + src/components/PoDetail/PoDetail.tsx | 8 +- src/components/PoDetail/PoDetailRow.tsx | 150 ++++++++++++++++++++--- src/components/PoDetail/PoInputGrid.tsx | 7 +- src/components/PoDetail/poPurchaseUom.ts | 96 +++++++++++++++ src/i18n/en/purchaseOrder.json | 2 + src/i18n/zh/purchaseOrder.json | 2 + 8 files changed, 245 insertions(+), 23 deletions(-) create mode 100644 src/components/PoDetail/poPurchaseUom.ts diff --git a/src/app/api/po/index.ts b/src/app/api/po/index.ts index ed051581..ac6dc583 100644 --- a/src/app/api/po/index.ts +++ b/src/app/api/po/index.ts @@ -54,6 +54,7 @@ export interface StockUomForPoLine { id: number; stockUomCode: string; stockUomDesc: string; + stockUomShortDesc?: string | null; stockQty: number; stockRatioN: number; stockRatioD: number; diff --git a/src/app/api/settings/uom/index.ts b/src/app/api/settings/uom/index.ts index e7e8526e..4f5765ca 100644 --- a/src/app/api/settings/uom/index.ts +++ b/src/app/api/settings/uom/index.ts @@ -9,6 +9,8 @@ export interface Uom { id: number; code: string; udfudesc: string; + /** Short UoM label from M18 (`udfShortDesc`), e.g. 箱. */ + udfShortDesc?: string | null; unit1: string; unit1Qty: number; unit2?: string; diff --git a/src/components/PoDetail/PoDetail.tsx b/src/components/PoDetail/PoDetail.tsx index 4b16fc53..993433dc 100644 --- a/src/components/PoDetail/PoDetail.tsx +++ b/src/components/PoDetail/PoDetail.tsx @@ -875,7 +875,13 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { {t("status")} {/* {renderFieldCondition(FIRST_IN_FIELD) ? {t("receivedQty")} : undefined} */} {t("productLotNo")} - {renderFieldCondition(SECOND_IN_FIELD) ? {t("dnQty")}
(以訂單單位計算)
: undefined} + {renderFieldCondition(SECOND_IN_FIELD) ? ( + + {t("dnQty")} +
+ ({t("dnQtyOrderUnitHint")}) +
+ ) : undefined} diff --git a/src/components/PoDetail/PoDetailRow.tsx b/src/components/PoDetail/PoDetailRow.tsx index 682aa4dd..0118fbb0 100644 --- a/src/components/PoDetail/PoDetailRow.tsx +++ b/src/components/PoDetail/PoDetailRow.tsx @@ -4,10 +4,13 @@ import { PurchaseOrderLine } from "@/app/api/po"; import { Box, Button, + InputAdornment, Radio, + Stack, TableCell, TableRow, TextField, + Typography, alpha, } from "@mui/material"; import { memo, useCallback, useEffect, useRef, useState } from "react"; @@ -16,6 +19,12 @@ import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; import { submitDialogWithWarning } from "../Swal/CustomAlerts"; import { createStockInLine } from "@/app/api/stockIn/actions"; import { previewPoBatchStockQty } from "./stockQtyRound"; +import { + formatQtyWithPurchaseUom, + formatQtyWithStockUom, + poLineNeedsStockQtyConversion, + purchaseUomShortDesc, +} from "./poPurchaseUom"; const PURCHASE_STOCK_IN_ALERT_STATUSES = new Set(["pending", "receiving"]); @@ -186,6 +195,27 @@ export const PoDetailRow = memo(function PoDetailRow({ Number(receivedTotalText.replace(/,/g, "")) <= 0 ? "red" : "inherit"; const needsStockInAttention = canSeeStockInReminders && purchaseOrderLineHasIncompleteStockIn(row); + const uomShort = purchaseUomShortDesc(row.uom); + const orderQtyWithUnit = formatQtyWithPurchaseUom(row.qty, row.uom); + const enteredBatchQty = Number(dnQtyInput.trim()); + const hasEnteredBatchQty = Number.isInteger(enteredBatchQty) && enteredBatchQty > 0; + const showStockConversion = + hasEnteredBatchQty && + poLineNeedsStockQtyConversion( + Number(row.qty ?? 0), + Number(row.stockUom?.stockQty ?? 0), + row.uom, + row.stockUom, + ); + const convertedStockQty = previewPoBatchStockQty( + Number(row.qty ?? 0), + Number(row.stockUom?.stockQty ?? 0), + enteredBatchQty, + ); + const convertedStockQtyWithUnit = formatQtyWithStockUom( + convertedStockQty, + row.stockUom, + ); return ( {row.itemName} - {integerFormatter.format(row.qty)} + + {orderQtyWithUnit} + {integerFormatter.format(row.processed)} {row.uom?.udfudesc} @@ -268,25 +310,95 @@ export const PoDetailRow = memo(function PoDetailRow({ /> {showDnQty ? ( - - setDnQtyInput(e.target.value.replace(/[^\d]/g, ""))} - onBlur={() => onInputBlur(row.id, lotNoInput, dnQtyInput)} + + e.stopPropagation()} - InputProps={{ - inputProps: { - min: 1, - step: 1, - inputMode: "numeric", - pattern: "[0-9]*", - }, - }} - /> + > + setDnQtyInput(e.target.value.replace(/[^\d]/g, ""))} + onBlur={() => onInputBlur(row.id, lotNoInput, dnQtyInput)} + onClick={(e) => e.stopPropagation()} + sx={{ minWidth: 132, width: 148, flexShrink: 0 }} + InputProps={{ + endAdornment: uomShort ? ( + + + {uomShort} + + + ) : undefined, + inputProps: { + min: 1, + step: 1, + inputMode: "numeric", + pattern: "[0-9]*", + "aria-label": `${t("dnQty")} ${uomShort}`.trim(), + }, + }} + /> + {showStockConversion && Number.isFinite(convertedStockQty) && convertedStockQty > 0 ? ( + alpha(theme.palette.primary.main, 0.1), + border: 1, + borderColor: "primary.main", + textAlign: "center", + }} + title={row.stockUom?.stockUomDesc || convertedStockQtyWithUnit} + > + + {t("stockQtyRef")} + + + {convertedStockQtyWithUnit} + + + ) : null} + ) : null} diff --git a/src/components/PoDetail/PoInputGrid.tsx b/src/components/PoDetail/PoInputGrid.tsx index b4572b58..c8c038b8 100644 --- a/src/components/PoDetail/PoInputGrid.tsx +++ b/src/components/PoDetail/PoInputGrid.tsx @@ -61,7 +61,7 @@ import DoDisturbIcon from "@mui/icons-material/DoDisturb"; import { useSession } from "next-auth/react"; // import { SessionWithTokens } from "src/config/authConfig"; import QcStockInModal from "../Qc/QcStockInModal"; -import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; +import { decimalFormatter } from "@/app/utils/formatUtil"; import { PrinterCombo } from "@/app/api/settings/printer"; import { EscalationResult } from "@/app/api/escalation"; import { fetchEscalationLogsByStockInLines } from "@/app/api/escalation/actions"; @@ -70,10 +70,11 @@ import { EscalationCombo } from "@/app/api/user"; import { deleteDialog } from "../Swal/CustomAlerts"; import StockInLineRowActions from "./StockInLineRowActions"; import { StockQtyRoundMode, needsPoQcStockQtyRound } from "./stockQtyRound"; +import { formatQtyWithPurchaseUom } from "./poPurchaseUom"; // 3 buttons after QC (view + print QR + delete): 176*3 + gap + cell padding const ACTIONS_COLUMN_WIDTH = 580; -const PURCHASE_QTY_COLUMN_WIDTH = 72; +const PURCHASE_QTY_COLUMN_WIDTH = 96; const UOM_COLUMN_WIDTH = 124; const STOCK_QTY_COLUMN_WIDTH = 110; const STOCK_IN_ROW_HEIGHT = 58; @@ -719,7 +720,7 @@ function PoInputGrid({ type: "number", renderCell: (params) => { const qty = params.row.purchaseAcceptedQty ?? 0; - return integerFormatter.format(qty); + return formatQtyWithPurchaseUom(qty, itemDetail.uom); }, }, { diff --git a/src/components/PoDetail/poPurchaseUom.ts b/src/components/PoDetail/poPurchaseUom.ts new file mode 100644 index 00000000..84b07f2f --- /dev/null +++ b/src/components/PoDetail/poPurchaseUom.ts @@ -0,0 +1,96 @@ +import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; + +type PurchaseUomLike = { + code?: string | null; + udfudesc?: string | null; + udfShortDesc?: string | null; +} | null | undefined; + +type StockUomLike = { + stockUomCode?: string | null; + stockUomDesc?: string | null; + stockUomShortDesc?: string | null; +} | null | undefined; + +function isCompactUnitLabel(value: string): boolean { + return value.length > 0 && value.length <= 6 && !/[x×]/i.test(value); +} + +function compactUomLabel( + shortDesc?: string | null, + fullDesc?: string | null, + code?: string | null, +): string { + const short = shortDesc?.trim(); + if (short) return short; + const full = fullDesc?.trim() ?? ""; + if (isCompactUnitLabel(full)) return full; + const unitCode = code?.trim() ?? ""; + if (isCompactUnitLabel(unitCode)) return unitCode; + return ""; +} + +function normUnitKey(...parts: Array): string { + return parts.map((p) => p?.trim().toLowerCase()).find((p) => p) ?? ""; +} + +/** Prefer UoM short desc (箱). Do not return long packing strings like 150克X100包X1箱. */ +export function purchaseUomShortDesc(uom: PurchaseUomLike): string { + return compactUomLabel(uom?.udfShortDesc, uom?.udfudesc, uom?.code); +} + +export function stockUomShortLabel(stockUom: StockUomLike): string { + return compactUomLabel( + stockUom?.stockUomShortDesc, + stockUom?.stockUomDesc, + stockUom?.stockUomCode, + ); +} + +export function purchaseUnitDiffersFromStock( + uom: PurchaseUomLike, + stockUom: StockUomLike, +): boolean { + const purchaseKey = normUnitKey(uom?.code, uom?.udfShortDesc, uom?.udfudesc); + const stockKey = normUnitKey( + stockUom?.stockUomCode, + stockUom?.stockUomShortDesc, + stockUom?.stockUomDesc, + ); + if (!purchaseKey || !stockKey) return false; + return purchaseKey !== stockKey; +} + +export function poLineNeedsStockQtyConversion( + orderQty: number, + orderStockQty: number, + uom: PurchaseUomLike, + stockUom: StockUomLike, +): boolean { + if (purchaseUnitDiffersFromStock(uom, stockUom)) return true; + return ( + Number.isFinite(orderQty) && + Number.isFinite(orderStockQty) && + orderQty > 0 && + orderStockQty > 0 && + Math.abs(orderStockQty - orderQty) > 1e-6 + ); +} + +/** e.g. 384箱 */ +export function formatQtyWithPurchaseUom(qty: number, uom: PurchaseUomLike): string { + const n = integerFormatter.format(qty); + const unit = purchaseUomShortDesc(uom); + return unit ? `${n}${unit}` : n; +} + +/** e.g. 57,600克 */ +export function formatQtyWithStockUom(qty: number, stockUom: StockUomLike): string { + const rounded = Math.round(qty); + const n = + Number.isFinite(qty) && Math.abs(qty - rounded) < 1e-9 + ? integerFormatter.format(rounded) + : decimalFormatter.format(qty); + const unit = stockUomShortLabel(stockUom) || stockUom?.stockUomDesc?.trim() || ""; + return unit ? `${n}${unit}` : n; +} diff --git a/src/i18n/en/purchaseOrder.json b/src/i18n/en/purchaseOrder.json index dd920e44..f75266b5 100644 --- a/src/i18n/en/purchaseOrder.json +++ b/src/i18n/en/purchaseOrder.json @@ -126,6 +126,8 @@ "Please scan warehouse qr code.": "Please scan warehouse QR code.", "receivedQty": "Received Qty", "dnQty": "Delivery Qty (This Batch)", + "dnQtyOrderUnitHint": "in order unit", + "stockQtyRef": "Stock", "Accept submit": "Accept Delivery", "qc processing": "Delivery & QC Processing", "putaway processing": "Delivery & Put Away Processing", diff --git a/src/i18n/zh/purchaseOrder.json b/src/i18n/zh/purchaseOrder.json index 0cac29af..99664477 100644 --- a/src/i18n/zh/purchaseOrder.json +++ b/src/i18n/zh/purchaseOrder.json @@ -126,6 +126,8 @@ "Please scan warehouse qr code.": "請掃描倉庫 QR 碼。", "receivedQty": "已來貨數量", "dnQty": "本批來貨數量", + "dnQtyOrderUnitHint": "以訂單單位計算", + "stockQtyRef": "庫存", "Accept submit": "接受來貨", "qc processing": "處理來貨及品檢", "putaway processing": "處理來貨及上架",