Просмотр исходного кода

工單建議排除全部 4F,同到期日先 3F 再 2F;盤點核准差異改跟畫面選擇數量。

Co-authored-by: Cursor <[email protected]>
fix負數倉
CANCERYS\kw093 2 дней назад
Родитель
Сommit
cd9fc251a9
6 измененных файлов: 93 добавлений и 44 удалений
  1. +2
    -2
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DoWorkbenchMainService.kt
  2. +2
    -3
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt
  3. +5
    -24
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchPickConstants.kt
  4. +2
    -2
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/ConsumableWorkbenchPickConstants.kt
  5. +32
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  6. +50
    -12
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt

+ 2
- 2
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). * 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). * - 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` → service default excludes).
*/ */


+ 2
- 3
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.entity.enum.InventoryLotLineStatus
import com.ffii.fpsms.modules.stock.service.StockOutLineWorkbenchService import com.ffii.fpsms.modules.stock.service.StockOutLineWorkbenchService
import com.ffii.fpsms.modules.stock.service.SuggestedPickLotWorkbenchService 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 com.ffii.fpsms.modules.user.service.UserService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal import java.math.BigDecimal
import java.time.LocalDate import java.time.LocalDate
import java.util.Locale


/** /**
* Job Order Workbench–only flows (assign prime, hierarchical payload for scan-pick UI). * 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 availableStatus = notDeleted.filter { it.status == InventoryLotLineStatus.AVAILABLE }
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)
code.isNullOrEmpty() || code !in workbenchDefaultExcludeWarehouseCodes
!isWorkbenchWarehouseCodeExcluded(lot.warehouse?.code, workbenchDefaultExcludeWarehouseCodes)
} }
val positiveQty = notExcluded.filter { lot -> val positiveQty = notExcluded.filter { lot ->
val inQ = lot.inQty ?: BigDecimal.ZERO val inQ = lot.inQty ?: BigDecimal.ZERO


+ 5
- 24
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchPickConstants.kt Просмотреть файл

@@ -1,37 +1,18 @@
package com.ffii.fpsms.modules.jobOrder.service 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. * JO workbench pick constants.
* *
* [DEFAULT_EXCLUDE_WAREHOUSE_CODES] applies on **assign / first prime** ([JoWorkbenchMainService]) * [DEFAULT_EXCLUDE_WAREHOUSE_CODES] applies on **assign / first prime** ([JoWorkbenchMainService])
* and on **scan-pick re-suggest** ([com.ffii.fpsms.modules.deliveryOrder.service.DoWorkbenchMainService]). * 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. * 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 { object JoWorkbenchPickConstants {
val DEFAULT_EXCLUDE_WAREHOUSE_CODES: Set<String> = setOf( 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",
"4F",
) )
} }

+ 2
- 2
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. * 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: * - All other users → empty list (no warehouse limit). Do not return null:
* [SuggestedPickLotWorkbenchService] treats null as DO default 2F excludes. * [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. * 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 { 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.1 | 2026-11-02 */
fun resolveExcludeWarehouseCodes(userId: Long): List<String> { fun resolveExcludeWarehouseCodes(userId: Long): 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 JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES.toList()


+ 32
- 1
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 { 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 second = response.secondStockTakeQty
val first = response.firstStockTakeQty val first = response.firstStockTakeQty
return when { 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 { private fun computeInventoryLotDetailVariancePercentage(response: InventoryLotDetailResponse): BigDecimal {
val selectedQty = resolvePickerQtyForVarianceFilter(response) val selectedQty = resolvePickerQtyForVarianceFilter(response)
val bookQty = response.bookQty ?: response.availableQty ?: BigDecimal.ZERO val bookQty = response.bookQty ?: response.availableQty ?: BigDecimal.ZERO
@@ -646,6 +676,7 @@ open class StockTakeRecordService(
return stockTakeRepository.findByIdAndDeletedIsFalse(roundKey) return stockTakeRepository.findByIdAndDeletedIsFalse(roundKey)
} }


/** FP-MTMS Version Checklist | Functions Ref. No. 90 | v1.0.0 | 2026-11-02 */
open fun getApproverInventoryLotDetailsAll( open fun getApproverInventoryLotDetailsAll(
stockTakeId: Long? = null, stockTakeId: Long? = null,
pageNum: Int = 0, pageNum: Int = 0,


+ 50
- 12
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt Просмотреть файл

@@ -23,6 +23,21 @@ data class WorkbenchSuggestionSummary(
val skippedExisting: Int, 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<String>): 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 @Service
open class SuggestedPickLotWorkbenchService( open class SuggestedPickLotWorkbenchService(
private val pickOrderLineRepository: PickOrderLineRepository, private val pickOrderLineRepository: PickOrderLineRepository,
@@ -127,7 +142,7 @@ open class SuggestedPickLotWorkbenchService(


/** /**
* Backend-default avoid warehouse list for workbench suggestions. * 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. * - When request.excludeWarehouseCodes is provided, it overrides this default.
* *
* Keep it empty if you don't need any default excludes. * 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<String>): Boolean { private fun isWarehouseExcluded(lot: InventoryLotLine, excludeWarehouseCodes: Set<String>): 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<String>?): Set<String> { private fun effectiveExcludeWarehouseCodes(raw: List<String>?): Set<String> {
@@ -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: * Build suggestions with no-hold rule:
* available = inQty - outQty (ignore holdQty). * 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). * 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.status == InventoryLotLineStatus.AVAILABLE }
.filter { it.inventoryLot?.expiryDate?.isBefore(today) != true } .filter { it.inventoryLot?.expiryDate?.isBefore(today) != true }
.filter { !isWarehouseExcluded(it, excludeSet) } .filter { !isWarehouseExcluded(it, excludeSet) }
.sortedBy { it.inventoryLot?.expiryDate }
.sortedWith(this::compareSuggestedLots)
.groupBy { it.inventoryLot?.item?.id } .groupBy { it.inventoryLot?.item?.id }


var created = 0 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]. * 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. * 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 if (storeKey == null) true
else normalizeStoreId(lot.warehouse?.store_id) == storeKey else normalizeStoreId(lot.warehouse?.store_id) == storeKey
} }
.sortedBy { it.inventoryLot?.expiryDate }
.sortedWith(this::compareSuggestedLots)


val desired = buildDesiredAllocations(lots, remaining, pickOrderLineId) val desired = buildDesiredAllocations(lots, remaining, pickOrderLineId)
val created = syncSuggestedPickLotsForLine(lineFresh, desired) 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): * 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. * 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). * 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 if (storeKey == null) true
else normalizeStoreId(lot.warehouse?.store_id) == storeKey else normalizeStoreId(lot.warehouse?.store_id) == storeKey
} }
.sortedBy { it.inventoryLot?.expiryDate }
.sortedWith(this::compareSuggestedLots)
.firstOrNull() .firstOrNull()


val desired = listOf(nextLot to targetQty) 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: * Workbench explicit-qty split support:
* Set suggestions for this POL to an exact [targetQty] (independent of POL.qty). * Set suggestions for this POL to an exact [targetQty] (independent of POL.qty).
* - If [targetQty] <= 0: soft-delete all active suggestions for the line. * - 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 { it.inventoryLot?.expiryDate?.isBefore(today) != true }
.filter { !isWarehouseExcluded(it, excludeSet) } .filter { !isWarehouseExcluded(it, excludeSet) }
.filter { matchesPolUom(it, line) } .filter { matchesPolUom(it, line) }
.sortedBy { it.inventoryLot?.expiryDate }
.sortedWith(this::compareSuggestedLots)


val desired = buildDesiredAllocations(lots, targetQty, pickOrderLineId) val desired = buildDesiredAllocations(lots, targetQty, pickOrderLineId)
val created = syncSuggestedPickLotsForLine(line, desired) val created = syncSuggestedPickLotsForLine(line, desired)


Загрузка…
Отмена
Сохранить