diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index db9f8a8..a4e7931 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -579,6 +579,18 @@ export interface PickOrderLineWithLotsResponse { status: string | null; handler: string | null; lots: LotDetailResponse[]; + stockouts?: StockOutLineDetailResponse[]; +} + +export interface StockOutLineDetailResponse { + id: number | null; + status: string | null; + qty: number | null; + lotId: number | null; + lotNo: string | null; + location: string | null; + availableQty: number | null; + noLot: boolean; } export interface LotDetailResponse { diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index 6b3044a..9cdc773 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -474,6 +474,7 @@ export interface QrPickSubmitLineRequest { export interface UpdateStockOutLineStatusByQRCodeAndLotNoRequest { pickOrderLineId: number, inventoryLotNo: string, + stockInLineId?: number | null, stockOutLineId: number, itemId: number, status: string diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx index 3742fc0..adfdd97 100644 --- a/src/components/Jodetail/newJobPickExecution.tsx +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -516,8 +516,28 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const allLots: any[] = []; data.pickOrderLines.forEach((line) => { + // 用来记录这一行已经通过 lots 出现过的 lotId(避免 stockouts 再渲染一次) + const lotIdSet = new Set(); + + // lots:按 lotId 去重并合并 requiredQty(对齐 GoodPickExecutiondetail) if (line.lots && line.lots.length > 0) { - line.lots.forEach((lot) => { + const lotMap = new Map(); + + line.lots.forEach((lot: any) => { + const lotId = lot.lotId; + if (lotId == null) return; + if (lotMap.has(lotId)) { + const existingLot = lotMap.get(lotId); + existingLot.requiredQty = + (existingLot.requiredQty || 0) + (lot.requiredQty || 0); + } else { + lotMap.set(lotId, { ...lot }); + } + }); + + lotMap.forEach((lot: any) => { + if (lot.lotId != null) lotIdSet.add(lot.lotId); + allLots.push({ ...lot, pickOrderLineId: line.id, @@ -530,7 +550,6 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { pickOrderLineStatus: line.status, jobOrderId: data.pickOrder.jobOrder.id, jobOrderCode: data.pickOrder.jobOrder.code, - // 添加 pickOrder 信息(如果需要) pickOrderId: data.pickOrder.id, pickOrderCode: data.pickOrder.code, pickOrderConsoCode: data.pickOrder.consoCode, @@ -539,6 +558,60 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { pickOrderStatus: data.pickOrder.status, pickOrderAssignTo: data.pickOrder.assignTo, handler: line.handler, + noLot: false, + }); + }); + } + + // stockouts:用于“无 suggested lot / noLot”场景也显示并可 submit 0 闭环 + if (line.stockouts && line.stockouts.length > 0) { + line.stockouts.forEach((stockout: any) => { + const hasLot = stockout.lotId != null; + const lotAlreadyInLots = hasLot && lotIdSet.has(stockout.lotId as number); + + // 有批次 & 已经通过 lots 渲染过 → 跳过,避免一条变两行 + if (!stockout.noLot && lotAlreadyInLots) { + return; + } + + allLots.push({ + pickOrderLineId: line.id, + itemId: line.itemId, + itemCode: line.itemCode, + itemName: line.itemName, + uomCode: line.uomCode, + uomDesc: line.uomDesc, + pickOrderLineRequiredQty: line.requiredQty, + pickOrderLineStatus: line.status, + jobOrderId: data.pickOrder.jobOrder.id, + jobOrderCode: data.pickOrder.jobOrder.code, + pickOrderId: data.pickOrder.id, + pickOrderCode: data.pickOrder.code, + pickOrderConsoCode: data.pickOrder.consoCode, + pickOrderTargetDate: data.pickOrder.targetDate, + pickOrderType: data.pickOrder.type, + pickOrderStatus: data.pickOrder.status, + pickOrderAssignTo: data.pickOrder.assignTo, + handler: line.handler, + + lotId: stockout.lotId || null, + lotNo: stockout.lotNo || null, + expiryDate: null, + location: stockout.location || null, + availableQty: stockout.availableQty ?? 0, + requiredQty: line.requiredQty ?? 0, + actualPickQty: stockout.qty ?? 0, + processingStatus: stockout.status || "pending", + lotAvailability: stockout.noLot ? "insufficient_stock" : "available", + suggestedPickLotId: null, + stockOutLineId: stockout.id || null, + stockOutLineQty: stockout.qty ?? 0, + stockOutLineStatus: stockout.status || null, + stockInLineId: null, + routerIndex: stockout.noLot ? 999999 : null, + routerArea: null, + routerRoute: null, + noLot: !!stockout.noLot, }); }); } @@ -1011,30 +1084,57 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { console.log("Scanned Lot No:", scannedLotData.lotNo); console.log("Scanned StockInLineId:", scannedLotData.stockInLineId); - // Call confirmLotSubstitution to update the suggested lot - console.log("🔄 [LOT CONFIRM] Calling confirmLotSubstitution..."); - const substitutionResult = await confirmLotSubstitution({ - pickOrderLineId: selectedLotForQr.pickOrderLineId, - stockOutLineId: selectedLotForQr.stockOutLineId, - originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId, - newInventoryLotNo: scannedLotData.lotNo || '', - // ✅ required by LotSubstitutionConfirmRequest - newStockInLineId: scannedLotData?.stockInLineId ?? null, - }); + const originalSuggestedPickLotId = + selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId; + + // noLot / missing suggestedPickLotId 场景:没有 originalSuggestedPickLotId,改用 updateStockOutLineStatusByQRCodeAndLotNo + if (!originalSuggestedPickLotId) { + if (!selectedLotForQr?.stockOutLineId) { + throw new Error("Missing stockOutLineId for noLot line"); + } + console.log("🔄 [LOT CONFIRM] No originalSuggestedPickLotId, using updateStockOutLineStatusByQRCodeAndLotNo..."); + const res = await updateStockOutLineStatusByQRCodeAndLotNo({ + pickOrderLineId: selectedLotForQr.pickOrderLineId, + inventoryLotNo: scannedLotData.lotNo || '', + stockInLineId: scannedLotData?.stockInLineId ?? null, + stockOutLineId: selectedLotForQr.stockOutLineId, + itemId: selectedLotForQr.itemId, + status: "checked", + }); + console.log("✅ [LOT CONFIRM] updateStockOutLineStatusByQRCodeAndLotNo result:", res); + const ok = res?.code === "checked" || res?.code === "SUCCESS"; + if (!ok) { + setQrScanError(true); + setQrScanSuccess(false); + setQrScanErrorMsg(res?.message || "换批失败:无法更新 stock out line"); + return; + } + } else { + // Call confirmLotSubstitution to update the suggested lot + console.log("🔄 [LOT CONFIRM] Calling confirmLotSubstitution..."); + const substitutionResult = await confirmLotSubstitution({ + pickOrderLineId: selectedLotForQr.pickOrderLineId, + stockOutLineId: selectedLotForQr.stockOutLineId, + originalSuggestedPickLotId, + newInventoryLotNo: scannedLotData.lotNo || '', + // ✅ required by LotSubstitutionConfirmRequest + newStockInLineId: scannedLotData?.stockInLineId ?? null, + }); - console.log("✅ [LOT CONFIRM] Lot substitution result:", substitutionResult); + console.log("✅ [LOT CONFIRM] Lot substitution result:", substitutionResult); - // ✅ CRITICAL: substitution failed => DO NOT mark original stockOutLine as checked. - // Keep modal open so user can cancel/rescan. - if (!substitutionResult || substitutionResult.code !== "SUCCESS") { - console.error("❌ [LOT CONFIRM] Lot substitution failed. Will NOT update stockOutLine status."); - setQrScanError(true); - setQrScanSuccess(false); - setQrScanErrorMsg( - substitutionResult?.message || - `换批失败:stockInLineId ${scannedLotData?.stockInLineId ?? ""} 不存在或无法匹配` - ); - return; + // ✅ CRITICAL: substitution failed => DO NOT mark original stockOutLine as checked. + // Keep modal open so user can cancel/rescan. + if (!substitutionResult || substitutionResult.code !== "SUCCESS") { + console.error("❌ [LOT CONFIRM] Lot substitution failed. Will NOT update stockOutLine status."); + setQrScanError(true); + setQrScanSuccess(false); + setQrScanErrorMsg( + substitutionResult?.message || + `换批失败:stockInLineId ${scannedLotData?.stockInLineId ?? ""} 不存在或无法匹配` + ); + return; + } } // Update stock out line status to 'checked' after substitution @@ -1280,6 +1380,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { const res = await updateStockOutLineStatusByQRCodeAndLotNo({ pickOrderLineId: exactMatch.pickOrderLineId, inventoryLotNo: exactMatch.lotNo, + stockInLineId: exactMatch.stockInLineId ?? null, stockOutLineId: exactMatch.stockOutLineId, itemId: exactMatch.itemId, status: "checked", @@ -2152,6 +2253,9 @@ const sortedData = [...sourceData].sort((a, b) => { }; }, [isManualScanning, stopScan, resetScan]); const getStatusMessage = useCallback((lot: any) => { + if (lot?.noLot === true || lot?.lotAvailability === 'insufficient_stock') { + return t("This order is insufficient, please pick another lot."); + } switch (lot.stockOutLineStatus?.toLowerCase()) { case 'pending': return t("Please finish QR code scan and pick order.");