| @@ -207,7 +207,7 @@ interface PolInputResult { | |||
| dnQty: string, | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ | |||
| const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| const cameras = useContext(CameraContext); | |||
| const { data: session } = useSession(); | |||
| @@ -77,7 +77,7 @@ type Props = { | |||
| onSubmitted: (row: PurchaseOrderLine) => void; | |||
| }; | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ | |||
| export const PoDetailRow = memo(function PoDetailRow({ | |||
| row, | |||
| selected, | |||
| @@ -155,7 +155,7 @@ class ProcessRowUpdateError extends Error { | |||
| } | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ | |||
| function PoInputGrid({ | |||
| // qc, | |||
| setRows, | |||
| @@ -55,6 +55,7 @@ import dayjs from "dayjs"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| import { dummyPutAwayLine } from "../Qc/dummyQcTemplate"; | |||
| import { GridRowModesModel } from "@mui/x-data-grid"; | |||
| import { displayPutAwayLineQtyFromOriginSil } from "./originSilPutAwayQty"; | |||
| dayjs.extend(arraySupport); | |||
| interface Props { | |||
| @@ -86,6 +87,7 @@ const style = { | |||
| width: "auto", | |||
| }; | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ | |||
| const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, suggestedLocationCode, setRowModesModel, setRowSelectionModel }) => { | |||
| const { t } = useTranslation("purchaseOrder"); | |||
| const apiRef = useGridApiRef(); | |||
| @@ -241,8 +243,13 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, sugg | |||
| headerAlign: "right", | |||
| align: "right", | |||
| renderCell(params) { | |||
| const displayQty = displayPutAwayLineQtyFromOriginSil( | |||
| itemDetail, | |||
| Number(params.value), | |||
| params.row._isNew, | |||
| ); | |||
| return <span style={{fontSize:24}}> | |||
| {params.value} | |||
| {displayQty} | |||
| </span> | |||
| } | |||
| }, | |||
| @@ -309,7 +316,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, sugg | |||
| // return <>100</> | |||
| // }, | |||
| // }, | |||
| ], []) | |||
| ], [itemDetail, t]) | |||
| const validation = useCallback( | |||
| (newRow: GridRowModel<PutAwayRow>): EntryError => { | |||
| @@ -395,7 +402,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, sugg | |||
| <TextField | |||
| label={t("acceptedPutawayQty")} // TODO: fix it back to acceptedQty after db is fixed | |||
| fullWidth | |||
| value={itemDetail.qty ?? itemDetail.purchaseAcceptedQty ?? itemDetail.acceptedQty ?? itemDetail.demandQty} | |||
| value={itemDetail.purchaseAcceptedQty ?? itemDetail.acceptedQty ?? itemDetail.qty ?? itemDetail.demandQty} | |||
| disabled | |||
| /> | |||
| </Grid> | |||
| @@ -21,6 +21,7 @@ import { useTranslation } from "react-i18next"; | |||
| import StockInForm from "../StockIn/StockInForm"; | |||
| import QcComponent from "../Qc/QcComponent"; | |||
| import PutAwayForm from "./PutAwayForm"; | |||
| import { mapPutAwayLinesFromOriginSil } from "./originSilPutAwayQty"; | |||
| import { GridRowModes, GridRowSelectionModel, useGridApiRef } from "@mui/x-data-grid"; | |||
| import {msg, submitDialogWithWarning} from "../Swal/CustomAlerts"; | |||
| import { INPUT_DATE_FORMAT, arrayToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil"; | |||
| @@ -71,7 +72,7 @@ interface CommonProps extends Omit<ModalProps, "children"> { | |||
| interface Props extends CommonProps { | |||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ | |||
| const PoQcStockInModalVer2: React.FC<Props> = ({ | |||
| open, | |||
| onClose, | |||
| @@ -205,7 +206,7 @@ const PoQcStockInModalVer2: React.FC<Props> = ({ | |||
| // escResult: (d.escResult && d.escResult?.length > 0) ? d.escResult : [], | |||
| // qcResult: (d.qcResult && d.qcResult?.length > 0) ? d.qcResult : [],//[...dummyQCData], | |||
| warehouseId: d.defaultWarehouseId ?? 1, | |||
| putAwayLines: d.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false, _disableDelete: true})) ?? [], | |||
| putAwayLines: mapPutAwayLinesFromOriginSil(d, d.putAwayLines), | |||
| } as ModalFormInput | |||
| ) | |||
| } return undefined | |||
| @@ -26,7 +26,7 @@ type Props = { | |||
| onRound?: (mode: StockQtyRoundMode) => void; | |||
| }; | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ | |||
| export default function StockInLineRowActions({ | |||
| btnSx, | |||
| onPrimaryClick, | |||
| @@ -0,0 +1,60 @@ | |||
| import { PutAwayLine, StockInLine } from "@/app/api/stockIn"; | |||
| type OriginSil = Pick< | |||
| StockInLine, | |||
| "jobOrderId" | "purchaseAcceptedQty" | "acceptedQty" | |||
| >; | |||
| /** | |||
| * FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 | |||
| * Path A: inventory_lot_line → inventory_lot → stock_in_line (the SIL that opened the lot). | |||
| * Never use inventory_lot_line.inQty — ADJ/TKE is added onto the same line later. | |||
| * | |||
| * PO: purchaseAcceptedQty (M18 / 採購單位) | |||
| * JO: acceptedQty (庫存/生產單位;JO 通常沒有 acceptedQtyM18) | |||
| */ | |||
| export function originSilPutAwayDisplayQty(origin: OriginSil): number { | |||
| if (origin.jobOrderId) { | |||
| const stock = Number(origin.acceptedQty); | |||
| return Number.isFinite(stock) && stock > 0 ? stock : 0; | |||
| } | |||
| const purchase = Number(origin.purchaseAcceptedQty); | |||
| if (Number.isFinite(purchase) && purchase > 0) return purchase; | |||
| const stock = Number(origin.acceptedQty); | |||
| return Number.isFinite(stock) && stock > 0 ? stock : 0; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ | |||
| export function originSilPutAwayStockQty(origin: OriginSil): number { | |||
| const stock = Number(origin.acceptedQty); | |||
| return Number.isFinite(stock) && stock > 0 ? stock : 0; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ | |||
| export function mapPutAwayLinesFromOriginSil( | |||
| origin: OriginSil, | |||
| lines: PutAwayLine[] | undefined, | |||
| ): PutAwayLine[] { | |||
| const existing = lines ?? []; | |||
| const displayQty = originSilPutAwayDisplayQty(origin); | |||
| const stockQty = originSilPutAwayStockQty(origin); | |||
| return existing.map((line) => ({ | |||
| ...line, | |||
| printQty: 1, | |||
| _isNew: false, | |||
| _disableDelete: true, | |||
| ...(displayQty > 0 ? { qty: displayQty } : {}), | |||
| ...(stockQty > 0 ? { stockQty } : {}), | |||
| })); | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ | |||
| export function displayPutAwayLineQtyFromOriginSil( | |||
| origin: OriginSil, | |||
| lineQty: number, | |||
| isNew: boolean | undefined, | |||
| ): number { | |||
| if (isNew) return lineQty; | |||
| const originQty = originSilPutAwayDisplayQty(origin); | |||
| return originQty > 0 ? originQty : lineQty; | |||
| } | |||
| @@ -81,6 +81,7 @@ function convertPoBatchToStockQty( | |||
| } | |||
| /** | |||
| * FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 | |||
| * Preview converted stock qty for a 來貨數 batch. | |||
| * Matches create-stock-in: ratio convert, then integer HALF_UP for 包/箱/PCS, else 2 decimals. | |||
| */ | |||
| @@ -104,7 +105,7 @@ export function isNotIntegerQty(qty: number): boolean { | |||
| } | |||
| /** | |||
| * FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10 | |||
| * FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 | |||
| * PO 收貨/品檢:pending 或 escalated 且庫存數量非整數時需選擇 CEILING / FLOOR | |||
| */ | |||
| export function needsPoQcStockQtyRound( | |||
| @@ -118,7 +119,7 @@ export function needsPoQcStockQtyRound( | |||
| return isNotIntegerQty(Number(acceptedQty ?? 0)); | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ | |||
| export function roundStockQty(before: number, mode: StockQtyRoundMode): number { | |||
| if (mode === "CEILING") return Math.ceil(before); | |||
| return Math.floor(before); | |||
| @@ -294,117 +294,47 @@ const PoSearch: React.FC<Props> = ({ | |||
| if (typeof v === "string" && (v as string).trim() === "") return; | |||
| cleanedQuery[k] = String(v); | |||
| }); | |||
| try { | |||
| const baseListResp = await clientAuthFetch( | |||
| `${NEXT_PUBLIC_API_URL}/po/list?${new URLSearchParams(cleanedQuery).toString()}`, | |||
| { method: "GET" }, | |||
| ); | |||
| if (!baseListResp.ok) { | |||
| throw new Error(`PO list fetch failed: ${baseListResp.status}`); | |||
| } | |||
| const res = await baseListResp.json(); | |||
| if (!res) return; | |||
| const records: PoResult[] = res.records ?? []; | |||
| const searchedCodeRaw = (filterArgs as any)?.code; | |||
| const searchedCode = | |||
| typeof searchedCodeRaw === "string" ? searchedCodeRaw.trim() : ""; | |||
| const isM18PoCode = | |||
| searchedCode.length > 14 && | |||
| (searchedCode.startsWith("PP") || searchedCode.startsWith("PF")); | |||
| const hasLocalPending = records.some( | |||
| (row) => String(row.status ?? "").toLowerCase() === "pending", | |||
| ); | |||
| const shouldLookupMissing = records.length === 0 && isM18PoCode; | |||
| const shouldRefreshPending = hasLocalPending && isM18PoCode; | |||
| if ( | |||
| (!shouldLookupMissing && !shouldRefreshPending) || | |||
| autoSyncInProgressRef.current | |||
| ) { | |||
| setFilteredPo(records); | |||
| setTotalCount(res.total); | |||
| return; | |||
| } | |||
| if (shouldRefreshPending) { | |||
| setFilteredPo(records); | |||
| setTotalCount(res.total); | |||
| if (isM18PoCode) { | |||
| cleanedQuery.refreshFromM18 = "true"; | |||
| setIsM18LookupLoading(true); | |||
| setAutoSyncStatus("載入中..."); | |||
| } | |||
| try { | |||
| autoSyncInProgressRef.current = true; | |||
| setIsM18LookupLoading(shouldLookupMissing); | |||
| setAutoSyncStatus( | |||
| shouldLookupMissing | |||
| ? "正在從M18找尋PO..." | |||
| : "正在檢查M18是否有更新...", | |||
| ); | |||
| const syncResp = await clientAuthFetch( | |||
| `${NEXT_PUBLIC_API_URL}/m18/test/po-by-code?code=${encodeURIComponent( | |||
| searchedCode, | |||
| )}&ifNewer=true`, | |||
| const baseListResp = await clientAuthFetch( | |||
| `${NEXT_PUBLIC_API_URL}/po/list?${new URLSearchParams(cleanedQuery).toString()}`, | |||
| { method: "GET" }, | |||
| ); | |||
| if (!syncResp.ok) { | |||
| throw new Error(`M18 sync failed: ${syncResp.status}`); | |||
| } | |||
| let syncJson: any = null; | |||
| try { | |||
| syncJson = await syncResp.json(); | |||
| } catch { | |||
| // Some endpoints may respond with plain text | |||
| const txt = await syncResp.text(); | |||
| syncJson = { raw: txt }; | |||
| } | |||
| const syncOk = Boolean(syncJson?.totalSuccess && syncJson.totalSuccess > 0); | |||
| const skippedIfNewer = String(syncJson?.query ?? "").includes( | |||
| "skipped (ifNewer)", | |||
| ); | |||
| if (syncOk) { | |||
| setAutoSyncStatus( | |||
| shouldLookupMissing ? "成功找到PO" : "已從M18更新PO", | |||
| ); | |||
| const listResp = await clientAuthFetch( | |||
| `${NEXT_PUBLIC_API_URL}/po/list?${new URLSearchParams( | |||
| cleanedQuery, | |||
| ).toString()}`, | |||
| { method: "GET" }, | |||
| ); | |||
| if (listResp.ok) { | |||
| const listJson = await listResp.json(); | |||
| setFilteredPo(listJson.records ?? []); | |||
| setTotalCount(listJson.total ?? 0); | |||
| setAutoSyncStatus( | |||
| shouldLookupMissing ? "成功找到PO" : "已從M18更新PO", | |||
| ); | |||
| return; | |||
| } | |||
| setAutoSyncStatus(shouldLookupMissing ? "找不到PO" : null); | |||
| } else if (skippedIfNewer || shouldRefreshPending) { | |||
| setAutoSyncStatus(null); | |||
| } else { | |||
| setAutoSyncStatus("找不到PO"); | |||
| if (!baseListResp.ok) { | |||
| throw new Error(`PO list fetch failed: ${baseListResp.status}`); | |||
| } | |||
| const res = await baseListResp.json(); | |||
| if (!res) return; | |||
| const records: PoResult[] = res.records ?? []; | |||
| setFilteredPo(records); | |||
| setTotalCount(res.total ?? 0); | |||
| if (isM18PoCode) { | |||
| const refreshAction = baseListResp.headers.get("X-M18-Po-Refresh"); | |||
| if (refreshAction === "synced") { | |||
| setAutoSyncStatus(records.length > 0 ? "已從M18同步PO" : "找不到PO"); | |||
| } else { | |||
| setAutoSyncStatus(null); | |||
| } | |||
| } | |||
| } catch (e) { | |||
| console.error("Auto sync error:", e); | |||
| setAutoSyncStatus(shouldLookupMissing ? "找不到PO" : null); | |||
| setFilteredPo(records); | |||
| setTotalCount(res.total ?? 0); | |||
| console.error("PO list fetch error:", e); | |||
| if (isM18PoCode) setAutoSyncStatus("找不到PO"); | |||
| } finally { | |||
| setIsM18LookupLoading(false); | |||
| autoSyncInProgressRef.current = false; | |||
| } | |||
| } catch (e) { | |||
| console.error("PO list fetch error:", e); | |||
| } | |||
| }, | |||
| [], | |||
| ); | |||
| @@ -501,7 +431,7 @@ const PoSearch: React.FC<Props> = ({ | |||
| > | |||
| <CircularProgress color="inherit" /> | |||
| <Typography variant="body1"> | |||
| {autoSyncStatus || "正在從M18找尋PO..."} | |||
| {autoSyncStatus || "載入中..."} | |||
| </Typography> | |||
| </Backdrop> | |||
| </> | |||
| @@ -24,6 +24,7 @@ import PutAwayForm from "../PoDetail/PutAwayForm"; | |||
| import { GridRowModes, GridRowSelectionModel, useGridApiRef } from "@mui/x-data-grid"; | |||
| import {msg, submitDialogWithWarning} from "../Swal/CustomAlerts"; | |||
| import { needsPoQcStockQtyRound } from "../PoDetail/stockQtyRound"; | |||
| import { mapPutAwayLinesFromOriginSil } from "../PoDetail/originSilPutAwayQty"; | |||
| import { INPUT_DATE_FORMAT, arrayToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil"; | |||
| import dayjs from "dayjs"; | |||
| import { fetchPoQrcode } from "@/app/api/pdf/actions"; | |||
| @@ -73,7 +74,7 @@ interface Props extends CommonProps { | |||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
| } | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10 */ | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.5 | 2026-09-18 */ | |||
| const QcStockInModal: React.FC<Props> = ({ | |||
| open, | |||
| onClose, | |||
| @@ -231,7 +232,7 @@ const QcStockInModal: React.FC<Props> = ({ | |||
| // escResult: (d.escResult && d.escResult?.length > 0) ? d.escResult : [], | |||
| // qcResult: (d.qcResult && d.qcResult?.length > 0) ? d.qcResult : [],//[...dummyQCData], | |||
| warehouseId: d.defaultWarehouseId ?? 1141, | |||
| putAwayLines: d.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false, _disableDelete: true})) ?? [], | |||
| putAwayLines: mapPutAwayLinesFromOriginSil(d, d.putAwayLines), | |||
| } as ModalFormInput | |||
| ) | |||
| } return undefined | |||