diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt index e54efd7..a45383f 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickOrderService.kt @@ -8,7 +8,6 @@ import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository import com.ffii.fpsms.modules.master.entity.ItemsRepository import com.ffii.fpsms.modules.master.entity.UomConversionRepository import com.ffii.fpsms.modules.master.service.ItemUomService -import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.pickOrder.entity.PickOrder import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus import java.time.LocalTime @@ -57,6 +56,8 @@ import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrder import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecord import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository +import com.ffii.fpsms.modules.pickOrder.web.models.LotSubstitutionConfirmRequest +import com.ffii.fpsms.modules.master.web.models.MessageResponse @Service open class PickOrderService( private val jdbcDao: JdbcDao, @@ -3579,4 +3580,87 @@ open fun getPickOrdersByDateAndStore(storeId: String): Map { ) } } +@Transactional(rollbackFor = [java.lang.Exception::class]) +open fun confirmLotSubstitution(req: LotSubstitutionConfirmRequest): MessageResponse { + val zero = BigDecimal.ZERO + + // Validate entities + 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) } + ?: return MessageResponse(id = null, name = "New lot line not found", code = "ERROR", type = "pickorder", + message = "Inventory lot line ${req.newInventoryLotLineId} not found", errorPosition = null) + + // Item consistency check + val polItemId = pol.item?.id + val newItemId = newIll.inventoryLot?.item?.id + if (polItemId == null || 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 + ) + } + + // 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) { + // 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) + } + } + + // 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)) + } + } + + // 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) + if (sol != null) { + sol.inventoryLotLine = newIll + sol.item = pol.item + 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}", + errorPosition = null + ) +} } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt index 7c263f6..962a4ae 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickOrderController.kt @@ -33,6 +33,7 @@ import java.time.format.DateTimeFormatter import java.time.LocalDate import com.ffii.fpsms.modules.pickOrder.entity.projection.PickOrderGroupInfo import com.ffii.fpsms.modules.pickOrder.web.models.GetPickOrderInfoResponse +import com.ffii.fpsms.modules.pickOrder.web.models.LotSubstitutionConfirmRequest @RestController @RequestMapping("/pickOrder") class PickOrderController( @@ -282,4 +283,8 @@ fun autoAssignAndReleasePickOrderByTicket( fun getPickOrdersByStore(@PathVariable storeId: String): Map { return pickOrderService.getPickOrdersByDateAndStore(storeId) } +@PostMapping("/lot-substitution/confirm") +fun confirmLotSubstitution(@RequestBody req: LotSubstitutionConfirmRequest): MessageResponse { + return pickOrderService.confirmLotSubstitution(req) +} } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/LotSubstitutionConfirmRequest.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/LotSubstitutionConfirmRequest.kt new file mode 100644 index 0000000..9432cad --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/LotSubstitutionConfirmRequest.kt @@ -0,0 +1,9 @@ + +package com.ffii.fpsms.modules.pickOrder.web.models + +data class LotSubstitutionConfirmRequest( + val pickOrderLineId: Long, + val stockOutLineId: Long?, // optional + val originalSuggestedPickLotId: Long?, // optional + val newInventoryLotLineId: Long +) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt index 81b0f98..b0428b3 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt @@ -34,6 +34,11 @@ import kotlin.jvm.optionals.getOrNull import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.stock.web.model.UpdateInventoryLotLineQuantitiesRequest import com.ffii.fpsms.modules.stock.entity.InventoryRepository +import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisRequest +import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisResponse +import com.ffii.fpsms.modules.stock.web.model.ScannedLotInfo +import com.ffii.fpsms.modules.stock.web.model.SameItemLotInfo + @Service open class InventoryLotLineService( private val inventoryLotLineRepository: InventoryLotLineRepository, @@ -278,4 +283,60 @@ open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantit ) } } + +open fun analyzeQrCode(request: QrCodeAnalysisRequest): QrCodeAnalysisResponse { + val stockInLine = stockInLineRepository.findById(request.stockInLineId).orElseThrow() + + // Try direct link first; fall back to first lot line in the linked inventoryLot + val scannedInventoryLotLine = + stockInLine.inventoryLotLine + ?: stockInLine.inventoryLot?.inventoryLotLines?.firstOrNull() + ?: throw IllegalStateException("No inventory lot line found for stockInLineId=${request.stockInLineId}") + + val item = scannedInventoryLotLine.inventoryLot?.item + ?: throw IllegalStateException("Item not found for lot line id=${scannedInventoryLotLine.id}") + + // Collect same-item available lots; skip the scanned one; only remainingQty > 0 + val sameItemLots = inventoryLotLineRepository + .findAllByInventoryLotItemIdAndStatus(request.itemId, InventoryLotLineStatus.AVAILABLE) + .asSequence() + .filter { it.id != scannedInventoryLotLine.id } + .mapNotNull { lotLine -> + val lot = lotLine.inventoryLot ?: return@mapNotNull null + val lotNo = lot.stockInLine?.lotNo ?: return@mapNotNull null + val uomDesc = lotLine.stockUom?.uom?.udfudesc ?: return@mapNotNull null + + val inQty = lotLine.inQty ?: BigDecimal.ZERO + val outQty = lotLine.outQty ?: BigDecimal.ZERO + val holdQty = lotLine.holdQty ?: BigDecimal.ZERO + val remainingQty = inQty.minus(outQty).minus(holdQty) + + if (remainingQty > BigDecimal.ZERO) + SameItemLotInfo( + lotNo = lotNo, + inventoryLotLineId = lotLine.id!!, + availableQty = remainingQty, + uom = uomDesc + ) + else null + } + .toList() + + val scannedLotNo = stockInLine.lotNo + ?: stockInLine.inventoryLot?.stockInLine?.lotNo + ?: throw IllegalStateException("Lot number not found for stockInLineId=${request.stockInLineId}") + + return QrCodeAnalysisResponse( + itemId = request.itemId, + itemCode = item.code ?: "", + itemName = item.name ?: "", + scanned = ScannedLotInfo( + stockInLineId = request.stockInLineId, + lotNo = scannedLotNo, + inventoryLotLineId = scannedInventoryLotLine.id + ?: throw IllegalStateException("inventoryLotLineId missing on scanned lot line") + ), + sameItemLots = sameItemLots + ) +} } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt index 93e5d20..421112d 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt @@ -24,6 +24,8 @@ import java.math.BigDecimal import java.text.ParseException import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.stock.web.model.UpdateInventoryLotLineStatusRequest +import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisRequest +import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisResponse @RequestMapping("/inventoryLotLine") @RestController @@ -100,4 +102,8 @@ class InventoryLotLineController ( fun updateInventoryLotLineQuantities(@RequestBody request: UpdateInventoryLotLineQuantitiesRequest): MessageResponse { return inventoryLotLineService.updateInventoryLotLineQuantities(request) } + @PostMapping("/analyze-qr-code") + fun analyzeQrCode(@RequestBody request: QrCodeAnalysisRequest): QrCodeAnalysisResponse { + return inventoryLotLineService.analyzeQrCode(request) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt index 32f5d12..7cc686a 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/LotLineInfo.kt @@ -14,4 +14,29 @@ data class LotLineInfo( data class UpdateInventoryLotLineStatusRequest( val inventoryLotLineId: Long, val status: String +) +data class QrCodeAnalysisRequest( + val itemId: Long, + val stockInLineId: Long +) + +data class ScannedLotInfo( + val stockInLineId: Long, + val lotNo: String, + val inventoryLotLineId: Long +) + +data class SameItemLotInfo( + val lotNo: String, + val inventoryLotLineId: Long, + val availableQty: BigDecimal, + val uom: String +) + +data class QrCodeAnalysisResponse( + val itemId: Long, + val itemCode: String, + val itemName: String, + val scanned: ScannedLotInfo, + val sameItemLots: List ) \ No newline at end of file