| @@ -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<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 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<String, Any?> { | |||
| 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.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 | |||
| ) | |||
| } | |||
| } | |||
| @@ -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) | |||
| } | |||
| } | |||
| @@ -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<SameItemLotInfo> | |||
| ) | |||