Kaynağa Gözat

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

Co-authored-by: Cursor <[email protected]>
fix負數倉
CANCERYS\kw093 2 gün önce
ebeveyn
işleme
cd9fc251a9
6 değiştirilmiş dosya ile 93 ekleme ve 44 silme
  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 Dosyayı Görüntüle

@@ -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).
*/


+ 2
- 3
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchMainService.kt Dosyayı Görüntüle

@@ -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


+ 5
- 24
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoWorkbenchPickConstants.kt Dosyayı Görüntüle

@@ -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<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 Dosyayı Görüntüle

@@ -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<String> {
if (userId != HARDCODED_EXCLUDE_USER_ID) return emptyList()
return JoWorkbenchPickConstants.DEFAULT_EXCLUDE_WAREHOUSE_CODES.toList()


+ 32
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt Dosyayı Görüntüle

@@ -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,


+ 50
- 12
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotWorkbenchService.kt Dosyayı Görüntüle

@@ -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<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
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<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> {
@@ -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)


Yükleniyor…
İptal
Kaydet