Переглянути джерело

update expiry lot batch handle

master
CANCERYS\kw093 1 тиждень тому
джерело
коміт
92b2e35117
4 змінених файлів з 172 додано та 31 видалено
  1. +27
    -31
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  2. +14
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt
  3. +115
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  4. +16
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt

+ 27
- 31
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt Переглянути файл

@@ -42,13 +42,15 @@ 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.web.model.BatchStockOutResult
import com.ffii.fpsms.modules.stock.entity.StockLedger
import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository
import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository
import org.springframework.beans.factory.annotation.Value
import com.ffii.fpsms.modules.stock.web.model.BatchStockOutRequest
import com.ffii.fpsms.modules.stock.web.model.BatchStockOutLineRequest
@Service
open class PickExecutionIssueService(
private val pickExecutionIssueRepository: PickExecutionIssueRepository,
@@ -2202,54 +2204,48 @@ open fun batchSubmitBadItem(request: BatchSubmitIssueRequest): MessageResponse {
open fun batchSubmitExpiryItem(request: BatchSubmitExpiryRequest): MessageResponse {
try {
val lotLines = inventoryLotLineRepository.findAllById(request.lotLineIds)
.filter {
.filter {
val lot = it.inventoryLot
val today = LocalDate.now()
lot?.expiryDate != null && lot.expiryDate!!.isBefore(today) &&
lot?.expiryDate != null &&
lot.expiryDate!!.isBefore(today) &&
(it.inQty ?: BigDecimal.ZERO) != (it.outQty ?: BigDecimal.ZERO)
}
if (lotLines.isEmpty()) {
return MessageResponse(
id = null,
name = "Error",
code = "EMPTY",
type = "stock_issue",
message = "No valid expiry items to submit",
errorPosition = null
id = null, name = "Error", code = "EMPTY", type = "stock_issue",
message = "No valid expiry items to submit", errorPosition = null
)
}

val handler = request.handler ?: SecurityUtils.getUser().orElse(null)?.id ?: 0L
lotLines.forEach { lotLine ->
val remainingQty = (lotLine.inQty ?: BigDecimal.ZERO).subtract(lotLine.outQty ?: BigDecimal.ZERO)
stockOutLineService.createStockOut(
StockOutRequest(
inventoryLotLineId = lotLine.id!!,
qty = remainingQty.toDouble(),
type = "Expiry"
val batchReq = BatchStockOutRequest(
type = "Expiry",
handler = handler,
lines = lotLines.map { ll ->
val remaining = (ll.inQty ?: BigDecimal.ZERO).subtract(ll.outQty ?: BigDecimal.ZERO)
BatchStockOutLineRequest(
inventoryLotLineId = ll.id!!,
qty = remaining.toDouble()
)
)
}
}
)

val result = stockOutLineService.createStockOutBatch(batchReq)

return MessageResponse(
id = null,
id = result.stockOutId,
name = "Success",
code = "SUCCESS",
type = "stock_issue",
message = "Successfully submitted ${lotLines.size} expiry item(s)",
message = "Successfully submitted ${result.processedCount} expiry item(s), skipped ${result.skippedCount}",
errorPosition = null
)
} catch (e: Exception) {
return MessageResponse(
id = null,
name = "Error",
code = "ERROR",
type = "stock_issue",
message = "Failed to submit expiry items: ${e.message}",
errorPosition = null
id = null, name = "Error", code = "ERROR", type = "stock_issue",
message = "Failed to submit expiry items: ${e.message}", errorPosition = null
)
}
}


+ 14
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt Переглянути файл

@@ -262,4 +262,18 @@ WHERE ill.id = :id
"""
)
fun countByStatusAndDeletedIsFalse(@Param("status") status: InventoryLotLineStatus): Long

@EntityGraph(
type = EntityGraph.EntityGraphType.FETCH,
attributePaths = ["inventoryLot", "inventoryLot.item", "warehouse"]
)
@Query("""
SELECT ill
FROM InventoryLotLine ill
WHERE ill.id IN :ids
AND ill.deleted = false
""")
fun findAllByIdInAndDeletedFalseWithRefs(
@Param("ids") ids: Collection<Long>
): List<InventoryLotLine>
}

+ 115
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt Переглянути файл

@@ -2223,4 +2223,119 @@ fun applyStockOutLineDelta(

return savedSol
}
@Transactional(rollbackFor = [Exception::class])
open fun createStockOutBatch(request: BatchStockOutRequest): BatchStockOutResult {
if (request.lines.isEmpty()) return BatchStockOutResult(null, 0, 0)

val currentUserId = request.handler ?: SecurityUtils.getUser().orElseThrow().id ?: 0L
val lotLineIds = request.lines.map { it.inventoryLotLineId }.distinct()

// 1) 一次 preload lotLine + item + warehouse
val lotLines = inventoryLotLineRepository
.findAllByIdInAndDeletedFalseWithRefs(lotLineIds)

val lotLineMap = lotLines.associateBy { it.id!! }

// 2) 构造 header(每批一个,避免每笔新建)
val stockOutHeader = StockOut().apply {
this.type = request.type
this.completeDate = LocalDateTime.now()
this.handler = currentUserId
this.status = StockOutStatus.COMPLETE.status
}
val savedHeader = stockOutRepository.save(stockOutHeader)

val now = LocalDateTime.now()
val today = LocalDate.now()

val lotLinesToUpdate = mutableListOf<InventoryLotLine>()
val stockOutLinesToInsert = mutableListOf<StockOutLine>()
val ledgersToInsert = mutableListOf<StockLedger>()
var skipped = 0

// 3) 内存校验 + 组装对象(不在循环里 findById / saveAndFlush)
request.lines.forEach { lineReq ->
val lotLine = lotLineMap[lineReq.inventoryLotLineId]
if (lotLine == null) {
skipped++
return@forEach
}

val qtyBd = BigDecimal.valueOf(lineReq.qty)
if (qtyBd <= BigDecimal.ZERO) {
skipped++
return@forEach
}

// 可用量校验(按你现有规则)
val inQty = lotLine.inQty ?: BigDecimal.ZERO
val outQty = lotLine.outQty ?: BigDecimal.ZERO
val holdQty = lotLine.holdQty ?: BigDecimal.ZERO
val available = inQty.subtract(outQty) // Expiry 通常吃剩余量,可视业务改成 -holdQty
if (qtyBd > available) {
skipped++
return@forEach
}

// 更新 lot line
lotLine.outQty = outQty.add(qtyBd)
lotLine.holdQty = holdQty.subtract(qtyBd).coerceAtLeast(BigDecimal.ZERO)
lotLine.status = inventoryLotLineService.deriveInventoryLotLineStatus(
lotLine.status, lotLine.inQty, lotLine.outQty, lotLine.holdQty
)
lotLinesToUpdate += lotLine

val item = lotLine.inventoryLot?.item ?: run {
skipped++
return@forEach
}

// 组装 stock_out_line
val sol = StockOutLine().apply {
this.item = item
this.qty = lineReq.qty
this.stockOut = savedHeader
this.inventoryLotLine = lotLine
this.status = StockOutLineStatus.COMPLETE.status
this.pickTime = now
this.handledBy = currentUserId
this.type = request.type
}
stockOutLinesToInsert += sol
}

// 4) 批量写(关键)
inventoryLotLineRepository.saveAll(lotLinesToUpdate)
val savedLines = stockOutLineRepository.saveAll(stockOutLinesToInsert)

// 5) 批量组装 ledger(避免每笔 createStockLedgerForStockOut)
savedLines.forEach { sol ->
val item = sol.item ?: return@forEach
val inv = itemUomService.findInventoryForItemBaseUom(item.id!!) ?: return@forEach
val delta = BigDecimal.valueOf(sol.qty ?: 0.0)
val prevBalance = inv.onHandQty?.toDouble() ?: 0.0
val newBalance = prevBalance - delta.toDouble()

ledgersToInsert += StockLedger().apply {
this.stockOutLine = sol
this.inventory = inv
this.inQty = null
this.outQty = delta.toDouble()
this.balance = newBalance
this.type = request.type
this.itemId = item.id
this.itemCode = item.code
this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id
?: inv.uom?.id
this.date = today
}
}
stockLedgerRepository.saveAll(ledgersToInsert)

return BatchStockOutResult(
stockOutId = savedHeader.id,
processedCount = savedLines.size,
skippedCount = skipped
)
}
}

+ 16
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt Переглянути файл

@@ -102,3 +102,19 @@ data class BatchScanRequest(
val userId: Long,
val lines: List<BatchScanLineRequest>
)
data class BatchStockOutRequest(
val type: String, // "Expiry" / "Miss" / "Bad"
val handler: Long?,
val lines: List<BatchStockOutLineRequest>
)

data class BatchStockOutLineRequest(
val inventoryLotLineId: Long,
val qty: Double
)

data class BatchStockOutResult(
val stockOutId: Long?,
val processedCount: Int,
val skippedCount: Int = 0
)

Завантаження…
Відмінити
Зберегти