consumable pickOrder new ui like do consumable if user is 任文華, suggest and reusggest will reject W402 warehosueproduction
| @@ -30,6 +30,7 @@ import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus | |||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus | |||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderType | |||
| import com.ffii.fpsms.modules.pickOrder.service.PickOrderService | |||
| import com.ffii.fpsms.modules.pickOrder.service.ConsumableWorkbenchPickConstants | |||
| import com.ffii.fpsms.modules.pickOrder.service.assembleHierarchicalFgPayload | |||
| import com.ffii.fpsms.modules.stock.entity.Inventory | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | |||
| @@ -61,6 +62,7 @@ import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleasedDoPickOrderListIt | |||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.WorkbenchTicketReleaseTableResponse | |||
| import com.ffii.fpsms.modules.user.service.UserService | |||
| import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository | |||
| import com.ffii.fpsms.modules.jobOrder.service.JoWorkbenchPickConstants | |||
| import com.ffii.fpsms.modules.pickOrder.entity.TruckRepository | |||
| import kotlin.system.measureTimeMillis | |||
| import org.slf4j.LoggerFactory | |||
| @@ -239,6 +241,29 @@ open class DoWorkbenchMainService( | |||
| joPickOrderRepository.save(jpo) | |||
| } | |||
| /** | |||
| * Warehouse exclude list for workbench **re-suggest** after scan-pick (shortfall / lot split). | |||
| * - JO: same list as assign ([JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES]). | |||
| * - Consumable: hardcoded user [ConsumableWorkbenchPickConstants.HARDCODED_EXCLUDE_USER_ID] → JO list; else `null`. | |||
| * - DO / other: pass through request (`null` → service default excludes). | |||
| */ | |||
| private fun workbenchResuggestExcludeWarehouseCodes( | |||
| pickOrderId: Long?, | |||
| poType: PickOrderType?, | |||
| userId: Long?, | |||
| requestExcludeWarehouseCodes: List<String>?, | |||
| ): List<String>? { | |||
| val resolvedType = poType | |||
| ?: pickOrderId?.let { pickOrderRepository.findById(it).orElse(null)?.type } | |||
| if (resolvedType == PickOrderType.JOB_ORDER) { | |||
| return JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES.toList() | |||
| } | |||
| if (resolvedType == PickOrderType.Consumable && userId != null) { | |||
| return ConsumableWorkbenchPickConstants.resolveExcludeWarehouseCodes(userId) | |||
| } | |||
| return requestExcludeWarehouseCodes | |||
| } | |||
| /** | |||
| * Workbench scan-pick (DO FG): | |||
| * 1) Post outbound on scanned inventory lot line first; on failure return a clear message. | |||
| @@ -599,10 +624,12 @@ val saveSolMs = lapMs() | |||
| val pickOrderId = pol.pickOrder?.id | |||
| val poType = pol.pickOrder?.type | |||
| val effectiveExcludeWarehouseCodes = when (poType) { | |||
| PickOrderType.JOB_ORDER -> request.excludeWarehouseCodes ?: emptyList() | |||
| else -> request.excludeWarehouseCodes // null → DO 走 default;有傳則整份取代 default | |||
| } | |||
| val effectiveExcludeWarehouseCodes = workbenchResuggestExcludeWarehouseCodes( | |||
| pickOrderId = pickOrderId, | |||
| poType = poType, | |||
| userId = request.userId, | |||
| requestExcludeWarehouseCodes = request.excludeWarehouseCodes, | |||
| ) | |||
| val ledgerMs = measureTimeMillis { createWorkbenchPickLedger(sol, effectiveDelta) } | |||
| registerAfterCommit { | |||
| runInNewTransaction { | |||
| @@ -2127,6 +2154,12 @@ return MessageResponse( | |||
| var postMs = 0L | |||
| try { | |||
| if (pickOrderId != null) { | |||
| val resuggestExcludeWarehouseCodes = workbenchResuggestExcludeWarehouseCodes( | |||
| pickOrderId = pickOrderId, | |||
| poType = null, | |||
| userId = userId, | |||
| requestExcludeWarehouseCodes = effectiveExcludeWarehouseCodes, | |||
| ) | |||
| if (hasExplicitQty) { | |||
| rebuildMs = measureTimeMillis { | |||
| if (explicitRemainder > BigDecimal.ZERO) { | |||
| @@ -2135,13 +2168,13 @@ return MessageResponse( | |||
| targetQty = explicitRemainder, | |||
| storeId = requestStoreId, | |||
| excludeInventoryLotLineId = scannedIllId, | |||
| excludeWarehouseCodes = effectiveExcludeWarehouseCodes, | |||
| excludeWarehouseCodes = resuggestExcludeWarehouseCodes, | |||
| ) | |||
| } else { | |||
| suggestedPickLotWorkbenchService.setNoHoldSuggestionsForPickOrderLineExactQty( | |||
| pickOrderLineId = polId, | |||
| targetQty = BigDecimal.ZERO, | |||
| excludeWarehouseCodes = effectiveExcludeWarehouseCodes, | |||
| excludeWarehouseCodes = resuggestExcludeWarehouseCodes, | |||
| ) | |||
| } | |||
| } | |||
| @@ -2168,7 +2201,7 @@ return MessageResponse( | |||
| suggestedPickLotWorkbenchService.rebuildNoHoldSuggestionsForPickOrderLine( | |||
| pickOrderLineId = polId, | |||
| storeId = requestStoreId, | |||
| excludeWarehouseCodes = effectiveExcludeWarehouseCodes, | |||
| excludeWarehouseCodes = resuggestExcludeWarehouseCodes, | |||
| ) | |||
| } | |||
| ensureMs = measureTimeMillis { | |||
| @@ -50,35 +50,8 @@ open class JoWorkbenchMainService( | |||
| private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService, | |||
| private val stockOutLineWorkbenchService: StockOutLineWorkbenchService, | |||
| ) { | |||
| // Keep aligned with SuggestedPickLotWorkbenchService default excludes for diagnosis logs. | |||
| private val workbenchDefaultExcludeWarehouseCodes: Set<String> = setOf( | |||
| //"2F-W202-01-00", | |||
| //"2F-W200-#A-00", | |||
| "4F-W402-01-00", | |||
| "4F-W402-02-00", | |||
| "4F-W402-03-00", | |||
| "4F-W402-04-00", | |||
| "4F-W402-05-00", | |||
| "4F-W402-#A-00", | |||
| "4F-W402-#B-00", | |||
| "4F-W402-#C-00", | |||
| "4F-W402-#D-00", | |||
| "4F-W402-#E-00", | |||
| "4F-W402-#F-00", | |||
| "4F-W402-#G-00", | |||
| "4F-W402-#H-00", | |||
| "4F-W402-#I-00", | |||
| "4F-W402-#J-00", | |||
| "4F-W402-#K-00", | |||
| "4F-W402-#L-00", | |||
| "4F-W402-#M-00", | |||
| "4F-W402-#N-00", | |||
| "4F-W402-#O-00", | |||
| "4F-W402-#P-00", | |||
| "4F-W402-#Q-00", | |||
| "4F-W402-#R-00", | |||
| "4F-W402-#S-00" | |||
| ) | |||
| private val workbenchDefaultExcludeWarehouseCodes: Set<String> = | |||
| JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES | |||
| private fun debugPrintSuggestionNullReasons(pickOrderId: Long) { | |||
| val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) ?: return | |||
| @@ -0,0 +1,36 @@ | |||
| package com.ffii.fpsms.modules.jobOrder.service | |||
| /** | |||
| * JO workbench pick constants. | |||
| * | |||
| * [DEFAULT_EXCLUDE_WAREHOUSE_CODES] applies on **assign / first prime** ([JoWorkbenchMainService]) | |||
| * and on **scan-pick re-suggest** ([com.ffii.fpsms.modules.deliveryOrder.service.DoWorkbenchMainService]). | |||
| */ | |||
| object JoWorkbenchPickConstants { | |||
| val DEFAULT_EXCLUDE_WAREHOUSE_CODES: Set<String> = setOf( | |||
| "4F-W402-01-00", | |||
| "4F-W402-02-00", | |||
| "4F-W402-03-00", | |||
| "4F-W402-04-00", | |||
| "4F-W402-05-00", | |||
| "4F-W402-#A-00", | |||
| "4F-W402-#B-00", | |||
| "4F-W402-#C-00", | |||
| "4F-W402-#D-00", | |||
| "4F-W402-#E-00", | |||
| "4F-W402-#F-00", | |||
| "4F-W402-#G-00", | |||
| "4F-W402-#H-00", | |||
| "4F-W402-#I-00", | |||
| "4F-W402-#J-00", | |||
| "4F-W402-#K-00", | |||
| "4F-W402-#L-00", | |||
| "4F-W402-#M-00", | |||
| "4F-W402-#N-00", | |||
| "4F-W402-#O-00", | |||
| "4F-W402-#P-00", | |||
| "4F-W402-#Q-00", | |||
| "4F-W402-#R-00", | |||
| "4F-W402-#S-00", | |||
| ) | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| package com.ffii.fpsms.modules.pickOrder.service | |||
| import com.ffii.fpsms.modules.jobOrder.service.JoWorkbenchPickConstants | |||
| /** | |||
| * Temporary consumable workbench exclude list until per-user DB config (Scheme A) lands. | |||
| * | |||
| * - [HARDCODED_EXCLUDE_USER_ID] → same warehouses as JO ([JoWorkbenchPickConstants]). | |||
| * - All other users → `null` → DO 2F default excludes in [SuggestedPickLotWorkbenchService]. | |||
| */ | |||
| object ConsumableWorkbenchPickConstants { | |||
| const val HARDCODED_EXCLUDE_USER_ID: Long = 246L | |||
| fun resolveExcludeWarehouseCodes(userId: Long): List<String>? { | |||
| if (userId != HARDCODED_EXCLUDE_USER_ID) return null | |||
| return JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES.toList() | |||
| } | |||
| } | |||
| @@ -5,6 +5,7 @@ import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository | |||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | |||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus | |||
| import com.ffii.fpsms.modules.pickOrder.enums.PickOrderType | |||
| import com.ffii.fpsms.modules.pickOrder.web.models.PickOrderLineLotDetailResponse | |||
| import com.ffii.fpsms.modules.stock.service.StockOutLineWorkbenchService | |||
| import com.ffii.fpsms.modules.stock.service.SuggestedPickLotWorkbenchService | |||
| @@ -169,43 +170,76 @@ open class PickOrderWorkbenchService( | |||
| sol.id AS stockOutLineId, | |||
| sol.status AS stockOutLineStatus, | |||
| COALESCE(sol.qty, 0) AS stockOutLineQty, | |||
| sol.inventoryLotLineId AS inventoryLotLineId, | |||
| ill.id AS lotId, | |||
| il.lotNo AS lotNo, | |||
| il.stockInLineId AS stockInLineId, | |||
| DATE_FORMAT(il.expiryDate, '%Y-%m-%d') AS expiryDate, | |||
| w.name AS location, | |||
| w.code AS location, | |||
| COALESCE(ill.inQty,0) AS inQty, | |||
| COALESCE(ill.outQty,0) AS outQty, | |||
| COALESCE(ill.holdQty,0) AS holdQty, | |||
| (COALESCE(ill.inQty,0) - COALESCE(ill.outQty,0)) AS availableQty | |||
| (COALESCE(ill.inQty,0) - COALESCE(ill.outQty,0)) AS availableQty, | |||
| ill.status AS lotStatus, | |||
| spl.suggestedPickLotId AS suggestedPickLotId, | |||
| COALESCE(spl.splQty, sol.qty, 0) AS suggestedQty | |||
| FROM fpsmsdb.stock_out_line sol | |||
| LEFT JOIN fpsmsdb.inventory_lot_line ill ON ill.id = sol.inventoryLotLineId | |||
| LEFT JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId | |||
| LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId | |||
| LEFT JOIN ( | |||
| SELECT | |||
| s.stockOutLineId AS solId, | |||
| MAX(s.id) AS suggestedPickLotId, | |||
| SUM(COALESCE(s.qty, 0)) AS splQty | |||
| FROM fpsmsdb.suggested_pick_lot s | |||
| WHERE s.pickOrderLineId = :pickOrderLineId | |||
| AND s.deleted = false | |||
| AND s.stockOutLineId IS NOT NULL | |||
| GROUP BY s.stockOutLineId | |||
| ) spl ON spl.solId = sol.id | |||
| WHERE sol.pickOrderLineId = :pickOrderLineId | |||
| AND sol.deleted = false | |||
| ORDER BY sol.id | |||
| """.trimIndent() | |||
| val lotRows = jdbcDao.queryForList(sql, mapOf("pickOrderLineId" to polId)) | |||
| val zero = BigDecimal.ZERO | |||
| val lots = lotRows.map { r -> | |||
| val isNoLot = r["inventoryLotLineId"] == null | |||
| val inQty = toBigDecimal(r["inQty"]) ?: zero | |||
| val outQty = toBigDecimal(r["outQty"]) ?: zero | |||
| val availableQty = toBigDecimal(r["availableQty"]) ?: inQty.subtract(outQty) | |||
| val status = (r["stockOutLineStatus"]?.toString() ?: "").lowercase() | |||
| val lotStatusRaw = (r["lotStatus"]?.toString() ?: "").lowercase() | |||
| val suggestedQty = toBigDecimal(r["suggestedQty"]) ?: zero | |||
| val stockOutLineQty = toBigDecimal(r["stockOutLineQty"]) ?: zero | |||
| val lotAvailability = when { | |||
| isNoLot && status != "completed" -> "insufficient_stock" | |||
| status == "rejected" -> "rejected" | |||
| !isNoLot && lotStatusRaw == "unavailable" -> "status_unavailable" | |||
| !isNoLot && availableQty <= zero && status != "completed" -> "insufficient_stock" | |||
| else -> "available" | |||
| } | |||
| mapOf( | |||
| "id" to r["lotId"], | |||
| "lotNo" to r["lotNo"], | |||
| "expiryDate" to r["expiryDate"], | |||
| "location" to r["location"], | |||
| "stockUnit" to (pol.uom?.udfudesc ?: pol.uom?.code ?: ""), | |||
| "availableQty" to r["availableQty"], | |||
| "requiredQty" to pol.qty, | |||
| "actualPickQty" to r["stockOutLineQty"], | |||
| "inQty" to r["inQty"], | |||
| "outQty" to r["outQty"], | |||
| "holdQty" to r["holdQty"], | |||
| "lotStatus" to "available", | |||
| "lotAvailability" to "available", | |||
| "availableQty" to if (isNoLot) null else availableQty, | |||
| "requiredQty" to suggestedQty, | |||
| "actualPickQty" to stockOutLineQty, | |||
| "inQty" to if (isNoLot) null else inQty, | |||
| "outQty" to if (isNoLot) null else outQty, | |||
| "holdQty" to if (isNoLot) null else toBigDecimal(r["holdQty"]), | |||
| "lotStatus" to if (isNoLot) "unavailable" else r["lotStatus"], | |||
| "lotAvailability" to lotAvailability, | |||
| "noLot" to isNoLot, | |||
| "processingStatus" to (r["stockOutLineStatus"] ?: "pending"), | |||
| "suggestedPickLotId" to r["suggestedPickLotId"], | |||
| "stockOutLineId" to r["stockOutLineId"], | |||
| "stockOutLineStatus" to r["stockOutLineStatus"], | |||
| "stockOutLineQty" to r["stockOutLineQty"], | |||
| "stockOutLineQty" to stockOutLineQty, | |||
| "stockInLineId" to r["stockInLineId"], | |||
| ) | |||
| } | |||
| @@ -360,6 +394,7 @@ open class PickOrderWorkbenchService( | |||
| """.trimIndent() | |||
| val noLotRows = jdbcDao.queryForList(noLotSql, mapOf("pickOrderLineId" to pickOrderLineId)) | |||
| val noLot = noLotRows.map { r -> | |||
| val stockOutLineQty = toBigDecimal(r["stockOutLineQty"]) ?: zero | |||
| PickOrderLineLotDetailResponse( | |||
| lotId = null, | |||
| lotNo = null, | |||
| @@ -369,19 +404,19 @@ open class PickOrderWorkbenchService( | |||
| stockInLineId = null, | |||
| stockUnit = uomDesc, | |||
| availableQty = null, | |||
| requiredQty = pol.qty ?: zero, | |||
| requiredQty = stockOutLineQty, | |||
| inQty = null, | |||
| outQty = null, | |||
| holdQty = null, | |||
| actualPickQty = toBigDecimal(r["stockOutLineQty"]) ?: zero, | |||
| actualPickQty = stockOutLineQty, | |||
| suggestedPickLotId = null, | |||
| lotStatus = null, | |||
| lotStatus = "unavailable", | |||
| stockOutLineId = toLong(r["stockOutLineId"]), | |||
| stockOutLineStatus = r["stockOutLineStatus"]?.toString(), | |||
| stockOutLineQty = toBigDecimal(r["stockOutLineQty"]) ?: zero, | |||
| stockOutLineQty = stockOutLineQty, | |||
| totalPickedByAllPickOrders = null, | |||
| remainingAfterAllPickOrders = null, | |||
| lotAvailability = "available", | |||
| lotAvailability = "insufficient_stock", | |||
| noLot = true, | |||
| ) | |||
| } | |||
| @@ -407,10 +442,17 @@ open class PickOrderWorkbenchService( | |||
| errorPosition = null | |||
| ) | |||
| val excludeWarehouseCodes = | |||
| if (pickOrder.type == PickOrderType.Consumable) { | |||
| ConsumableWorkbenchPickConstants.resolveExcludeWarehouseCodes(userId) | |||
| } else { | |||
| null | |||
| } | |||
| val suggestSummary = suggestedPickLotWorkbenchService.primeNextSingleLotSuggestionsForPickOrder( | |||
| pickOrderId = pickOrderId, | |||
| storeId = null, | |||
| excludeWarehouseCodes = null, | |||
| excludeWarehouseCodes = excludeWarehouseCodes, | |||
| ) | |||
| val stockOutSummary = stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold( | |||
| pickOrderId = pickOrderId, | |||