Procházet zdrojové kódy

fix stock take efficient and jo picking dashboard efficient

master
CANCERYS\kw093 před 1 týdnem
rodič
revize
588eb7f0a8
7 změnil soubory, kde provedl 259 přidání a 80 odebrání
  1. +63
    -39
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt
  2. +8
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt
  3. +21
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt
  4. +134
    -41
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  5. +22
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt
  6. +6
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt
  7. +5
    -0
      src/main/resources/db/changelog/changes/20260427_01_Enson/01_alter_stock_take.sql

+ 63
- 39
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt Zobrazit soubor

@@ -2906,35 +2906,65 @@ open fun getMaterialPickStatus(date: String?): List<MaterialPickStatusItem> {
// Get all joPickOrders
val joPickOrders = joPickOrderRepository.findAll()
// Filter by date if provided (filter by jobOrder.planStart date)
val filteredJoPickOrders = if (filterDate != null) {
joPickOrders.filter { joPickOrder ->
val jobOrder = joPickOrder.jobOrderId?.let {
jobOrderRepository.findById(it).orElse(null)
}
jobOrder?.planStart?.toLocalDate() == filterDate
}
if (joPickOrders.isEmpty()) return emptyList()

// Batch load related Job Orders once to avoid repeated findById calls
val jobOrderIds = joPickOrders.mapNotNull { it.jobOrderId }.distinct()
val jobOrdersById = if (jobOrderIds.isEmpty()) {
emptyMap()
} else {
joPickOrders
jobOrderRepository.findAllById(jobOrderIds).associateBy { it.id }
}
// Group by jobOrderId
val groupedByJobOrder = filteredJoPickOrders
// Filter by date/status in-memory using preloaded Job Orders
val groupedByJobOrder = joPickOrders
.filter { joPickOrder ->
val jobOrder = joPickOrder.jobOrderId?.let {
jobOrderRepository.findById(it).orElse(null)
}
jobOrder?.status != JobOrderStatus.COMPLETED
val jobOrder = joPickOrder.jobOrderId?.let { jobOrdersById[it] } ?: return@filter false
val matchesDate = filterDate == null || jobOrder.planStart?.toLocalDate() == filterDate
val notCompleted = jobOrder.status != JobOrderStatus.COMPLETED
matchesDate && notCompleted
}
.groupBy { it.jobOrderId }

val allPickOrderIds = groupedByJobOrder.values
.flatten()
.mapNotNull { it.pickOrderId }
.distinct()

val pickOrdersById = if (allPickOrderIds.isEmpty()) {
emptyMap()
} else {
pickOrderRepository.findAllById(allPickOrderIds).associateBy { it.id }
}

val allPickOrderLines = if (allPickOrderIds.isEmpty()) {
emptyList()
} else {
pickOrderLineRepository.findAllByPickOrderIdInAndDeletedFalse(allPickOrderIds)
}
val pickOrderLinesByPickOrderId = allPickOrderLines.groupBy { it.pickOrder?.id }

val pickOrderLineIds = allPickOrderLines.mapNotNull { it.id }.distinct()
val allStockOutLines = if (pickOrderLineIds.isEmpty()) {
emptyList()
} else {
stockOutLineRepository.findAllByPickOrderLineIdInAndDeletedFalse(pickOrderLineIds)
}
val stockOutLinesByPickOrderLineId = allStockOutLines.groupBy { it.pickOrderLine?.id }

val allIssues = if (allPickOrderIds.isEmpty()) {
emptyList()
} else {
pickExecutionIssueRepository.findAllByPickOrderIdInAndDeletedFalse(allPickOrderIds)
}
val issuesByPickOrderId = allIssues.groupBy { it.pickOrderId }
// Map each job order group to a single MaterialPickStatusItem
return groupedByJobOrder.mapNotNull { (jobOrderId, joPickOrdersForJob) ->
if (jobOrderId == null) return@mapNotNull null
val jobOrder = jobOrderRepository.findById(jobOrderId).orElse(null)
?: return@mapNotNull null
val jobOrder = jobOrdersById[jobOrderId] ?: return@mapNotNull null
// Get BOM item (finished good/semi-finished product), not BOM Material item
val bomItem = jobOrder.bom?.item
@@ -2944,31 +2974,29 @@ open fun getMaterialPickStatus(date: String?): List<MaterialPickStatusItem> {
val pickOrderIds = joPickOrdersForJob.mapNotNull { it.pickOrderId }.distinct()
// Aggregate data from all pick orders for this job order
val allPickOrderLines = pickOrderIds.flatMap { poId ->
pickOrderLineRepository.findByPickOrderId(poId)
val allPickOrderLinesForJob = pickOrderIds.flatMap { poId ->
pickOrderLinesByPickOrderId[poId].orEmpty()
}
// Get all stock out lines for all pick orders of this job order
val allStockOutLines = allPickOrderLines.flatMap { pol ->
stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(pol.id!!)
.mapNotNull { solInfo ->
stockOutLineRepository.findById(solInfo.id).orElse(null)
}
val allStockOutLinesForJob = allPickOrderLinesForJob.flatMap { pol ->
val polId = pol.id ?: return@flatMap emptyList()
stockOutLinesByPickOrderLineId[polId].orEmpty()
}
// ✅ 修复:startTime = 第一个 item 开始提料的时间
// 只考虑已经开始的 items(status 不是 pending),取最早的 startTime
val pickStartTime = allStockOutLines
val pickStartTime = allStockOutLinesForJob
.filter { it.status != null && it.status != "pending" }
.mapNotNull { it.startTime }
.minOrNull()
// Count total items to pick (number of distinct pick order lines)
val numberOfItemsToPick = allPickOrderLines.size
val numberOfItemsToPick = allPickOrderLinesForJob.size
// 已结束行数:有至少一条 completed,或整行全是 rejected 都算「已结束」
val finishedItemsCount = allPickOrderLines.count { pol ->
val stockOutLinesForPol = allStockOutLines.filter {
val finishedItemsCount = allPickOrderLinesForJob.count { pol ->
val stockOutLinesForPol = allStockOutLinesForJob.filter {
it.pickOrderLine?.id == pol.id
}
stockOutLinesForPol.any { it.status == "completed" } ||
@@ -2977,13 +3005,13 @@ open fun getMaterialPickStatus(date: String?): List<MaterialPickStatusItem> {
// 只有当所有 items 都已结束(完成或全部拒绝)时,才返回 endTime
val pickEndTime = if (finishedItemsCount == numberOfItemsToPick && numberOfItemsToPick > 0) {
val completedEndTime = allStockOutLines
val completedEndTime = allStockOutLinesForJob
.filter { it.status == "completed" }
.mapNotNull { it.endTime }
.maxOrNull()
// 若没有任何 completed 的 endTime(例如全部 rejected),用 pick order 的 completeDate 作为结束时间
completedEndTime ?: pickOrderIds.mapNotNull { poId ->
pickOrderRepository.findById(poId).orElse(null)?.completeDate
pickOrdersById[poId]?.completeDate
}.maxOrNull()
} else {
null
@@ -2994,7 +3022,7 @@ open fun getMaterialPickStatus(date: String?): List<MaterialPickStatusItem> {
// Count total items with issues from all pick orders
val numberOfItemsWithIssue = pickOrderIds.sumOf { poId ->
pickExecutionIssueRepository.findByPickOrderIdAndDeletedFalse(poId).size
issuesByPickOrderId[poId]?.size ?: 0
}
// Get job order quantity and UOM
@@ -3002,9 +3030,7 @@ open fun getMaterialPickStatus(date: String?): List<MaterialPickStatusItem> {
val uom = jobOrder.bom?.uom?.code
// Determine pick status - check if all pick orders are completed
val pickOrders = pickOrderIds.mapNotNull { poId ->
pickOrderRepository.findById(poId).orElse(null)
}
val pickOrders = pickOrderIds.mapNotNull { poId -> pickOrdersById[poId] }
val pickStatus = when {
pickOrders.isEmpty() -> null
pickOrders.all { it.status == com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus.COMPLETED } -> "completed"
@@ -3013,9 +3039,7 @@ open fun getMaterialPickStatus(date: String?): List<MaterialPickStatusItem> {
}
// Use the first pick order code (or combine if multiple)
val pickOrderCode = pickOrderIds.firstOrNull()?.let { poId ->
pickOrderRepository.findById(poId).orElse(null)?.code
}
val pickOrderCode = pickOrderIds.firstOrNull()?.let { poId -> pickOrdersById[poId]?.code }
MaterialPickStatusItem(
id = jobOrderId, // Use jobOrderId as id


+ 8
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt Zobrazit soubor

@@ -12,6 +12,14 @@ import org.springframework.transaction.annotation.Transactional
@Repository
interface PickExecutionIssueRepository : JpaRepository<PickExecutionIssue, Long> {
fun findByPickOrderIdAndDeletedFalse(pickOrderId: Long): List<PickExecutionIssue>
@Query(
"""
SELECT p FROM PickExecutionIssue p
WHERE p.deleted = false
AND p.pickOrderId IN :pickOrderIds
"""
)
fun findAllByPickOrderIdInAndDeletedFalse(@Param("pickOrderIds") pickOrderIds: List<Long>): List<PickExecutionIssue>
fun findByPickOrderLineIdAndDeletedFalse(pickOrderLineId: Long): List<PickExecutionIssue>
fun findByLotIdAndDeletedFalse(lotId: Long): List<PickExecutionIssue>
fun findByPickOrderLineIdAndLotIdAndDeletedFalse(


+ 21
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt Zobrazit soubor

@@ -12,7 +12,28 @@ interface StockTakeRecordRepository : AbstractRepository<StockTakeRecord, Long>
fun findAllByStockTakeIdAndDeletedIsFalse(stockTakeId: Long): List<StockTakeRecord>;
fun findAllByStockTakeIdInAndDeletedIsFalse(stockTakeIds: Collection<Long>): List<StockTakeRecord>;
fun findAllByStockTakeRoundIdAndDeletedIsFalse(stockTakeRoundId: Long): List<StockTakeRecord>;
fun findAllByStockTakeRoundIdInAndDeletedIsFalse(stockTakeRoundIds: Collection<Long>): List<StockTakeRecord>;
fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeRecord?;
fun findAllByStockTakeIdAndWarehouseIdInAndDeletedIsFalse(
stockTakeId: Long,
warehouseIds: Collection<Long>
): List<StockTakeRecord>

@Query(
"""
SELECT r
FROM StockTakeRecord r
WHERE r.deleted = false
AND r.stockTake.id = :stockTakeId
AND r.stockTakeSection = :stockTakeSection
AND r.approverStockTakeQty IS NULL
AND (r.pickerFirstStockTakeQty IS NOT NULL OR r.pickerSecondStockTakeQty IS NOT NULL)
"""
)
fun findPendingApproverRecordsByStockTakeAndSection(
@Param("stockTakeId") stockTakeId: Long,
@Param("stockTakeSection") stockTakeSection: String
): List<StockTakeRecord>

@Query("""
SELECT sl FROM StockLedger sl


+ 134
- 41
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt Zobrazit soubor

@@ -616,7 +616,7 @@ open class StockTakeRecordService(
val warehouse = ill.warehouse
val availableQty = (ill.inQty ?: BigDecimal.ZERO)
.subtract(ill.outQty ?: BigDecimal.ZERO)
.subtract(ill.holdQty ?: BigDecimal.ZERO)
//.subtract(ill.holdQty ?: BigDecimal.ZERO)
val stockTakeRecord = if (stockTakeId != null && inventoryLot?.id != null && warehouse?.id != null) {
stockTakeRecordsMap[Pair(inventoryLot.id, warehouse.id)]
@@ -770,7 +770,7 @@ open class StockTakeRecordService(

val availableQty = (ill.inQty ?: BigDecimal.ZERO)
.subtract(ill.outQty ?: BigDecimal.ZERO)
.subtract(ill.holdQty ?: BigDecimal.ZERO)
//.subtract(ill.holdQty ?: BigDecimal.ZERO)

availableQty.compareTo(BigDecimal.ZERO) > 0 || recordKeySet.contains(Pair(lotId, whId))
}
@@ -873,7 +873,7 @@ open class StockTakeRecordService(
val warehouse = ill.warehouse
val availableQty = (ill.inQty ?: BigDecimal.ZERO)
.subtract(ill.outQty ?: BigDecimal.ZERO)
.subtract(ill.holdQty ?: BigDecimal.ZERO)
//.subtract(ill.holdQty ?: BigDecimal.ZERO)

InventoryLotDetailResponse(
id = ill.id ?: 0L,
@@ -972,7 +972,7 @@ open class StockTakeRecordService(
val warehouse = ill.warehouse
val availableQty = (ill.inQty ?: BigDecimal.ZERO)
.subtract(ill.outQty ?: BigDecimal.ZERO)
.subtract(ill.holdQty ?: BigDecimal.ZERO)
// .subtract(ill.holdQty ?: BigDecimal.ZERO)
val inventoryLotLineId = ill.id
val stockTakeLine =
if (effectiveStockTakeId != null && inventoryLotLineId != null) {
@@ -1062,7 +1062,7 @@ open class StockTakeRecordService(
val warehouse = ill.warehouse
val availableQty = (ill.inQty ?: BigDecimal.ZERO)
.subtract(ill.outQty ?: BigDecimal.ZERO)
.subtract(ill.holdQty ?: BigDecimal.ZERO)
// .subtract(ill.holdQty ?: BigDecimal.ZERO)

val stockTakeRecord = if (stockTakeId != null && inventoryLot?.id != null && warehouse?.id != null) {
stockTakeRecordsMap[Pair(inventoryLot.id, warehouse.id)]
@@ -1161,7 +1161,7 @@ open class StockTakeRecordService(
// 2. 计算 availableQty
val availableQty = (inventoryLotLine.inQty ?: BigDecimal.ZERO)
.subtract(inventoryLotLine.outQty ?: BigDecimal.ZERO)
.subtract(inventoryLotLine.holdQty ?: BigDecimal.ZERO)
//.subtract(inventoryLotLine.holdQty ?: BigDecimal.ZERO)

// 3. 新建、更新預建記錄之第一次盤點、或第二次盤點
val stockTakeRecord: StockTakeRecord = if (request.stockTakeRecordId != null) {
@@ -1303,12 +1303,8 @@ open class StockTakeRecordService(
println("Found ${inventoryLotLines.size} inventory lot lines")

// 4. 使用 stockTakeId 获取已创建的记录,建立映射以排除它们
val existingRecordsMap = stockTakeRecordRepository.findAll()
.filter {
!it.deleted &&
it.stockTake?.id == request.stockTakeId &&
it.warehouse?.id in warehouseIds
}
val existingRecordsMap = stockTakeRecordRepository
.findAllByStockTakeIdAndWarehouseIdInAndDeletedIsFalse(request.stockTakeId, warehouseIds)
.associateBy {
Pair(it.inventoryLotId ?: 0L, it.warehouse?.id ?: 0L)
}
@@ -1350,7 +1346,7 @@ open class StockTakeRecordService(
// 计算 availableQty
val availableQty = (ill.inQty ?: BigDecimal.ZERO)
.subtract(ill.outQty ?: BigDecimal.ZERO)
.subtract(ill.holdQty ?: BigDecimal.ZERO)
// .subtract(ill.holdQty ?: BigDecimal.ZERO)

// 使用 availableQty 作为 qty,badQty 为 0
val qty = availableQty
@@ -1398,12 +1394,6 @@ open class StockTakeRecordService(
}
}
if (successCount > 0) {
val existingRecordsCount = stockTakeRecordRepository.findAll()
.filter {
!it.deleted &&
it.stockTake?.id == request.stockTakeId
}
.count()
checkAndUpdateStockTakeStatus(request.stockTakeId, request.stockTakeSection)
}
println("batchSaveStockTakeRecords completed: success=$successCount, errors=$errorCount")
@@ -1585,15 +1575,11 @@ open fun batchSaveApproverStockTakeRecords(
?: throw IllegalArgumentException("Stock take not found: ${request.stockTakeId}")

val stockTakeRecords = stockTakeRecordRepository.findAll()
.filter {
!it.deleted &&
it.stockTake?.id == request.stockTakeId &&
it.stockTakeSection == request.stockTakeSection &&
// 只处理已经有 picker 盘点记录的行(第一或第二次盘点任意一个非 null 即视为已盘点)
(it.pickerFirstStockTakeQty != null || it.pickerSecondStockTakeQty != null) &&
it.approverStockTakeQty == null // 只处理未审批的记录
}
val stockTakeRecords = stockTakeRecordRepository
.findPendingApproverRecordsByStockTakeAndSection(
stockTakeId = request.stockTakeId,
stockTakeSection = request.stockTakeSection
)
println("Found ${stockTakeRecords.size} records to process")
@@ -1711,14 +1697,12 @@ open fun batchSaveApproverStockTakeRecordsAll(

val roundStockTakeIds: Set<Long> = resolveRoundStockTakeIds(stockTake)

var stockTakeRecords = stockTakeRecordRepository.findAll()
var stockTakeRecords = stockTakeRecordRepository
.findAllByStockTakeIdInAndDeletedIsFalse(roundStockTakeIds)
.filter {
!it.deleted &&
it.stockTake?.id != null &&
it.stockTake!!.id!! in roundStockTakeIds &&
// 只处理已经有 picker 盘点记录的行(第一或第二次盘点任意一个非 null 即视为已盘点)
(it.pickerFirstStockTakeQty != null || it.pickerSecondStockTakeQty != null) &&
it.approverStockTakeQty == null
// 只处理已经有 picker 盘点记录的行(第一或第二次盘点任意一个非 null 即视为已盘点)
(it.pickerFirstStockTakeQty != null || it.pickerSecondStockTakeQty != null) &&
it.approverStockTakeQty == null
}
val sectionParts = request.stockTakeSections
?.split(",")
@@ -1855,6 +1839,111 @@ if (itemParts.isNotEmpty()) {
)
}

open fun batchSaveApproverStockTakeRecordsByIds(
request: BatchSaveApproverStockTakeByIdsRequest
): BatchSaveApproverStockTakeRecordResponse {
println("batchSaveApproverStockTakeRecordsByIds called for stockTakeId: ${request.stockTakeId}, ids=${request.recordIds.size}")
if (request.recordIds.isEmpty()) {
return BatchSaveApproverStockTakeRecordResponse(0, 0, listOf("No record IDs provided"))
}

val user = userRepository.findById(request.approverId).orElse(null)
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)
.filter {
!it.deleted &&
(it.id in idSet) &&
(it.pickerFirstStockTakeQty != null || it.pickerSecondStockTakeQty != null) &&
it.approverStockTakeQty == null
}
println("Found ${stockTakeRecords.size} records to process by IDs")
if (stockTakeRecords.isEmpty()) {
return BatchSaveApproverStockTakeRecordResponse(0, 0, listOf("No records found matching criteria"))
}

var successCount = 0
var errorCount = 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)

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()
}
}

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
}
} else {
completeStockTakeLineForApproverNoVariance(record.stockTake ?: stockTake, record, qty)
}

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("batchSaveApproverStockTakeRecordsByIds completed: success=$successCount, errors=$errorCount")
return BatchSaveApproverStockTakeRecordResponse(
successCount = successCount,
errorCount = errorCount,
errors = errors
)
}

/**
* stockTakeRecord 上存的是 inventory_lot.id(批次),不是 inventory_lot_line.id;用倉庫 + 批次找唯一庫存行。
*/
@@ -1980,10 +2069,11 @@ private fun applyVarianceAdjustment(
// 避免同一批多筆盤虧時每筆都用同一個 inventory.onHandQty 導致 balance 錯誤。
val itemIdForLedger = inventoryLot.item?.id
?: throw IllegalArgumentException("Item ID not found for stock take ledger")
val latestLedger = stockLedgerRepository.findLatestByItemId(itemIdForLedger).firstOrNull()
val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemIdForLedger)
val previousBalance = latestLedger?.balance
?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
val newBalance = previousBalance - qtyToRemove.toDouble()

val stockLedger = StockLedger().apply {
this.inventory = inventory
this.itemId = inventoryLot.item?.id
@@ -1996,7 +2086,8 @@ private fun applyVarianceAdjustment(
this.uomId = latestLine.stockUom?.uom?.id ?: inventory.uom?.id
this.date = LocalDate.now()
}
stockLedgerRepository.saveAndFlush(stockLedger)

stockLedgerRepository.save(stockLedger)

val newOutQty = (latestLine.outQty ?: zero).add(qtyToRemove)
val updateRequest = SaveInventoryLotLineRequest(
@@ -2058,10 +2149,11 @@ private fun applyVarianceAdjustment(

val itemIdForLedger = inventoryLot.item?.id
?: throw IllegalArgumentException("Item ID not found for stock take ledger (in)")
val latestLedger = stockLedgerRepository.findLatestByItemId(itemIdForLedger).firstOrNull()
val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemIdForLedger)
val previousBalance = latestLedger?.balance
?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
val newBalance = previousBalance + plusQty.toDouble()

val stockLedger = StockLedger().apply {
this.inventory = inventory
this.itemId = inventoryLot.item?.id
@@ -2074,7 +2166,8 @@ private fun applyVarianceAdjustment(
this.uomId = latestLine.stockUom?.uom?.id ?: inventory.uom?.id
this.date = LocalDate.now()
}
stockLedgerRepository.saveAndFlush(stockLedger)

stockLedgerRepository.save(stockLedger)
}
}
open fun updateStockTakeRecordStatusToNotMatch(stockTakeRecordId: Long): StockTakeRecord {
@@ -2146,7 +2239,7 @@ open fun getInventoryLotDetailsByStockTakeSectionNotMatch(
val warehouse = ill.warehouse
val availableQty = (ill.inQty ?: BigDecimal.ZERO)
.subtract(ill.outQty ?: BigDecimal.ZERO)
.subtract(ill.holdQty ?: BigDecimal.ZERO)
// .subtract(ill.holdQty ?: BigDecimal.ZERO)
val inventoryLotLineId = ill.id
val stockTakeLine =
if (effectiveStockTakeId != null && inventoryLotLineId != null) {
@@ -2238,7 +2331,7 @@ open fun getInventoryLotDetailsByStockTakeSectionNotMatch(
val warehouse = ill.warehouse
val availableQty = (ill.inQty ?: BigDecimal.ZERO)
.subtract(ill.outQty ?: BigDecimal.ZERO)
.subtract(ill.holdQty ?: BigDecimal.ZERO)
//.subtract(ill.holdQty ?: BigDecimal.ZERO)

val stockTakeRecord = if (stockTakeId != null && inventoryLot?.id != null && warehouse?.id != null) {
stockTakeRecordsMap[Pair(inventoryLot.id, warehouse.id)]


+ 22
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/StockTakeRecordController.kt Zobrazit soubor

@@ -300,6 +300,28 @@ class StockTakeRecordController(
))
}
}
@PostMapping("/batchSaveApproverStockTakeRecordsByIds")
fun batchSaveApproverStockTakeRecordsByIds(
@RequestBody request: BatchSaveApproverStockTakeByIdsRequest
): ResponseEntity<Any> {
return try {
val result = stockOutRecordService.batchSaveApproverStockTakeRecordsByIds(request)
logger.info("Batch approver save by ids 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 by ids", e)
ResponseEntity.status(500).body(mapOf(
"error" to "INTERNAL_ERROR",
"message" to (e.message ?: "Failed to batch save approver stock take records by ids")
))
}
}
@PostMapping("/updateStockTakeRecordStatusToNotMatch")
fun updateStockTakeRecordStatusToNotMatch(
@RequestParam stockTakeRecordId: Long


+ 6
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt Zobrazit soubor

@@ -143,6 +143,12 @@ data class BatchSaveApproverStockTakeAllRequest(
val stockTakeSections: String? = null,
)

data class BatchSaveApproverStockTakeByIdsRequest(
val stockTakeId: Long,
val approverId: Long,
val recordIds: List<Long>,
)

data class BatchSaveApproverStockTakeRecordResponse(
val successCount: Int,
val errorCount: Int,


+ 5
- 0
src/main/resources/db/changelog/changes/20260427_01_Enson/01_alter_stock_take.sql Zobrazit soubor

@@ -0,0 +1,5 @@
--liquibase formatted sql

--changeset Enson:20260427-01
CREATE INDEX idx_ledger_item_deleted_date_id
ON stock_ledger (itemId, deleted, date, id);

Načítá se…
Zrušit
Uložit