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