kelvin.yau 1 week ago
parent
commit
6d9191ab3c
8 changed files with 238 additions and 71 deletions
  1. +44
    -51
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt
  2. +1
    -1
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/LotSubstitutionConfirmRequest.kt
  3. +2
    -1
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
  4. +9
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt
  5. +7
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotRepository.kt
  6. +157
    -2
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  7. +10
    -9
      src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt
  8. +8
    -7
      src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt

+ 44
- 51
src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt View File

@@ -4493,75 +4493,68 @@ 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 entities
// Validate pick order line
val pol = req.pickOrderLineId.let { pickOrderLineRepository.findById(it).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 newIll = req.newInventoryLotLineId.let { inventoryLotLineRepository.findById(it).orElse(null) }
val polItemId = pol.item?.id
if (polItemId == null) {
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 = inventoryLotLineRepository.findByLotNoAndItemId(req.newInventoryLotNo, polItemId)
?: return MessageResponse(
id = null, name = "New lot line not found", code = "ERROR", type = "pickorder",
message = "Inventory lot line ${req.newInventoryLotLineId} not found", errorPosition = null
message = "Inventory lot line with lotNo '${req.newInventoryLotNo}' and itemId ${polItemId} not found",
errorPosition = null
)

// Item consistency check
val polItemId = pol.item?.id
// Item consistency check (应该已经通过上面的查询保证了,但再次确认)
val newItemId = newIll.inventoryLot?.item?.id
if (polItemId == null || newItemId == null || polItemId != newItemId) {
if (newItemId == null || polItemId != newItemId) {
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
)
// 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) {
// Get current suggested ILL id and qty
val row = jdbcDao.queryForMap(
"""
SELECT spl.suggestedLotLineId AS oldIllId, COALESCE(spl.qty,0) AS qty
FROM suggested_pick_lot spl
WHERE spl.id = :splId
""".trimIndent(), mapOf("splId" to req.originalSuggestedPickLotId)
).orElse(null)

if (row != null) {
val oldIllId = (row["oldIllId"] as Number?)?.toLong()
val qty = when (val qtyObj = row["qty"]) {
is BigDecimal -> qtyObj
is Number -> qtyObj.toDouble().toBigDecimal()
is String -> qtyObj.toBigDecimalOrNull() ?: zero
else -> zero
}

if (oldIllId != null && oldIllId != req.newInventoryLotLineId) {
// ✅ 使用 repository 而不是 SQL
val originalSpl = suggestPickLotRepository.findById(req.originalSuggestedPickLotId).orElse(null)
if (originalSpl != null) {
val oldIll = originalSpl.suggestedLotLine
val qty = originalSpl.qty ?: zero
if (oldIll != null && oldIll.id != newIllId) {
// Decrease hold on old, increase on new
val oldIll = inventoryLotLineRepository.findById(oldIllId).orElse(null)
if (oldIll != null) {
oldIll.holdQty = (oldIll.holdQty ?: zero).minus(qty).max(zero)
inventoryLotLineRepository.save(oldIll)
}
val newIllEntity = inventoryLotLineRepository.findById(req.newInventoryLotLineId).orElse(null)
if (newIllEntity != null) {
newIllEntity.holdQty = (newIllEntity.holdQty ?: zero).plus(qty)
inventoryLotLineRepository.save(newIllEntity)
}
oldIll.holdQty = (oldIll.holdQty ?: zero).minus(qty).max(zero)
inventoryLotLineRepository.save(oldIll)
newIll.holdQty = (newIll.holdQty ?: zero).plus(qty)
inventoryLotLineRepository.save(newIll)
}

// Re-point suggestion to new ILL
jdbcDao.executeUpdate(
"""
UPDATE suggested_pick_lot
SET suggestedLotLineId = :newIllId
WHERE id = :splId
""".trimIndent(), mapOf("newIllId" to req.newInventoryLotLineId, "splId" to req.originalSuggestedPickLotId)
)
// ✅ 使用 repository 更新 suggestion
originalSpl.suggestedLotLine = newIll
suggestPickLotRepository.save(originalSpl)
}
}
// 2) Update stock out line (if provided): re-point to new ILL; keep qty and status unchanged
if (req.stockOutLineId != null && req.stockOutLineId > 0) {
val sol = stockOutLIneRepository.findById(req.stockOutLineId).orElse(null)
@@ -4571,13 +4564,13 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto
stockOutLIneRepository.save(sol)
}
}
return MessageResponse(
id = null,
name = "Lot substitution confirmed",
code = "SUCCESS",
type = "pickorder",
message = "Updated suggestion and stock out line to new lot line ${req.newInventoryLotLineId}",
message = "Updated suggestion and stock out line to new lot line with lotNo '${req.newInventoryLotNo}'",
errorPosition = null
)
}


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/LotSubstitutionConfirmRequest.kt View File

@@ -5,5 +5,5 @@ data class LotSubstitutionConfirmRequest(
val pickOrderLineId: Long,
val stockOutLineId: Long?, // optional
val originalSuggestedPickLotId: Long?, // optional
val newInventoryLotLineId: Long
val newInventoryLotNo: String
)

+ 2
- 1
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt View File

@@ -579,7 +579,8 @@ open class ProductProcessService(
scrapRate = bom?.scrapRate?:-1,
allergicSubstance = calculateAllergicSubstanceScore(bom?.allergicSubstances),
outputQtyUom = bom?.outputQtyUom?:"",
outputQty = bom?.outputQty?.toInt()?:0,
//outputQty = bom?.outputQty?.toInt()?:0,
outputQty = jobOrder?.reqQty?.toInt()?:0,
productProcessCode = process.productProcessCode?:"",
status = process.status?:ProductProcessStatus.PENDING,
startTime = process.startTime?:LocalDateTime.now(),


+ 9
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt View File

@@ -43,4 +43,13 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long

fun findAllByInventoryLotItemIdAndStatus(itemId: Long, status: String): List<InventoryLotLine>
fun findAllByInventoryLotItemIdAndStatus(itemId: Long, status: InventoryLotLineStatus): List<InventoryLotLine>
@Query("SELECT ill FROM InventoryLotLine ill WHERE ill.inventoryLot.item.id IN :itemIds")
fun findAllByItemIdIn(@Param("itemIds") itemIds: List<Long>): List<InventoryLotLine>
@Query("""
SELECT ill FROM InventoryLotLine ill
WHERE ill.inventoryLot.lotNo = :lotNo
AND ill.inventoryLot.item.id = :itemId
AND ill.deleted = false
""")
fun findByLotNoAndItemId(lotNo: String, itemId: Long): InventoryLotLine?
}

+ 7
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotRepository.kt View File

@@ -16,4 +16,11 @@ interface InventoryLotRepository: AbstractRepository<InventoryLot, Long> {
""")
fun findLatestLotNoByPrefix(prefix: String): String?
fun findAllByIdIn(ids: List<Long>): List<InventoryLot>
@Query("""
SELECT il FROM InventoryLot il
WHERE il.lotNo = :lotNo
AND il.item.id = :itemId
AND il.deleted = false
""")
fun findByLotNoAndItemId(lotNo: String, itemId: Long): InventoryLot?
}

+ 157
- 2
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt View File

@@ -31,6 +31,7 @@ import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecord
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecord
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository
import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineStatusByQRCodeAndLotNoRequest

import com.ffii.fpsms.modules.common.CodeGenerator
import org.springframework.context.annotation.Lazy
@@ -53,6 +54,7 @@ private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
private val deliveryOrderRepository: DeliveryOrderRepository,
private val doPickOrderLineRepository: DoPickOrderLineRepository,
private val doPickOrderLineRecordRepository: DoPickOrderLineRecordRepository,
private val inventoryLotLineService: InventoryLotLineService
): AbstractBaseEntityService<StockOutLine, Long, StockOutLIneRepository>(jdbcDao, stockOutLineRepository) {
@Throws(IOException::class)
@Transactional
@@ -686,7 +688,7 @@ open fun batchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
val errors = mutableListOf<String>()
val processedIds = mutableListOf<Long>()

request.lines.forEach { line ->
request.lines.forEach { line:QrPickSubmitLineRequest ->
val lineStartTime = System.currentTimeMillis()
try {
// 1) noLot 情况:等价于前端 handleSubmitAllScanned 里 noLot 分支
@@ -920,4 +922,157 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta
)
}
}
}

@Transactional(rollbackFor = [Exception::class])
open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse {
val startTime = System.currentTimeMillis()
println("=== BATCH SUBMIT START ===")
println("Start time: ${java.time.LocalDateTime.now()}")
println("Request lines count: ${request.lines.size}")

if (request.lines.isEmpty()) {
return MessageResponse(
id = null,
name = "No lines",
code = "EMPTY",
type = "batch_submit",
message = "No scanned lines",
errorPosition = null
)
}

val errors = mutableListOf<String>()
val processedIds = mutableListOf<Long>()

try {
// 1) Bulk load all lot lines and inventories
val lotLineIds = request.lines.mapNotNull { it.inventoryLotLineId }
println("Loading ${lotLineIds.size} lot lines...")
val lotLines = if (lotLineIds.isNotEmpty()) {
inventoryLotLineRepository.findAllById(lotLineIds).associateBy { it.id }
} else {
emptyMap()
}

val itemIds = lotLines.values.mapNotNull { it.inventoryLot?.item?.id }
println("Loading ${itemIds.size} inventories...")
val inventories = itemIds.mapNotNull { itemId ->
inventoryRepository.findByItemId(itemId).orElse(null)
}.associateBy { it.item?.id }

// 2) Bulk load all stock out lines to get current quantities
val stockOutLineIds = request.lines.map { it.stockOutLineId }
println("Loading ${stockOutLineIds.size} stock out lines...")
val stockOutLines = stockOutLineRepository.findAllById(stockOutLineIds).associateBy { it.id }

// 3) Process each request line
request.lines.forEach { line: QrPickSubmitLineRequest ->
try {
println("Processing line: stockOutLineId=${line.stockOutLineId}, noLot=${line.noLot}")
if (line.noLot) {
// noLot branch
updateStatus(UpdateStockOutLineStatusRequest(
id = line.stockOutLineId,
status = "completed",
qty = 0.0
))
processedIds += line.stockOutLineId
println(" ✓ noLot item processed")
return@forEach
}

// 修复:从数据库获取当前实际数量
val stockOutLine = stockOutLines[line.stockOutLineId]
?: throw IllegalStateException("StockOutLine ${line.stockOutLineId} not found")
// 修复:qty 是 Double?,需要转换为 BigDecimal
val currentActual = (stockOutLine.qty ?: 0.0).toBigDecimal()
val targetActual = line.actualPickQty ?: BigDecimal.ZERO
val required = line.requiredQty ?: BigDecimal.ZERO
println(" Current qty: $currentActual, Target qty: $targetActual, Required: $required")
// 修复:计算增量(前端发送的 actualPickQty 是目标累计值)
val submitQty = targetActual - currentActual
println(" Submit qty (increment): $submitQty")
// 修复:使用前端发送的状态,而不是重新计算
val newStatus = line.stockOutLineStatus ?: if (targetActual >= required) "completed" else "partially_completed"

// 修复:updateStatus 期望增量,所以传入 submitQty
updateStatus(UpdateStockOutLineStatusRequest(
id = line.stockOutLineId,
status = newStatus,
qty = submitQty.toDouble()
))

// Inventory updates - 修复:使用增量数量
if (submitQty > BigDecimal.ZERO && line.inventoryLotLineId != null) {
println(" Updating inventory lot line ${line.inventoryLotLineId} with qty $submitQty")
inventoryLotLineService.updateInventoryLotLineQuantities(
UpdateInventoryLotLineQuantitiesRequest(
inventoryLotLineId = line.inventoryLotLineId,
qty = submitQty,
operation = "pick"
)
)
}

processedIds += line.stockOutLineId
println(" ✓ Line processed successfully")
} catch (e: Exception) {
println(" ✗ Error processing line ${line.stockOutLineId}: ${e.message}")
e.printStackTrace()
errors += "stockOutLineId=${line.stockOutLineId}: ${e.message}"
}
}

// 4) 移除:不需要保存 lotLines 和 inventories,因为它们没有被修改
// inventoryLotLineRepository.saveAll(lotLines.values.toList())
// inventoryRepository.saveAll(inventories.values.toList())

val msg = if (errors.isEmpty()) {
"Batch submit success (${processedIds.size} lines)."
} else {
"Batch submit partial success (${processedIds.size} lines), errors: ${errors.joinToString("; ")}"
}

val totalTime = System.currentTimeMillis() - startTime
println("Processed: ${processedIds.size}/${request.lines.size} items")
println("Total time: ${totalTime}ms (${totalTime / 1000.0}s)")
println("Average time per item: ${if (processedIds.isNotEmpty()) totalTime / processedIds.size else 0}ms")
println("End time: ${java.time.LocalDateTime.now()}")
println("=== BATCH SUBMIT END ===")

return MessageResponse(
id = null,
name = "batch_submit",
code = if (errors.isEmpty()) "SUCCESS" else "PARTIAL_SUCCESS",
type = "batch_submit",
message = msg,
errorPosition = null,
entity = mapOf(
"processedIds" to processedIds,
"errors" to errors
)
)
} catch (e: Exception) {
println("=== BATCH SUBMIT ERROR ===")
println("Error: ${e.message}")
e.printStackTrace()
return MessageResponse(
id = null,
name = "batch_submit",
code = "ERROR",
type = "batch_submit",
message = "Error: ${e.message}",
errorPosition = null,
entity = mapOf(
"processedIds" to processedIds,
"errors" to listOf(e.message ?: "Unknown error")
)
)
}
}}

+ 10
- 9
src/main/java/com/ffii/fpsms/modules/stock/web/StockOutLineController.kt View File

@@ -10,11 +10,12 @@ import com.ffii.fpsms.modules.stock.web.model.CreateStockOutLineRequest
import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineRequest
import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineStatusRequest
import jakarta.validation.Valid
import com.ffii.fpsms.modules.stock.web.model.QrPickBatchSubmitRequest
import org.springframework.web.bind.annotation.*
import com.ffii.fpsms.modules.stock.web.model.CreateStockOutLineWithoutConsoRequest
import com.ffii.fpsms.modules.stock.web.model.QrPickBatchSubmitRequest
import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineStatusByQRCodeAndLotNoRequest


@RestController
@RequestMapping("/stockOutLine")
class StockOutLineController(
@@ -45,16 +46,16 @@ class StockOutLineController(
}

@PostMapping("/batchQrSubmit")
@PostMapping("/batchSubmitList")
fun batchQrSubmit(@Valid @RequestBody request: QrPickBatchSubmitRequest): MessageResponse {
return stockOutLineService.batchSubmit(request)
return stockOutLineService.newBatchSubmit(request)
}
@PostMapping("/updateStatusByQRCodeAndLotNo")
fun updateStatusByQRCodeAndLotNo(@Valid @RequestBody request: UpdateStockOutLineStatusByQRCodeAndLotNoRequest): MessageResponse {
try {
println("=== 📥 CONTROLLER: updateStatusByQRCodeAndLotNo called ===")
println("📋 Request received:")
println(" - stockOutLineId: ${request.stockOutLineId}")
println("=== CONTROLLER: updateStatusByQRCodeAndLotNo called ===")
println(" Request received:")
println(" - stockOutLineId: ${request.stockOutLineId}")
println(" - pickOrderLineId: ${request.pickOrderLineId}")
println(" - inventoryLotNo: ${request.inventoryLotNo}")
println(" - itemId: ${request.itemId}")
@@ -62,11 +63,11 @@ class StockOutLineController(
val result = stockOutLineService.updateStockOutLineStatusByQRCodeAndLotNo(request)
println(" CONTROLLER: Service call completed, returning result")
println(" CONTROLLER: Service call completed, returning result")
return result
} catch (e: Exception) {
println(" CONTROLLER ERROR: ${e.message}")
println(" Exception type: ${e.javaClass.simpleName}")
println(" CONTROLLER ERROR: ${e.message}")
println(" Exception type: ${e.javaClass.simpleName}")
e.printStackTrace()
// 返回错误响应而不是抛出异常


+ 8
- 7
src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveStockOutRequest.kt View File

@@ -61,6 +61,13 @@ data class UpdateStockOutLineStatusRequest(
val qty: Double? = null,
val remarks: String? = null
)
data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest(
val pickOrderLineId: Long,
val inventoryLotNo: String,
val stockOutLineId: Long,
val itemId: Long,
val status: String
)
data class QrPickSubmitLineRequest(
val stockOutLineId: Long,
val pickOrderLineId: Long,
@@ -76,10 +83,4 @@ data class QrPickBatchSubmitRequest(
val userId: Long,
val lines: List<QrPickSubmitLineRequest>
)
data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest(
val pickOrderLineId: Long,
val inventoryLotNo: String,
val stockOutLineId: Long,
val itemId: Long,
val status: String
)


Loading…
Cancel
Save