Sfoglia il codice sorgente

update stock take search

reset-do-picking-order
CANCERYS\kw093 1 settimana fa
parent
commit
3804d5e4fb
2 ha cambiato i file con 285 aggiunte e 35 eliminazioni
  1. +242
    -24
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  2. +43
    -11
      src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt

+ 242
- 24
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt Vedi File

@@ -154,6 +154,10 @@ class StockTakeRecordService(
} else {
null
}
val sectionDescription = warehouses
.mapNotNull { it.stockTakeSectionDescription } // 先去掉 null
.distinct() // 去重(防止误填多个不同值)
.firstOrNull()
// 9. 计算 TotalItemNumber:获取该 section 下所有 InventoryLotLine,按 item 分组,计算不同的 item 数量
val totalItemNumber = inventoryLotLineRepository.countDistinctItemsByWarehouseIds(warehouseIds).toInt()
val totalInventoryLotNumber = inventoryLotLineRepository.countAllByWarehouseIds(warehouseIds).toInt()
@@ -185,7 +189,8 @@ class StockTakeRecordService(
startTime = latestStockTake?.actualStart,
endTime = latestStockTake?.actualEnd,
ReStockTakeTrueFalse = reStockTakeTrueFalse,
planStartDate = latestStockTake?.planStart?.toLocalDate()
planStartDate = latestStockTake?.planStart?.toLocalDate(),
stockTakeSectionDescription = sectionDescription

)
)
@@ -214,16 +219,36 @@ class StockTakeRecordService(
// 2. 拿所有这些仓库下面的 lot line
val inventoryLotLines = inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds)
println("Found ${inventoryLotLines.size} inventory lot lines for ALL sections")
// 3. 如果传了 stockTakeId,就把对应的 stockTakeRecord 预先查出来建 map(跟 section 版一样)

// 3. 如果传了 stockTakeId,就把「同一轮」的所有 stockTake 找出来:
// 以该 stockTake 的 planStart 作为一轮的标识,取 planStart 相同的所有记录
val roundStockTakeIds: Set<Long> = if (stockTakeId != null) {
val baseStockTake = stockTakeRepository.findByIdAndDeletedIsFalse(stockTakeId)
?: throw IllegalArgumentException("Stock take not found: $stockTakeId")
val planStart = baseStockTake.planStart

val roundStockTakes = if (planStart != null) {
stockTakeRepository.findAll()
.filter { !it.deleted && it.planStart == planStart }
} else {
listOf(baseStockTake)
}

roundStockTakes.mapNotNull { it.id }.toSet()
} else {
emptySet()
}

// 4. 如果有 stockTakeId,则预先把这一轮相关的 stockTakeRecord 查出来建 map(跟 section 版类似)
val stockTakeRecordsMap = if (stockTakeId != null) {
val allStockTakeRecords = stockTakeRecordRepository.findAll()
.filter {
!it.deleted &&
it.stockTake?.id == stockTakeId &&
it.warehouse?.id in warehouseIds
it.stockTake?.id != null &&
it.stockTake!!.id!! in roundStockTakeIds &&
it.warehouse?.id in warehouseIds
}
allStockTakeRecords.associateBy {
Pair(it.lotId ?: 0L, it.warehouse?.id ?: 0L)
}
@@ -231,7 +256,7 @@ class StockTakeRecordService(
emptyMap()
}
// 4. 组装 InventoryLotDetailResponse(基本复制 section 版里的 map 那段)
// 5. 组装 InventoryLotDetailResponse(基本复制 section 版里的 map 那段)
val allResults = inventoryLotLines.map { ill ->
val inventoryLot = ill.inventoryLot
val item = inventoryLot?.item
@@ -245,12 +270,12 @@ class StockTakeRecordService(
} else {
null
}
val inventoryLotLineId = ill.id
val stockTakeLine = if (stockTakeId != null && inventoryLotLineId != null) {
val stockTakeLine = if (stockTakeRecord != null && stockTakeRecord.stockTake?.id != null && inventoryLotLineId != null) {
stockTakeLineRepository.findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(
inventoryLotLineId,
stockTakeId
stockTakeRecord.stockTake!!.id!!
)
} else {
null
@@ -292,13 +317,21 @@ class StockTakeRecordService(
)
}
// 5. 可选过滤:比如只保留 availableQty > 0 或有盘点记录的(跟 section 版一样)
val filteredResults = allResults.filter { response ->
val av = response.availableQty ?: BigDecimal.ZERO
av.compareTo(BigDecimal.ZERO) > 0 || response.stockTakeRecordId != null
// 6. 过滤结果:
// 如果带了 stockTakeId,表示只看这一轮盘点的记录,此时只保留「已有 stockTakeRecord 的行」(即 picker 已经盘点过的行)
// 如果没有带 stockTakeId,则沿用原逻辑:availableQty > 0 或已有盘点记录
val filteredResults = if (stockTakeId != null) {
allResults.filter { response ->
response.stockTakeRecordId != null
}
} else {
allResults.filter { response ->
val av = response.availableQty ?: BigDecimal.ZERO
av.compareTo(BigDecimal.ZERO) > 0 || response.stockTakeRecordId != null
}
}
// 6. 分页(和 section 版一模一样)
// 7. 分页(和 section 版一模一样)
val pageable = PageRequest.of(pageNum, pageSize)
val startIndex = pageable.offset.toInt()
val endIndex = minOf(startIndex + pageSize, filteredResults.size)
@@ -422,9 +455,44 @@ class StockTakeRecordService(
} else {
false
}
// 9. 计算 TotalItemNumber:获取该 section 下所有 InventoryLotLine,按 item 分组,计算不同的 item 数量
val totalItemNumber = inventoryLotLineRepository.countDistinctItemsByWarehouseIds(warehouseIds).toInt()
val totalInventoryLotNumber = inventoryLotLineRepository.countAllByWarehouseIds(warehouseIds).toInt()
// 9. 计算 TotalItemNumber / TotalInventoryLotNumber(只统计“需要盘点的行”):
// 规则与前端 Picker / Approver 明细一致:
// - availableQty > 0,或
// - 在 latestStockTake 下已经有 stockTakeRecord(即本轮参与盘点的 lot)
val inventoryLotLinesForSection =
inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds)

val relevantInventoryLotLines = inventoryLotLinesForSection.filter { ill ->
val inventoryLot = ill.inventoryLot
val warehouse = ill.warehouse
if (inventoryLot?.id == null || warehouse?.id == null || latestStockTake?.id == null) {
false
} else {
val inQty = ill.inQty ?: BigDecimal.ZERO
val outQty = ill.outQty ?: BigDecimal.ZERO
val holdQty = ill.holdQty ?: BigDecimal.ZERO
val availableQty = inQty.subtract(outQty).subtract(holdQty)

val hasRecordForLatest = allStockTakeRecords.any { record ->
!record.deleted &&
record.stockTake?.id == latestStockTake.id &&
record.inventoryLotId == inventoryLot.id &&
record.warehouse?.id == warehouse.id
}
availableQty.compareTo(BigDecimal.ZERO) > 0 || hasRecordForLatest
}
}

val totalItemNumber = relevantInventoryLotLines
.mapNotNull { it.inventoryLot?.item?.id }
.distinct()
.count()

val totalInventoryLotNumber = relevantInventoryLotLines.size
val sectionDescription = warehouses
.mapNotNull { it.stockTakeSectionDescription }
.distinct()
.firstOrNull()
// 9. 使用 stockTakeSection 作为 stockTakeSession
result.add(
AllPickedStockTakeListReponse(
@@ -441,7 +509,9 @@ class StockTakeRecordService(
startTime = latestStockTake?.actualStart,
endTime = latestStockTake?.actualEnd,
ReStockTakeTrueFalse = reStockTakeTrueFalse,
planStartDate = latestStockTake?.planStart?.toLocalDate()
planStartDate = latestStockTake?.planStart?.toLocalDate(),
stockTakeSectionDescription = sectionDescription

)
)
}
@@ -897,17 +967,36 @@ return RecordsRes(paginatedResult, filteredResults.size)
it.stockTakeSection == stockTakeSection
}

// 4. 检查是否所有 inventory lot lines 都有对应的记录
val allLinesHaveRecords = inventoryLotLines.all { ill ->
// 4. 仅对「前端会显示的库存行」做检查:
// 规则与 getInventoryLotDetailsByStockTakeSection 一致:
// - availableQty > 0,或
// - 已经有 stockTakeRecord(即使 availableQty 为 0)
val relevantInventoryLotLines = inventoryLotLines.filter { ill ->
val inventoryLot = ill.inventoryLot
val warehouse = ill.warehouse
if (inventoryLot?.id == null || warehouse?.id == null) {
false
} else {
stockTakeRecords.any { record ->
val inQty = ill.inQty ?: BigDecimal.ZERO
val outQty = ill.outQty ?: BigDecimal.ZERO
val holdQty = ill.holdQty ?: BigDecimal.ZERO
val availableQty = inQty.subtract(outQty).subtract(holdQty)

val hasRecord = stockTakeRecords.any { record ->
record.inventoryLotId == inventoryLot.id &&
record.warehouse?.id == warehouse.id
}
availableQty.compareTo(BigDecimal.ZERO) > 0 || hasRecord
}
}

// 5. 检查这些「相关行」是否都有对应的记录
val allLinesHaveRecords = relevantInventoryLotLines.all { ill ->
val inventoryLot = ill.inventoryLot
val warehouse = ill.warehouse
stockTakeRecords.any { record ->
record.inventoryLotId == inventoryLot?.id &&
record.warehouse?.id == warehouse?.id
}
}

@@ -1043,7 +1132,8 @@ open fun batchSaveApproverStockTakeRecords(
!it.deleted &&
it.stockTake?.id == request.stockTakeId &&
it.stockTakeSection == request.stockTakeSection &&
it.pickerFirstStockTakeQty != null && it.pickerFirstStockTakeQty!! > BigDecimal.ZERO &&
// 只处理已经有 picker 盘点记录的行(第一或第二次盘点任意一个非 null 即视为已盘点)
(it.pickerFirstStockTakeQty != null || it.pickerSecondStockTakeQty != null) &&
it.approverStockTakeQty == null // 只处理未审批的记录
}
@@ -1081,7 +1171,8 @@ open fun batchSaveApproverStockTakeRecords(

val tolerancePercent = request.variancePercentTolerance ?: BigDecimal.ZERO
val shouldSkip = if (tolerancePercent.compareTo(BigDecimal.ZERO) <= 0) {
varianceQty.compareTo(BigDecimal.ZERO) != 0 // 原有逻辑:仅 variance=0 可保存
// 0 或负数表示不过滤差异:所有已盘点记录都参与批量保存
false
} else {
if (bookQty.compareTo(BigDecimal.ZERO) == 0) {
varianceQty.compareTo(BigDecimal.ZERO) != 0
@@ -1141,6 +1232,133 @@ open fun batchSaveApproverStockTakeRecords(
)
}

open fun batchSaveApproverStockTakeRecordsAll(
request: BatchSaveApproverStockTakeAllRequest
): BatchSaveApproverStockTakeRecordResponse {
println("batchSaveApproverStockTakeRecordsAll called for stockTakeId: ${request.stockTakeId}")
val user = userRepository.findById(request.approverId).orElse(null)

val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId)
?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}")

// 以该 stockTake 的 planStart 作为一轮的标识,找到这一轮下所有的 stockTake(各个 section)
val planStart = stockTake.planStart
val roundStockTakeIds: Set<Long> = if (planStart != null) {
stockTakeRepository.findAll()
.filter { !it.deleted && it.planStart == planStart }
.mapNotNull { it.id }
.toSet()
} else {
listOfNotNull(stockTake.id).toSet()
}

val stockTakeRecords = stockTakeRecordRepository.findAll()
.filter {
!it.deleted &&
it.stockTake?.id != null &&
it.stockTake!!.id!! in roundStockTakeIds &&
// 只处理已经有 picker 盘点记录的行(第一或第二次盘点任意一个非 null 即视为已盘点)
(it.pickerFirstStockTakeQty != null || it.pickerSecondStockTakeQty != null) &&
it.approverStockTakeQty == null
}

println("Found ${stockTakeRecords.size} records to process for round (all sections)")

if (stockTakeRecords.isEmpty()) {
return BatchSaveApproverStockTakeRecordResponse(0, 0, listOf("No records found matching criteria"))
}

var successCount = 0
var errorCount = 0
var skippedCount = 0
val errors = mutableListOf<String>()

val processedStockTakes = mutableSetOf<Pair<Long, String>>()

stockTakeRecords.forEach { record ->
try {
val qty: BigDecimal
val badQty: BigDecimal

if (record.pickerSecondStockTakeQty != null && record.pickerSecondStockTakeQty!! > BigDecimal.ZERO) {
qty = record.pickerSecondStockTakeQty!!
badQty = record.pickerSecondBadQty ?: BigDecimal.ZERO
} else {
qty = record.pickerFirstStockTakeQty ?: BigDecimal.ZERO
badQty = record.pickerFirstBadQty ?: BigDecimal.ZERO
}

val bookQty = record.bookQty ?: BigDecimal.ZERO
val varianceQty = qty.subtract(bookQty)

val tolerancePercent = request.variancePercentTolerance ?: BigDecimal.ZERO
val shouldSkip = if (tolerancePercent.compareTo(BigDecimal.ZERO) <= 0) {
// 0 或负数表示不过滤差异:所有已盘点记录都参与批量保存
false
} else {
if (bookQty.compareTo(BigDecimal.ZERO) == 0) {
varianceQty.compareTo(BigDecimal.ZERO) != 0
} else {
val threshold = bookQty.abs()
.multiply(tolerancePercent)
.divide(BigDecimal("100"), 10, RoundingMode.HALF_UP)
varianceQty.abs().compareTo(threshold) > 0
}
}
if (shouldSkip) {
skippedCount++
println("Skipping record ${record.id}: |variance| > ${tolerancePercent}% of bookQty (variance=$varianceQty, bookQty=$bookQty)")
return@forEach
}

record.apply {
this.approverId = request.approverId
this.approverName = user?.name
this.approverStockTakeQty = qty
this.approverBadQty = badQty
this.varianceQty = varianceQty
this.status = "completed"
}

stockTakeRecordRepository.save(record)
if (varianceQty != BigDecimal.ZERO) {
try {
applyVarianceAdjustment(record.stockTake ?: stockTake, record, qty, varianceQty, request.approverId)
} catch (e: Exception) {
logger.error("Failed to apply variance adjustment for record ${record.id}", e)
errorCount++
errors.add("Record ${record.id}: ${e.message}")
return@forEach
}
}
val stId = record.stockTake?.id
val section = record.stockTakeSection
if (stId != null && section != null) {
processedStockTakes.add(Pair(stId, section))
}
successCount++
} catch (e: Exception) {
errorCount++
val errorMsg = "Error processing record ${record.id}: ${e.message}"
errors.add(errorMsg)
logger.error(errorMsg, e)
}
}

if (successCount > 0) {
processedStockTakes.forEach { (stId, section) ->
checkAndUpdateStockTakeStatus(stId, section)
}
}

println("batchSaveApproverStockTakeRecordsAll completed: success=$successCount, skipped=$skippedCount, errors=$errorCount")
return BatchSaveApproverStockTakeRecordResponse(
successCount = successCount,
errorCount = errorCount,
errors = errors
)
}

/**
* 根据 variance 调整库存并创建 Stock Ledger。
* 当 variance != 0 时:创建 StockTakeLine,并根据 variance 正负创建 StockIn/StockOut 及 Ledger。


+ 43
- 11
src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt Vedi File

@@ -27,20 +27,30 @@ class StockTakeRecordController(
@GetMapping("/AllPickedStockOutRecordList")
fun allPickedStockOutRecordList(
@RequestParam(required = false, defaultValue = "0") pageNum: Int,
@RequestParam(required = false, defaultValue = "6") pageSize: Int
@RequestParam(required = false, defaultValue = "6") pageSize: Int,
@RequestParam(required = false) sectionDescription: String?,
@RequestParam(required = false) stockTakeSections: String?
): RecordsRes<AllPickedStockTakeListReponse> {
val all = stockOutRecordService.AllPickedStockTakeList()
var all = stockOutRecordService.AllPickedStockTakeList()
if (sectionDescription != null && sectionDescription != "All") {
all = all.filter { it.stockTakeSectionDescription == sectionDescription }
}
if (!stockTakeSections.isNullOrBlank()) {
val sections = stockTakeSections.split(",").map { it.trim() }.filter { it.isNotBlank() }
if (sections.isNotEmpty()) {
all = all.filter { session ->
sections.any { part ->
session.stockTakeSession.equals(part, ignoreCase = true) ||
session.stockTakeSession.contains(part, ignoreCase = true)
}
}
}
}
val total = all.size
val fromIndex = pageNum * pageSize
val toIndex = kotlin.math.min(fromIndex + pageSize, total)
val pageList =
if (fromIndex >= total) emptyList()
else all.subList(fromIndex, toIndex)
return RecordsRes<AllPickedStockTakeListReponse>(
pageList,
total
)
val toIndex = minOf(fromIndex + pageSize, total)
val pageList = if (fromIndex >= total) emptyList() else all.subList(fromIndex, toIndex)
return RecordsRes(pageList, total)
}
@GetMapping("/approverInventoryLotDetailsAll")
fun getApproverInventoryLotDetailsAll(
@@ -205,6 +215,28 @@ fun getInventoryLotDetailsByStockTakeSection(
))
}
}
@PostMapping("/batchSaveApproverStockTakeRecordsAll")
fun batchSaveApproverStockTakeRecordsAll(
@RequestBody request: BatchSaveApproverStockTakeAllRequest
): ResponseEntity<Any> {
return try {
val result = stockOutRecordService.batchSaveApproverStockTakeRecordsAll(request)
logger.info("Batch approver save all completed: success=${result.successCount}, errors=${result.errorCount}")
ResponseEntity.ok(result)
} catch (e: IllegalArgumentException) {
logger.warn("Validation error: ${e.message}")
ResponseEntity.badRequest().body(mapOf(
"error" to "VALIDATION_ERROR",
"message" to (e.message ?: "Validation failed")
))
} catch (e: Exception) {
logger.error("Error batch saving approver stock take records all", e)
ResponseEntity.status(500).body(mapOf(
"error" to "INTERNAL_ERROR",
"message" to (e.message ?: "Failed to batch save approver stock take records all")
))
}
}
@PostMapping("/updateStockTakeRecordStatusToNotMatch")
fun updateStockTakeRecordStatusToNotMatch(
@RequestParam stockTakeRecordId: Long


Caricamento…
Annulla
Salva