Parcourir la source

Merge branch 'production' of http://svn.2fi-solutions.com:8300/derek/FPSMS-frontend into fix負數倉

do_workbench_fix
Harry Groves il y a 4 jours
Parent
révision
42160adf8b
10 fichiers modifiés avec 105 ajouts et 105 suppressions
  1. +1
    -1
      src/components/PoDetail/PoDetail.tsx
  2. +1
    -1
      src/components/PoDetail/PoDetailRow.tsx
  3. +1
    -1
      src/components/PoDetail/PoInputGrid.tsx
  4. +10
    -3
      src/components/PoDetail/PutAwayForm.tsx
  5. +3
    -2
      src/components/PoDetail/QcStockInModal.tsx
  6. +1
    -1
      src/components/PoDetail/StockInLineRowActions.tsx
  7. +60
    -0
      src/components/PoDetail/originSilPutAwayQty.ts
  8. +3
    -2
      src/components/PoDetail/stockQtyRound.ts
  9. +22
    -92
      src/components/PoSearch/PoSearch.tsx
  10. +3
    -2
      src/components/Qc/QcStockInModal.tsx

+ 1
- 1
src/components/PoDetail/PoDetail.tsx Voir le fichier

@@ -207,7 +207,7 @@ interface PolInputResult {
dnQty: string, 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 PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => {
const cameras = useContext(CameraContext); const cameras = useContext(CameraContext);
const { data: session } = useSession(); const { data: session } = useSession();


+ 1
- 1
src/components/PoDetail/PoDetailRow.tsx Voir le fichier

@@ -77,7 +77,7 @@ type Props = {
onSubmitted: (row: PurchaseOrderLine) => void; 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({ export const PoDetailRow = memo(function PoDetailRow({
row, row,
selected, selected,


+ 1
- 1
src/components/PoDetail/PoInputGrid.tsx Voir le fichier

@@ -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({ function PoInputGrid({
// qc, // qc,
setRows, setRows,


+ 10
- 3
src/components/PoDetail/PutAwayForm.tsx Voir le fichier

@@ -55,6 +55,7 @@ import dayjs from "dayjs";
import arraySupport from "dayjs/plugin/arraySupport"; import arraySupport from "dayjs/plugin/arraySupport";
import { dummyPutAwayLine } from "../Qc/dummyQcTemplate"; import { dummyPutAwayLine } from "../Qc/dummyQcTemplate";
import { GridRowModesModel } from "@mui/x-data-grid"; import { GridRowModesModel } from "@mui/x-data-grid";
import { displayPutAwayLineQtyFromOriginSil } from "./originSilPutAwayQty";
dayjs.extend(arraySupport); dayjs.extend(arraySupport);


interface Props { interface Props {
@@ -86,6 +87,7 @@ const style = {
width: "auto", 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 PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, suggestedLocationCode, setRowModesModel, setRowSelectionModel }) => {
const { t } = useTranslation("purchaseOrder"); const { t } = useTranslation("purchaseOrder");
const apiRef = useGridApiRef(); const apiRef = useGridApiRef();
@@ -241,8 +243,13 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, sugg
headerAlign: "right", headerAlign: "right",
align: "right", align: "right",
renderCell(params) { renderCell(params) {
const displayQty = displayPutAwayLineQtyFromOriginSil(
itemDetail,
Number(params.value),
params.row._isNew,
);
return <span style={{fontSize:24}}> return <span style={{fontSize:24}}>
{params.value}
{displayQty}
</span> </span>
} }
}, },
@@ -309,7 +316,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, sugg
// return <>100</> // return <>100</>
// }, // },
// }, // },
], [])
], [itemDetail, t])


const validation = useCallback( const validation = useCallback(
(newRow: GridRowModel<PutAwayRow>): EntryError => { (newRow: GridRowModel<PutAwayRow>): EntryError => {
@@ -395,7 +402,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, sugg
<TextField <TextField
label={t("acceptedPutawayQty")} // TODO: fix it back to acceptedQty after db is fixed label={t("acceptedPutawayQty")} // TODO: fix it back to acceptedQty after db is fixed
fullWidth fullWidth
value={itemDetail.qty ?? itemDetail.purchaseAcceptedQty ?? itemDetail.acceptedQty ?? itemDetail.demandQty}
value={itemDetail.purchaseAcceptedQty ?? itemDetail.acceptedQty ?? itemDetail.qty ?? itemDetail.demandQty}
disabled disabled
/> />
</Grid> </Grid>


+ 3
- 2
src/components/PoDetail/QcStockInModal.tsx Voir le fichier

@@ -21,6 +21,7 @@ import { useTranslation } from "react-i18next";
import StockInForm from "../StockIn/StockInForm"; import StockInForm from "../StockIn/StockInForm";
import QcComponent from "../Qc/QcComponent"; import QcComponent from "../Qc/QcComponent";
import PutAwayForm from "./PutAwayForm"; import PutAwayForm from "./PutAwayForm";
import { mapPutAwayLinesFromOriginSil } from "./originSilPutAwayQty";
import { GridRowModes, GridRowSelectionModel, useGridApiRef } from "@mui/x-data-grid"; import { GridRowModes, GridRowSelectionModel, useGridApiRef } from "@mui/x-data-grid";
import {msg, submitDialogWithWarning} from "../Swal/CustomAlerts"; import {msg, submitDialogWithWarning} from "../Swal/CustomAlerts";
import { INPUT_DATE_FORMAT, arrayToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil"; import { INPUT_DATE_FORMAT, arrayToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil";
@@ -71,7 +72,7 @@ interface CommonProps extends Omit<ModalProps, "children"> {
interface Props extends CommonProps { interface Props extends CommonProps {
// itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; // 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> = ({ const PoQcStockInModalVer2: React.FC<Props> = ({
open, open,
onClose, onClose,
@@ -205,7 +206,7 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
// escResult: (d.escResult && d.escResult?.length > 0) ? d.escResult : [], // escResult: (d.escResult && d.escResult?.length > 0) ? d.escResult : [],
// qcResult: (d.qcResult && d.qcResult?.length > 0) ? d.qcResult : [],//[...dummyQCData], // qcResult: (d.qcResult && d.qcResult?.length > 0) ? d.qcResult : [],//[...dummyQCData],
warehouseId: d.defaultWarehouseId ?? 1, warehouseId: d.defaultWarehouseId ?? 1,
putAwayLines: d.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false, _disableDelete: true})) ?? [],
putAwayLines: mapPutAwayLinesFromOriginSil(d, d.putAwayLines),
} as ModalFormInput } as ModalFormInput
) )
} return undefined } return undefined


+ 1
- 1
src/components/PoDetail/StockInLineRowActions.tsx Voir le fichier

@@ -26,7 +26,7 @@ type Props = {
onRound?: (mode: StockQtyRoundMode) => void; 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({ export default function StockInLineRowActions({
btnSx, btnSx,
onPrimaryClick, onPrimaryClick,


+ 60
- 0
src/components/PoDetail/originSilPutAwayQty.ts Voir le fichier

@@ -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;
}

+ 3
- 2
src/components/PoDetail/stockQtyRound.ts Voir le fichier

@@ -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. * Preview converted stock qty for a 來貨數 batch.
* Matches create-stock-in: ratio convert, then integer HALF_UP for 包/箱/PCS, else 2 decimals. * 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 * PO 收貨/品檢:pending 或 escalated 且庫存數量非整數時需選擇 CEILING / FLOOR
*/ */
export function needsPoQcStockQtyRound( export function needsPoQcStockQtyRound(
@@ -118,7 +119,7 @@ export function needsPoQcStockQtyRound(
return isNotIntegerQty(Number(acceptedQty ?? 0)); 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 { export function roundStockQty(before: number, mode: StockQtyRoundMode): number {
if (mode === "CEILING") return Math.ceil(before); if (mode === "CEILING") return Math.ceil(before);
return Math.floor(before); return Math.floor(before);


+ 22
- 92
src/components/PoSearch/PoSearch.tsx Voir le fichier

@@ -294,117 +294,47 @@ const PoSearch: React.FC<Props> = ({
if (typeof v === "string" && (v as string).trim() === "") return; if (typeof v === "string" && (v as string).trim() === "") return;
cleanedQuery[k] = String(v); 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 searchedCodeRaw = (filterArgs as any)?.code;
const searchedCode = const searchedCode =
typeof searchedCodeRaw === "string" ? searchedCodeRaw.trim() : ""; typeof searchedCodeRaw === "string" ? searchedCodeRaw.trim() : "";
const isM18PoCode = const isM18PoCode =
searchedCode.length > 14 && searchedCode.length > 14 &&
(searchedCode.startsWith("PP") || searchedCode.startsWith("PF")); (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 { 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" }, { 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); setFilteredPo(records);
setTotalCount(res.total ?? 0); 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) { } 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 { } finally {
setIsM18LookupLoading(false); setIsM18LookupLoading(false);
autoSyncInProgressRef.current = 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" /> <CircularProgress color="inherit" />
<Typography variant="body1"> <Typography variant="body1">
{autoSyncStatus || "正在從M18找尋PO..."}
{autoSyncStatus || "載入中..."}
</Typography> </Typography>
</Backdrop> </Backdrop>
</> </>


+ 3
- 2
src/components/Qc/QcStockInModal.tsx Voir le fichier

@@ -24,6 +24,7 @@ import PutAwayForm from "../PoDetail/PutAwayForm";
import { GridRowModes, GridRowSelectionModel, useGridApiRef } from "@mui/x-data-grid"; import { GridRowModes, GridRowSelectionModel, useGridApiRef } from "@mui/x-data-grid";
import {msg, submitDialogWithWarning} from "../Swal/CustomAlerts"; import {msg, submitDialogWithWarning} from "../Swal/CustomAlerts";
import { needsPoQcStockQtyRound } from "../PoDetail/stockQtyRound"; import { needsPoQcStockQtyRound } from "../PoDetail/stockQtyRound";
import { mapPutAwayLinesFromOriginSil } from "../PoDetail/originSilPutAwayQty";
import { INPUT_DATE_FORMAT, arrayToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil"; import { INPUT_DATE_FORMAT, arrayToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { fetchPoQrcode } from "@/app/api/pdf/actions"; import { fetchPoQrcode } from "@/app/api/pdf/actions";
@@ -73,7 +74,7 @@ interface Props extends CommonProps {
// itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; // 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> = ({ const QcStockInModal: React.FC<Props> = ({
open, open,
onClose, onClose,
@@ -231,7 +232,7 @@ const QcStockInModal: React.FC<Props> = ({
// escResult: (d.escResult && d.escResult?.length > 0) ? d.escResult : [], // escResult: (d.escResult && d.escResult?.length > 0) ? d.escResult : [],
// qcResult: (d.qcResult && d.qcResult?.length > 0) ? d.qcResult : [],//[...dummyQCData], // qcResult: (d.qcResult && d.qcResult?.length > 0) ? d.qcResult : [],//[...dummyQCData],
warehouseId: d.defaultWarehouseId ?? 1141, warehouseId: d.defaultWarehouseId ?? 1141,
putAwayLines: d.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false, _disableDelete: true})) ?? [],
putAwayLines: mapPutAwayLinesFromOriginSil(d, d.putAwayLines),
} as ModalFormInput } as ModalFormInput
) )
} return undefined } return undefined


Chargement…
Annuler
Enregistrer