浏览代码

update

master
CANCERYS\kw093 11 小时前
父节点
当前提交
a7ea2bcc5f
共有 5 个文件被更改,包括 676 次插入568 次删除
  1. +66
    -30
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  2. +49
    -49
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt
  3. +6
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt
  4. +13
    -5
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  5. +542
    -484
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt

+ 66
- 30
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt 查看文件

@@ -155,21 +155,15 @@ open class PickExecutionIssueService(
// 4. 计算 issueQty(实际的问题数量)
val issueQty = when {
// 情况1: 已拣完但有坏品
actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> {
println(" Case 1: actualPickQty == requiredQty && badItemQty > 0")
println(" issueQty = badItemQty = $badItemQty")
badItemQty // issueQty = badItemQty
}
badReason == "package_problem" && badItemQty > BigDecimal.ZERO -> {
println(" Case 2: badReason == 'package_problem' && badItemQty > 0")
println(" issueQty = badItemQty = $badItemQty")
// Bad item 或 bad package:一律用用户输入的 bad 数量,不用 bookQty - actualPickQty
badItemQty > BigDecimal.ZERO -> {
println(" Bad item/package: issueQty = badItemQty = $badItemQty")
badItemQty
}
// 仅 miss/少拣:用短少数量
actualPickQty < requiredQty -> {
println(" Case 3: actualPickQty < requiredQty")
val calculatedIssueQty = bookQty.subtract(actualPickQty)
println(" issueQty = bookQty - actualPickQty = $bookQty - $actualPickQty = $calculatedIssueQty")
println(" Miss/short: issueQty = bookQty - actualPickQty = $bookQty - $actualPickQty = $calculatedIssueQty")
if (missQty > BigDecimal.ZERO && missQty > calculatedIssueQty) {
println("⚠️ Warning: User reported missQty ($missQty) exceeds calculated issueQty ($calculatedIssueQty)")
println(" BookQty: $bookQty, ActualPickQty: $actualPickQty")
@@ -177,8 +171,7 @@ open class PickExecutionIssueService(
calculatedIssueQty
}
else -> {
println(" Case 4: Default case")
println(" issueQty = 0")
println(" Default: issueQty = 0")
BigDecimal.ZERO
}
}
@@ -255,14 +248,23 @@ open class PickExecutionIssueService(
println(" hasMissItemWithPartialPick: $hasMissItemWithPartialPick")
if (!isMissItemOnly && !hasMissItemWithPartialPick) {
// 只有非 miss item 的情况才更新 issueQty
val currentIssueQty = inventoryLotLine.issueQty ?: BigDecimal.ZERO
val newIssueQty = currentIssueQty.add(issueQty)
inventoryLotLine.issueQty = newIssueQty
inventoryLotLine.modified = LocalDateTime.now()
inventoryLotLine.modifiedBy = "system"
inventoryLotLineRepository.saveAndFlush(inventoryLotLine)
println("✅ Updated inventory_lot_line ${request.lotId} issueQty: $currentIssueQty -> $newIssueQty")
// Bad item only(含 package_problem):lot.issueQty = 本次输入数量(= bad_item_qty / issue_qty),不累加
val isBadItemOnly = badItemQty > BigDecimal.ZERO && missQty == BigDecimal.ZERO
if (isBadItemOnly) {
inventoryLotLine.issueQty = issueQty // = badItemQty,与 pick_execution_issue.bad_item_qty / issue_qty 一致
inventoryLotLine.modified = LocalDateTime.now()
inventoryLotLine.modifiedBy = "system"
inventoryLotLineRepository.saveAndFlush(inventoryLotLine)
println("✅ Updated inventory_lot_line ${request.lotId} issueQty = input qty only: $issueQty (bad item only / package problem)")
} else {
val currentIssueQty = inventoryLotLine.issueQty ?: BigDecimal.ZERO
val newIssueQty = currentIssueQty.add(issueQty)
inventoryLotLine.issueQty = newIssueQty
inventoryLotLine.modified = LocalDateTime.now()
inventoryLotLine.modifiedBy = "system"
inventoryLotLineRepository.saveAndFlush(inventoryLotLine)
println("✅ Updated inventory_lot_line ${request.lotId} issueQty: $currentIssueQty -> $newIssueQty")
}
} else {
println("⏭️ Skipped updating issueQty for miss item (lot ${request.lotId})")
}
@@ -2706,13 +2708,40 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
// ✅ 新增:如果提交数量为0,只标记issue为已处理,不创建stock_out_line
if (submitQty == BigDecimal.ZERO) {
println("ℹ️ Submit quantity is 0 - marking issues as handled without creating stock out")
// Mark all issues as handled
val isMissItem = request.issueType == "miss"

// Bad + 有 lotId:重置 lot(issueQty=0, status=AVAILABLE),并释放该 lot 下 rejected 的 stock out line
if (!isMissItem && request.lotId != null) {
val lotLine = inventoryLotLineRepository.findById(request.lotId).orElse(null)
if (lotLine != null) {
lotLine.issueQty = BigDecimal.ZERO
lotLine.status = InventoryLotLineStatus.AVAILABLE
lotLine.modified = LocalDateTime.now()
lotLine.modifiedBy = "system"
inventoryLotLineRepository.saveAndFlush(lotLine)
updateInventoryAfterLotLineChange(lotLine)
println("✅ Reset lot ${request.lotId}: issueQty=0, status=AVAILABLE")
}
val rejectedLines = stockOutLineRepository
.findByInventoryLotLineIdAndStatusAndDeletedFalse(
request.lotId,
StockOutLineStatus.REJECTED.status
)
if (rejectedLines.isNotEmpty()) {
rejectedLines.forEach { sol ->
sol.status = "pending"
sol.modified = LocalDateTime.now()
sol.modifiedBy = "system"
}
stockOutLineRepository.saveAllAndFlush(rejectedLines)
println("✅ Released ${rejectedLines.size} rejected stock out line(s) for lot ${request.lotId} to pending")
}
}

issues.forEach { issue ->
println(" Marking issue ${issue.id} as handled by handler $handler")
markIssueHandled(issue, handler)
}
println("✅ All issues marked as handled (no stock out created)")
return MessageResponse(
id = null,
@@ -2776,12 +2805,19 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse
println("Created stock out line: id=${savedStockOutLine.id}, qty=${savedStockOutLine.qty}, status=${savedStockOutLine.status}")
if (!isMissItem && request.lotId != null) {
val lotLineForReset = inventoryLotLineRepository.findById(request.lotId).orElse(null)
if (lotLineForReset != null) {
val oldIssueQty = lotLineForReset.issueQty
lotLineForReset.issueQty = BigDecimal.ZERO
inventoryLotLineRepository.saveAndFlush(lotLineForReset)
println("✅ Reset issueQty for lot ${request.lotId}: $oldIssueQty -> 0")
val rejectedLines = stockOutLineRepository
.findByInventoryLotLineIdAndStatusAndDeletedFalse(
request.lotId,
StockOutLineStatus.REJECTED.status
)
if (rejectedLines.isNotEmpty()) {
rejectedLines.forEach { sol ->
sol.status = "pending" // 或 StockOutLineStatus.PENDING.status(若有该枚举)
sol.modified = LocalDateTime.now()
sol.modifiedBy = "system"
}
stockOutLineRepository.saveAllAndFlush(rejectedLines)
println("✅ Released ${rejectedLines.size} rejected stock out line(s) for lot ${request.lotId} to pending")
}
}


+ 49
- 49
src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt 查看文件

@@ -766,12 +766,11 @@ fun searchMaterialStockOutTraceabilityReport(
return jdbcDao.queryForList(sql, args)
}


/**
* Queries the database for Stock Balance Report data.
* Shows stock balances by item code and lot number, including opening balance,
* cumulative stock in/out, current balance, store locations, and last in/out dates.
* Opening balance comes from stock_ledger where type = 'adj'.
* Shows stock balances by item code and lot number (per-lot quantities from inventory_lot_line),
* including opening balance (item-level from stock_ledger type='adj'), cumulative stock in/out,
* current balance, store locations, and last in/out dates per lot.
*/
fun searchStockBalanceReport(
stockCategory: String?,
@@ -802,19 +801,19 @@ fun searchMaterialStockOutTraceabilityReport(
COALESCE(uc.code, '') as unitOfMeasure,
COALESCE(il.lotNo, sil.lotNo, '') as lotNo,
COALESCE(DATE_FORMAT(COALESCE(il.expiryDate, sil.expiryDate), '%Y-%m-%d'), '') as expiryDate,
FORMAT(ROUND(COALESCE(opening_bal.openingBalance, 0), 0), 0) as openingBalance,
FORMAT(ROUND(COALESCE(cum_in.cumStockIn, 0), 0), 0) as cumStockIn,
FORMAT(ROUND(COALESCE(cum_out.cumStockOut, 0), 0), 0) as cumStockOut,
FORMAT(ROUND(COALESCE(opening_bal.openingBalance, 0) + COALESCE(cum_in.cumStockIn, 0) - COALESCE(cum_out.cumStockOut, 0), 0), 0) as currentBalance,
FORMAT(ROUND(COALESCE(lot_agg.lotOpening, 0), 0), 0) as openingBalance,
FORMAT(ROUND(COALESCE(lot_agg.lotCumIn, 0), 0), 0) as cumStockIn,
FORMAT(ROUND(COALESCE(lot_agg.lotCumOut, 0), 0), 0) as cumStockOut,
FORMAT(ROUND(COALESCE(lot_agg.lotOpening, 0) + COALESCE(lot_agg.lotCumIn, 0) - COALESCE(lot_agg.lotCumOut, 0), 0), 0) as currentBalance,
'' as reOrderLevel,
'' as reOrderQty,
COALESCE(GROUP_CONCAT(DISTINCT wh.code ORDER BY wh.code SEPARATOR ', '), '') as storeLocation,
COALESCE(DATE_FORMAT(cum_in.lastInDate, '%Y-%m-%d'), '') as lastInDate,
COALESCE(DATE_FORMAT(cum_out.lastOutDate, '%Y-%m-%d'), '') as lastOutDate,
FORMAT(ROUND(SUM(COALESCE(opening_bal.openingBalance, 0)) OVER (PARTITION BY it.code), 0), 0) as totalOpeningBalance,
FORMAT(ROUND(SUM(COALESCE(cum_in.cumStockIn, 0)) OVER (PARTITION BY it.code), 0), 0) as totalCumStockIn,
FORMAT(ROUND(SUM(COALESCE(cum_out.cumStockOut, 0)) OVER (PARTITION BY it.code), 0), 0) as totalCumStockOut,
FORMAT(ROUND(SUM(COALESCE(opening_bal.openingBalance, 0) + COALESCE(cum_in.cumStockIn, 0) - COALESCE(cum_out.cumStockOut, 0)) OVER (PARTITION BY it.code), 0), 0) as totalCurrentBalance
COALESCE(DATE_FORMAT(lot_agg.lotLastInDate, '%Y-%m-%d'), '') as lastInDate,
COALESCE(DATE_FORMAT(lot_agg.lotLastOutDate, '%Y-%m-%d'), '') as lastOutDate,
FORMAT(ROUND(MAX(COALESCE(opening_bal.openingBalance, 0)) OVER (PARTITION BY it.code), 0), 0) as totalOpeningBalance,
FORMAT(ROUND(SUM(COALESCE(lot_agg.lotCumIn, 0)) OVER (PARTITION BY it.code), 0), 0) as totalCumStockIn,
FORMAT(ROUND(SUM(COALESCE(lot_agg.lotCumOut, 0)) OVER (PARTITION BY it.code), 0), 0) as totalCumStockOut,
FORMAT(ROUND(SUM(COALESCE(lot_agg.lotOpening, 0) + COALESCE(lot_agg.lotCumIn, 0) - COALESCE(lot_agg.lotCumOut, 0)) OVER (PARTITION BY it.code), 0), 0) as totalCurrentBalance
FROM inventory_lot il
LEFT JOIN items it ON il.itemId = it.id AND it.deleted = false
LEFT JOIN stock_in_line sil ON il.stockInLineId = sil.id AND sil.deleted = false
@@ -824,84 +823,85 @@ fun searchMaterialStockOutTraceabilityReport(
LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
LEFT JOIN (
SELECT
sl.itemCode,
SUM(COALESCE(sl.balance, 0)) as openingBalance
FROM stock_ledger sl
WHERE sl.deleted = false
AND sl.type = 'adj'
AND sl.itemCode IS NOT NULL
AND sl.itemCode != ''
GROUP BY sl.itemCode
) opening_bal ON it.code = opening_bal.itemCode
ill_agg.inventoryLotId,
SUM(COALESCE(ill_agg.inQty, 0)) as lotCumIn,
SUM(COALESCE(ill_agg.outQty, 0)) as lotCumOut,
NULL as lotOpening,
last_in.lotLastInDate,
last_out.lotLastOutDate
FROM inventory_lot_line ill_agg
LEFT JOIN (
SELECT sil2.inventoryLotId, MAX(sil2.receiptDate) as lotLastInDate
FROM stock_in_line sil2
WHERE sil2.deleted = false AND sil2.inventoryLotId IS NOT NULL
GROUP BY sil2.inventoryLotId
) last_in ON last_in.inventoryLotId = ill_agg.inventoryLotId
LEFT JOIN (
SELECT ill2.inventoryLotId, MAX(sol.endTime) as lotLastOutDate
FROM stock_out_line sol
INNER JOIN inventory_lot_line ill2 ON sol.inventoryLotLineId = ill2.id AND ill2.deleted = false
WHERE sol.deleted = false
GROUP BY ill2.inventoryLotId
) last_out ON last_out.inventoryLotId = ill_agg.inventoryLotId
WHERE ill_agg.deleted = false
GROUP BY ill_agg.inventoryLotId, last_in.lotLastInDate, last_out.lotLastOutDate
) lot_agg ON lot_agg.inventoryLotId = il.id
LEFT JOIN (
SELECT
sl.itemCode,
SUM(COALESCE(sl.inQty, 0)) as cumStockIn,
MAX(CASE WHEN sl.inQty > 0 THEN sl.date ELSE NULL END) as lastInDate
FROM stock_ledger sl
WHERE sl.deleted = false
AND sl.itemCode IS NOT NULL
AND sl.itemCode != ''
AND COALESCE(sl.inQty, 0) > 0
GROUP BY sl.itemCode
) cum_in ON it.code = cum_in.itemCode
LEFT JOIN (
SELECT
sl.itemCode,
SUM(COALESCE(sl.outQty, 0)) as cumStockOut,
MAX(CASE WHEN sl.outQty > 0 THEN sl.date ELSE NULL END) as lastOutDate
SUM(COALESCE(sl.balance, 0)) as openingBalance
FROM stock_ledger sl
WHERE sl.deleted = false
AND LOWER(sl.type) = 'adj'
AND sl.itemCode IS NOT NULL
AND sl.itemCode != ''
AND COALESCE(sl.outQty, 0) > 0
GROUP BY sl.itemCode
) cum_out ON it.code = cum_out.itemCode
) opening_bal ON it.code = opening_bal.itemCode
WHERE il.deleted = false
$stockCategorySql
$itemCodeSql
$storeLocationSql
GROUP BY it.code, it.name, uc.code, il.lotNo, sil.lotNo, il.expiryDate, sil.expiryDate,
opening_bal.openingBalance, cum_in.cumStockIn, cum_in.lastInDate,
cum_out.cumStockOut, cum_out.lastOutDate
GROUP BY it.code, it.name, uc.code, il.id, il.lotNo, sil.lotNo, il.expiryDate, sil.expiryDate,
lot_agg.lotCumIn, lot_agg.lotCumOut, lot_agg.lotOpening, lot_agg.lotLastInDate, lot_agg.lotLastOutDate,
opening_bal.openingBalance
HAVING 1=1
""".trimIndent()
// Apply filters that need to be in HAVING clause
val havingConditions = mutableListOf<String>()
val lotCurrentBalanceExpr = "(COALESCE(lot_agg.lotOpening, 0) + COALESCE(lot_agg.lotCumIn, 0) - COALESCE(lot_agg.lotCumOut, 0))"
if (!balanceFilterStart.isNullOrBlank()) {
args["balanceFilterStart"] = balanceFilterStart.toDoubleOrNull() ?: 0.0
havingConditions.add("(COALESCE(opening_bal.openingBalance, 0) + COALESCE(cum_in.cumStockIn, 0) - COALESCE(cum_out.cumStockOut, 0)) >= :balanceFilterStart")
havingConditions.add("$lotCurrentBalanceExpr >= :balanceFilterStart")
}
if (!balanceFilterEnd.isNullOrBlank()) {
args["balanceFilterEnd"] = balanceFilterEnd.toDoubleOrNull() ?: 0.0
havingConditions.add("(COALESCE(opening_bal.openingBalance, 0) + COALESCE(cum_in.cumStockIn, 0) - COALESCE(cum_out.cumStockOut, 0)) <= :balanceFilterEnd")
havingConditions.add("$lotCurrentBalanceExpr <= :balanceFilterEnd")
}
if (!lastInDateStart.isNullOrBlank()) {
val formattedDate = lastInDateStart.replace("/", "-")
args["lastInDateStart"] = formattedDate
havingConditions.add("(cum_in.lastInDate IS NOT NULL AND DATE(cum_in.lastInDate) >= DATE(:lastInDateStart))")
havingConditions.add("(lot_agg.lotLastInDate IS NOT NULL AND DATE(lot_agg.lotLastInDate) >= DATE(:lastInDateStart))")
}
if (!lastInDateEnd.isNullOrBlank()) {
val formattedDate = lastInDateEnd.replace("/", "-")
args["lastInDateEnd"] = formattedDate
havingConditions.add("(cum_in.lastInDate IS NOT NULL AND DATE(cum_in.lastInDate) <= DATE(:lastInDateEnd))")
havingConditions.add("(lot_agg.lotLastInDate IS NOT NULL AND DATE(lot_agg.lotLastInDate) <= DATE(:lastInDateEnd))")
}
if (!lastOutDateStart.isNullOrBlank()) {
val formattedDate = lastOutDateStart.replace("/", "-")
args["lastOutDateStart"] = formattedDate
havingConditions.add("(cum_out.lastOutDate IS NOT NULL AND DATE(cum_out.lastOutDate) >= DATE(:lastOutDateStart))")
havingConditions.add("(lot_agg.lotLastOutDate IS NOT NULL AND DATE(lot_agg.lotLastOutDate) >= DATE(:lastOutDateStart))")
}
if (!lastOutDateEnd.isNullOrBlank()) {
val formattedDate = lastOutDateEnd.replace("/", "-")
args["lastOutDateEnd"] = formattedDate
havingConditions.add("(cum_out.lastOutDate IS NOT NULL AND DATE(cum_out.lastOutDate) <= DATE(:lastOutDateEnd))")
havingConditions.add("(lot_agg.lotLastOutDate IS NOT NULL AND DATE(lot_agg.lotLastOutDate) <= DATE(:lastOutDateEnd))")
}
val finalSql = if (havingConditions.isNotEmpty()) {


+ 6
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutLIneRepository.kt 查看文件

@@ -80,4 +80,10 @@ fun findAllByPickOrderLineIdInAndInventoryLotLineIdInAndDeletedFalse(
fun findAllByInventoryLotLineIdInAndNotCompletedOrRejected(
@Param("inventoryLotLineIds") inventoryLotLineIds: List<Long>
): List<StockOutLine>


fun findByInventoryLotLineIdAndStatusAndDeletedFalse(
inventoryLotLineId: Long,
status: String
): List<StockOutLine>
}

+ 13
- 5
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt 查看文件

@@ -1263,14 +1263,22 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) {
// inventoryRepository.saveAll(inventories.values.toList())

// ✅ 修复:批处理完成后,检查所有受影响的 pick order lines 是否应该标记为完成
val affectedPickOrderLineIds = request.lines
val affectedPickOrderIds = request.lines
.mapNotNull { line ->
stockOutLines[line.stockOutLineId]?.pickOrderLine?.id
stockOutLines[line.stockOutLineId]?.pickOrderLine?.pickOrder?.id
}
.distinct()
println("=== Checking ${affectedPickOrderLineIds.size} affected pick order lines after batch submit ===")
affectedPickOrderLineIds.forEach { pickOrderLineId ->

val allPickOrderLineIdsToCheck = if (affectedPickOrderIds.isNotEmpty()) {
affectedPickOrderIds.flatMap { pickOrderId ->
pickOrderLineRepository.findAllByPickOrderIdAndDeletedFalse(pickOrderId).mapNotNull { it.id }
}.distinct()
} else {
emptyList()
}

println("=== Checking ${allPickOrderLineIdsToCheck.size} pick order lines (all lines of affected pick orders) after batch submit ===")
allPickOrderLineIdsToCheck.forEach { pickOrderLineId ->
try {
checkIsStockOutLineCompleted(pickOrderLineId)
} catch (e: Exception) {


+ 542
- 484
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeRecordService.kt
文件差异内容过多而无法显示
查看文件


正在加载...
取消
保存