| @@ -191,6 +191,45 @@ function hasPendingActiveRowForStockInLine( | |||
| ); | |||
| } | |||
| /** | |||
| * True when this item still has a pending SOL that scan can bind to — including | |||
| * noLot / expired / unavailable rows (those are excluded from activeLotsByItemId). | |||
| * Used so a brand-new stockInLineId can still reach auto-switch. | |||
| */ | |||
| function hasPendingSwitchableRowForItem( | |||
| indexes: { byItemId: Map<number, any[]> }, | |||
| itemId: number, | |||
| processedByItemId: ProcessedStockOutLinesByItemId, | |||
| ): boolean { | |||
| const rows = indexes.byItemId.get(itemId) ?? []; | |||
| return rows.some( | |||
| (lot) => | |||
| isLotRowPending(lot) && | |||
| !isStockOutLineAlreadyProcessed(processedByItemId, itemId, lot.stockOutLineId), | |||
| ); | |||
| } | |||
| /** Allow scan when scanned SIL still needed, or item has any pending/noLot row for auto-switch. */ | |||
| function canProcessWorkbenchScanPick( | |||
| indexes: { | |||
| byStockInLineId: Map<number, any[]>; | |||
| activeLotsByItemId: Map<number, any[]>; | |||
| byItemId: Map<number, any[]>; | |||
| }, | |||
| itemId: number, | |||
| stockInLineId: number, | |||
| processedByItemId: ProcessedStockOutLinesByItemId, | |||
| ): boolean { | |||
| return ( | |||
| hasPendingActiveRowForStockInLine( | |||
| indexes, | |||
| itemId, | |||
| stockInLineId, | |||
| processedByItemId, | |||
| ) || hasPendingSwitchableRowForItem(indexes, itemId, processedByItemId) | |||
| ); | |||
| } | |||
| function findExactActiveMatchForStockInLine( | |||
| stockInLineLots: any[], | |||
| scannedItemId: number, | |||
| @@ -1625,7 +1664,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| const scannedStockInLineId = qrData.stockInLineId; | |||
| if ( | |||
| !hasPendingActiveRowForStockInLine( | |||
| !canProcessWorkbenchScanPick( | |||
| indexes, | |||
| scannedItemId, | |||
| scannedStockInLineId, | |||
| @@ -1730,20 +1769,38 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| console.log(`⚠️ [QR PROCESS] No active suggested lots, auto-switching to scanned lot.`); | |||
| // Find a rejected lot as expected lot (the one that was rejected) | |||
| const rejectedLot = allLotsForItem.find((lot: any) => | |||
| lot.stockOutLineStatus?.toLowerCase() === 'rejected' || | |||
| lot.lotAvailability === 'rejected' || | |||
| isInventoryLotLineUnavailable(lot) | |||
| // Prefer pending noLot / rejected / unavailable rows (not completed ones). | |||
| const switchableLot = allLotsForItem.find( | |||
| (lot: any) => | |||
| isLotRowPending(lot) && | |||
| !isStockOutLineAlreadyProcessed( | |||
| processedQrCombinations, | |||
| scannedItemId, | |||
| lot.stockOutLineId, | |||
| ) && | |||
| (lot.noLot === true || | |||
| lot.stockOutLineStatus?.toLowerCase() === "rejected" || | |||
| lot.lotAvailability === "rejected" || | |||
| isInventoryLotLineUnavailable(lot) || | |||
| isLotAvailabilityExpired(lot)), | |||
| ); | |||
| const expectedLot = | |||
| rejectedLot || | |||
| switchableLot || | |||
| pickExpectedLotForSubstitution( | |||
| allLotsForItem.filter( | |||
| (l: any) => l.lotNo != null && String(l.lotNo).trim() !== "" | |||
| ), | |||
| processedQrCombinations, | |||
| ) || | |||
| allLotsForItem.find( | |||
| (lot: any) => | |||
| isLotRowPending(lot) && | |||
| !isStockOutLineAlreadyProcessed( | |||
| processedQrCombinations, | |||
| scannedItemId, | |||
| lot.stockOutLineId, | |||
| ), | |||
| ) || | |||
| allLotsForItem[0]; | |||
| let scannedLotNo: string | null = scannedLot?.lotNo || null; | |||
| @@ -2210,7 +2267,7 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO | |||
| // Workbench 策略:不彈窗,直接切換到掃到的批次並提交一次掃描 | |||
| const mismatchCheckStartTime = performance.now(); | |||
| if ( | |||
| !hasPendingActiveRowForStockInLine( | |||
| !canProcessWorkbenchScanPick( | |||
| indexes, | |||
| scannedItemId, | |||
| scannedStockInLineId, | |||