Browse Source

update stock take

master
CANCERYS\kw093 10 hours ago
parent
commit
2be079ca2e
3 changed files with 228 additions and 152 deletions
  1. +200
    -149
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  2. +26
    -3
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt
  3. +2
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt

+ 200
- 149
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt View File

@@ -42,6 +42,8 @@ import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import com.ffii.fpsms.modules.stock.entity.StockLedger
import java.time.LocalDate
import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
import java.math.RoundingMode
import com.ffii.fpsms.modules.stock.entity.StockTake
@Service
class StockTakeRecordService(
val stockTakeRepository: StockTakeRepository,
@@ -392,7 +394,8 @@ class StockTakeRecordService(
remarks = null,
warehouseSlot = warehouse?.slot,
warehouseArea = warehouse?.area,
warehouse = warehouse?.warehouse
warehouse = warehouse?.warehouse,
bookQty = null,

)
}
@@ -483,20 +486,26 @@ class StockTakeRecordService(
approverQty = stockTakeRecord?.approverStockTakeQty,
approverBadQty = stockTakeRecord?.approverBadQty,
finalQty = stockTakeLine?.finalQty,
bookQty = stockTakeRecord?.bookQty,
)
}

// Apply pagination
val pageable = PageRequest.of(pageNum, pageSize)
val startIndex = pageable.offset.toInt()
val endIndex = minOf(startIndex + pageSize, allResults.size)
val paginatedResult = if (startIndex < allResults.size) {
allResults.subList(startIndex, endIndex)
} else {
emptyList()
}

return RecordsRes(paginatedResult, allResults.size)
val startIndex = pageable.offset.toInt()
val filteredResults = allResults.filter { response ->
val av = response.availableQty ?: BigDecimal.ZERO
// 显示: availableQty > 0,或已有盘点记录(即使盘点结果为 0)
av.compareTo(BigDecimal.ZERO) > 0 || response.stockTakeRecordId != null
}
// endIndex 必須基於 filteredResults.size,不能用 allResults.size
val endIndex = minOf(startIndex + pageSize, filteredResults.size)
val paginatedResult = if (startIndex < filteredResults.size) {
filteredResults.subList(startIndex, endIndex)
} else {
emptyList()
}
return RecordsRes(paginatedResult, filteredResults.size)
}

open fun saveStockTakeRecord(
@@ -888,142 +897,10 @@ class StockTakeRecordService(
inventoryLot.item?.id ?: throw IllegalArgumentException("Item ID not found")
).orElse(null) ?: throw IllegalArgumentException("Inventory not found for item")

// 只有 variance != 0 时才做调整
if (varianceQty != BigDecimal.ZERO) {
// 新增一条 StockTakeLine 记录
val stockTakeLine = StockTakeLine().apply {
this.stockTake = stockTake
this.inventoryLotLine = inventoryLotLine
this.initialQty = stockTakeRecord.bookQty
this.finalQty = finalQty
this.status = StockTakeLineStatus.COMPLETED
this.completeDate = java.time.LocalDateTime.now()
}
stockTakeLineRepository.save(stockTakeLine)

// variance < 0:做出库,影响 outQty(减少库存)
if (varianceQty < BigDecimal.ZERO) {
val absVariance = varianceQty.negate() // 正数,表示实际出库数量

var stockOut = stockOutRepository.findByStockTakeIdAndDeletedFalse(stockTakeId)
?: StockOut().apply {
this.type = "stockTake"
this.status = "completed"
this.handler = request.approverId
}.also { stockOutRepository.save(it) }

val stockOutLine = StockOutLine().apply {
this.item = inventoryLot.item
this.qty = absVariance.toDouble()
this.stockOut = stockOut
this.inventoryLotLine = inventoryLotLine
this.status = "completed"
this.type = "ADJ"
}
stockOutLineRepository.save(stockOutLine)

val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() - absVariance.toDouble()
val stockLedger = StockLedger().apply {
this.inventory = inventory
this.itemId = inventoryLot.item?.id
this.itemCode = stockTakeRecord.itemCode
this.inQty = null
this.outQty = absVariance.toDouble()
this.stockOutLine = stockOutLine
this.balance = newBalance
this.type = "TKE"
this.date = LocalDate.now()
}
stockLedgerRepository.save(stockLedger)

// 更新原来这条 InventoryLotLine 的 outQty,而不是 inQty
val newOutQty = (inventoryLotLine.outQty ?: BigDecimal.ZERO).add(absVariance)
val updateRequest = SaveInventoryLotLineRequest(
id = inventoryLotLine.id,
inventoryLotId = inventoryLotLine.inventoryLot?.id,
warehouseId = inventoryLotLine.warehouse?.id,
stockUomId = inventoryLotLine.stockUom?.id,
inQty = inventoryLotLine.inQty, // 不改 inQty
outQty = newOutQty, // 增加出库数量
holdQty = inventoryLotLine.holdQty,
status = inventoryLotLine.status?.value,
remarks = inventoryLotLine.remarks
)
inventoryLotLineService.saveInventoryLotLine(updateRequest)
}

// variance > 0:做入库,创建新 lot / lotLine,StockInLine 指向新 lot / lotLine
if (varianceQty > BigDecimal.ZERO) {
val plusQty = varianceQty
var stockIn = stockInRepository.findByStockTakeIdAndDeletedFalse(stockTakeId)
?: StockIn().apply {
this.code = stockTake.code
this.status = "completed"
this.stockTake = stockTake
}.also { stockInRepository.save(it) }
// 1. 先创建 StockInLine(不设置 inventoryLot,因为还没有创建)
val stockInLine = StockInLine().apply {
this.stockTakeLine = stockTakeLine
this.item = inventoryLot.item
this.itemNo = stockTakeRecord.itemCode
this.stockIn = stockIn
this.demandQty = plusQty
this.acceptedQty = plusQty
this.expiryDate = inventoryLot.expiryDate
this.lotNo = inventoryLot.lotNo
this.status = "completed"
this.type = "TKE"
// 注意:此时不设置 inventoryLot 和 inventoryLotLine,因为还没有创建
}
stockInLineRepository.save(stockInLine)
// 2. 创建 InventoryLot(设置 stockInLine)
val newInventoryLot = InventoryLot().apply {
this.item = inventoryLot.item
this.lotNo = inventoryLot.lotNo
this.expiryDate = inventoryLot.expiryDate
this.productionDate = inventoryLot.productionDate
this.stockInDate = LocalDateTime.now()
this.stockInLine = stockInLine // 设置 stockInLine
}
inventoryLotRepository.save(newInventoryLot)
// 3. 创建 InventoryLotLine
val newInventoryLotLine = InventoryLotLine().apply {
this.inventoryLot = newInventoryLot
this.warehouse = inventoryLotLine.warehouse
this.stockUom = inventoryLotLine.stockUom
this.inQty = plusQty
this.outQty = BigDecimal.ZERO
this.holdQty = BigDecimal.ZERO
this.status = inventoryLotLine.status
this.remarks = "Stock take adjustment (+$plusQty)"
}
inventoryLotLineRepository.save(newInventoryLotLine)
// 4. 更新 StockInLine,设置 inventoryLot 和 inventoryLotLine
stockInLine.inventoryLot = newInventoryLot
stockInLine.inventoryLotLine = newInventoryLotLine
stockInLineRepository.save(stockInLine)
val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() + plusQty.toDouble()
val stockLedger = StockLedger().apply {
this.inventory = inventory
this.itemId = newInventoryLot.item?.id
this.itemCode = newInventoryLot.item?.code
this.inQty = plusQty.toDouble()
this.outQty = null
this.stockInLine = stockInLine
this.balance = newBalance
this.type = "Adj"
this.date = LocalDate.now()
}
stockLedgerRepository.save(stockLedger)
// 注意:正差额不再修改原来的 inventoryLotLine
// 只有 variance != 0 时才做调整
if (varianceQty != BigDecimal.ZERO) {
applyVarianceAdjustment(stockTake, stockTakeRecord, finalQty!!, varianceQty, request.approverId)
}
}
val stockTakeSection = savedRecord.stockTakeSection
if (stockTakeSection != null) {
checkAndUpdateStockTakeStatus(stockTakeId, stockTakeSection)
@@ -1083,11 +960,23 @@ open fun batchSaveApproverStockTakeRecords(
val bookQty = record.bookQty ?: BigDecimal.ZERO
val varianceQty = qty.subtract(bookQty)
if (varianceQty.compareTo(BigDecimal.ZERO) != 0) {

val tolerancePercent = request.variancePercentTolerance ?: BigDecimal.ZERO
val shouldSkip = if (tolerancePercent.compareTo(BigDecimal.ZERO) <= 0) {
varianceQty.compareTo(BigDecimal.ZERO) != 0 // 原有逻辑:仅 variance=0 可保存
} 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 // |variance| > threshold 则跳过
}
}
if (shouldSkip) {
skippedCount++
println("Skipping record ${record.id}: variance = $varianceQty (qty=$qty, bookQty=$bookQty)")
println("Skipping record ${record.id}: |variance| > ${tolerancePercent}% of bookQty (variance=$varianceQty, bookQty=$bookQty)")
return@forEach
}
@@ -1102,6 +991,16 @@ open fun batchSaveApproverStockTakeRecords(
}
stockTakeRecordRepository.save(record)
if (varianceQty != BigDecimal.ZERO) {
try {
applyVarianceAdjustment(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
}
}
successCount++
} catch (e: Exception) {
errorCount++
@@ -1124,7 +1023,158 @@ open fun batchSaveApproverStockTakeRecords(
)
}

/**
* 根据 variance 调整库存并创建 Stock Ledger。
* 当 variance != 0 时:创建 StockTakeLine,并根据 variance 正负创建 StockIn/StockOut 及 Ledger。
*/
private fun applyVarianceAdjustment(
stockTake: StockTake,
stockTakeRecord: StockTakeRecord,
finalQty: BigDecimal,
varianceQty: BigDecimal,
approverId: Long?
) {
if (varianceQty == BigDecimal.ZERO) return

val inventoryLotLine = inventoryLotLineRepository.findByIdAndDeletedIsFalse(
stockTakeRecord.inventoryLotId ?: throw IllegalArgumentException("Inventory lot ID not found")
) ?: throw IllegalArgumentException("Inventory lot line not found")

val inventoryLot = inventoryLotRepository.findByIdAndDeletedFalse(
inventoryLotLine.inventoryLot?.id ?: throw IllegalArgumentException("Inventory lot ID not found")
) ?: throw IllegalArgumentException("Inventory lot not found")

val inventory = inventoryRepository.findByItemId(
inventoryLot.item?.id ?: throw IllegalArgumentException("Item ID not found")
).orElse(null) ?: throw IllegalArgumentException("Inventory not found for item")

// 1. 创建 StockTakeLine
val stockTakeLine = StockTakeLine().apply {
this.stockTake = stockTake
this.inventoryLotLine = inventoryLotLine
this.initialQty = stockTakeRecord.bookQty
this.finalQty = finalQty
this.status = StockTakeLineStatus.COMPLETED
this.completeDate = java.time.LocalDateTime.now()
}
stockTakeLineRepository.save(stockTakeLine)

// 2. variance < 0:出库 + StockLedger
if (varianceQty < BigDecimal.ZERO) {
val absVariance = varianceQty.negate()

var stockOut = stockOutRepository.findByStockTakeIdAndDeletedFalse(stockTake.id!!)
?: StockOut().apply {
this.type = "stockTake"
this.status = "completed"
this.handler = approverId
}.also { stockOutRepository.save(it) }

val stockOutLine = StockOutLine().apply {
this.item = inventoryLot.item
this.qty = absVariance.toDouble()
this.stockOut = stockOut
this.inventoryLotLine = inventoryLotLine
this.status = "completed"
this.type = "TKE"
}
stockOutLineRepository.save(stockOutLine)

val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() - absVariance.toDouble()
val stockLedger = StockLedger().apply {
this.inventory = inventory
this.itemId = inventoryLot.item?.id
this.itemCode = stockTakeRecord.itemCode
this.inQty = null
this.outQty = absVariance.toDouble()
this.stockOutLine = stockOutLine
this.balance = newBalance
this.type = "TKE"
this.date = LocalDate.now()
}
stockLedgerRepository.save(stockLedger)

val newOutQty = (inventoryLotLine.outQty ?: BigDecimal.ZERO).add(absVariance)
val updateRequest = SaveInventoryLotLineRequest(
id = inventoryLotLine.id,
inventoryLotId = inventoryLotLine.inventoryLot?.id,
warehouseId = inventoryLotLine.warehouse?.id,
stockUomId = inventoryLotLine.stockUom?.id,
inQty = inventoryLotLine.inQty,
outQty = newOutQty,
holdQty = inventoryLotLine.holdQty,
status = inventoryLotLine.status?.value,
remarks = inventoryLotLine.remarks
)
inventoryLotLineService.saveInventoryLotLine(updateRequest)
}

// 3. variance > 0:入库 + StockLedger
if (varianceQty > BigDecimal.ZERO) {
val plusQty = varianceQty

var stockIn = stockInRepository.findByStockTakeIdAndDeletedFalse(stockTake.id!!)
?: StockIn().apply {
this.code = stockTake.code
this.status = "completed"
this.stockTake = stockTake
}.also { stockInRepository.save(it) }

val stockInLine = StockInLine().apply {
this.stockTakeLine = stockTakeLine
this.item = inventoryLot.item
this.itemNo = stockTakeRecord.itemCode
this.stockIn = stockIn
this.demandQty = plusQty
this.acceptedQty = plusQty
this.expiryDate = inventoryLot.expiryDate
this.lotNo = inventoryLot.lotNo
this.status = "completed"
this.type = "TKE"
}
stockInLineRepository.save(stockInLine)

val newInventoryLot = InventoryLot().apply {
this.item = inventoryLot.item
this.lotNo = inventoryLot.lotNo
this.expiryDate = inventoryLot.expiryDate
this.productionDate = inventoryLot.productionDate
this.stockInDate = LocalDateTime.now()
this.stockInLine = stockInLine
}
inventoryLotRepository.save(newInventoryLot)

val newInventoryLotLine = InventoryLotLine().apply {
this.inventoryLot = newInventoryLot
this.warehouse = inventoryLotLine.warehouse
this.stockUom = inventoryLotLine.stockUom
this.inQty = plusQty
this.outQty = BigDecimal.ZERO
this.holdQty = BigDecimal.ZERO
this.status = inventoryLotLine.status
this.remarks = "Stock take adjustment (+$plusQty)"
}
inventoryLotLineRepository.save(newInventoryLotLine)

stockInLine.inventoryLot = newInventoryLot
stockInLine.inventoryLotLine = newInventoryLotLine
stockInLineRepository.save(stockInLine)

val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() + plusQty.toDouble()
val stockLedger = StockLedger().apply {
this.inventory = inventory
this.itemId = newInventoryLot.item?.id
this.itemCode = newInventoryLot.item?.code
this.inQty = plusQty.toDouble()
this.outQty = null
this.stockInLine = stockInLine
this.balance = newBalance
this.type = "TKE"
this.date = LocalDate.now()
}
stockLedgerRepository.save(stockLedger)
}
}
open fun updateStockTakeRecordStatusToNotMatch(stockTakeRecordId: Long): StockTakeRecord {
println("updateStockTakeRecordStatusToNotMatch called with stockTakeRecordId: $stockTakeRecordId")
@@ -1233,6 +1283,7 @@ open fun getInventoryLotDetailsByStockTakeSectionNotMatch(
approverQty = stockTakeRecord.approverStockTakeQty,
approverBadQty = stockTakeRecord.approverBadQty,
finalQty = stockTakeLine?.finalQty,
bookQty = stockTakeRecord.bookQty,
)
}


+ 26
- 3
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt View File

@@ -251,7 +251,7 @@ class StockTakeService(
val allStockTakes = stockTakeRepository.findAll()
.filter { !it.deleted }
.groupBy { it.stockTakeSection }
/*
// 3. 为每个 stockTakeSection 检查并创建
distinctSections.forEach { section ->
val stockTakesForSection = allStockTakes[section] ?: emptyList()
@@ -288,9 +288,32 @@ class StockTakeService(
logger.info("Skipped section $section: Has non-completed records")
}
}
*/
// 移除 null section 处理逻辑,因为 warehouse 表中没有 null 的 stockTakeSection
distinctSections.forEach { section ->
try {
val now = LocalDateTime.now()
val code = assignStockTakeNo()
val saveStockTakeReq = SaveStockTakeRequest(
code = code,
planStart = now,
planEnd = now.plusDays(1),
actualStart = null,
actualEnd = null,
status = StockTakeStatus.PENDING.value,
remarks = null,
stockTakeSection = section
)
val savedStockTake = saveStockTake(saveStockTakeReq)
result[section] = "Created: ${savedStockTake.code}"
logger.info("Created stock take for section $section: ${savedStockTake.code}")
} catch (e: Exception) {
result[section] = "Error: ${e.message}"
logger.error("Error creating stock take for section $section: ${e.message}")
}
}
logger.info("--------- End - Create Stock Take for Sections -------")
return result
}

+ 2
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt View File

@@ -57,6 +57,7 @@ data class InventoryLotDetailResponse(
val approverQty: BigDecimal? = null,
val approverBadQty: BigDecimal? = null,
val finalQty: BigDecimal? = null,
val bookQty: BigDecimal? = null,
)
data class InventoryLotLineListRequest(
val warehouseCode: String
@@ -94,6 +95,7 @@ data class BatchSaveApproverStockTakeRecordRequest(
val stockTakeId: Long,
val stockTakeSection: String,
val approverId: Long,
val variancePercentTolerance: BigDecimal? = null,
)

data class BatchSaveApproverStockTakeRecordResponse(


Loading…
Cancel
Save