Ver a proveniência

update

master
CANCERYS\kw093 há 11 horas
ascendente
cometimento
2dee12d5c8
9 ficheiros alterados com 133 adições e 85 eliminações
  1. +6
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  2. +5
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedger.kt
  3. +4
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt
  4. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt
  5. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
  6. +4
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  7. +103
    -85
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
  8. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt
  9. +8
    -0
      src/main/resources/db/changelog/changes/20260323_02_Enson/01_alter_stock_take.sql

+ 6
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt Ver ficheiro

@@ -40,6 +40,7 @@ import com.ffii.fpsms.modules.common.SecurityUtils
import com.ffii.fpsms.modules.stock.entity.StockOut
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLine
import com.ffii.fpsms.modules.master.entity.ItemsRepository
import com.ffii.fpsms.modules.master.entity.ItemUomRespository
import com.ffii.fpsms.modules.common.CodeGenerator

import com.ffii.fpsms.modules.stock.entity.StockLedger
@@ -64,6 +65,7 @@ open class PickExecutionIssueService(
private val joPickOrderRepository: JoPickOrderRepository,
private val joPickOrderRecordRepository: JoPickOrderRecordRepository,
private val itemsRepository: ItemsRepository,
private val itemUomRespository: ItemUomRespository,
private val stockLedgerRepository: StockLedgerRepository,
private val deliveryOrderRepository: DeliveryOrderRepository,
private val suggestedPickLotRepository: SuggestPickLotRepository,
@@ -2357,6 +2359,8 @@ private fun createStockLedgerForStockOut(
this.type = ledgerTypeToUse
this.itemId = item.id
this.itemCode = item.code
this.uomId = inventory.uom?.id
?: itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id
this.date = LocalDate.now()
}
@@ -2874,6 +2878,8 @@ private fun createStockLedgerForStockOutWithBalance(
this.type = ledgerTypeToUse
this.itemId = item.id
this.itemCode = item.code
this.uomId = inventory.uom?.id
?: itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id
this.date = LocalDate.now()
}


+ 5
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockLedger.kt Ver ficheiro

@@ -32,6 +32,11 @@ open class StockLedger: BaseEntity<Long>() {

@Column(name = "balance")
open var balance: Double? = null

/** 物料庫存單位對應之 uom_conversion.id(與 item_uom / inventory 之 stock UOM 一致) */
@Column(name = "uomId")
open var uomId: Long? = null

@Column(name = "type")
open var type: String? = null
@Column(name = "itemId")


+ 4
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeLineRepository.kt Ver ficheiro

@@ -8,4 +8,8 @@ import java.io.Serializable
interface StockTakeLineRepository : AbstractRepository<StockTakeLine, Long> {
fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeLine?;
fun findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(inventoryLotLineId: Serializable, stockTakeId: Serializable): StockTakeLine?;
fun findAllByStockTakeIdInAndInventoryLotLineIdInAndDeletedIsFalse(
stockTakeIds: Collection<Long>,
inventoryLotLineIds: Collection<Long>
): List<StockTakeLine>
}

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockTakeRecordRepository.kt Ver ficheiro

@@ -10,6 +10,7 @@ import java.time.LocalDate
@Repository
interface StockTakeRecordRepository : AbstractRepository<StockTakeRecord, Long> {
fun findAllByStockTakeIdAndDeletedIsFalse(stockTakeId: Long): List<StockTakeRecord>;
fun findAllByStockTakeIdInAndDeletedIsFalse(stockTakeIds: Collection<Long>): List<StockTakeRecord>;
fun findByIdAndDeletedIsFalse(id: Serializable): StockTakeRecord?;

@Query("""


+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Ver ficheiro

@@ -1162,6 +1162,7 @@ open class StockInLineService(
this.type = stockInLine.type
this.itemId = item.id
this.itemCode = item.code
this.uomId = inventory.uom?.id ?: itemUomService.findStockUnitByItemId(item.id!!)?.uom?.id
this.date = LocalDate.now()
}



+ 4
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt Ver ficheiro

@@ -1509,6 +1509,8 @@ affectedConsoCodes.forEach { consoCode ->
this.type = stockOutLine.type
this.itemId = item.id
this.itemCode = item.code
this.uomId = inventory.uom?.id
?: itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id
this.date = LocalDate.now()
}

@@ -1605,6 +1607,8 @@ affectedConsoCodes.forEach { consoCode ->
this.type = "NOR"
this.itemId = item.id
this.itemCode = item.code
this.uomId = inventory.uom?.id
?: itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id
this.date = LocalDate.now()
}


+ 103
- 85
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt Ver ficheiro

@@ -204,6 +204,7 @@ class StockTakeRecordService(
currentStockTakeItemNumber = 0,
totalInventoryLotNumber = totalInventoryLotNumber,
stockTakeId = latestStockTake?.id ?: 0,
stockTakeRoundId = latestStockTake?.stockTakeRoundId ?: latestStockTake?.id,
stockTakerName = stockTakerName,
TotalItemNumber = totalItemNumber,
approverName = approverName,
@@ -250,19 +251,28 @@ class StockTakeRecordService(
emptySet()
}

// 4. 如果有 stockTakeId,则预先把这一轮相关的 stockTakeRecord 查出来建 map(跟 section 版类似)
val stockTakeRecordsMap = if (stockTakeId != null) {
val allStockTakeRecords = stockTakeRecordRepository.findAll()
.filter {
!it.deleted &&
it.stockTake?.id != null &&
it.stockTake!!.id!! in roundStockTakeIds &&
it.warehouse?.id in warehouseIds
// 4. 如果有 stockTakeId,则预先把这一轮相关的 stockTakeRecord 查出来建 map(避免全表扫描 + N^2)
val roundStockTakeRecords = if (stockTakeId != null && roundStockTakeIds.isNotEmpty()) {
stockTakeRecordRepository.findAllByStockTakeIdInAndDeletedIsFalse(roundStockTakeIds)
.filter { it.warehouse?.id in warehouseIds }
} else {
emptyList()
}
val stockTakeRecordsMap = roundStockTakeRecords
.groupBy { Pair(it.lotId ?: 0L, it.warehouse?.id ?: 0L) }
.mapValues { (_, records) -> records.maxByOrNull { it.id ?: 0L }!! }

// 4.1 预先拉取本轮 stockTakeLine,避免每行调用 repository 形成 N+1
val stockTakeLineMap = if (stockTakeId != null && roundStockTakeRecords.isNotEmpty()) {
val lineIds = inventoryLotLines.mapNotNull { it.id }.toSet()
val roundIds = roundStockTakeIds
stockTakeLineRepository.findAllByStockTakeIdInAndInventoryLotLineIdInAndDeletedIsFalse(roundIds, lineIds)
.associateBy {
Pair(
it.inventoryLotLine?.id ?: 0L,
it.stockTake?.id ?: 0L
)
}

allStockTakeRecords.associateBy {
Pair(it.lotId ?: 0L, it.warehouse?.id ?: 0L)
}
} else {
emptyMap()
}
@@ -284,10 +294,7 @@ class StockTakeRecordService(

val inventoryLotLineId = ill.id
val stockTakeLine = if (stockTakeRecord != null && stockTakeRecord.stockTake?.id != null && inventoryLotLineId != null) {
stockTakeLineRepository.findByInventoryLotLineIdAndStockTakeIdAndDeletedIsFalse(
inventoryLotLineId,
stockTakeRecord.stockTake!!.id!!
)
stockTakeLineMap[Pair(inventoryLotLineId, stockTakeRecord.stockTake!!.id!!)]
} else {
null
}
@@ -357,36 +364,70 @@ class StockTakeRecordService(
return RecordsRes(paginatedResult, filteredResults.size)
}
open fun AllApproverStockTakeList(): List<AllPickedStockTakeListReponse> {
// 1. 获取所有不同的 stockTakeSection(从 warehouse 表)
// 1. 获取 section 与仓库基础数据
val allWarehouses = warehouseRepository.findAllByDeletedIsFalse()
val distinctSections = allWarehouses
.mapNotNull { it.stockTakeSection }
.distinct()
.filter { !it.isBlank() }
.filter { it.isNotBlank() }
if (distinctSections.isEmpty()) return emptyList()

if (distinctSections.isEmpty()) {
return emptyList()
}

// 2. 批量获取所有相关的数据(优化性能 - 只查询一次)
// 2.1 按 section 分组 warehouse
val warehousesBySection = allWarehouses
.filter { it.stockTakeSection != null && !it.stockTakeSection!!.isBlank() }
.filter { !it.stockTakeSection.isNullOrBlank() }
.groupBy { it.stockTakeSection!! }
val sectionByWarehouseId = allWarehouses
.mapNotNull { w -> w.id?.let { id -> id to (w.stockTakeSection ?: "") } }
.toMap()
val allWarehouseIds = allWarehouses.mapNotNull { it.id }

// 2.2 批量获取所有 stock_take 记录(只获取一次),按 stockTakeSection 分组
// 2. 批量加载核心数据(一次拿齐,避免循环中 repository 查询)
val allStockTakes = stockTakeRepository.findAll()
.filter { !it.deleted }
.groupBy { it.stockTakeSection }

// 2.3 批量获取所有 stocktakeRecord(只获取一次)
val allStockTakeRecords = stockTakeRecordRepository.findAll()
.filter { !it.deleted }
.filter { !it.deleted && !it.stockTakeSection.isNullOrBlank() }
.groupBy { it.stockTakeSection!! }
val latestStockTakeBySection = allStockTakes.mapValues { (_, takes) ->
takes.maxByOrNull { it.actualStart ?: it.planStart ?: LocalDateTime.MIN }
}
val latestStockTakeIds = latestStockTakeBySection.values.mapNotNull { it?.id }.toSet()
val allStockTakeRecords = if (latestStockTakeIds.isEmpty()) {
emptyList()
} else {
stockTakeRecordRepository.findAllByStockTakeIdInAndDeletedIsFalse(latestStockTakeIds)
}
val allInventoryLotLines = if (allWarehouseIds.isEmpty()) {
emptyList()
} else {
inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(allWarehouseIds)
}
val inventoryLotLinesBySection = allInventoryLotLines.groupBy { ill ->
sectionByWarehouseId[ill.warehouse?.id] ?: ""
}

// 2.4 按 warehouseId 分组 stocktakeRecord,便于快速查找
val recordsByWarehouseId = allStockTakeRecords
.filter { it.warehouse?.id != null }
.groupBy { it.warehouse!!.id }
// 3. 预计算:同一 stockTake 下已盘点 key、人员名称、notMatch
val recordKeyByStockTakeId = allStockTakeRecords
.groupBy { it.stockTake?.id ?: -1L }
.mapValues { (_, records) ->
records.mapNotNull { r ->
val lotId = r.inventoryLotId
val whId = r.warehouse?.id
if (lotId != null && whId != null) Pair(lotId, whId) else null
}.toSet()
}
val latestStockTakerNameByStockTakeAndSection = allStockTakeRecords
.filter { !it.stockTakeSection.isNullOrBlank() && it.stockTake?.id != null && it.stockTakerName != null }
.groupBy { Pair(it.stockTake!!.id!!, it.stockTakeSection!!) }
.mapValues { (_, records) ->
records.maxByOrNull { it.stockTakeStartTime ?: LocalDateTime.MIN }?.stockTakerName
}
val latestApproverNameByStockTakeAndSection = allStockTakeRecords
.filter { !it.stockTakeSection.isNullOrBlank() && it.stockTake?.id != null && it.approverName != null }
.groupBy { Pair(it.stockTake!!.id!!, it.stockTakeSection!!) }
.mapValues { (_, records) ->
records.maxByOrNull { it.stockTakeStartTime ?: LocalDateTime.MIN }?.approverName
}
val hasNotMatchByStockTakeAndSection = allStockTakeRecords
.filter { it.stockTake?.id != null && !it.stockTakeSection.isNullOrBlank() && it.status == "notMatch" }
.map { Pair(it.stockTake!!.id!!, it.stockTakeSection!!) }
.toSet()

val result = mutableListOf<AllPickedStockTakeListReponse>()
var idCounter = 1L
@@ -427,52 +468,15 @@ class StockTakeRecordService(
} else {
null
}
val stockTakerName = if (latestStockTake != null) {
// 从该 stock take 的 records 中获取最新的 stockTakerName
val recordsForStockTake = allStockTakeRecords
.filter {
it.stockTake?.id == latestStockTake.id &&
it.stockTakeSection == stockTakeSection &&
it.stockTakerName != null
}
.sortedByDescending { it.stockTakeStartTime ?: LocalDateTime.MIN }

recordsForStockTake.firstOrNull()?.stockTakerName
} else {
null
}
val approverName = if (latestStockTake != null) {
// 从该 stock take 的 records 中获取最新的 approverName
val recordsForStockTake = allStockTakeRecords
.filter {
it.stockTake?.id == latestStockTake.id &&
it.stockTakeSection == stockTakeSection &&
it.approverName != null
}
.sortedByDescending { it.stockTakeStartTime ?: LocalDateTime.MIN }

recordsForStockTake.firstOrNull()?.approverName
} else {
null
}
val reStockTakeTrueFalse = if (latestStockTake != null) {
// 检查该 stock take 下该 section 的记录中是否有 notMatch 状态
val hasNotMatch = allStockTakeRecords.any {
!it.deleted &&
it.stockTake?.id == latestStockTake.id &&
it.stockTakeSection == stockTakeSection &&
it.status == "notMatch"
}
hasNotMatch
} else {
false
}
val stockTakerName = latestStockTake?.id?.let { latestStockTakerNameByStockTakeAndSection[Pair(it, stockTakeSection)] }
val approverName = latestStockTake?.id?.let { latestApproverNameByStockTakeAndSection[Pair(it, stockTakeSection)] }
val reStockTakeTrueFalse = latestStockTake?.id?.let { hasNotMatchByStockTakeAndSection.contains(Pair(it, stockTakeSection)) } ?: false
// 9. 计算 TotalItemNumber / TotalInventoryLotNumber(只统计“需要盘点的行”):
// 规则与前端 Picker / Approver 明细一致:
// - availableQty > 0,或
// - 在 latestStockTake 下已经有 stockTakeRecord(即本轮参与盘点的 lot)
val inventoryLotLinesForSection =
inventoryLotLineRepository.findAllByWarehouseIdInAndDeletedIsFalse(warehouseIds)
val inventoryLotLinesForSection = inventoryLotLinesBySection[stockTakeSection] ?: emptyList()
val recordedKeysForLatest = latestStockTake?.id?.let { recordKeyByStockTakeId[it] } ?: emptySet()

val relevantInventoryLotLines = inventoryLotLinesForSection.filter { ill ->
val inventoryLot = ill.inventoryLot
@@ -485,12 +489,7 @@ class StockTakeRecordService(
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
}
val hasRecordForLatest = recordedKeysForLatest.contains(Pair(inventoryLot.id, warehouse.id))
availableQty.compareTo(BigDecimal.ZERO) > 0 || hasRecordForLatest
}
}
@@ -515,6 +514,7 @@ class StockTakeRecordService(
currentStockTakeItemNumber = 0, // 临时设为 0,测试性能
totalInventoryLotNumber = totalInventoryLotNumber, // 临时设为 0,测试性能
stockTakeId = latestStockTake?.id ?: 0,
stockTakeRoundId = latestStockTake?.stockTakeRoundId ?: latestStockTake?.id,
stockTakerName = stockTakerName,
approverName = approverName,
TotalItemNumber = totalItemNumber,
@@ -528,7 +528,23 @@ class StockTakeRecordService(
)
}

return result.sortedBy { it.stockTakeSession }
// 10. 以 stockTakeRoundId 聚合为“每轮一张卡”;旧资料 roundId 为空时退回 stockTakeId
val groupedByRound = result.groupBy { it.stockTakeRoundId ?: it.stockTakeId }
val roundCards = groupedByRound.values.mapIndexed { idx, cardsInRound ->
val representative = cardsInRound.maxByOrNull { it.startTime ?: LocalDateTime.MIN } ?: cardsInRound.first()
val totalItems = cardsInRound.sumOf { it.TotalItemNumber }
val totalLots = cardsInRound.sumOf { it.totalInventoryLotNumber }

representative.copy(
id = (idx + 1).toLong(),
stockTakeSession = "",
TotalItemNumber = totalItems,
totalInventoryLotNumber = totalLots,
stockTakeSectionDescription = null
)
}

return roundCards.sortedByDescending { it.planStartDate ?: LocalDate.MIN }
}

open fun getInventoryLotDetailsByWarehouseCode(warehouseCode: String): List<InventoryLotDetailResponse> {
@@ -1429,6 +1445,7 @@ private fun applyVarianceAdjustment(
this.stockOutLine = stockOutLine
this.balance = newBalance
this.type = "TKE"
this.uomId = inventoryLotLine.stockUom?.uom?.id ?: inventory.uom?.id
this.date = LocalDate.now()
}
stockLedgerRepository.save(stockLedger)
@@ -1509,6 +1526,7 @@ private fun applyVarianceAdjustment(
this.stockInLine = stockInLine
this.balance = newBalance
this.type = "TKE"
this.uomId = inventoryLotLine.stockUom?.uom?.id ?: inventory.uom?.id
this.date = LocalDate.now()
}
stockLedgerRepository.save(stockLedger)


+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/StockTakeRecordReponse.kt Ver ficheiro

@@ -19,6 +19,7 @@ data class AllPickedStockTakeListReponse(
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val endTime: LocalDateTime?,
val stockTakeId: Long,
val stockTakeRoundId: Long?,
val stockTakerName: String?,
val approverName: String?,
val TotalItemNumber: Int,


+ 8
- 0
src/main/resources/db/changelog/changes/20260323_02_Enson/01_alter_stock_take.sql Ver ficheiro

@@ -0,0 +1,8 @@
-- liquibase formatted sql
-- changeset Enson:alter_do_pick_order_record_box_number

ALTER TABLE `fpsmsdb`.`do_pick_order_record`
ADD COLUMN `BoxNumber` INT NULL AFTER `deleted`;

ALTER TABLE `fpsmsdb`.`stock_ledger`
ADD COLUMN `uomId` INT NULL AFTER `balance`;

Carregando…
Cancelar
Guardar