| @@ -1,5 +1,6 @@ | |||||
| package com.ffii.fpsms.modules.deliveryOrder.service | package com.ffii.fpsms.modules.deliveryOrder.service | ||||
| import com.ffii.fpsms.modules.jobOrder.service.JoWorkbenchPickConstants | |||||
| import com.ffii.fpsms.modules.settings.entity.SettingsRepository | import com.ffii.fpsms.modules.settings.entity.SettingsRepository | ||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import java.util.Locale | import java.util.Locale | ||||
| @@ -12,9 +13,22 @@ open class DoFloorSupplierSettingsService( | |||||
| companion object { | companion object { | ||||
| private const val SETTING_DO_FLOOR_SUPPLIERS_2F = "DO.floor.suppliers.2F" | private const val SETTING_DO_FLOOR_SUPPLIERS_2F = "DO.floor.suppliers.2F" | ||||
| private const val SETTING_DO_FLOOR_SUPPLIERS_4F = "DO.floor.suppliers.4F" | private const val SETTING_DO_FLOOR_SUPPLIERS_4F = "DO.floor.suppliers.4F" | ||||
| const val SETTING_DO_PICK_EXCLUDE_WAREHOUSES = "DO.pick.excludeWarehouses" | |||||
| const val SETTING_JO_PICK_EXCLUDE_WAREHOUSES = "JO.pick.excludeWarehouses" | |||||
| const val SETTING_DO_PICK_EXCLUDE_PRINT_MODE = "DO.pick.excludePrintMode" | |||||
| const val SETTING_JO_PICK_EXCLUDE_PRINT_MODE = "JO.pick.excludePrintMode" | |||||
| const val PRINT_MODE_HIDE_LIST = "hideList" | |||||
| const val PRINT_MODE_HIDE_QR = "hideQr" | |||||
| /** Saved when the list is cleared. Settings update rejects a blank value. */ | |||||
| const val EMPTY_CODE_LIST_MARKER = "-" | |||||
| private val DEFAULT_SUPPLIERS_2F = listOf("P07", "P06D", "P06Y") | private val DEFAULT_SUPPLIERS_2F = listOf("P07", "P06D", "P06Y") | ||||
| private val DEFAULT_SUPPLIERS_4F = listOf("P06B") | private val DEFAULT_SUPPLIERS_4F = listOf("P06B") | ||||
| private val DEFAULT_DO_PICK_EXCLUDE_WAREHOUSES = listOf( | |||||
| "2F-W202-01-00", | |||||
| "2F-W200-#A-00", | |||||
| ) | |||||
| } | } | ||||
| open fun supplierCodesFromSetting(settingName: String, defaultList: List<String>): List<String> { | open fun supplierCodesFromSetting(settingName: String, defaultList: List<String>): List<String> { | ||||
| @@ -32,6 +46,54 @@ open class DoFloorSupplierSettingsService( | |||||
| return suppliers2F to suppliers4F | return suppliers2F to suppliers4F | ||||
| } | } | ||||
| /** DO pick / workbench default: lots in these warehouses are not suggested. */ | |||||
| open fun loadDoPickExcludeWarehouseCodes(): Set<String> = | |||||
| warehouseCodesFromSettingOrDefault( | |||||
| SETTING_DO_PICK_EXCLUDE_WAREHOUSES, | |||||
| DEFAULT_DO_PICK_EXCLUDE_WAREHOUSES, | |||||
| ) | |||||
| /** JO workbench (and consumable user 246): lots in these warehouses are not suggested. */ | |||||
| open fun loadJoPickExcludeWarehouseCodes(): Set<String> = | |||||
| warehouseCodesFromSettingOrDefault( | |||||
| SETTING_JO_PICK_EXCLUDE_WAREHOUSES, | |||||
| JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES.toList(), | |||||
| ) | |||||
| /** | |||||
| * hideList: excluded warehouses are omitted from the print list, and scanning their physical QR does not pass. | |||||
| * hideQr: they stay on the list without a QR, and scanning the physical QR still passes. | |||||
| * Missing row is hideList. | |||||
| */ | |||||
| open fun loadDoPickExcludePrintMode(): String = printModeFromSetting(SETTING_DO_PICK_EXCLUDE_PRINT_MODE) | |||||
| open fun loadJoPickExcludePrintMode(): String = printModeFromSetting(SETTING_JO_PICK_EXCLUDE_PRINT_MODE) | |||||
| open fun rejectsScanOfExcludedWarehouse(printMode: String): Boolean = | |||||
| printMode != PRINT_MODE_HIDE_QR | |||||
| private fun printModeFromSetting(settingName: String): String { | |||||
| val raw = settingsRepository.findByName(settingName).map { it.value }.orElse(null)?.trim().orEmpty() | |||||
| return if (raw == PRINT_MODE_HIDE_QR) PRINT_MODE_HIDE_QR else PRINT_MODE_HIDE_LIST | |||||
| } | |||||
| /** | |||||
| * Missing row uses [defaultList]. | |||||
| * A saved blank value or [EMPTY_CODE_LIST_MARKER] means exclude nothing. | |||||
| */ | |||||
| open fun warehouseCodesFromSettingOrDefault(settingName: String, defaultList: List<String>): Set<String> { | |||||
| val row = settingsRepository.findByName(settingName) | |||||
| if (row.isEmpty) { | |||||
| return defaultList.map { it.trim().uppercase(Locale.ROOT) }.filter { it.isNotEmpty() }.toSet() | |||||
| } | |||||
| val raw = row.get().value?.trim().orEmpty() | |||||
| if (raw.isEmpty() || raw == EMPTY_CODE_LIST_MARKER) return emptySet() | |||||
| return raw.split(",") | |||||
| .map { it.trim().uppercase(Locale.ROOT) } | |||||
| .filter { it.isNotEmpty() && it != EMPTY_CODE_LIST_MARKER } | |||||
| .toSet() | |||||
| } | |||||
| open fun allowedSupplierCodesForFloor(floor: String?): List<String> { | open fun allowedSupplierCodesForFloor(floor: String?): List<String> { | ||||
| val f = floor?.trim()?.uppercase(Locale.ROOT).orEmpty() | val f = floor?.trim()?.uppercase(Locale.ROOT).orEmpty() | ||||
| val (codes2F, codes4F) = loadDoFloorSupplierLists() | val (codes2F, codes4F) = loadDoFloorSupplierLists() | ||||
| @@ -63,7 +63,6 @@ import com.ffii.fpsms.modules.deliveryOrder.web.models.ReleasedDoPickOrderListIt | |||||
| import com.ffii.fpsms.modules.deliveryOrder.web.models.WorkbenchTicketReleaseTableResponse | import com.ffii.fpsms.modules.deliveryOrder.web.models.WorkbenchTicketReleaseTableResponse | ||||
| import com.ffii.fpsms.modules.user.service.UserService | import com.ffii.fpsms.modules.user.service.UserService | ||||
| import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository | 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 com.ffii.fpsms.modules.pickOrder.entity.TruckRepository | ||||
| import kotlin.system.measureTimeMillis | import kotlin.system.measureTimeMillis | ||||
| import org.slf4j.LoggerFactory | import org.slf4j.LoggerFactory | ||||
| @@ -246,9 +245,9 @@ open class DoWorkbenchMainService( | |||||
| /** | /** | ||||
| * FP-MTMS Version Checklist | Functions Ref. No. 38 | v1.0.2 | 2026-08-13 | * FP-MTMS Version Checklist | Functions Ref. No. 38 | v1.0.2 | 2026-08-13 | ||||
| * Warehouse exclude list for workbench **re-suggest** after scan-pick (shortfall / lot split). | * Warehouse exclude list for workbench **re-suggest** after scan-pick (shortfall / lot split). | ||||
| * - JO: same list as assign ([JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES]). | |||||
| * - JO: setting `JO.pick.excludeWarehouses` (fallback is JoWorkbenchPickConstants when the row is missing). | |||||
| * - Consumable: hardcoded user [ConsumableWorkbenchPickConstants.HARDCODED_EXCLUDE_USER_ID] → JO list; else empty (no limit). | * - Consumable: hardcoded user [ConsumableWorkbenchPickConstants.HARDCODED_EXCLUDE_USER_ID] → JO list; else empty (no limit). | ||||
| * - DO / other: pass through request (`null` → service default excludes). | |||||
| * - DO / other: pass through request (`null` → `DO.pick.excludeWarehouses`). | |||||
| */ | */ | ||||
| private fun workbenchResuggestExcludeWarehouseCodes( | private fun workbenchResuggestExcludeWarehouseCodes( | ||||
| pickOrderId: Long?, | pickOrderId: Long?, | ||||
| @@ -259,14 +258,49 @@ open class DoWorkbenchMainService( | |||||
| val resolvedType = poType | val resolvedType = poType | ||||
| ?: pickOrderId?.let { pickOrderRepository.findById(it).orElse(null)?.type } | ?: pickOrderId?.let { pickOrderRepository.findById(it).orElse(null)?.type } | ||||
| if (resolvedType == PickOrderType.JOB_ORDER) { | if (resolvedType == PickOrderType.JOB_ORDER) { | ||||
| return JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES.toList() | |||||
| return doFloorSupplierSettingsService.loadJoPickExcludeWarehouseCodes().toList() | |||||
| } | } | ||||
| if (resolvedType == PickOrderType.Consumable && userId != null) { | if (resolvedType == PickOrderType.Consumable && userId != null) { | ||||
| return ConsumableWorkbenchPickConstants.resolveExcludeWarehouseCodes(userId) | |||||
| return ConsumableWorkbenchPickConstants.resolveExcludeWarehouseCodes( | |||||
| userId, | |||||
| doFloorSupplierSettingsService.loadJoPickExcludeWarehouseCodes(), | |||||
| ) | |||||
| } | } | ||||
| return requestExcludeWarehouseCodes | return requestExcludeWarehouseCodes | ||||
| } | } | ||||
| /** | |||||
| * hideList: a lot in an excluded warehouse is not on the print list, and scanning its physical QR does not pass. | |||||
| * hideQr: the lot stays on the list without a printed QR, and scanning the physical QR still passes. | |||||
| */ | |||||
| private fun rejectScanWhenWarehouseHiddenFromList( | |||||
| pickOrderType: PickOrderType?, | |||||
| userId: Long?, | |||||
| warehouseCode: String?, | |||||
| ): String? { | |||||
| val code = warehouseCode?.trim()?.uppercase(Locale.ROOT).orEmpty() | |||||
| if (code.isEmpty()) return null | |||||
| val joCodes = doFloorSupplierSettingsService.loadJoPickExcludeWarehouseCodes() | |||||
| val (codes, printMode) = when (pickOrderType) { | |||||
| PickOrderType.JOB_ORDER -> joCodes to doFloorSupplierSettingsService.loadJoPickExcludePrintMode() | |||||
| PickOrderType.Consumable -> { | |||||
| val limited = if (userId == null) { | |||||
| emptySet() | |||||
| } else { | |||||
| ConsumableWorkbenchPickConstants.resolveExcludeWarehouseCodes(userId, joCodes) | |||||
| .map { it.uppercase(Locale.ROOT) } | |||||
| .toSet() | |||||
| } | |||||
| limited to doFloorSupplierSettingsService.loadJoPickExcludePrintMode() | |||||
| } | |||||
| else -> doFloorSupplierSettingsService.loadDoPickExcludeWarehouseCodes() to | |||||
| doFloorSupplierSettingsService.loadDoPickExcludePrintMode() | |||||
| } | |||||
| if (code !in codes) return null | |||||
| if (!doFloorSupplierSettingsService.rejectsScanOfExcludedWarehouse(printMode)) return null | |||||
| return "此儲位不在揀貨清單,掃到實物 QR 也不可以揀。" | |||||
| } | |||||
| /** | /** | ||||
| * FP-MTMS Version Checklist | Functions Ref. No. 38 | v1.0.2 | 2026-08-13 | * FP-MTMS Version Checklist | Functions Ref. No. 38 | v1.0.2 | 2026-08-13 | ||||
| * Workbench scan-pick (DO FG): | * Workbench scan-pick (DO FG): | ||||
| @@ -450,6 +484,18 @@ open class DoWorkbenchMainService( | |||||
| println("Scanned lot does not match pick line item") | println("Scanned lot does not match pick line item") | ||||
| } | } | ||||
| val hiddenWarehouseReject = rejectScanWhenWarehouseHiddenFromList( | |||||
| pickOrderType = pol.pickOrder?.type, | |||||
| userId = request.userId, | |||||
| warehouseCode = scannedIll.warehouse?.code, | |||||
| ) | |||||
| if (hiddenWarehouseReject != null) { | |||||
| return MessageResponse( | |||||
| id = sol.id, name = "scan_pick", code = "INVALID", type = "workbench_scan_pick", | |||||
| message = hiddenWarehouseReject, errorPosition = null, entity = null, | |||||
| ) | |||||
| } | |||||
| sol.inventoryLotLine = scannedIll | sol.inventoryLotLine = scannedIll | ||||
| val required = pol.qty ?: BigDecimal.ZERO | val required = pol.qty ?: BigDecimal.ZERO | ||||
| @@ -1,5 +1,6 @@ | |||||
| package com.ffii.fpsms.modules.jobOrder.service | package com.ffii.fpsms.modules.jobOrder.service | ||||
| import com.ffii.fpsms.modules.deliveryOrder.service.DoFloorSupplierSettingsService | |||||
| import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository | import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository | ||||
| import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository | import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository | ||||
| import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus | import com.ffii.fpsms.modules.jobOrder.enums.JobOrderStatus | ||||
| @@ -50,9 +51,8 @@ open class JoWorkbenchMainService( | |||||
| private val stockOutRepository: StockOutRepository, | private val stockOutRepository: StockOutRepository, | ||||
| private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService, | private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService, | ||||
| private val stockOutLineWorkbenchService: StockOutLineWorkbenchService, | private val stockOutLineWorkbenchService: StockOutLineWorkbenchService, | ||||
| private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, | |||||
| ) { | ) { | ||||
| private val workbenchDefaultExcludeWarehouseCodes: Set<String> = | |||||
| JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES | |||||
| private fun debugPrintSuggestionNullReasons(pickOrderId: Long) { | private fun debugPrintSuggestionNullReasons(pickOrderId: Long) { | ||||
| val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) ?: return | val pickOrder = pickOrderRepository.findById(pickOrderId).orElse(null) ?: return | ||||
| @@ -80,7 +80,7 @@ open class JoWorkbenchMainService( | |||||
| val notExpired = availableStatus.filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } | val notExpired = availableStatus.filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } | ||||
| val notExcluded = notExpired.filter { lot -> | val notExcluded = notExpired.filter { lot -> | ||||
| val code = lot.warehouse?.code?.trim()?.uppercase(Locale.ROOT) | val code = lot.warehouse?.code?.trim()?.uppercase(Locale.ROOT) | ||||
| code.isNullOrEmpty() || code !in workbenchDefaultExcludeWarehouseCodes | |||||
| code.isNullOrEmpty() || code !in doFloorSupplierSettingsService.loadJoPickExcludeWarehouseCodes() | |||||
| } | } | ||||
| val positiveQty = notExcluded.filter { lot -> | val positiveQty = notExcluded.filter { lot -> | ||||
| val inQ = lot.inQty ?: BigDecimal.ZERO | val inQ = lot.inQty ?: BigDecimal.ZERO | ||||
| @@ -539,7 +539,7 @@ open class JoWorkbenchMainService( | |||||
| suggestedPickLotWorkbenchService.primeNextSingleLotSuggestionsForPickOrder( | suggestedPickLotWorkbenchService.primeNextSingleLotSuggestionsForPickOrder( | ||||
| pickOrderId = pickOrderId, | pickOrderId = pickOrderId, | ||||
| storeId = null, | storeId = null, | ||||
| excludeWarehouseCodes = workbenchDefaultExcludeWarehouseCodes.toList() | |||||
| excludeWarehouseCodes = doFloorSupplierSettingsService.loadJoPickExcludeWarehouseCodes().toList() | |||||
| ) | ) | ||||
| stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(pickOrderId, userId) | stockOutLineWorkbenchService.ensureStockOutLinesForPickOrderNoHold(pickOrderId, userId) | ||||
| val splAfter = loadSplDebugRowsByPol(pickOrderId) | val splAfter = loadSplDebugRowsByPol(pickOrderId) | ||||
| @@ -3,8 +3,8 @@ package com.ffii.fpsms.modules.jobOrder.service | |||||
| /** | /** | ||||
| * JO workbench pick constants. | * 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]). | |||||
| * [DEFAULT_EXCLUDE_WAREHOUSE_CODES] is the fallback when setting `JO.pick.excludeWarehouses` is missing. | |||||
| * Assign / first prime and scan-pick re-suggest read the setting at runtime. | |||||
| * JO re-suggest keeps [storeId] null (same as assign); it does not use DO floor store resolution. | * JO re-suggest keeps [storeId] null (same as assign); it does not use DO floor store resolution. | ||||
| */ | */ | ||||
| object JoWorkbenchPickConstants { | object JoWorkbenchPickConstants { | ||||
| @@ -1,21 +1,19 @@ | |||||
| package com.ffii.fpsms.modules.pickOrder.service | 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. | * Temporary consumable workbench exclude list until per-user DB config (Scheme A) lands. | ||||
| * | * | ||||
| * - [HARDCODED_EXCLUDE_USER_ID] → same warehouses as JO ([JoWorkbenchPickConstants]). | |||||
| * - [HARDCODED_EXCLUDE_USER_ID] → the JO exclude list passed in (setting `JO.pick.excludeWarehouses`). | |||||
| * - All other users → empty list (no warehouse limit). Do not return null: | * - All other users → empty list (no warehouse limit). Do not return null: | ||||
| * [SuggestedPickLotWorkbenchService] treats null as DO default 2F excludes. | |||||
| * [com.ffii.fpsms.modules.stock.service.SuggestedPickLotWorkbenchService] treats null as the DO exclude setting. | |||||
| * Consumable re-suggest keeps storeId null (same as first prime / JO); it does not use DO floor store resolution. | * Consumable re-suggest keeps storeId null (same as first prime / JO); it does not use DO floor store resolution. | ||||
| */ | */ | ||||
| object ConsumableWorkbenchPickConstants { | object ConsumableWorkbenchPickConstants { | ||||
| const val HARDCODED_EXCLUDE_USER_ID: Long = 246L | const val HARDCODED_EXCLUDE_USER_ID: Long = 246L | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 66 | v1.0.0 | 2026-08-13 */ | /** FP-MTMS Version Checklist | Functions Ref. No. 66 | v1.0.0 | 2026-08-13 */ | ||||
| fun resolveExcludeWarehouseCodes(userId: Long): List<String> { | |||||
| fun resolveExcludeWarehouseCodes(userId: Long, joExcludeWarehouseCodes: Collection<String>): List<String> { | |||||
| if (userId != HARDCODED_EXCLUDE_USER_ID) return emptyList() | if (userId != HARDCODED_EXCLUDE_USER_ID) return emptyList() | ||||
| return JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES.toList() | |||||
| return joExcludeWarehouseCodes.map { it.trim() }.filter { it.isNotEmpty() } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,6 +1,7 @@ | |||||
| package com.ffii.fpsms.modules.pickOrder.service | package com.ffii.fpsms.modules.pickOrder.service | ||||
| import com.ffii.core.support.JdbcDao | import com.ffii.core.support.JdbcDao | ||||
| import com.ffii.fpsms.modules.deliveryOrder.service.DoFloorSupplierSettingsService | |||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | import com.ffii.fpsms.modules.master.web.models.MessageResponse | ||||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository | import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository | ||||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | ||||
| @@ -23,6 +24,7 @@ open class PickOrderWorkbenchService( | |||||
| private val userService: UserService, | private val userService: UserService, | ||||
| private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService, | private val suggestedPickLotWorkbenchService: SuggestedPickLotWorkbenchService, | ||||
| private val stockOutLineWorkbenchService: StockOutLineWorkbenchService, | private val stockOutLineWorkbenchService: StockOutLineWorkbenchService, | ||||
| private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, | |||||
| ) { | ) { | ||||
| private fun toBigDecimal(v: Any?): BigDecimal? = when (v) { | private fun toBigDecimal(v: Any?): BigDecimal? = when (v) { | ||||
| null -> null | null -> null | ||||
| @@ -447,7 +449,10 @@ open class PickOrderWorkbenchService( | |||||
| val excludeWarehouseCodes = | val excludeWarehouseCodes = | ||||
| if (pickOrder.type == PickOrderType.Consumable) { | if (pickOrder.type == PickOrderType.Consumable) { | ||||
| ConsumableWorkbenchPickConstants.resolveExcludeWarehouseCodes(userId) | |||||
| ConsumableWorkbenchPickConstants.resolveExcludeWarehouseCodes( | |||||
| userId, | |||||
| doFloorSupplierSettingsService.loadJoPickExcludeWarehouseCodes(), | |||||
| ) | |||||
| } else { | } else { | ||||
| null | null | ||||
| } | } | ||||
| @@ -106,10 +106,7 @@ open class SuggestedPickLotService( | |||||
| val suggestedList: MutableList<SuggestedPickLot> = mutableListOf() | val suggestedList: MutableList<SuggestedPickLot> = mutableListOf() | ||||
| val holdQtyMap: MutableMap<Long?, BigDecimal?> = request.holdQtyMap | val holdQtyMap: MutableMap<Long?, BigDecimal?> = request.holdQtyMap | ||||
| val excludedWarehouseCodes = setOf<String>( | |||||
| "2F-W202-01-00", | |||||
| "2F-W200-#A-00", | |||||
| ) | |||||
| val excludedWarehouseCodes = doFloorSupplierSettingsService.loadDoPickExcludeWarehouseCodes() | |||||
| val availableInventoryLotLines = inventoryLotLineService | val availableInventoryLotLines = inventoryLotLineService | ||||
| .allInventoryLotLinesByItemIdIn(itemIds) | .allInventoryLotLinesByItemIdIn(itemIds) | ||||
| .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | ||||
| @@ -173,7 +170,7 @@ open class SuggestedPickLotService( | |||||
| val warehouseStoreId=inventoryLotLine?.warehouse?.store_id | val warehouseStoreId=inventoryLotLine?.warehouse?.store_id | ||||
| // 3F filter only applies to delivery order release; no limit for job order, AssignAndRelease, etc. | // 3F filter only applies to delivery order release; no limit for job order, AssignAndRelease, etc. | ||||
| val isDeliveryOrderPick = line.pickOrder?.type?.value == "do" | val isDeliveryOrderPick = line.pickOrder?.type?.value == "do" | ||||
| val warehouseCodeForExclusion = inventoryLotLine?.warehouse?.code | |||||
| val warehouseCodeForExclusion = inventoryLotLine?.warehouse?.code?.trim()?.uppercase(java.util.Locale.ROOT) | |||||
| if (isDeliveryOrderPick && | if (isDeliveryOrderPick && | ||||
| warehouseCodeForExclusion != null && | warehouseCodeForExclusion != null && | ||||
| excludedWarehouseCodes.contains(warehouseCodeForExclusion) | excludedWarehouseCodes.contains(warehouseCodeForExclusion) | ||||
| @@ -1,5 +1,6 @@ | |||||
| package com.ffii.fpsms.modules.stock.service | package com.ffii.fpsms.modules.stock.service | ||||
| import com.ffii.fpsms.modules.deliveryOrder.service.DoFloorSupplierSettingsService | |||||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine | import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine | ||||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository | import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | ||||
| @@ -29,6 +30,7 @@ open class SuggestedPickLotWorkbenchService( | |||||
| private val inventoryLotLineRepository: InventoryLotLineRepository, | private val inventoryLotLineRepository: InventoryLotLineRepository, | ||||
| private val stockOutLIneRepository: StockOutLIneRepository, | private val stockOutLIneRepository: StockOutLIneRepository, | ||||
| private val suggestPickLotRepository: SuggestPickLotRepository, | private val suggestPickLotRepository: SuggestPickLotRepository, | ||||
| private val doFloorSupplierSettingsService: DoFloorSupplierSettingsService, | |||||
| ) { | ) { | ||||
| private val log = LoggerFactory.getLogger(SuggestedPickLotWorkbenchService::class.java) | private val log = LoggerFactory.getLogger(SuggestedPickLotWorkbenchService::class.java) | ||||
| @@ -126,16 +128,9 @@ open class SuggestedPickLotWorkbenchService( | |||||
| } | } | ||||
| /** | /** | ||||
| * Backend-default avoid warehouse list for workbench suggestions. | |||||
| * - Case-insensitive match against `warehouse.code`. | |||||
| * - When request.excludeWarehouseCodes is provided, it overrides this default. | |||||
| * | |||||
| * Keep it empty if you don't need any default excludes. | |||||
| * DO workbench default exclude list comes from settings (`DO.pick.excludeWarehouses`). | |||||
| * When request.excludeWarehouseCodes is provided, it overrides that list. | |||||
| */ | */ | ||||
| private val defaultExcludeWarehouseCodes: Set<String> = setOf( | |||||
| "2F-W202-01-00", | |||||
| "2F-W200-#A-00", | |||||
| ) | |||||
| private fun normalizeStoreId(raw: String?): String? { | private fun normalizeStoreId(raw: String?): String? { | ||||
| val s = raw?.trim()?.uppercase() ?: return null | val s = raw?.trim()?.uppercase() ?: return null | ||||
| if (s.isEmpty()) return null | if (s.isEmpty()) return null | ||||
| @@ -149,7 +144,7 @@ open class SuggestedPickLotWorkbenchService( | |||||
| } | } | ||||
| private fun effectiveExcludeWarehouseCodes(raw: List<String>?): Set<String> { | private fun effectiveExcludeWarehouseCodes(raw: List<String>?): Set<String> { | ||||
| if (raw == null) return defaultExcludeWarehouseCodes | |||||
| if (raw == null) return doFloorSupplierSettingsService.loadDoPickExcludeWarehouseCodes() | |||||
| val normalized = raw | val normalized = raw | ||||
| .map { it.trim() } | .map { it.trim() } | ||||
| .filter { it.isNotEmpty() } | .filter { it.isNotEmpty() } | ||||
| @@ -0,0 +1,79 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset fpsms:20260922_auto_putaway | |||||
| -- QC accept auto put-away. Warehouse 2F-W200-#A-00 is warehouse id 1141. | |||||
| -- Item keyword FA matches item codes that contain FA. | |||||
| -- JO also auto put-away when BOM description is WIP or FG. | |||||
| -- "-" means that list is empty (condition off, or no fallback warehouse). | |||||
| INSERT INTO `settings` (`name`, `value`, `category`, `type`) | |||||
| SELECT v.name, v.value, v.category, v.type | |||||
| FROM ( | |||||
| SELECT 'PO.autoPutAway.warehouse' AS name, | |||||
| '2F-W200-#A-00' AS value, | |||||
| 'PICK_RULES' AS category, | |||||
| 'string' AS type | |||||
| ) v | |||||
| WHERE NOT EXISTS ( | |||||
| SELECT 1 FROM `settings` s WHERE s.name = 'PO.autoPutAway.warehouse' | |||||
| ); | |||||
| INSERT INTO `settings` (`name`, `value`, `category`, `type`) | |||||
| SELECT v.name, v.value, v.category, v.type | |||||
| FROM ( | |||||
| SELECT 'PO.autoPutAway.itemKeywords' AS name, | |||||
| 'FA' AS value, | |||||
| 'PICK_RULES' AS category, | |||||
| 'string' AS type | |||||
| ) v | |||||
| WHERE NOT EXISTS ( | |||||
| SELECT 1 FROM `settings` s WHERE s.name = 'PO.autoPutAway.itemKeywords' | |||||
| ); | |||||
| INSERT INTO `settings` (`name`, `value`, `category`, `type`) | |||||
| SELECT v.name, v.value, v.category, v.type | |||||
| FROM ( | |||||
| SELECT 'PO.autoPutAway.bomTypes' AS name, | |||||
| '-' AS value, | |||||
| 'PICK_RULES' AS category, | |||||
| 'string' AS type | |||||
| ) v | |||||
| WHERE NOT EXISTS ( | |||||
| SELECT 1 FROM `settings` s WHERE s.name = 'PO.autoPutAway.bomTypes' | |||||
| ); | |||||
| INSERT INTO `settings` (`name`, `value`, `category`, `type`) | |||||
| SELECT v.name, v.value, v.category, v.type | |||||
| FROM ( | |||||
| SELECT 'JO.autoPutAway.warehouse' AS name, | |||||
| '2F-W200-#A-00' AS value, | |||||
| 'PICK_RULES' AS category, | |||||
| 'string' AS type | |||||
| ) v | |||||
| WHERE NOT EXISTS ( | |||||
| SELECT 1 FROM `settings` s WHERE s.name = 'JO.autoPutAway.warehouse' | |||||
| ); | |||||
| INSERT INTO `settings` (`name`, `value`, `category`, `type`) | |||||
| SELECT v.name, v.value, v.category, v.type | |||||
| FROM ( | |||||
| SELECT 'JO.autoPutAway.itemKeywords' AS name, | |||||
| 'FA' AS value, | |||||
| 'PICK_RULES' AS category, | |||||
| 'string' AS type | |||||
| ) v | |||||
| WHERE NOT EXISTS ( | |||||
| SELECT 1 FROM `settings` s WHERE s.name = 'JO.autoPutAway.itemKeywords' | |||||
| ); | |||||
| INSERT INTO `settings` (`name`, `value`, `category`, `type`) | |||||
| SELECT v.name, v.value, v.category, v.type | |||||
| FROM ( | |||||
| SELECT 'JO.autoPutAway.bomTypes' AS name, | |||||
| 'WIP,FG' AS value, | |||||
| 'PICK_RULES' AS category, | |||||
| 'string' AS type | |||||
| ) v | |||||
| WHERE NOT EXISTS ( | |||||
| SELECT 1 FROM `settings` s WHERE s.name = 'JO.autoPutAway.bomTypes' | |||||
| ); | |||||
| @@ -0,0 +1,30 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset fpsms:20260922_auto_putaway_rules | |||||
| -- Multiple auto put-away rules. Empty itemKeywords means every item. | |||||
| -- locationMode itemLocation uses the item location code. warehouse uses warehouseCode. | |||||
| -- First matching rule wins. whitelist puts away. blacklist does not. | |||||
| INSERT INTO `settings` (`name`, `value`, `category`, `type`) | |||||
| SELECT v.name, v.value, v.category, v.type | |||||
| FROM ( | |||||
| SELECT 'PO.autoPutAway.rules' AS name, | |||||
| '[{"itemKeywords":"FA","bomTypes":"","locationMode":"warehouse","warehouseCode":"2F-W200-#A-00","listMode":"whitelist"}]' AS value, | |||||
| 'PICK_RULES' AS category, | |||||
| 'string' AS type | |||||
| ) v | |||||
| WHERE NOT EXISTS ( | |||||
| SELECT 1 FROM `settings` s WHERE s.name = 'PO.autoPutAway.rules' | |||||
| ); | |||||
| INSERT INTO `settings` (`name`, `value`, `category`, `type`) | |||||
| SELECT v.name, v.value, v.category, v.type | |||||
| FROM ( | |||||
| SELECT 'JO.autoPutAway.rules' AS name, | |||||
| '[{"itemKeywords":"","bomTypes":"FG","locationMode":"itemLocation","warehouseCode":"2F-W200-#A-00","listMode":"whitelist"},{"itemKeywords":"","bomTypes":"WIP","locationMode":"warehouse","warehouseCode":"2F-W200-#A-00","listMode":"whitelist"}]' AS value, | |||||
| 'PICK_RULES' AS category, | |||||
| 'string' AS type | |||||
| ) v | |||||
| WHERE NOT EXISTS ( | |||||
| SELECT 1 FROM `settings` s WHERE s.name = 'JO.autoPutAway.rules' | |||||
| ); | |||||
| @@ -0,0 +1,8 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset fpsms:20260922_auto_putaway_rules_drop_jo_fa | |||||
| -- Job order auto put-away does not include an item-code FA rule. | |||||
| UPDATE `settings` | |||||
| SET `value` = '[{"itemKeywords":"","bomTypes":"FG","locationMode":"itemLocation","warehouseCode":"2F-W200-#A-00","listMode":"whitelist"},{"itemKeywords":"","bomTypes":"WIP","locationMode":"warehouse","warehouseCode":"2F-W200-#A-00","listMode":"whitelist"}]' | |||||
| WHERE `name` = 'JO.autoPutAway.rules' | |||||
| AND `value` = '[{"itemKeywords":"","bomTypes":"FG","locationMode":"itemLocation","warehouseCode":"2F-W200-#A-00","listMode":"whitelist"},{"itemKeywords":"","bomTypes":"WIP","locationMode":"warehouse","warehouseCode":"2F-W200-#A-00","listMode":"whitelist"},{"itemKeywords":"FA","bomTypes":"","locationMode":"warehouse","warehouseCode":"2F-W200-#A-00","listMode":"whitelist"}]'; | |||||
| @@ -0,0 +1,28 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset fpsms:20260922_pick_exclude_print_mode | |||||
| -- hideList: print modal omits these warehouses. hideQr: list them but hide QR. | |||||
| INSERT INTO `settings` (`name`, `value`, `category`, `type`) | |||||
| SELECT v.name, v.value, v.category, v.type | |||||
| FROM ( | |||||
| SELECT 'DO.pick.excludePrintMode' AS name, | |||||
| 'hideList' AS value, | |||||
| 'PICK_RULES' AS category, | |||||
| 'string' AS type | |||||
| ) v | |||||
| WHERE NOT EXISTS ( | |||||
| SELECT 1 FROM `settings` s WHERE s.name = 'DO.pick.excludePrintMode' | |||||
| ); | |||||
| INSERT INTO `settings` (`name`, `value`, `category`, `type`) | |||||
| SELECT v.name, v.value, v.category, v.type | |||||
| FROM ( | |||||
| SELECT 'JO.pick.excludePrintMode' AS name, | |||||
| 'hideList' AS value, | |||||
| 'PICK_RULES' AS category, | |||||
| 'string' AS type | |||||
| ) v | |||||
| WHERE NOT EXISTS ( | |||||
| SELECT 1 FROM `settings` s WHERE s.name = 'JO.pick.excludePrintMode' | |||||
| ); | |||||
| @@ -0,0 +1,29 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset fpsms:20260922_pick_exclude_warehouses | |||||
| -- DO / JO pick exclude warehouse codes (comma-separated). "-" means exclude nothing. | |||||
| -- Missing row falls back to the same defaults in DoFloorSupplierSettingsService. | |||||
| INSERT INTO `settings` (`name`, `value`, `category`, `type`) | |||||
| SELECT v.name, v.value, v.category, v.type | |||||
| FROM ( | |||||
| SELECT 'DO.pick.excludeWarehouses' AS name, | |||||
| '2F-W202-01-00,2F-W200-#A-00' AS value, | |||||
| 'PICK_RULES' AS category, | |||||
| 'string' AS type | |||||
| ) v | |||||
| WHERE NOT EXISTS ( | |||||
| SELECT 1 FROM `settings` s WHERE s.name = 'DO.pick.excludeWarehouses' | |||||
| ); | |||||
| INSERT INTO `settings` (`name`, `value`, `category`, `type`) | |||||
| SELECT v.name, v.value, v.category, v.type | |||||
| FROM ( | |||||
| SELECT 'JO.pick.excludeWarehouses' AS name, | |||||
| '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' AS value, | |||||
| 'PICK_RULES' AS category, | |||||
| 'string' AS type | |||||
| ) v | |||||
| WHERE NOT EXISTS ( | |||||
| SELECT 1 FROM `settings` s WHERE s.name = 'JO.pick.excludeWarehouses' | |||||
| ); | |||||