| @@ -1536,7 +1536,7 @@ open class DeliveryOrderService( | |||||
| this.type = "do" | this.type = "do" | ||||
| this.consoPickOrderCode = consoCode | this.consoPickOrderCode = consoCode | ||||
| this.status = StockOutStatus.PENDING.status | this.status = StockOutStatus.PENDING.status | ||||
| this.handler = request.userId | |||||
| //this.handler = request.userId | |||||
| } | } | ||||
| val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | ||||
| @@ -457,8 +457,8 @@ open class JoPickOrderService( | |||||
| val lotAvailability = when { | val lotAvailability = when { | ||||
| il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | ||||
| sol?.status == "rejected" -> "rejected" | sol?.status == "rejected" -> "rejected" | ||||
| availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock" | |||||
| ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable" | ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable" | ||||
| availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock" | |||||
| else -> "available" | else -> "available" | ||||
| } | } | ||||
| @@ -685,8 +685,8 @@ open class JoPickOrderService( | |||||
| val lotAvailability = when { | val lotAvailability = when { | ||||
| il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | ||||
| sol?.status == "rejected" -> "rejected" | sol?.status == "rejected" -> "rejected" | ||||
| availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock" | |||||
| ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable" | ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable" | ||||
| availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock" | |||||
| else -> "available" | else -> "available" | ||||
| } | } | ||||
| @@ -2175,8 +2175,8 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo | |||||
| val lotAvailability = when { | val lotAvailability = when { | ||||
| il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | ||||
| sol?.status == "rejected" -> "rejected" | sol?.status == "rejected" -> "rejected" | ||||
| availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock" | |||||
| ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable" | ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable" | ||||
| availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock" | |||||
| else -> "available" | else -> "available" | ||||
| } | } | ||||
| @@ -920,8 +920,8 @@ open class PickOrderService( | |||||
| val lotAvailability = when { | val lotAvailability = when { | ||||
| isExpired -> "expired" | isExpired -> "expired" | ||||
| sol?.status == "rejected" -> "rejected" | sol?.status == "rejected" -> "rejected" | ||||
| availableQty <= zero -> "insufficient_stock" | |||||
| ill.status?.value == "unavailable" -> "status_unavailable" | ill.status?.value == "unavailable" -> "status_unavailable" | ||||
| availableQty <= zero -> "insufficient_stock" | |||||
| else -> "available" | else -> "available" | ||||
| } | } | ||||
| @@ -4061,6 +4061,17 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto | |||||
| id = null, name = "New lot line not found", code = "ERROR", type = "pickorder", | id = null, name = "New lot line not found", code = "ERROR", type = "pickorder", | ||||
| message = "Cannot resolve new inventory lot line", errorPosition = null | message = "Cannot resolve new inventory lot line", errorPosition = null | ||||
| ) | ) | ||||
| if (newIll.status == InventoryLotLineStatus.UNAVAILABLE) { | |||||
| return MessageResponse( | |||||
| id = null, | |||||
| name = "Lot line unavailable", | |||||
| code = "LOT_UNAVAILABLE", | |||||
| type = "pickorder", | |||||
| message = "Cannot switch to unavailable inventory lot line", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| // Item consistency check | // Item consistency check | ||||
| val newItemId = newIll.inventoryLot?.item?.id | val newItemId = newIll.inventoryLot?.item?.id | ||||
| @@ -4578,8 +4589,8 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto | |||||
| lotAvailability = when { | lotAvailability = when { | ||||
| isExpired -> "expired" | isExpired -> "expired" | ||||
| stockOutLine?.status == "rejected" -> "rejected" | stockOutLine?.status == "rejected" -> "rejected" | ||||
| availableQty <= zero -> "insufficient_stock" | |||||
| illEntity.status?.value == "unavailable" -> "status_unavailable" | illEntity.status?.value == "unavailable" -> "status_unavailable" | ||||
| availableQty <= zero -> "insufficient_stock" | |||||
| else -> "available" | else -> "available" | ||||
| }, | }, | ||||
| processingStatus = when { | processingStatus = when { | ||||
| @@ -137,7 +137,7 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long | |||||
| SELECT ill FROM InventoryLotLine ill | SELECT ill FROM InventoryLotLine ill | ||||
| JOIN ill.inventoryLot il | JOIN ill.inventoryLot il | ||||
| WHERE il.expiryDate < :today | WHERE il.expiryDate < :today | ||||
| AND ill.inQty != ill.outQty | |||||
| AND coalesce(ill.inQty, 0) <> coalesce(ill.outQty, 0) | |||||
| AND ill.deleted = false | AND ill.deleted = false | ||||
| ORDER BY il.expiryDate ASC | ORDER BY il.expiryDate ASC | ||||
| """) | """) | ||||
| @@ -83,19 +83,34 @@ open class InventoryLotLineService( | |||||
| return RecordsRes<InventoryLotLineInfo>(records, total.toInt()); | return RecordsRes<InventoryLotLineInfo>(records, total.toInt()); | ||||
| } | } | ||||
| /** | |||||
| * Same rules as [saveInventoryLotLine]: only stay AVAILABLE if previously AVAILABLE and remaining > 0. | |||||
| */ | |||||
| open fun deriveInventoryLotLineStatus( | |||||
| previousStatus: InventoryLotLineStatus?, | |||||
| inQty: BigDecimal?, | |||||
| outQty: BigDecimal?, | |||||
| holdQty: BigDecimal? | |||||
| ): InventoryLotLineStatus { | |||||
| val remainingQty = | |||||
| (inQty ?: BigDecimal.ZERO) - (outQty ?: BigDecimal.ZERO) - (holdQty ?: BigDecimal.ZERO) | |||||
| val status = previousStatus | |||||
| val qtyStatus = | |||||
| if (remainingQty > BigDecimal.ZERO) InventoryLotLineStatus.AVAILABLE else InventoryLotLineStatus.UNAVAILABLE | |||||
| return when { | |||||
| status == InventoryLotLineStatus.AVAILABLE && qtyStatus == InventoryLotLineStatus.AVAILABLE -> | |||||
| InventoryLotLineStatus.AVAILABLE | |||||
| else -> InventoryLotLineStatus.UNAVAILABLE | |||||
| } | |||||
| } | |||||
| open fun saveInventoryLotLine(request: SaveInventoryLotLineRequest): InventoryLotLine { | open fun saveInventoryLotLine(request: SaveInventoryLotLineRequest): InventoryLotLine { | ||||
| val inventoryLotLine = | val inventoryLotLine = | ||||
| request.id?.let { inventoryLotLineRepository.findById(it).getOrNull() } ?: InventoryLotLine() | request.id?.let { inventoryLotLineRepository.findById(it).getOrNull() } ?: InventoryLotLine() | ||||
| val inventoryLot = request.inventoryLotId?.let { inventoryLotRepository.findById(it).getOrNull() } | val inventoryLot = request.inventoryLotId?.let { inventoryLotRepository.findById(it).getOrNull() } | ||||
| val warehouse = request.warehouseId?.let { warehouseRepository.findById(it).getOrNull() } | val warehouse = request.warehouseId?.let { warehouseRepository.findById(it).getOrNull() } | ||||
| val stockUom = request.stockUomId?.let { itemUomRespository.findById(it).getOrNull() } | val stockUom = request.stockUomId?.let { itemUomRespository.findById(it).getOrNull() } | ||||
| val remainingQty = | |||||
| (request.inQty ?: BigDecimal(0)) - (request.outQty ?: BigDecimal(0)) - (request.holdQty ?: BigDecimal(0)) | |||||
| val status = request.status?.let { _status -> InventoryLotLineStatus.entries.find { it.value == _status } } | val status = request.status?.let { _status -> InventoryLotLineStatus.entries.find { it.value == _status } } | ||||
| val qtyStatus = when (remainingQty > BigDecimal(0)) { | |||||
| true -> InventoryLotLineStatus.AVAILABLE | |||||
| else -> InventoryLotLineStatus.UNAVAILABLE | |||||
| } | |||||
| println("status: ${request.status}") | println("status: ${request.status}") | ||||
| println("status123: ${status?.value}") | println("status123: ${status?.value}") | ||||
| @@ -107,11 +122,7 @@ open class InventoryLotLineService( | |||||
| outQty = request.outQty | outQty = request.outQty | ||||
| holdQty = request.holdQty | holdQty = request.holdQty | ||||
| this.stockUom = stockUom | this.stockUom = stockUom | ||||
| this.status = | |||||
| when (status == InventoryLotLineStatus.AVAILABLE && qtyStatus == InventoryLotLineStatus.AVAILABLE) { | |||||
| true -> InventoryLotLineStatus.AVAILABLE | |||||
| else -> InventoryLotLineStatus.UNAVAILABLE | |||||
| } | |||||
| this.status = deriveInventoryLotLineStatus(status, request.inQty, request.outQty, request.holdQty) | |||||
| remarks = request.remarks | remarks = request.remarks | ||||
| } | } | ||||
| @@ -1012,6 +1012,17 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta | |||||
| errorPosition = null | errorPosition = null | ||||
| ) | ) | ||||
| } | } | ||||
| if (resolved.status == InventoryLotLineStatus.UNAVAILABLE) { | |||||
| println(" Reject noLot bind: resolved InventoryLotLine id=${resolved.id} is UNAVAILABLE") | |||||
| return MessageResponse( | |||||
| id = null, | |||||
| name = "Lot line unavailable", | |||||
| code = "LOT_UNAVAILABLE", | |||||
| type = "error", | |||||
| message = "Cannot confirm scan: target inventory lot line is unavailable", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| // Bind the lot line to this stockOutLine so subsequent operations can proceed | // Bind the lot line to this stockOutLine so subsequent operations can proceed | ||||
| stockOutLine.inventoryLotLine = resolved | stockOutLine.inventoryLotLine = resolved | ||||
| stockOutLine.item = stockOutLine.item ?: resolved.inventoryLot?.item | stockOutLine.item = stockOutLine.item ?: resolved.inventoryLot?.item | ||||
| @@ -1487,19 +1498,29 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { | |||||
| open fun createStockOut(request: StockOutRequest): StockOutLine { | open fun createStockOut(request: StockOutRequest): StockOutLine { | ||||
| val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() | val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() | ||||
| val qtyBd = BigDecimal.valueOf(request.qty) | |||||
| val oldHold = inventoryLotLine.holdQty ?: BigDecimal.ZERO | |||||
| // Step 1: Increase outQty in inventory_lot_line table | |||||
| // Step 1: Increase outQty in inventory_lot_line table and release hold (same idea as pick / QR scan) | |||||
| val updatedInventoryLotLine = inventoryLotLine.apply { | val updatedInventoryLotLine = inventoryLotLine.apply { | ||||
| val currentOutQty = this.outQty ?: BigDecimal.ZERO | val currentOutQty = this.outQty ?: BigDecimal.ZERO | ||||
| val newOutQty = currentOutQty + BigDecimal.valueOf(request.qty) | |||||
| val newOutQty = currentOutQty + qtyBd | |||||
| this.outQty = newOutQty | this.outQty = newOutQty | ||||
| val currentInQty = this.inQty ?: BigDecimal.ZERO | |||||
| if (newOutQty.compareTo(currentInQty) == 0) { | |||||
| this.status = InventoryLotLineStatus.UNAVAILABLE | |||||
| } | |||||
| val newHold = oldHold.subtract(qtyBd).coerceAtLeast(BigDecimal.ZERO) | |||||
| this.holdQty = newHold | |||||
| this.status = inventoryLotLineService.deriveInventoryLotLineStatus( | |||||
| this.status, | |||||
| this.inQty, | |||||
| this.outQty, | |||||
| this.holdQty | |||||
| ) | |||||
| } | } | ||||
| inventoryLotLineRepository.save(updatedInventoryLotLine) | inventoryLotLineRepository.save(updatedInventoryLotLine) | ||||
| // inventory.onHandQty / onHoldQty / unavailableQty: 由 DB trigger `inventory_lot_line_AFTER_UPDATE` | |||||
| // 依 old/new 的 in、out、hold、status 同步;此處勿再手動改 inventory.onHoldQty,否則與 trigger 重複扣會變負數。 | |||||
| val itemId = updatedInventoryLotLine.inventoryLot?.item?.id | |||||
| ?: throw IllegalArgumentException("InventoryLotLine must have an associated item") | |||||
| // Step 2: Create a row of stock_out | // Step 2: Create a row of stock_out | ||||
| val currentUser = SecurityUtils.getUser().orElseThrow() | val currentUser = SecurityUtils.getUser().orElseThrow() | ||||
| @@ -1512,8 +1533,6 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { | |||||
| val savedStockOut = stockOutRepository.save(stockOut) | val savedStockOut = stockOutRepository.save(stockOut) | ||||
| // Step 3: Create a row in stock_out_line table | // Step 3: Create a row in stock_out_line table | ||||
| val itemId = updatedInventoryLotLine.inventoryLot?.item?.id | |||||
| ?: throw IllegalArgumentException("InventoryLotLine must have an associated item") | |||||
| val item = itemRepository.findById(itemId).orElseThrow() | val item = itemRepository.findById(itemId).orElseThrow() | ||||
| val stockOutLine = StockOutLine().apply { | val stockOutLine = StockOutLine().apply { | ||||
| @@ -1917,10 +1936,17 @@ fun applyStockOutLineDelta( | |||||
| if (isIssuePosting) { | if (isIssuePosting) { | ||||
| val latestLotLine = inventoryLotLineRepository.findById(lotLine.id!!).orElse(null) | val latestLotLine = inventoryLotLineRepository.findById(lotLine.id!!).orElse(null) | ||||
| if (latestLotLine != null) { | if (latestLotLine != null) { | ||||
| val prevStatus = latestLotLine.status | |||||
| val currentHoldQty = latestLotLine.holdQty ?: BigDecimal.ZERO | val currentHoldQty = latestLotLine.holdQty ?: BigDecimal.ZERO | ||||
| val currentOutQty = latestLotLine.outQty ?: BigDecimal.ZERO | val currentOutQty = latestLotLine.outQty ?: BigDecimal.ZERO | ||||
| latestLotLine.holdQty = currentHoldQty.subtract(deltaQty).coerceAtLeast(BigDecimal.ZERO) | latestLotLine.holdQty = currentHoldQty.subtract(deltaQty).coerceAtLeast(BigDecimal.ZERO) | ||||
| latestLotLine.outQty = currentOutQty.add(deltaQty) | latestLotLine.outQty = currentOutQty.add(deltaQty) | ||||
| latestLotLine.status = inventoryLotLineService.deriveInventoryLotLineStatus( | |||||
| prevStatus, | |||||
| latestLotLine.inQty, | |||||
| latestLotLine.outQty, | |||||
| latestLotLine.holdQty | |||||
| ) | |||||
| latestLotLine.modified = eventTime | latestLotLine.modified = eventTime | ||||
| if (!operator.isNullOrBlank()) { | if (!operator.isNullOrBlank()) { | ||||
| latestLotLine.modifiedBy = operator | latestLotLine.modifiedBy = operator | ||||