|
|
|
@@ -631,6 +631,21 @@ open class StockTakeRecordService( |
|
|
|
return LatestStockTakeRoundMetaResponse(stockTakeRoundId = roundKey, planStartDate = planStart) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 與盤點員列表一致:先取全系統 MAX(stockTakeRoundId),再在該輪內選一筆代表 stock_take(供審核 API 解析整輪)。 |
|
|
|
*/ |
|
|
|
private fun resolveLatestRoundRepresentativeStockTake(): StockTake? { |
|
|
|
val roundKey = resolveGlobalLatestStockTakeRoundKey() ?: return null |
|
|
|
val inRound = stockTakeRepository.findAllByStockTakeRoundIdAndDeletedIsFalse(roundKey) |
|
|
|
if (inRound.isNotEmpty()) { |
|
|
|
return inRound.maxWithOrNull( |
|
|
|
compareBy<StockTake> { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } |
|
|
|
.thenBy { it.id ?: 0L } |
|
|
|
) |
|
|
|
} |
|
|
|
return stockTakeRepository.findByIdAndDeletedIsFalse(roundKey) |
|
|
|
} |
|
|
|
|
|
|
|
open fun getApproverInventoryLotDetailsAll( |
|
|
|
stockTakeId: Long? = null, |
|
|
|
pageNum: Int = 0, |
|
|
|
@@ -883,11 +898,7 @@ open class StockTakeRecordService( |
|
|
|
val warehouseIds = allWarehouses.mapNotNull { it.id } |
|
|
|
if (warehouseIds.isEmpty()) return emptyList() |
|
|
|
|
|
|
|
val allStockTakes = stockTakeRepository.findAll().filter { !it.deleted } |
|
|
|
if (allStockTakes.isEmpty()) return emptyList() |
|
|
|
|
|
|
|
val latestBaseStockTake = allStockTakes |
|
|
|
.maxByOrNull { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } |
|
|
|
val latestBaseStockTake = resolveLatestRoundRepresentativeStockTake() |
|
|
|
?: return emptyList() |
|
|
|
|
|
|
|
// 优先用 stocktakerecord.stockTakeRoundId 直接取该轮次记录(避免再算 stockTakeId 列表 + in 查询) |
|
|
|
@@ -966,11 +977,21 @@ open class StockTakeRecordService( |
|
|
|
* 用于前端先拿 stockTakeId,再调用 pending/approved 明细接口。 |
|
|
|
*/ |
|
|
|
open fun getLatestApproverStockTakeHeader(): AllPickedStockTakeListReponse? { |
|
|
|
val latestBaseStockTake = stockTakeRepository.findAll() |
|
|
|
.filter { !it.deleted } |
|
|
|
.maxByOrNull { it.actualStart ?: it.planStart ?: LocalDateTime.MIN } |
|
|
|
val latestBaseStockTake = resolveLatestRoundRepresentativeStockTake() |
|
|
|
?: return null |
|
|
|
|
|
|
|
val roundKey = latestBaseStockTake.stockTakeRoundId ?: latestBaseStockTake.id |
|
|
|
val roundStockTakes = if (roundKey != null) { |
|
|
|
stockTakeRepository.findAllByStockTakeRoundIdAndDeletedIsFalse(roundKey) |
|
|
|
} else { |
|
|
|
emptyList() |
|
|
|
} |
|
|
|
val planStartDate = if (roundStockTakes.isNotEmpty()) { |
|
|
|
roundStockTakes.mapNotNull { it.planStart }.maxOrNull()?.toLocalDate() |
|
|
|
} else { |
|
|
|
latestBaseStockTake.planStart?.toLocalDate() |
|
|
|
} |
|
|
|
|
|
|
|
val statusValue = latestBaseStockTake.status?.let { st -> |
|
|
|
if (st == StockTakeStatus.APPROVING || st == StockTakeStatus.COMPLETED) st.value else "" |
|
|
|
} ?: "" |
|
|
|
@@ -983,14 +1004,14 @@ open class StockTakeRecordService( |
|
|
|
currentStockTakeItemNumber = 0, |
|
|
|
totalInventoryLotNumber = 0, |
|
|
|
stockTakeId = latestBaseStockTake.id ?: 0, |
|
|
|
stockTakeRoundId = latestBaseStockTake.stockTakeRoundId ?: latestBaseStockTake.id, |
|
|
|
stockTakeRoundId = roundKey, |
|
|
|
stockTakerName = null, |
|
|
|
approverName = null, |
|
|
|
TotalItemNumber = 0, |
|
|
|
startTime = latestBaseStockTake.actualStart, |
|
|
|
endTime = latestBaseStockTake.actualEnd, |
|
|
|
ReStockTakeTrueFalse = false, |
|
|
|
planStartDate = latestBaseStockTake.planStart?.toLocalDate(), |
|
|
|
planStartDate = planStartDate, |
|
|
|
stockTakeSectionDescription = null, |
|
|
|
warehouseArea = null, |
|
|
|
storeId = null |
|
|
|
@@ -1674,6 +1695,64 @@ open class StockTakeRecordService( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private fun resolveApproverFinalQuantities( |
|
|
|
request: SaveApproverStockTakeRecordRequest |
|
|
|
): Pair<BigDecimal, BigDecimal> { |
|
|
|
val finalQty = if (request.approverQty != null && request.approverBadQty != null) { |
|
|
|
request.approverQty!! |
|
|
|
} else { |
|
|
|
request.qty |
|
|
|
} |
|
|
|
val finalBadQty = if (request.approverQty != null && request.approverBadQty != null) { |
|
|
|
request.approverBadQty!! |
|
|
|
} else { |
|
|
|
request.badQty |
|
|
|
} |
|
|
|
return Pair(finalQty, finalBadQty) |
|
|
|
} |
|
|
|
|
|
|
|
private fun resolveApproverLastSelect( |
|
|
|
request: SaveApproverStockTakeRecordRequest, |
|
|
|
stockTakeRecord: StockTakeRecord |
|
|
|
): Int { |
|
|
|
return request.lastSelect ?: run { |
|
|
|
if (request.approverQty != null && request.approverBadQty != null) { |
|
|
|
3 |
|
|
|
} else if ( |
|
|
|
stockTakeRecord.pickerSecondStockTakeQty != null && |
|
|
|
stockTakeRecord.pickerSecondStockTakeQty!!.compareTo(request.qty) == 0 |
|
|
|
) { |
|
|
|
2 |
|
|
|
} else { |
|
|
|
1 |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private fun applyApproverSaveToRecord( |
|
|
|
stockTakeRecord: StockTakeRecord, |
|
|
|
request: SaveApproverStockTakeRecordRequest, |
|
|
|
approverId: Long, |
|
|
|
approverName: String? |
|
|
|
): Pair<BigDecimal, BigDecimal> { |
|
|
|
val (finalQty, finalBadQty) = resolveApproverFinalQuantities(request) |
|
|
|
val varianceQty = finalQty.subtract(stockTakeRecord.bookQty ?: BigDecimal.ZERO) |
|
|
|
stockTakeRecord.apply { |
|
|
|
this.approverId = approverId |
|
|
|
this.approverName = approverName |
|
|
|
this.approverStockTakeQty = finalQty |
|
|
|
this.approverBadQty = finalBadQty |
|
|
|
this.varianceQty = varianceQty |
|
|
|
this.status = "completed" |
|
|
|
this.approverTime = java.time.LocalDateTime.now() |
|
|
|
this.lastSelect = resolveApproverLastSelect(request, this) |
|
|
|
if (this.stockTakeEndTime == null) { |
|
|
|
this.stockTakeEndTime = java.time.LocalDateTime.now() |
|
|
|
} |
|
|
|
} |
|
|
|
return Pair(finalQty, varianceQty) |
|
|
|
} |
|
|
|
|
|
|
|
open fun saveApproverStockTakeRecord( |
|
|
|
request: SaveApproverStockTakeRecordRequest, |
|
|
|
stockTakeId: Long |
|
|
|
@@ -1691,53 +1770,17 @@ open class StockTakeRecordService( |
|
|
|
val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(stockTakeId) |
|
|
|
?: throw IllegalArgumentException("Stock take not found: $stockTakeId") |
|
|
|
|
|
|
|
val finalQty = if (request.approverQty != null && request.approverBadQty != null) { |
|
|
|
request.approverQty |
|
|
|
} else { |
|
|
|
request.qty |
|
|
|
} |
|
|
|
|
|
|
|
val finalBadQty = if (request.approverQty != null && request.approverBadQty != null) { |
|
|
|
request.approverBadQty |
|
|
|
} else { |
|
|
|
request.badQty |
|
|
|
} |
|
|
|
|
|
|
|
val varianceQty = (finalQty ?: BigDecimal.ZERO) |
|
|
|
.subtract(stockTakeRecord.bookQty ?: BigDecimal.ZERO) |
|
|
|
val (finalQty, varianceQty) = applyApproverSaveToRecord( |
|
|
|
stockTakeRecord, |
|
|
|
request, |
|
|
|
request.approverId ?: 0L, |
|
|
|
user?.name |
|
|
|
) |
|
|
|
|
|
|
|
println("finalQty: $finalQty") |
|
|
|
println("stockTakeRecord.bookQty: ${stockTakeRecord.bookQty}") |
|
|
|
println("varianceQty: $varianceQty") |
|
|
|
|
|
|
|
stockTakeRecord.apply { |
|
|
|
this.approverId = request.approverId |
|
|
|
this.approverName = user?.name |
|
|
|
this.approverStockTakeQty = finalQty |
|
|
|
this.approverBadQty = finalBadQty |
|
|
|
this.varianceQty = varianceQty |
|
|
|
this.status = "completed" |
|
|
|
this.approverTime = java.time.LocalDateTime.now() |
|
|
|
this.lastSelect = request.lastSelect ?: run { |
|
|
|
// 兼容:旧客户端未传 lastSelect 时,尝试根据请求类型推断 |
|
|
|
if (request.approverQty != null && request.approverBadQty != null) { |
|
|
|
3 |
|
|
|
} else if ( |
|
|
|
this.pickerSecondStockTakeQty != null && |
|
|
|
this.pickerSecondStockTakeQty!!.compareTo(request.qty) == 0 |
|
|
|
) { |
|
|
|
2 |
|
|
|
} else { |
|
|
|
1 |
|
|
|
} |
|
|
|
} |
|
|
|
// stockTakeEndTime 目前只在 saveStockTakeRecord「第二次盤點」時寫入;只做第一次盤點時會一直是 null。 |
|
|
|
// 審核通過時若仍為空,補上時間,讓列表「審核/完成時間」有值(不覆寫已有第二次盤點結束時間)。 |
|
|
|
if (this.stockTakeEndTime == null) { |
|
|
|
this.stockTakeEndTime = java.time.LocalDateTime.now() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
val savedRecord = stockTakeRecordRepository.save(stockTakeRecord) |
|
|
|
|
|
|
|
// variance != 0:過帳並更新/新建 StockTakeLine;= 0:僅將開輪預建 line 標記完成(不查庫存) |
|
|
|
@@ -2034,8 +2077,24 @@ open fun batchSaveApproverStockTakeRecordsByIds( |
|
|
|
): BatchSaveApproverStockTakeRecordResponse { |
|
|
|
val totalStartNs = System.nanoTime() |
|
|
|
fun elapsedMs(startNs: Long): Long = (System.nanoTime() - startNs) / 1_000_000 |
|
|
|
logger.info("batchSaveApproverStockTakeRecordsByIds start: stockTakeId={}, ids={}", request.stockTakeId, request.recordIds.size) |
|
|
|
if (request.recordIds.isEmpty()) { |
|
|
|
|
|
|
|
val recordPayloadById = request.records |
|
|
|
.mapNotNull { r -> r.stockTakeRecordId?.let { id -> id to r } } |
|
|
|
.toMap() |
|
|
|
val useClientPayload = recordPayloadById.isNotEmpty() |
|
|
|
val idsToLoad = when { |
|
|
|
useClientPayload -> recordPayloadById.keys.toList() |
|
|
|
request.recordIds.isNotEmpty() -> request.recordIds |
|
|
|
else -> emptyList() |
|
|
|
} |
|
|
|
|
|
|
|
logger.info( |
|
|
|
"batchSaveApproverStockTakeRecordsByIds start: stockTakeId={}, ids={}, clientPayload={}", |
|
|
|
request.stockTakeId, |
|
|
|
idsToLoad.size, |
|
|
|
useClientPayload |
|
|
|
) |
|
|
|
if (idsToLoad.isEmpty()) { |
|
|
|
return BatchSaveApproverStockTakeRecordResponse(0, 0, listOf("No record IDs provided")) |
|
|
|
} |
|
|
|
|
|
|
|
@@ -2044,8 +2103,8 @@ open fun batchSaveApproverStockTakeRecordsByIds( |
|
|
|
val stockTake = stockTakeRepository.findByIdAndDeletedIsFalse(request.stockTakeId) |
|
|
|
?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}") |
|
|
|
|
|
|
|
val idSet = request.recordIds.toSet() |
|
|
|
val stockTakeRecords = stockTakeRecordRepository.findAllById(request.recordIds) |
|
|
|
val idSet = idsToLoad.toSet() |
|
|
|
val stockTakeRecords = stockTakeRecordRepository.findAllById(idsToLoad) |
|
|
|
.filter { |
|
|
|
!it.deleted && |
|
|
|
(it.id in idSet) && |
|
|
|
@@ -2081,35 +2140,48 @@ open fun batchSaveApproverStockTakeRecordsByIds( |
|
|
|
|
|
|
|
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 payload = recordPayloadById[record.id] |
|
|
|
if (useClientPayload && payload == null) { |
|
|
|
errorCount++ |
|
|
|
errors.add("No client payload for record ${record.id}") |
|
|
|
return@forEach |
|
|
|
} |
|
|
|
|
|
|
|
val bookQty = record.bookQty ?: BigDecimal.ZERO |
|
|
|
val varianceQty = qty.subtract(bookQty) |
|
|
|
|
|
|
|
record.apply { |
|
|
|
this.approverId = request.approverId |
|
|
|
this.approverName = user?.name |
|
|
|
this.approverStockTakeQty = qty |
|
|
|
this.approverBadQty = badQty |
|
|
|
this.varianceQty = varianceQty |
|
|
|
this.status = "completed" |
|
|
|
this.approverTime = java.time.LocalDateTime.now() |
|
|
|
this.lastSelect = if ( |
|
|
|
record.pickerSecondStockTakeQty != null && |
|
|
|
record.pickerSecondStockTakeQty!! > BigDecimal.ZERO |
|
|
|
) 2 else 1 |
|
|
|
if (this.stockTakeEndTime == null) { |
|
|
|
this.stockTakeEndTime = java.time.LocalDateTime.now() |
|
|
|
val (qty, varianceQty) = if (payload != null) { |
|
|
|
applyApproverSaveToRecord( |
|
|
|
record, |
|
|
|
payload.copy(approverId = request.approverId), |
|
|
|
request.approverId, |
|
|
|
user?.name |
|
|
|
) |
|
|
|
} else { |
|
|
|
val legacyQty: BigDecimal |
|
|
|
val legacyBadQty: BigDecimal |
|
|
|
if (record.pickerSecondStockTakeQty != null && record.pickerSecondStockTakeQty!! > BigDecimal.ZERO) { |
|
|
|
legacyQty = record.pickerSecondStockTakeQty!! |
|
|
|
legacyBadQty = record.pickerSecondBadQty ?: BigDecimal.ZERO |
|
|
|
} else { |
|
|
|
legacyQty = record.pickerFirstStockTakeQty ?: BigDecimal.ZERO |
|
|
|
legacyBadQty = record.pickerFirstBadQty ?: BigDecimal.ZERO |
|
|
|
} |
|
|
|
val legacyVariance = legacyQty.subtract(record.bookQty ?: BigDecimal.ZERO) |
|
|
|
record.apply { |
|
|
|
this.approverId = request.approverId |
|
|
|
this.approverName = user?.name |
|
|
|
this.approverStockTakeQty = legacyQty |
|
|
|
this.approverBadQty = legacyBadQty |
|
|
|
this.varianceQty = legacyVariance |
|
|
|
this.status = "completed" |
|
|
|
this.approverTime = java.time.LocalDateTime.now() |
|
|
|
this.lastSelect = if ( |
|
|
|
record.pickerSecondStockTakeQty != null && |
|
|
|
record.pickerSecondStockTakeQty!! > BigDecimal.ZERO |
|
|
|
) 2 else 1 |
|
|
|
if (this.stockTakeEndTime == null) { |
|
|
|
this.stockTakeEndTime = java.time.LocalDateTime.now() |
|
|
|
} |
|
|
|
} |
|
|
|
Pair(legacyQty, legacyVariance) |
|
|
|
} |
|
|
|
|
|
|
|
recordsToPersist.add(record) |
|
|
|
|