| @@ -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.ItemsRepository | ||||
| import com.ffii.fpsms.modules.master.entity.UomConversionRepository | import com.ffii.fpsms.modules.master.entity.UomConversionRepository | ||||
| import com.ffii.fpsms.modules.master.service.ItemUomService | 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.pickOrder.entity.PickOrder | ||||
| import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus | import com.ffii.fpsms.modules.deliveryOrder.enums.DoPickOrderStatus | ||||
| import java.time.LocalTime | 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.DoPickOrderRecord | ||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository | import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRecordRepository | ||||
| import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderRepository | 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 | @Service | ||||
| open class PickOrderService( | open class PickOrderService( | ||||
| private val jdbcDao: JdbcDao, | private val jdbcDao: JdbcDao, | ||||
| @@ -3579,4 +3580,87 @@ open fun getPickOrdersByDateAndStore(storeId: String): Map<String, Any?> { | |||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| @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 | |||||
| ) | |||||
| } | |||||
| } | } | ||||
| @@ -33,6 +33,7 @@ import java.time.format.DateTimeFormatter | |||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import com.ffii.fpsms.modules.pickOrder.entity.projection.PickOrderGroupInfo | 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.GetPickOrderInfoResponse | ||||
| import com.ffii.fpsms.modules.pickOrder.web.models.LotSubstitutionConfirmRequest | |||||
| @RestController | @RestController | ||||
| @RequestMapping("/pickOrder") | @RequestMapping("/pickOrder") | ||||
| class PickOrderController( | class PickOrderController( | ||||
| @@ -282,4 +283,8 @@ fun autoAssignAndReleasePickOrderByTicket( | |||||
| fun getPickOrdersByStore(@PathVariable storeId: String): Map<String, Any?> { | fun getPickOrdersByStore(@PathVariable storeId: String): Map<String, Any?> { | ||||
| return pickOrderService.getPickOrdersByDateAndStore(storeId) | return pickOrderService.getPickOrdersByDateAndStore(storeId) | ||||
| } | } | ||||
| @PostMapping("/lot-substitution/confirm") | |||||
| fun confirmLotSubstitution(@RequestBody req: LotSubstitutionConfirmRequest): MessageResponse { | |||||
| return pickOrderService.confirmLotSubstitution(req) | |||||
| } | |||||
| } | } | ||||
| @@ -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 | |||||
| ) | |||||
| @@ -34,6 +34,11 @@ import kotlin.jvm.optionals.getOrNull | |||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | import com.ffii.fpsms.modules.master.web.models.MessageResponse | ||||
| import com.ffii.fpsms.modules.stock.web.model.UpdateInventoryLotLineQuantitiesRequest | import com.ffii.fpsms.modules.stock.web.model.UpdateInventoryLotLineQuantitiesRequest | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryRepository | 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 | @Service | ||||
| open class InventoryLotLineService( | open class InventoryLotLineService( | ||||
| private val inventoryLotLineRepository: InventoryLotLineRepository, | 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 | |||||
| ) | |||||
| } | |||||
| } | } | ||||
| @@ -24,6 +24,8 @@ import java.math.BigDecimal | |||||
| import java.text.ParseException | import java.text.ParseException | ||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | 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.UpdateInventoryLotLineStatusRequest | ||||
| import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisRequest | |||||
| import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisResponse | |||||
| @RequestMapping("/inventoryLotLine") | @RequestMapping("/inventoryLotLine") | ||||
| @RestController | @RestController | ||||
| @@ -100,4 +102,8 @@ class InventoryLotLineController ( | |||||
| fun updateInventoryLotLineQuantities(@RequestBody request: UpdateInventoryLotLineQuantitiesRequest): MessageResponse { | fun updateInventoryLotLineQuantities(@RequestBody request: UpdateInventoryLotLineQuantitiesRequest): MessageResponse { | ||||
| return inventoryLotLineService.updateInventoryLotLineQuantities(request) | return inventoryLotLineService.updateInventoryLotLineQuantities(request) | ||||
| } | } | ||||
| @PostMapping("/analyze-qr-code") | |||||
| fun analyzeQrCode(@RequestBody request: QrCodeAnalysisRequest): QrCodeAnalysisResponse { | |||||
| return inventoryLotLineService.analyzeQrCode(request) | |||||
| } | |||||
| } | } | ||||
| @@ -14,4 +14,29 @@ data class LotLineInfo( | |||||
| data class UpdateInventoryLotLineStatusRequest( | data class UpdateInventoryLotLineStatusRequest( | ||||
| val inventoryLotLineId: Long, | val inventoryLotLineId: Long, | ||||
| val status: String | 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<SameItemLotInfo> | |||||
| ) | ) | ||||