diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt index 74d9e7f7..cc3cabc0 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt @@ -259,9 +259,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.3 | 2026-11-02 * Warehouse exclude list for workbench **re-suggest** after scan-pick (shortfall / lot split). - * - JO: same list as assign ([JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES]). + * - JO: same list as assign ([JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES], all `4F-` bins). * - Consumable: hardcoded user [ConsumableWorkbenchPickConstants.HARDCODED_EXCLUDE_USER_ID] → JO list; else empty (no limit). * - DO / other: pass through request (`null` → service default excludes). */ diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt index a2ef1724..2a41fc58 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt @@ -23,12 +23,12 @@ import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus import com.ffii.fpsms.modules.stock.service.StockOutLineWorkbenchService import com.ffii.fpsms.modules.stock.service.SuggestedPickLotWorkbenchService +import com.ffii.fpsms.modules.stock.service.isWorkbenchWarehouseCodeExcluded import com.ffii.fpsms.modules.user.service.UserService import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.math.BigDecimal import java.time.LocalDate -import java.util.Locale /** * Job Order Workbench–only flows (assign prime, hierarchical payload for scan-pick UI). @@ -79,8 +79,7 @@ open class JoWorkbenchMainService( val availableStatus = notDeleted.filter { it.status == InventoryLotLineStatus.AVAILABLE } val notExpired = availableStatus.filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } val notExcluded = notExpired.filter { lot -> - val code = lot.warehouse?.code?.trim()?.uppercase(Locale.ROOT) - code.isNullOrEmpty() || code !in workbenchDefaultExcludeWarehouseCodes + !isWorkbenchWarehouseCodeExcluded(lot.warehouse?.code, workbenchDefaultExcludeWarehouseCodes) } val positiveQty = notExcluded.filter { lot -> val inQ = lot.inQty ?: BigDecimal.ZERO diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchPickConstants.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchPickConstants.kt index 727c54a0..e5aaaff1 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchPickConstants.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchPickConstants.kt @@ -1,37 +1,18 @@ package com.ffii.fpsms.modules.jobOrder.service /** + * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.3 | 2026-11-02 * 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]). * JO re-suggest keeps [storeId] null (same as assign); it does not use DO floor store resolution. + * + * Each entry matches a warehouse code exactly, or any code with that prefix plus `-`. + * `"4F"` therefore excludes every 4F bin (`4F-W402-…`, `4F-W401-…`, and the rest). */ object JoWorkbenchPickConstants { val DEFAULT_EXCLUDE_WAREHOUSE_CODES: Set = 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", + "4F", ) } diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/ConsumableWorkbenchPickConstants.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/ConsumableWorkbenchPickConstants.kt index caaadd87..9372a772 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/ConsumableWorkbenchPickConstants.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/ConsumableWorkbenchPickConstants.kt @@ -5,7 +5,7 @@ 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]). + * - [HARDCODED_EXCLUDE_USER_ID] → same warehouses as JO ([JoWorkbenchPickConstants], all `4F-` bins). * - All other users → empty list (no warehouse limit). Do not return null: * [SuggestedPickLotWorkbenchService] treats null as DO default 2F excludes. * Consumable re-suggest keeps storeId null (same as first prime / JO); it does not use DO floor store resolution. @@ -13,7 +13,7 @@ import com.ffii.fpsms.modules.jobOrder.service.JoWorkbenchPickConstants object ConsumableWorkbenchPickConstants { 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.1 | 2026-11-02 */ fun resolveExcludeWarehouseCodes(userId: Long): List { if (userId != HARDCODED_EXCLUDE_USER_ID) return emptyList() return JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES.toList() diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt index d4d3c308..a7f51df3 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt @@ -316,8 +316,31 @@ open class StockTakeRecordService( } } - /** 與 Approver 列表前端一致:優先第二次盤點數,否則第一次 */ + /** + * 與核准列表「差異百分比」同一個選擇數量。 + * lastSelect:1 第一次、2 第二次、3 審核員(盤點數 − 不良數)。 + * 沒有 lastSelect 時用 finalQty 對應;再沒有才退回第二次,否則第一次。 + */ private fun resolvePickerQtyForVarianceFilter(response: InventoryLotDetailResponse): BigDecimal { + when (response.lastSelect) { + 1 -> return response.firstStockTakeQty ?: BigDecimal.ZERO + 2 -> return response.secondStockTakeQty ?: BigDecimal.ZERO + 3 -> return approverGoodQty(response) + } + val finalQty = response.finalQty + if (finalQty != null) { + val approverGood = approverGoodQty(response) + if (response.approverQty != null && sameStockTakeQty(approverGood, finalQty)) { + return approverGood + } + val second = response.secondStockTakeQty + if (second != null && sameStockTakeQty(second, finalQty)) return second + val first = response.firstStockTakeQty + if (first != null && sameStockTakeQty(first, finalQty)) return first + if (response.approverQty != null && sameStockTakeQty(response.approverQty, finalQty)) { + return approverGood + } + } val second = response.secondStockTakeQty val first = response.firstStockTakeQty return when { @@ -327,6 +350,13 @@ open class StockTakeRecordService( } } + /** 與前端選擇數量一致:審核員盤點數減不良數。 */ + private fun approverGoodQty(response: InventoryLotDetailResponse): BigDecimal = + (response.approverQty ?: BigDecimal.ZERO).subtract(response.approverBadQty ?: BigDecimal.ZERO) + + private fun sameStockTakeQty(a: BigDecimal?, b: BigDecimal?): Boolean = + a != null && b != null && a.compareTo(b) == 0 + private fun computeInventoryLotDetailVariancePercentage(response: InventoryLotDetailResponse): BigDecimal { val selectedQty = resolvePickerQtyForVarianceFilter(response) val bookQty = response.bookQty ?: response.availableQty ?: BigDecimal.ZERO @@ -646,6 +676,7 @@ open class StockTakeRecordService( return stockTakeRepository.findByIdAndDeletedIsFalse(roundKey) } + /** FP-MTMS Version Checklist | Functions Ref. No. 90 | v1.0.0 | 2026-11-02 */ open fun getApproverInventoryLotDetailsAll( stockTakeId: Long? = null, pageNum: Int = 0, diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt index 50f02a6c..e8dd5f0d 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt @@ -23,6 +23,21 @@ data class WorkbenchSuggestionSummary( val skippedExisting: Int, ) +/** + * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.3 | 2026-11-02 + * Case-insensitive match of [warehouseCode] against [excludeCodes]. + * An entry matches the code exactly, or any code that starts with `{entry}-`. + * `"4F"` therefore excludes every 4F bin. + */ +fun isWorkbenchWarehouseCodeExcluded(warehouseCode: String?, excludeCodes: Collection): Boolean { + val code = warehouseCode?.trim()?.uppercase() ?: return false + if (code.isEmpty() || excludeCodes.isEmpty()) return false + return excludeCodes.any { raw -> + val entry = raw.trim().uppercase() + entry.isNotEmpty() && (code == entry || code.startsWith("$entry-")) + } +} + @Service open class SuggestedPickLotWorkbenchService( private val pickOrderLineRepository: PickOrderLineRepository, @@ -127,7 +142,7 @@ open class SuggestedPickLotWorkbenchService( /** * Backend-default avoid warehouse list for workbench suggestions. - * - Case-insensitive match against `warehouse.code`. + * - Case-insensitive match against `warehouse.code` (exact, or `{entry}-` prefix). * - When request.excludeWarehouseCodes is provided, it overrides this default. * * Keep it empty if you don't need any default excludes. @@ -143,9 +158,32 @@ open class SuggestedPickLotWorkbenchService( } private fun isWarehouseExcluded(lot: InventoryLotLine, excludeWarehouseCodes: Set): Boolean { - if (excludeWarehouseCodes.isEmpty()) return false - val code = lot.warehouse?.code?.trim()?.uppercase() - return !code.isNullOrEmpty() && code in excludeWarehouseCodes + return isWorkbenchWarehouseCodeExcluded(lot.warehouse?.code, excludeWarehouseCodes) + } + + /** 3F before 2F when expiry is the same. Other floors stay after those two. */ + private fun suggestionFloorRank(lot: InventoryLotLine): Int { + val store = normalizeStoreId(lot.warehouse?.store_id) + ?: floorTokenFromWarehouseCode(lot.warehouse?.code) + return when (store) { + "3F" -> 0 + "2F" -> 1 + else -> 2 + } + } + + private fun floorTokenFromWarehouseCode(code: String?): String? { + val head = code?.trim()?.substringBefore("-") ?: return null + return normalizeStoreId(head) + } + + /** FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.3 | 2026-11-02 + * Closest expiry first; same day prefers 3F, then 2F. + */ + private fun compareSuggestedLots(a: InventoryLotLine, b: InventoryLotLine): Int { + val byExpiry = compareValues(a.inventoryLot?.expiryDate, b.inventoryLot?.expiryDate) + if (byExpiry != 0) return byExpiry + return suggestionFloorRank(a).compareTo(suggestionFloorRank(b)) } private fun effectiveExcludeWarehouseCodes(raw: List?): Set { @@ -168,7 +206,7 @@ open class SuggestedPickLotWorkbenchService( } /** - * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.2 | 2026-08-03 + * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.3 | 2026-11-02 * Build suggestions with no-hold rule: * available = inQty - outQty (ignore holdQty). * When [desired] is a single segment, reuses one SPL row (max id) and repoints lot + qty (switch-lot without new id). @@ -204,7 +242,7 @@ open class SuggestedPickLotWorkbenchService( .filter { it.status == InventoryLotLineStatus.AVAILABLE } .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } .filter { !isWarehouseExcluded(it, excludeSet) } - .sortedBy { it.inventoryLot?.expiryDate } + .sortedWith(this::compareSuggestedLots) .groupBy { it.inventoryLot?.item?.id } var created = 0 @@ -301,7 +339,7 @@ open class SuggestedPickLotWorkbenchService( } /** - * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.2 | 2026-08-03 + * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.3 | 2026-11-02 * Same no-hold allocation as [rebuildNoHoldSuggestionsForPickOrder], but only for one [pickOrderLineId]. * Used by workbench scan-pick to avoid rebuilding suggestions for every line on the pick order. */ @@ -365,7 +403,7 @@ open class SuggestedPickLotWorkbenchService( if (storeKey == null) true else normalizeStoreId(lot.warehouse?.store_id) == storeKey } - .sortedBy { it.inventoryLot?.expiryDate } + .sortedWith(this::compareSuggestedLots) val desired = buildDesiredAllocations(lots, remaining, pickOrderLineId) val created = syncSuggestedPickLotsForLine(lineFresh, desired) @@ -393,7 +431,7 @@ open class SuggestedPickLotWorkbenchService( } /** - * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.2 | 2026-08-03 + * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.3 | 2026-11-02 * Workbench explicit-qty remainder support (single-next-lot): * Create exactly ONE suggested lot row for this POL with [targetQty] pointing to the next available lot. * When [storeId] is provided, the next lot must be within the same store (warehouse.store_id). @@ -441,7 +479,7 @@ open class SuggestedPickLotWorkbenchService( if (storeKey == null) true else normalizeStoreId(lot.warehouse?.store_id) == storeKey } - .sortedBy { it.inventoryLot?.expiryDate } + .sortedWith(this::compareSuggestedLots) .firstOrNull() val desired = listOf(nextLot to targetQty) @@ -454,7 +492,7 @@ open class SuggestedPickLotWorkbenchService( } /** - * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.2 | 2026-08-03 + * FP-MTMS Version Checklist | Functions Ref. No. 32 | v1.0.3 | 2026-11-02 * Workbench explicit-qty split support: * Set suggestions for this POL to an exact [targetQty] (independent of POL.qty). * - If [targetQty] <= 0: soft-delete all active suggestions for the line. @@ -491,7 +529,7 @@ open class SuggestedPickLotWorkbenchService( .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } .filter { !isWarehouseExcluded(it, excludeSet) } .filter { matchesPolUom(it, line) } - .sortedBy { it.inventoryLot?.expiryDate } + .sortedWith(this::compareSuggestedLots) val desired = buildDesiredAllocations(lots, targetQty, pickOrderLineId) val created = syncSuggestedPickLotsForLine(line, desired)