Quellcode durchsuchen

Merge branch 'master' of https://git.2fi-solutions.com/derek/FPSMS-backend

# Conflicts:
#	src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt
master
kelvin.yau vor 4 Tagen
Ursprung
Commit
ad48036eff
16 geänderte Dateien mit 750 neuen und 1112 gelöschten Zeilen
  1. +125
    -604
      python/Bag1.py
  2. +6
    -7
      src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt
  3. +7
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrderLineRepository.kt
  4. +9
    -0
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderLineService.kt
  5. +45
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt
  6. +16
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt
  7. +90
    -125
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt
  8. +201
    -360
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  9. +101
    -0
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt
  10. +14
    -0
      src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt
  11. +8
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt
  12. +2
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutRepository.kt
  13. +3
    -1
      src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt
  14. +90
    -13
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  15. +32
    -2
      src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt
  16. +1
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt

+ 125
- 604
python/Bag1.py
Datei-Diff unterdrückt, da er zu groß ist
Datei anzeigen


+ 6
- 7
src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt Datei anzeigen

@@ -462,18 +462,17 @@ open class M18DeliveryOrderService(
}

// End of save. Check result
// logger.info("Total Success (${doRefType}) (${successList.size}): $successList")
logger.info("Total Success (${doRefType}) (${successList.size})")
// if (failList.size > 0) {
logger.error("Total Fail (${doRefType}) (${failList.size}): $failList")
// }

// logger.info("Total Success (${doLineRefType}) (${successDetailList.size}): $successDetailList")
logger.info("Total Success (${doLineRefType}) (${successDetailList.size})")
// if (failDetailList.size > 0) {
logger.error("Total Fail (${doLineRefType}) (${failDetailList.size}): $failDetailList")
// logger.error("Total Fail M18 Items (${doLineRefType}) (${failItemDetailList.distinct().size}): ${failItemDetailList.distinct()}")
// }

val feeMarked = deliveryOrderLineService.markDeletedLinesWithFeeItems()
if (feeMarked > 0) {
logger.info("Marked $feeMarked DO line(s) as deleted (isFee items).")
}

logger.info("--------------------------------------------End - Saving M18 Delivery Order--------------------------------------------")

return SyncResult(


+ 7
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/entity/DeliveryOrderLineRepository.kt Datei anzeigen

@@ -1,10 +1,17 @@
package com.ffii.fpsms.modules.deliveryOrder.entity

import com.ffii.core.support.AbstractRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
import java.io.Serializable

@Repository
interface DeliveryOrderLineRepository : AbstractRepository<DeliveryOrderLine, Long> {
fun findByM18DataLogIdAndDeletedIsFalse(m18datalogId: Serializable): DeliveryOrderLine?

@Query(
"SELECT dol FROM DeliveryOrderLine dol " +
"WHERE dol.deleted = false AND dol.item IS NOT NULL AND dol.item.isFee = true"
)
fun findAllByDeletedIsFalseAndItemIsFeeTrue(): List<DeliveryOrderLine>
}

+ 9
- 0
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderLineService.kt Datei anzeigen

@@ -68,4 +68,13 @@ open class DeliveryOrderLineService(

return savedDeliveryOrderLine
}

open fun markDeletedLinesWithFeeItems(): Int {
val feeLines = deliveryOrderLineRepository.findAllByDeletedIsFalseAndItemIsFeeTrue()
feeLines.forEach { line ->
line.deleted = true
deliveryOrderLineRepository.saveAndFlush(line)
}
return feeLines.size
}
}

+ 45
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt Datei anzeigen

@@ -45,6 +45,7 @@ import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderInfoResponse
import com.ffii.fpsms.modules.jobOrder.web.model.JobOrderBasicInfoResponse
import com.ffii.fpsms.modules.jobOrder.web.model.PickOrderLineWithLotsResponse
import com.ffii.fpsms.modules.jobOrder.web.model.LotDetailResponse
import com.ffii.fpsms.modules.jobOrder.web.model.StockOutLineDetailResponse

import com.ffii.fpsms.modules.stock.entity.enum.StockInLineStatus
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
@@ -1960,6 +1961,23 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo
val stockOutLinesByPickOrderLine = pickOrderLineIds.associateWith { polId ->
stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(polId)
}

// stockouts 可能包含不在 suggestedPickLots 內的 inventoryLotLineId,需補齊以便計算 location/availableQty
val stockOutInventoryLotLineIds = stockOutLinesByPickOrderLine.values
.flatten()
.mapNotNull { it.inventoryLotLineId }
.distinct()

val stockOutInventoryLotLines = if (stockOutInventoryLotLineIds.isNotEmpty()) {
inventoryLotLineRepository.findAllByIdIn(stockOutInventoryLotLineIds)
.filter { it.deleted == false }
} else {
emptyList()
}

val inventoryLotLineById = (inventoryLotLines + stockOutInventoryLotLines)
.filter { it.id != null }
.associateBy { it.id!! }
// 获取 stock in lines 通过 inventoryLotLineId(用于填充 stockInLineId)
val stockInLinesByInventoryLotLineId = if (inventoryLotLineIds.isNotEmpty()) {
@@ -2080,6 +2098,32 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo
matchQty = jpo?.matchQty?.toDouble()
)
}

// 构建 stockouts 数据:用于无 suggested lot / noLot 场景也能显示并闭环(submit 0)
val stockouts = (stockOutLinesByPickOrderLine[lineId] ?: emptyList()).map { sol ->
val illId = sol.inventoryLotLineId
val ill = if (illId != null) inventoryLotLineById[illId] else null
val lot = ill?.inventoryLot
val warehouse = ill?.warehouse
val availableQty = if (sol.status == "rejected") {
null
} else if (ill == null || ill.deleted == true) {
null
} else {
(ill.inQty ?: BigDecimal.ZERO) - (ill.outQty ?: BigDecimal.ZERO) - (ill.holdQty ?: BigDecimal.ZERO)
}

StockOutLineDetailResponse(
id = sol.id,
status = sol.status,
qty = sol.qty.toDouble(),
lotId = illId,
lotNo = sol.lotNo ?: lot?.lotNo,
location = warehouse?.code,
availableQty = availableQty?.toDouble(),
noLot = (illId == null)
)
}
PickOrderLineWithLotsResponse(
id = pol.id!!,
@@ -2091,6 +2135,7 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo
uomDesc = uom?.udfudesc,
status = pol.status?.value,
lots = lots,
stockouts = stockouts,
handler=handlerName
)
}


+ 16
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/web/model/CreateJobOrderRequest.kt Datei anzeigen

@@ -96,9 +96,25 @@ data class PickOrderLineWithLotsResponse(
val uomDesc: String?,
val status: String?,
val lots: List<LotDetailResponse>,
val stockouts: List<StockOutLineDetailResponse> = emptyList(),
val handler: String?
)

/**
* Stock-out line rows that should be shown even when there is no suggested lot.
* `noLot=true` indicates this line currently has no lot assigned / insufficient inventory lot.
*/
data class StockOutLineDetailResponse(
val id: Long?,
val status: String?,
val qty: Double?,
val lotId: Long?,
val lotNo: String?,
val location: String?,
val availableQty: Double?,
val noLot: Boolean
)

data class LotDetailResponse(
val lotId: Long?,
val lotNo: String?,


+ 90
- 125
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt Datei anzeigen

@@ -92,10 +92,18 @@ open class PickExecutionIssueService(
println(" issueCategory: ${request.issueCategory}")
println("========================================")
// 1. 检查是否已经存在相同的 pick execution issue 记录
// 1. 解析 lot:
// request.lotId 在前端目前传的是 inventory_lot_line.id(用于 SOL 关联/计算 bookQty 等)
// 但 pick_execution_issue.lot_id 在 DB 上外键指向 inventory_lot.id
val inventoryLotLine = request.lotId?.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
val inventoryLotIdForIssue = inventoryLotLine?.inventoryLot?.id

// 2. 检查是否已经存在相同的 pick execution issue 记录(以 inventory_lot.id 去重)
val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
request.pickOrderLineId,
inventoryLotIdForIssue ?: 0L
)
println("Checking for existing issues...")
@@ -119,12 +127,8 @@ open class PickExecutionIssueService(
val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null)
println("Pick order: id=${pickOrder?.id}, code=${pickOrder?.code}, type=${pickOrder?.type?.value}")
// 2. 获取 inventory_lot_line 并计算账面数量 (bookQty)
val inventoryLotLine = request.lotId?.let {
inventoryLotLineRepository.findById(it).orElse(null)
}
println("Inventory lot line: id=${inventoryLotLine?.id}, lotNo=${inventoryLotLine?.inventoryLot?.lotNo}")
// 3. 计算账面数量 (bookQty)(用 inventory_lot_line 快照)
println("Inventory lot line: id=${inventoryLotLine?.id}, lotNo=${inventoryLotLine?.inventoryLot?.lotNo}, inventoryLotId=${inventoryLotIdForIssue}")
// 计算账面数量(创建 issue 时的快照)
val bookQty = if (inventoryLotLine != null) {
@@ -138,13 +142,47 @@ open class PickExecutionIssueService(
BigDecimal.ZERO
}
// 3. 获取数量值
// 4. 获取数量值
val requiredQty = request.requiredQty ?: BigDecimal.ZERO
val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO
val missQty = request.missQty ?: BigDecimal.ZERO
val badItemQty = request.badItemQty ?: BigDecimal.ZERO
val badReason = request.badReason ?: "quantity_problem"
val relatedStockOutLines = stockOutLineRepository
.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
)

val currentStatus = relatedStockOutLines.firstOrNull()?.status ?: ""

if (currentStatus.equals("pending", ignoreCase = true)
&& actualPickQty > BigDecimal.ZERO
&& missQty == BigDecimal.ZERO
&& badItemQty == BigDecimal.ZERO
) {
return MessageResponse(
id = null,
name = "Invalid issue for pending stock out line",
code = "ERROR",
type = "pick_execution_issue",
message = "Cannot submit only actual pick qty when stock out line is pending. Please rescan the lot or use normal pick flow.",
errorPosition = null
)
}
val lotRemainAvailable = bookQty // 当前 lot 剩余
val maxAllowed = requiredQty + lotRemainAvailable
if (actualPickQty > maxAllowed) {
return MessageResponse(
id = null,
name = "Actual pick qty too large",
code = "ERROR",
type = "pick_execution_issue",
message = "Actual pick qty cannot exceed required qty plus lot remaining available.",
errorPosition = null
)
}
println("=== Quantity Summary ===")
println(" Required Qty: $requiredQty")
println(" Actual Pick Qty: $actualPickQty")
@@ -153,7 +191,7 @@ open class PickExecutionIssueService(
println(" Bad Reason: $badReason")
println(" Book Qty: $bookQty")
// 4. 计算 issueQty(实际的问题数量)
// 5. 计算 issueQty(实际的问题数量)
val issueQty = when {
// Bad item 或 bad package:一律用用户输入的 bad 数量,不用 bookQty - actualPickQty
badItemQty > BigDecimal.ZERO -> {
@@ -179,10 +217,12 @@ open class PickExecutionIssueService(
println("=== Final IssueQty Calculation ===")
println(" Calculated IssueQty: $issueQty")
println("================================================")
// 5. 创建 pick execution issue 记录
println("=== Processing Logic Selection ===")

// 6. 创建 pick execution issue 记录
val issueNo = generateIssueNo()
println("Generated issue number: $issueNo")
val lotNoForIssue = request.lotNo ?: inventoryLotLine?.inventoryLot?.lotNo
val pickExecutionIssue = PickExecutionIssue(
id = null,
@@ -200,8 +240,8 @@ open class PickExecutionIssueService(
itemId = request.itemId,
itemCode = request.itemCode,
itemDescription = request.itemDescription,
lotId = request.lotId,
lotNo = request.lotNo,
lotId = inventoryLotIdForIssue,
lotNo = lotNoForIssue,
storeLocation = request.storeLocation,
requiredQty = request.requiredQty,
actualPickQty = request.actualPickQty,
@@ -230,7 +270,7 @@ open class PickExecutionIssueService(
println(" Handle Status: ${savedIssue.handleStatus}")
println(" Issue Qty: ${savedIssue.issueQty}")
// 6. NEW: Update inventory_lot_line.issueQty
// 7. NEW: Update inventory_lot_line.issueQty(仍然用 lotLineId)
if (request.lotId != null && inventoryLotLine != null) {
println("Updating inventory_lot_line.issueQty...")
// ✅ 修改:如果只有 missQty,不更新 issueQty
@@ -270,95 +310,19 @@ open class PickExecutionIssueService(
}
}
// 7. 获取相关数据用于后续处理
val actualPickQtyForProcessing = request.actualPickQty ?: BigDecimal.ZERO
val missQtyForProcessing = request.missQty ?: BigDecimal.ZERO
val badItemQtyForProcessing = request.badItemQty ?: BigDecimal.ZERO
val lotId = request.lotId
val itemId = request.itemId
println("=== Processing Logic Selection ===")
println("Actual Pick Qty: $actualPickQtyForProcessing")
println("Miss Qty: $missQtyForProcessing")
println("Bad Item Qty: $badItemQtyForProcessing")
println("Bad Reason: ${request.badReason}")
println("Lot ID: $lotId")
println("Item ID: $itemId")
println("================================================")
// 8. 新的统一处理逻辑(根据 badReason 决定处理方式)
when {
// 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0)
actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> {
println("→ Handling: Miss Item Only")
handleMissItemOnly(request, missQtyForProcessing)
}
// 情况2: 只有 bad item (badItemQty > 0, missQty = 0)
badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> {
println("→ Handling: Bad Item Only")
// NEW: Check bad reason
if (request.badReason == "package_problem") {
println(" Bad reason is 'package_problem' - calling handleBadItemPackageProblem")
handleBadItemPackageProblem(request, badItemQtyForProcessing)
} else {
println(" Bad reason is 'quantity_problem' - calling handleBadItemOnly")
// quantity_problem or default: handle as normal bad item
handleBadItemOnly(request, badItemQtyForProcessing)
}
}
// 情况3: 既有 miss item 又有 bad item
missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> {
println("→ Handling: Both Miss and Bad Item")
// NEW: Check bad reason
if (request.badReason == "package_problem") {
println(" Bad reason is 'package_problem' - calling handleBothMissAndBadItemPackageProblem")
handleBothMissAndBadItemPackageProblem(request, missQtyForProcessing, badItemQtyForProcessing)
} else {
println(" Bad reason is 'quantity_problem' - calling handleBothMissAndBadItem")
handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing)
}
}
// 情况4: 有 miss item 的情况(无论 actualPickQty 是多少)
missQtyForProcessing > BigDecimal.ZERO -> {
println("→ Handling: Miss Item With Partial Pick")
handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing)
}
// 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item)
actualPickQtyForProcessing > BigDecimal.ZERO -> {
println("→ Handling: Normal Pick")
handleNormalPick(request, actualPickQtyForProcessing)
}
else -> {
println("⚠️ Unknown case: actualPickQty=$actualPickQtyForProcessing, missQty=$missQtyForProcessing, badItemQty=$badItemQtyForProcessing")
}
}
val pickOrderForCompletion = pickOrderRepository.findById(request.pickOrderId).orElse(null)
val consoCode = pickOrderForCompletion?.consoCode
if (!consoCode.isNullOrBlank()) {
// 优先走原来的 consoCode 逻辑(兼容已有 DO 流程)
println("🔍 Checking if pick order $consoCode should be completed after lot rejection...")
try {
checkAndCompletePickOrder(consoCode)
} catch (e: Exception) {
println("⚠️ Error checking pick order completion by consoCode: ${e.message}")
}
} else if (pickOrderForCompletion != null) {
// 🔁 没有 consoCode 的情况:改用 pickOrderId 去检查是否可以完结
println("🔍 Checking if pick order ${pickOrderForCompletion.code} (ID=${pickOrderForCompletion.id}) " +
"should be completed after lot rejection (no consoCode)...")
try {
checkAndCompletePickOrderByPickOrderId(pickOrderForCompletion.id!!)
} catch (e: Exception) {
println("⚠️ Error checking pick order completion by pickOrderId: ${e.message}")
}
// 7. 按规则:issue form 只记录问题 +(可选)把 SOL 标记为 checked
val stockOutLines = stockOutLineRepository
.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
)
stockOutLines.forEach { sol ->
sol.status = "checked"
sol.modified = LocalDateTime.now()
sol.modifiedBy = "system"
stockOutLineRepository.save(sol)
}
stockOutLineRepository.flush()
println("=== recordPickExecutionIssue: SUCCESS ===")
println("Issue ID: ${savedIssue.id}, Issue No: ${savedIssue.issueNo}")
@@ -385,6 +349,23 @@ open class PickExecutionIssueService(
)
}
}
private fun handleAllZeroMarkCompleted(request: PickExecutionIssueRequest) {
val stockOutLines = stockOutLineRepository
.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
)
stockOutLines.forEach { sol ->
// issue form 不完结,只标记 checked,让 submit/batch submit 决定 completed(允许 0)
sol.status = "checked"
sol.modified = LocalDateTime.now()
sol.modifiedBy = "system"
stockOutLineRepository.save(sol)
println("All-zero case: mark stock out line ${sol.id} as checked (qty kept as ${sol.qty})")
}
stockOutLineRepository.flush()
}
private fun generateIssueNo(): String {
val now = LocalDateTime.now()
val yearMonth = now.format(java.time.format.DateTimeFormatter.ofPattern("yyMM"))
@@ -717,34 +698,18 @@ private fun handleMissItemWithPartialPick(request: PickExecutionIssueRequest, ac
// ✅ 修改:不更新 unavailableQty(因为不 reject lot)
// ✅ 修改:不 reject stock_out_line,根据 actualPickQty 设置状态
// ✅ 按规则:issue form 不负责完结/数量提交,只记录问题 +(可选)把 SOL 标记为 checked
val stockOutLines = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse(
request.pickOrderLineId,
request.lotId ?: 0L
)
stockOutLines.forEach { stockOutLine ->
val requiredQty = request.requiredQty?.toDouble() ?: 0.0
val actualPickQtyDouble = actualPickQty.toDouble()
// 设置状态:如果 actualPickQty >= requiredQty,则为 completed,否则为 partially_completed
val newStatus = if (actualPickQtyDouble >= requiredQty) {
"completed"
} else {
"partially_completed"
}
stockOutLine.status = newStatus
stockOutLine.qty = actualPickQtyDouble
stockOutLine.status = "checked"
stockOutLine.modified = LocalDateTime.now()
stockOutLine.modifiedBy = "system"
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
println("Updated stock out line ${stockOutLine.id} status to: ${newStatus} (NOT rejected)")
// ✅ 修复:使用更新前的 onHandQty 计算 balance
val balance = onHandQtyBeforeUpdate - actualPickQtyDouble
createStockLedgerForStockOut(savedStockOutLine, "Nor", balance)
stockOutLineRepository.saveAndFlush(stockOutLine)
println("Issue form: marked stock out line ${stockOutLine.id} as checked (no completion)")
}
// ✅ 修复:检查 pick order line 是否应该标记为完成


+ 201
- 360
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt Datei anzeigen

@@ -21,8 +21,10 @@ import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderGroup
import com.ffii.fpsms.modules.pickOrder.entity.PickOrderGroupRepository
import com.ffii.fpsms.modules.pickOrder.web.models.*
import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.entity.StockOut
import com.ffii.fpsms.modules.stock.entity.StockOutLine
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
import com.ffii.fpsms.modules.stock.entity.StockOutRepository
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus
@@ -1458,7 +1460,7 @@ open class PickOrderService(
println("=== DEBUG: checkAndCompletePickOrderByConsoCode ===")
println("consoCode: $consoCode")

val stockOut = stockOutRepository.findByConsoPickOrderCode(consoCode).orElse(null)
val stockOut = stockOutRepository.findFirstByConsoPickOrderCodeOrderByIdDesc(consoCode)
if (stockOut == null) {
println("❌ No stock_out found for consoCode: $consoCode")
return MessageResponse(
@@ -3357,286 +3359,7 @@ ORDER BY
val enrichedResults = filteredResults
return enrichedResults
}
// 修改后的逻辑
/*
open fun getAllPickOrderLotsWithDetailsHierarchicalold(userId: Long): Map<String, Any?> {
println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (Repository-based) ===")
println("userId filter: $userId")

val user = userService.find(userId).orElse(null)
if (user == null) {
println("❌ User not found: $userId")
return emptyMap()
}

// Step 1:直接按 handledBy 查当前用户的活动 do_pick_order(一个 ticket)
val activeTicketStatuses = listOf("released", "picking") // 如果你用的是 DoPickOrderStatus 枚举,也可以改成 List<DoPickOrderStatus>
val doPickOrder = doPickOrderRepository
.findFirstByHandledByAndDeletedFalseAndTicketStatusIn(user.id!!, activeTicketStatuses)

if (doPickOrder == null) {
println("❌ No active do_pick_order found for handledBy user $userId")
return mapOf(
"fgInfo" to null,
"pickOrders" to emptyList<Any>()
)
}

val doPickOrderId = doPickOrder.id!!
println(" Using do_pick_order ID (by handledBy): $doPickOrderId")

// Step 2:用这个 do_pick_orderId 查对应的 do_pick_order_line / pick_order
val allDoPickOrderLines = doPickOrderLineRepository.findByDoPickOrderIdAndDeletedFalse(doPickOrderId)
val allPickOrderIdsForThisTicket = allDoPickOrderLines.mapNotNull { it.pickOrderId }.distinct()

println(" Found ${allPickOrderIdsForThisTicket.size} pick orders in this do_pick_order (including completed)")

// Step 3:加载这些 pick orders(包括 COMPLETED)
val pickOrders = pickOrderRepository.findAllById(allPickOrderIdsForThisTicket)
.filter {
it.deleted == false &&
it.assignTo?.id == userId &&
it.type?.value == "do"
}

println(" Loaded ${pickOrders.size} pick orders (including completed)")

// Step 4:原来你从 3413 行开始的收集所有 line / lots 的逻辑,全部保留
val allPickOrderLineIds = pickOrders.flatMap { it.pickOrderLines }.mapNotNull { it.id }

val allSuggestions = suggestPickLotRepository.findAllByPickOrderLineIdIn(allPickOrderLineIds)
val allStockOutLines = allPickOrderLineIds.flatMap { lineId ->
stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(lineId)
}

val suggestionsByLineId = allSuggestions.groupBy { spl: SuggestedPickLot ->
spl.pickOrderLine?.id
}
val stockOutLinesByLineId = allStockOutLines.groupBy { sol: StockOutLineInfo ->
sol.pickOrderLineId
}

val allPickOrderLines = mutableListOf<Map<String, Any?>>()
val lineCountsPerPickOrder = mutableListOf<Int>()
val pickOrderIdsList = mutableListOf<Long>()
val pickOrderCodesList = mutableListOf<String>()
val doOrderIdsList = mutableListOf<Long>()
val deliveryOrderCodesList = mutableListOf<String>()

pickOrders.forEach { po ->
pickOrderIdsList.add(po.id!!)
pickOrderCodesList.add(po.code ?: "")

val doOrderId = po.deliveryOrder?.id
if (doOrderId != null) doOrderIdsList.add(doOrderId)
deliveryOrderCodesList.add(po.deliveryOrder?.code ?: "")

val lines = po.pickOrderLines.filter { !it.deleted }

val lineDtos = po.pickOrderLines
.filter { !it.deleted }
.map { pol ->
val lineId = pol.id
val item = pol.item
val uom = pol.uom
// 获取该 line 的 suggestions 和 stock out lines
val suggestions = lineId?.let { suggestionsByLineId[it] } ?: emptyList()
val stockOutLines = lineId?.let { stockOutLinesByLineId[it] } ?: emptyList()
// 构建 lots(合并相同 lot 的多个 suggestions)
val lotMap = mutableMapOf<Long?, LotDetailResponse>()
suggestions.forEach { spl ->
val ill = spl.suggestedLotLine
if (ill != null && ill.id != null) {
val illId = ill.id!!
val illEntity = inventoryLotLinesMap[illId] ?: ill
val il = illEntity.inventoryLot
val w = illEntity.warehouse
val isExpired = il?.expiryDate?.let { exp -> exp.isBefore(today) } == true
val availableQty = (illEntity.inQty ?: zero)
.minus(illEntity.outQty ?: zero)
.minus(illEntity.holdQty ?: zero)
// 查找对应的 stock out line
val stockOutLine = stockOutLines.find { sol ->
sol.inventoryLotLineId == illId
}
// 计算 actualPickQty
val actualPickQty = stockOutLine?.qty?.let { numToBigDecimal(it as? Number) }
if (lotMap.containsKey(illId)) {
// 合并 requiredQty
val existing = lotMap[illId]!!
val newRequiredQty = (existing.requiredQty ?: zero).plus(spl.qty ?: zero)
lotMap[illId] = existing.copy(requiredQty = newRequiredQty)
} else {
lotMap[illId] = LotDetailResponse(
id = illId,
lotNo = il?.lotNo,
expiryDate = il?.expiryDate,
location = w?.code,
stockUnit = illEntity.stockUom?.uom?.udfudesc ?: uom?.udfudesc ?: "N/A",
availableQty = availableQty,
requiredQty = spl.qty,
actualPickQty = actualPickQty,
inQty = illEntity.inQty,
outQty = illEntity.outQty,
holdQty = illEntity.holdQty,
lotStatus = illEntity.status?.value,
lotAvailability = when {
isExpired -> "expired"
stockOutLine?.status == "rejected" -> "rejected"
availableQty <= zero -> "insufficient_stock"
illEntity.status?.value == "unavailable" -> "status_unavailable"
else -> "available"
},
processingStatus = when {
stockOutLine?.status == "completed" -> "completed"
stockOutLine?.status == "rejected" -> "rejected"
else -> "pending"
},
suggestedPickLotId = spl.id,
stockOutLineId = stockOutLine?.id,
stockOutLineStatus = stockOutLine?.status,
stockOutLineQty = stockOutLine?.qty?.let { numToBigDecimal(it as? Number) },
router = RouterInfoResponse(
id = null,
index = w?.order.toString(),
route = w?.code,
area = w?.code
)
)
}
}
}
val lots = lotMap.values.toList()
// 构建 stockouts(包括没有 lot 的)
val stockouts = stockOutLines.map { sol ->
val illId = sol.inventoryLotLineId
val ill = illId?.let { inventoryLotLinesMap[it] }
val il = ill?.inventoryLot
val w = ill?.warehouse
val available = if (ill == null) null else
(ill.inQty ?: zero)
.minus(ill.outQty ?: zero)
.minus(ill.holdQty ?: zero)
StockOutDetailResponse(
id = sol.id,
status = sol.status,
qty = sol.qty?.let { numToBigDecimal(it as? Number) },
lotId = ill?.id,
lotNo = il?.lotNo ?: "",
location = w?.code ?: "",
availableQty = available,
noLot = (ill == null)
)
}
PickOrderLineDetailResponse(
id = lineId,
requiredQty = pol.qty,
status = pol.status?.value,
item = ItemInfoResponse(
id = item?.id,
code = item?.code,
name = item?.name,
uomCode = uom?.code,
uomDesc = uom?.udfudesc,
uomShortDesc = uom?.udfShortDesc
),
lots = lots,
stockouts = stockouts
)
}


lineCountsPerPickOrder.add(lineDtos.size)
allPickOrderLines.addAll(lineDtos)
}

// 排序、fgInfo、mergedPickOrder 这些也全部沿用你当前代码,只要用上面定义好的 doPickOrder/doPickOrderId 即可:
allPickOrderLines.sortWith(compareBy(
{ line ->
val lots = line["lots"] as? List<Map<String, Any?>>
val firstLot = lots?.firstOrNull()
val router = firstLot?.get("router") as? Map<String, Any?>
val indexValue = router?.get("index")
val floorSortValue = when (indexValue) {
is String -> {
val parts = indexValue.split("-")
if (parts.isNotEmpty()) {
val floorPart = parts[0].uppercase()
when (floorPart) {
"1F" -> 1
"2F", "4F" -> 2
else -> 3
}
} else 3
}
else -> 3
}
floorSortValue
},
{ line ->
val lots = line["lots"] as? List<Map<String, Any?>>
val firstLot = lots?.firstOrNull()
val router = firstLot?.get("router") as? Map<String, Any?>
val indexValue = router?.get("index")
when (indexValue) {
is Number -> indexValue.toInt()
is String -> {
val parts = indexValue.split("-")
if (parts.size > 1) {
parts.last().toIntOrNull() ?: 999999
} else {
indexValue.toIntOrNull() ?: 999999
}
}
else -> 999999
}
}
))

val fgInfo = mapOf(
"doPickOrderId" to doPickOrderId,
"ticketNo" to doPickOrder.ticketNo,
"storeId" to doPickOrder.storeId,
"shopCode" to doPickOrder.shopCode,
"shopName" to doPickOrder.shopName,
"truckLanceCode" to doPickOrder.truckLanceCode,
"departureTime" to doPickOrder.truckDepartureTime?.toString()
)

val mergedPickOrder = if (pickOrders.isNotEmpty()) {
val firstPickOrder = pickOrders.first()
mapOf(
"pickOrderIds" to pickOrderIdsList,
"pickOrderCodes" to pickOrderCodesList,
"doOrderIds" to doOrderIdsList,
"deliveryOrderCodes" to deliveryOrderCodesList,
"lineCountsPerPickOrder" to lineCountsPerPickOrder,
"consoCodes" to pickOrders.mapNotNull { it.consoCode }.distinct(),
"status" to doPickOrder.ticketStatus?.value,
"targetDate" to firstPickOrder.targetDate?.toLocalDate()?.toString(),
"pickOrderLines" to allPickOrderLines
)
} else {
null
}

return mapOf(
"fgInfo" to fgInfo,
"pickOrders" to listOfNotNull(mergedPickOrder)
)
}
*/
open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, Any?> {
println("=== Debug: getAllPickOrderLotsWithDetailsHierarchical (NEW STRUCTURE) ===")
println("userId filter: $userId")
@@ -4159,112 +3882,202 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto
)
}
}

@Transactional(rollbackFor = [java.lang.Exception::class])
open fun confirmLotSubstitution(req: LotSubstitutionConfirmRequest): MessageResponse {
val zero = BigDecimal.ZERO
// Validate pick order line
val pol = req.pickOrderLineId.let { pickOrderLineRepository.findById(it).orElse(null) }
val pol = pickOrderLineRepository.findById(req.pickOrderLineId).orElse(null)
?: return MessageResponse(
id = null, name = "Pick order line not found", code = "ERROR", type = "pickorder",
message = "Pick order line ${req.pickOrderLineId} not found", errorPosition = null
)
val polItemId = pol.item?.id
if (polItemId == null) {
return MessageResponse(
?: return MessageResponse(
id = null, name = "Item not found", code = "ERROR", type = "pickorder",
message = "Pick order line item is null", errorPosition = null
)
}
// ✅ 根据 lotNo 和 itemId 查找新的 InventoryLotLine
val newIll = when {
// 优先使用 stockInLineId(更可靠)
req.newStockInLineId != null && req.newStockInLineId > 0 -> {
// 通过 stockInLineId 查找 InventoryLot
val inventoryLot = inventoryLotRepository.findByStockInLineIdAndDeletedFalse(req.newStockInLineId)
?: return MessageResponse(
id = null, name = "Inventory lot not found", code = "ERROR", type = "pickorder",
message = "Inventory lot with stockInLineId ${req.newStockInLineId} not found",
errorPosition = null
)
// 通过 InventoryLot 和 itemId 查找 InventoryLotLine
val lotLines = inventoryLotLineRepository.findAllByInventoryLotId(inventoryLot.id!!)
.filter { it.inventoryLot?.item?.id == polItemId && !it.deleted }
if (lotLines.isEmpty()) {
return MessageResponse(
id = null, name = "Lot line not found", code = "ERROR", type = "pickorder",
message = "Inventory lot line with stockInLineId ${req.newStockInLineId} and itemId ${polItemId} not found",
errorPosition = null
)
}
// 如果有多个,取第一个(通常应该只有一个)
lotLines.first()
}
// 兼容旧方式:使用 lotNo
req.newInventoryLotNo != null && req.newInventoryLotNo.isNotBlank() -> {
inventoryLotLineRepository.findByLotNoAndItemId(req.newInventoryLotNo, polItemId)
?: return MessageResponse(
id = null, name = "New lot line not found", code = "ERROR", type = "pickorder",
message = "Inventory lot line with lotNo '${req.newInventoryLotNo}' and itemId ${polItemId} not found",
errorPosition = null
)
}
else -> {
return MessageResponse(
id = null, name = "Invalid request", code = "ERROR", type = "pickorder",
message = "Either newStockInLineId or newInventoryLotNo must be provided",
errorPosition = null
)
}
}
// Find new InventoryLotLine (from stockInLineId first, fallback lotNo)
val newIll = resolveNewInventoryLotLine(req, polItemId)
?: return MessageResponse(
id = null, name = "New lot line not found", code = "ERROR", type = "pickorder",
message = "Cannot resolve new inventory lot line", errorPosition = null
)
// Item consistency check (应该已经通过上面的查询保证了,但再次确认)
// Item consistency check
val newItemId = newIll.inventoryLot?.item?.id
if (newItemId == null || polItemId != newItemId) {
if (newItemId == null || newItemId != polItemId) {
return MessageResponse(
id = null, name = "Item mismatch", code = "ERROR", type = "pickorder",
message = "New lot line item does not match pick order line item", errorPosition = null
)
}
val newIllId = newIll.id ?: return MessageResponse(
id = null, name = "Invalid lot line", code = "ERROR", type = "pickorder",
message = "New inventory lot line has no ID", errorPosition = null
)
val newLotNo = newIll.inventoryLot?.lotNo ?: req.newInventoryLotNo ?: "unknown"
// 1) Update suggested pick lot (if provided): move holdQty from old ILL to new ILL and re-point the suggestion
if (req.originalSuggestedPickLotId != null && req.originalSuggestedPickLotId > 0) {
// ✅ 使用 repository 而不是 SQL
val originalSpl = suggestPickLotRepository.findById(req.originalSuggestedPickLotId).orElse(null)
if (originalSpl != null) {
val oldIll = originalSpl.suggestedLotLine
val qty = originalSpl.qty ?: zero
// Resolve SuggestedPickLot:
// - If originalSuggestedPickLotId provided: use it
// - Else (1:1 assumption): find by pickOrderLineId (optionally also by stockOutLineId if you add repository method)
val spl = if (req.originalSuggestedPickLotId != null && req.originalSuggestedPickLotId > 0) {
suggestPickLotRepository.findById(req.originalSuggestedPickLotId).orElse(null)
} else {
// 1:1 assumption fallback (you need a repository method; replace with your actual one)
// e.g. suggestPickLotRepository.findFirstByPickOrderLineIdAndDeletedFalseOrderByIdDesc(req.pickOrderLineId)
suggestPickLotRepository.findFirstByPickOrderLineId(req.pickOrderLineId)
}
if (oldIll != null && oldIll.id != newIllId) {
// Decrease hold on old, increase on new
oldIll.holdQty = (oldIll.holdQty ?: zero).minus(qty).max(zero)
inventoryLotLineRepository.save(oldIll)
newIll.holdQty = (newIll.holdQty ?: zero).plus(qty)
inventoryLotLineRepository.save(newIll)
}
if (spl == null) {
return MessageResponse(
id = null, name = "Suggested pick lot not found", code = "ERROR", type = "pickorder",
message = "SuggestedPickLot not found for pickOrderLineId=${req.pickOrderLineId}", errorPosition = null
)
}
val qtyToHold = spl.qty ?: zero
if (qtyToHold.compareTo(zero) <= 0) {
return MessageResponse(
id = null, name = "Invalid qty", code = "ERROR", type = "pickorder",
message = "SuggestedPickLot qty is invalid: $qtyToHold", errorPosition = null
)
}
// Availability check on newIll BEFORE updates
val inQty = newIll.inQty ?: zero
val outQty = newIll.outQty ?: zero
val holdQty = newIll.holdQty ?: zero
val issueQty = newIll.issueQty ?: zero
val available = inQty.subtract(outQty).subtract(holdQty).subtract(issueQty)
if (available.compareTo(qtyToHold) < 0) {
return MessageResponse(
id = null, name = "Insufficient lot qty", code = "REJECT", type = "pickorder",
message = "Reject switch lot: available=$available < required=$qtyToHold", errorPosition = null
)
}
// ✅ 使用 repository 更新 suggestion
originalSpl.suggestedLotLine = newIll
suggestPickLotRepository.save(originalSpl)
val oldIll = spl.suggestedLotLine

// Load stock out line (if provided) to decide "bind vs split"
val existingSol = if (req.stockOutLineId != null && req.stockOutLineId > 0) {
stockOutLIneRepository.findById(req.stockOutLineId).orElse(null)
} else null
val pickedQty = existingSol?.qty?.let { numToBigDecimal(it as? Number) } ?: zero

// Switch lot rule:
// - actual pick == 0: replace/bind (no new line)
// - actual pick > 0: split remaining qty into a NEW suggested pick lot + stock out line
if (pickedQty.compareTo(zero) > 0) {
val remaining = qtyToHold.subtract(pickedQty)
if (remaining.compareTo(zero) <= 0) {
return MessageResponse(
id = null,
name = "No remaining qty",
code = "REJECT",
type = "pickorder",
message = "Reject switch lot: picked=$pickedQty already >= required=$qtyToHold",
errorPosition = null
)
}

// Move HOLD for remaining qty (old -> new)
if (oldIll != null && oldIll.id != null && oldIll.id != newIll.id) {
val oldHold = oldIll.holdQty ?: zero
val newOldHold = oldHold.subtract(remaining)
oldIll.holdQty = if (newOldHold.compareTo(zero) < 0) zero else newOldHold
inventoryLotLineRepository.save(oldIll)

val newHold = (newIll.holdQty ?: zero).add(remaining)
newIll.holdQty = newHold
inventoryLotLineRepository.save(newIll)
}
if (oldIll == null) {
val newHold = (newIll.holdQty ?: zero).add(remaining)
newIll.holdQty = newHold
inventoryLotLineRepository.save(newIll)
}

// Lock current suggestion qty to the picked qty (picked part stays on oldIll)
spl.qty = pickedQty
suggestPickLotRepository.saveAndFlush(spl)

// Create a NEW stock out line + suggestion for the remaining qty
val stockOut: StockOut = if (existingSol != null) {
existingSol.stockOut ?: throw IllegalStateException("Existing StockOutLine has null stockOut")
} else {
val consoCode = pol.pickOrder?.consoCode ?: ""
val existing = stockOutRepository.findByConsoPickOrderCode(consoCode).orElse(null)
if (existing != null) {
existing
} else {
val handlerId = pol.pickOrder?.assignTo?.id ?: SecurityUtils.getUser().orElse(null)?.id
require(handlerId != null) { "Cannot create StockOut: handlerId is null" }
val newStockOut = StockOut().apply {
this.consoPickOrderCode = consoCode
this.type = pol.pickOrder?.type?.value ?: ""
this.status = StockOutStatus.PENDING.status
this.handler = handlerId
}
stockOutRepository.save(newStockOut)
}
}

val newSol = StockOutLine().apply {
this.stockOut = stockOut
this.pickOrderLine = pol
this.item = pol.item
this.inventoryLotLine = newIll
this.qty = 0.0
this.status = StockOutLineStatus.CHECKED.status
this.startTime = LocalDateTime.now()
this.type = existingSol?.type ?: "Nor"
}
stockOutLIneRepository.saveAndFlush(newSol)

val newSpl = SuggestedPickLot().apply {
this.type = spl.type
this.pickOrderLine = pol
this.suggestedLotLine = newIll
this.stockOutLine = newSol
this.qty = remaining
this.pickSuggested = spl.pickSuggested
}
suggestPickLotRepository.saveAndFlush(newSpl)

val newLotNo = newIll.inventoryLot?.lotNo ?: req.newInventoryLotNo
return MessageResponse(
id = null,
name = "Lot substitution confirmed (split line)",
code = "SUCCESS",
type = "pickorder",
message = "Picked=$pickedQty, created new line for remaining=$remaining on lotNo '$newLotNo'",
errorPosition = null
)
}
// If oldIll exists and different: move hold old -> new
if (oldIll != null && oldIll.id != null && oldIll.id != newIll.id) {
val oldHold = oldIll.holdQty ?: zero
val newOldHold = oldHold.subtract(qtyToHold)
oldIll.holdQty = if (newOldHold.compareTo(zero) < 0) zero else newOldHold
inventoryLotLineRepository.save(oldIll)
val newHold = (newIll.holdQty ?: zero).add(qtyToHold)
newIll.holdQty = newHold
inventoryLotLineRepository.save(newIll)
}
// If first bind (oldIll == null): just hold on new
if (oldIll == null) {
val newHold = (newIll.holdQty ?: zero).add(qtyToHold)
newIll.holdQty = newHold
inventoryLotLineRepository.save(newIll)
}
// 2) Update stock out line (if provided): re-point to new ILL; keep qty and status unchanged
// Point suggestion to new lot line
spl.suggestedLotLine = newIll
suggestPickLotRepository.save(spl)
// Update stock out line if provided
if (req.stockOutLineId != null && req.stockOutLineId > 0) {
val sol = stockOutLIneRepository.findById(req.stockOutLineId).orElse(null)
if (sol != null) {
@@ -4274,15 +4087,43 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto
}
}
val newLotNo = newIll.inventoryLot?.lotNo ?: req.newInventoryLotNo
return MessageResponse(
id = null,
name = "Lot substitution confirmed",
code = "SUCCESS",
type = "pickorder",
message = "Updated suggestion and stock out line to new lot line with lotNo '${newLotNo}'",
errorPosition = null
message = "Updated suggestion and stock out line to new lot line with lotNo '$newLotNo'",
errorPosition = null
)
}
private fun resolveNewInventoryLotLine(
req: LotSubstitutionConfirmRequest,
itemId: Long
): InventoryLotLine? {
// Prefer stockInLineId
if (req.newStockInLineId != null && req.newStockInLineId > 0) {
val inventoryLot = inventoryLotRepository.findByStockInLineIdAndDeletedFalse(req.newStockInLineId)
?: return null
val lotLines = inventoryLotLineRepository.findAllByInventoryLotId(inventoryLot.id!!)
.filter { it.inventoryLot?.item?.id == itemId && !it.deleted }
return lotLines.firstOrNull()
}
// Fallback lotNo (req.newInventoryLotNo is non-null String in your model)
if (req.newInventoryLotNo.isNotBlank()) {
return inventoryLotLineRepository
.findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc(
req.newInventoryLotNo,
itemId
)
}
return null
}

open fun getCompletedDoPickOrders(
userId: Long,


+ 101
- 0
src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt Datei anzeigen

@@ -815,6 +815,107 @@ fun searchMaterialStockOutTraceabilityReport(
return jdbcDao.queryForList(sql, args)
}

/**
* GRN (Goods Received Note) report: stock-in lines with PO/delivery note, filterable by receipt date range and item code.
* Returns rows for Excel export: poCode, deliveryNoteNo, receiptDate, itemCode, itemName, acceptedQty, demandQty, uom, etc.
*/
fun searchGrnReport(
receiptDateStart: String?,
receiptDateEnd: String?,
itemCode: String?
): List<Map<String, Any?>> {
val args = mutableMapOf<String, Any>()
val receiptDateStartSql = if (!receiptDateStart.isNullOrBlank()) {
val formatted = receiptDateStart.replace("/", "-")
args["receiptDateStart"] = formatted
"AND DATE(sil.receiptDate) >= DATE(:receiptDateStart)"
} else ""
val receiptDateEndSql = if (!receiptDateEnd.isNullOrBlank()) {
val formatted = receiptDateEnd.replace("/", "-")
args["receiptDateEnd"] = formatted
"AND DATE(sil.receiptDate) <= DATE(:receiptDateEnd)"
} else ""
val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args)

val sql = """
SELECT
po.code AS poCode,
CASE
WHEN sil.dnNo = 'DN00000' OR sil.dnNo IS NULL THEN ''
ELSE sil.dnNo
END AS deliveryNoteNo,
DATE_FORMAT(sil.receiptDate, '%Y-%m-%d') AS receiptDate,
COALESCE(it.code, '') AS itemCode,
COALESCE(it.name, '') AS itemName,
COALESCE(sil.acceptedQty, 0) AS acceptedQty,
COALESCE(sil.demandQty, 0) AS demandQty,
COALESCE(uc_stock.udfudesc, uc_pol.udfudesc, '') AS uom,
COALESCE(uc_pol.udfudesc, '') AS purchaseUomDesc,
COALESCE(uc_stock.udfudesc, '') AS stockUomDesc,
COALESCE(sil.productLotNo, '') AS productLotNo,
DATE_FORMAT(sil.expiryDate, '%Y-%m-%d') AS expiryDate,
COALESCE(sp.code, '') AS supplierCode,
COALESCE(sp.name, '') AS supplier,
COALESCE(sil.status, '') AS status,
MAX(grn.m18_record_id) AS grnId
FROM stock_in_line sil
LEFT JOIN items it ON sil.itemId = it.id
LEFT JOIN purchase_order po ON sil.purchaseOrderId = po.id
LEFT JOIN shop sp ON po.supplierId = sp.id
LEFT JOIN purchase_order_line pol ON sil.purchaseOrderLineId = pol.id
LEFT JOIN uom_conversion uc_pol ON pol.uomId = uc_pol.id
LEFT JOIN item_uom iu_stock ON it.id = iu_stock.itemId AND iu_stock.stockUnit = true AND iu_stock.deleted = false
LEFT JOIN uom_conversion uc_stock ON iu_stock.uomId = uc_stock.id
LEFT JOIN m18_goods_receipt_note_log grn
ON grn.stock_in_line_id = sil.id
WHERE sil.deleted = false
AND sil.receiptDate IS NOT NULL
AND sil.purchaseOrderId IS NOT NULL
$receiptDateStartSql
$receiptDateEndSql
$itemCodeSql
GROUP BY
po.code,
deliveryNoteNo,
receiptDate,
itemCode,
itemName,
acceptedQty,
demandQty,
uom,
purchaseUomDesc,
stockUomDesc,
productLotNo,
expiryDate,
supplierCode,
supplier,
status
ORDER BY sil.receiptDate, po.code, sil.id
""".trimIndent()
val rows = jdbcDao.queryForList(sql, args)
return rows.map { row ->
mapOf(
"poCode" to row["poCode"],
"deliveryNoteNo" to row["deliveryNoteNo"],
"receiptDate" to row["receiptDate"],
"itemCode" to row["itemCode"],
"itemName" to row["itemName"],
"acceptedQty" to (row["acceptedQty"]?.let { n -> (n as? Number)?.toDouble() } ?: 0.0),
"receivedQty" to (row["acceptedQty"]?.let { n -> (n as? Number)?.toDouble() } ?: 0.0),
"demandQty" to (row["demandQty"]?.let { n -> (n as? Number)?.toDouble() } ?: 0.0),
"uom" to row["uom"],
"purchaseUomDesc" to row["purchaseUomDesc"],
"stockUomDesc" to row["stockUomDesc"],
"productLotNo" to row["productLotNo"],
"expiryDate" to row["expiryDate"],
"supplierCode" to row["supplierCode"],
"supplier" to row["supplier"],
"status" to row["status"],
"grnId" to row["grnId"]
)
}
}

/**
* Queries the database for Stock Balance Report data (one summarized row per item).
* Uses stock_ledger with report period (fromDate/toDate): opening = before fromDate, cum in/out = in period, current = up to toDate.


+ 14
- 0
src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt Datei anzeigen

@@ -320,4 +320,18 @@ class ReportController(
return ResponseEntity(pdfBytes, headers, HttpStatus.OK)
}

/**
* GRN (Goods Received Note) report data for Excel export.
* Query by receipt date range and optional item code. Returns JSON { "rows": [ ... ] }.
*/
@GetMapping("/grn-report")
fun getGrnReport(
@RequestParam(required = false) receiptDateStart: String?,
@RequestParam(required = false) receiptDateEnd: String?,
@RequestParam(required = false) itemCode: String?
): Map<String, Any> {
val rows = reportService.searchGrnReport(receiptDateStart, receiptDateEnd, itemCode)
return mapOf("rows" to rows)
}

}

+ 8
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt Datei anzeigen

@@ -53,6 +53,7 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long
""")
fun findByLotNoAndItemId(lotNo: String, itemId: Long): InventoryLotLine?

<<<<<<< HEAD
@Query("""
SELECT DISTINCT ill.inventoryLot.item.id
FROM InventoryLotLine ill
@@ -60,6 +61,13 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long
AND ill.inventoryLot.lotNo = :lotNo
""")
fun findDistinctItemIdsByLotNo(@Param("lotNo") lotNo: String): List<Long>
=======
// lotNo + itemId may not be unique (multiple warehouses/lines); pick one deterministically
fun findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc(
lotNo: String,
itemId: Long
): InventoryLotLine?
>>>>>>> 9760717ed6a5c59383467921464fb2b89a7f85a8
// InventoryLotLineRepository.kt 中添加
@Query("SELECT ill FROM InventoryLotLine ill WHERE ill.warehouse.id IN :warehouseIds AND ill.deleted = false")
fun findAllByWarehouseIdInAndDeletedIsFalse(@Param("warehouseIds") warehouseIds: List<Long>): List<InventoryLotLine>


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockOutRepository.kt Datei anzeigen

@@ -7,5 +7,7 @@ import java.util.Optional
@Repository
interface StockOutRepository: AbstractRepository<StockOut, Long> {
fun findByConsoPickOrderCode(consoPickOrderCode: String) : Optional<StockOut>
// consoPickOrderCode 可能在 DB 中存在重复,避免 single-result exception
fun findFirstByConsoPickOrderCodeOrderByIdDesc(consoPickOrderCode: String): StockOut?
fun findByStockTakeIdAndDeletedFalse(stockTakeId: Long): StockOut?
}

+ 3
- 1
src/main/java/com/ffii/fpsms/modules/stock/entity/SuggestPickLotRepository.kt Datei anzeigen

@@ -8,7 +8,9 @@ import org.springframework.stereotype.Repository
interface SuggestPickLotRepository : AbstractRepository<SuggestedPickLot, Long> {
fun findAllByPickOrderLineIn(lines: List<PickOrderLine>): List<SuggestedPickLot>
fun findAllByPickOrderLineIdIn(pickOrderLineIds: List<Long>): List<SuggestedPickLot>
fun findFirstByPickOrderLineId(pickOrderLineId: Long): SuggestedPickLot?
fun findAllByPickOrderLineId(pickOrderLineId: Long): List<SuggestedPickLot>

fun findFirstByStockOutLineId(stockOutLineId: Long): SuggestedPickLot?
}

+ 90
- 13
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt Datei anzeigen

@@ -43,6 +43,8 @@ import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository
import java.time.LocalTime
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository
@Service
open class StockOutLineService(
private val jdbcDao: JdbcDao,
@@ -53,7 +55,9 @@ open class StockOutLineService(
private val itemUomRespository: ItemUomRespository,
private val pickOrderRepository: PickOrderRepository,
private val inventoryLotLineRepository: InventoryLotLineRepository,
private val stockInLineRepository: StockInLineRepository,
@Lazy private val suggestedPickLotService: SuggestedPickLotService,
private val suggestPickLotRepository: SuggestPickLotRepository,
private val inventoryLotRepository: InventoryLotRepository,
private val doPickOrderRepository: DoPickOrderRepository,
private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
@@ -68,6 +72,35 @@ private val inventoryLotLineService: InventoryLotLineService,
private val inventoryRepository: InventoryRepository,
private val pickExecutionIssueRepository: PickExecutionIssueRepository
): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) {
private fun isEndStatus(status: String?): Boolean {
val s = status?.trim()?.lowercase() ?: return false
return s == "completed" || s == "rejected" || s == "partially_completed"
}

@Transactional
private fun tryCompletePickOrderLine(pickOrderLineId: Long) {
val sols = stockOutLineRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId)
if (sols.isEmpty()) return

val allEnded = sols.all { isEndStatus(it.status) }
if (!allEnded) return

val pol = pickOrderLineRepository.findById(pickOrderLineId).orElse(null) ?: return
if (pol.status != PickOrderLineStatus.COMPLETED) {
pol.status = PickOrderLineStatus.COMPLETED
pickOrderLineRepository.save(pol)
}

// Optionally bubble up to pick order completion (safe no-op if not ready)
val consoCode = pol.pickOrder?.consoCode
if (!consoCode.isNullOrBlank()) {
try {
pickOrderService.checkAndCompletePickOrderByConsoCode(consoCode)
} catch (e: Exception) {
println("⚠️ Error checking pick order completion for consoCode=$consoCode: ${e.message}")
}
}
}
@Throws(IOException::class)
@Transactional
open fun findAllByStockOutId(stockOutId: Long): List<StockOutLine> {
@@ -620,6 +653,10 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long {
}
val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine)
println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}")
// If this stock out line is in end status, try completing its pick order line
if (isEndStatus(savedStockOutLine.status)) {
savedStockOutLine.pickOrderLine?.id?.let { tryCompletePickOrderLine(it) }
}
try {
val item = savedStockOutLine.item
val inventoryLotLine = savedStockOutLine.inventoryLotLine
@@ -946,22 +983,62 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta
// Step 2: Get InventoryLotLine
val getInventoryLotLineStart = System.currentTimeMillis()
// 修复:从 stockOutLine.inventoryLotLine 获取 inventoryLot,而不是使用错误的参数
val inventoryLotLine = stockOutLine.inventoryLotLine
// If StockOutLine has no lot (noLot row), resolve InventoryLotLine by scanned lotNo + itemId and bind it
var inventoryLotLine = stockOutLine.inventoryLotLine
if (inventoryLotLine == null) {
// Prefer stockInLineId from QR for deterministic binding
val resolved = if (request.stockInLineId != null && request.stockInLineId > 0) {
println(" Resolving InventoryLotLine by stockInLineId=${request.stockInLineId} ...")
val sil = stockInLineRepository.findById(request.stockInLineId).orElse(null)
val ill = sil?.inventoryLotLine
if (ill == null) {
println(" StockInLine ${request.stockInLineId} has no associated InventoryLotLine")
null
} else {
// item consistency guard
val illItemId = ill.inventoryLot?.item?.id
if (illItemId != null && illItemId != request.itemId) {
println(" InventoryLotLine item mismatch for stockInLineId=${request.stockInLineId}: $illItemId != ${request.itemId}")
null
} else {
ill
}
}
} else {
println(" StockOutLine has no associated InventoryLotLine, resolving by lotNo+itemId...")
inventoryLotLineRepository
.findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc(
request.inventoryLotNo,
request.itemId
)
}
if (resolved == null) {
println(" Cannot resolve InventoryLotLine by lotNo=${request.inventoryLotNo}, itemId=${request.itemId}")
return MessageResponse(
id = null,
name = "No inventory lot line",
code = "NO_INVENTORY_LOT_LINE",
type = "error",
message = "Cannot resolve InventoryLotLine (stockInLineId=${request.stockInLineId ?: "null"}, lotNo=${request.inventoryLotNo}, itemId=${request.itemId})",
errorPosition = null
)
}
// Bind the lot line to this stockOutLine so subsequent operations can proceed
stockOutLine.inventoryLotLine = resolved
stockOutLine.item = stockOutLine.item ?: resolved.inventoryLot?.item
inventoryLotLine = resolved

// Also update SuggestedPickLot to point to the resolved lot line (so UI/holdQty logic matches DO confirmLotSubstitution)
val spl = suggestPickLotRepository.findFirstByStockOutLineId(stockOutLine.id!!)
if (spl != null) {
spl.suggestedLotLine = resolved
suggestPickLotRepository.saveAndFlush(spl)
}
}
val getInventoryLotLineTime = System.currentTimeMillis() - getInventoryLotLineStart
println("⏱️ [STEP 2] Get InventoryLotLine: ${getInventoryLotLineTime}ms")
if (inventoryLotLine == null) {
println(" StockOutLine has no associated InventoryLotLine")
return MessageResponse(
id = null,
name = "No inventory lot line",
code = "NO_INVENTORY_LOT_LINE",
type = "error",
message = "StockOutLine ${request.stockOutLineId} has no associated InventoryLotLine",
errorPosition = null
)
}
// inventoryLotLine is guaranteed non-null here
// Step 3: Get InventoryLot
val getInventoryLotStart = System.currentTimeMillis()


+ 32
- 2
src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt Datei anzeigen

@@ -40,6 +40,7 @@ import com.ffii.fpsms.modules.pickOrder.enums.PickOrderStatus
import com.ffii.fpsms.modules.stock.entity.projection.StockOutLineInfo
import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository
import com.ffii.fpsms.modules.stock.web.model.StockOutStatus
import com.ffii.fpsms.modules.common.SecurityUtils
@Service
open class SuggestedPickLotService(
val suggestedPickLotRepository: SuggestPickLotRepository,
@@ -433,7 +434,32 @@ open class SuggestedPickLotService(
}

open fun saveAll(request: List<SuggestedPickLot>): List<SuggestedPickLot> {
return suggestedPickLotRepository.saveAllAndFlush(request)
val saved = suggestedPickLotRepository.saveAllAndFlush(request)

// For insufficient stock (suggestedLotLine == null), create a no-lot stock_out_line so UI can display & close the line.
// Also backfill SuggestedPickLot.stockOutLineId for downstream flows (e.g. hierarchical API -> stockouts).
val toBackfill = saved.filter { it.suggestedLotLine == null && it.pickOrderLine != null }
if (toBackfill.isNotEmpty()) {
val updated = mutableListOf<SuggestedPickLot>()
toBackfill.forEach { spl ->
val pickOrder = spl.pickOrderLine?.pickOrder
if (pickOrder == null) return@forEach

// Only create/backfill when stockOutLine is missing
if (spl.stockOutLine == null) {
val sol = createStockOutLineForSuggestion(spl, pickOrder)
if (sol != null) {
spl.stockOutLine = sol
updated.add(spl)
}
}
}
if (updated.isNotEmpty()) {
suggestedPickLotRepository.saveAllAndFlush(updated)
}
}

return saved
}
private fun createStockOutLineForSuggestion(
suggestion: SuggestedPickLot,
@@ -470,10 +496,13 @@ open class SuggestedPickLotService(
// Get or create StockOut
val stockOut = stockOutRepository.findByConsoPickOrderCode(pickOrder.consoCode ?: "")
.orElseGet {
val handlerId = pickOrder.assignTo?.id ?: SecurityUtils.getUser().orElse(null)?.id
require(handlerId != null) { "Cannot create StockOut: handlerId is null" }
val newStockOut = StockOut().apply {
this.consoPickOrderCode = pickOrder.consoCode ?: ""
this.type = pickOrderTypeValue // Use pick order type (do, job, material, etc.)
this.status = StockOutStatus.PENDING.status
this.handler = handlerId
}
stockOutRepository.save(newStockOut)
}
@@ -484,7 +513,8 @@ open class SuggestedPickLotService(
this.pickOrderLine = pickOrderLine
this.item = item
this.inventoryLotLine = null // No lot available
this.qty = (suggestion.qty ?: BigDecimal.ZERO).toDouble()
// qty on StockOutLine represents picked qty; for no-lot placeholder it must start from 0
this.qty = 0.0
this.status = StockOutLineStatus.PENDING.status
this.deleted = false
this.type = "Nor"


+ 1
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt Datei anzeigen

@@ -64,6 +64,7 @@ data class UpdateStockOutLineStatusRequest(
data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest(
val pickOrderLineId: Long,
val inventoryLotNo: String,
val stockInLineId: Long? = null,
val stockOutLineId: Long,
val itemId: Long,
val status: String


Laden…
Abbrechen
Speichern