| @@ -1536,7 +1536,7 @@ open class DeliveryOrderService( | |||
| this.type = "do" | |||
| this.consoPickOrderCode = consoCode | |||
| this.status = StockOutStatus.PENDING.status | |||
| this.handler = request.userId | |||
| //this.handler = request.userId | |||
| } | |||
| val savedStockOut = stockOutRepository.saveAndFlush(stockOut) | |||
| @@ -457,8 +457,8 @@ open class JoPickOrderService( | |||
| val lotAvailability = when { | |||
| il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | |||
| sol?.status == "rejected" -> "rejected" | |||
| availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock" | |||
| ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable" | |||
| availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock" | |||
| else -> "available" | |||
| } | |||
| @@ -685,8 +685,8 @@ open class JoPickOrderService( | |||
| val lotAvailability = when { | |||
| il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | |||
| sol?.status == "rejected" -> "rejected" | |||
| availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock" | |||
| ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable" | |||
| availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock" | |||
| else -> "available" | |||
| } | |||
| @@ -2175,8 +2175,8 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo | |||
| val lotAvailability = when { | |||
| il.expiryDate != null && il.expiryDate!!.isBefore(LocalDate.now()) -> "expired" | |||
| sol?.status == "rejected" -> "rejected" | |||
| availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock" | |||
| ill.status == InventoryLotLineStatus.UNAVAILABLE -> "status_unavailable" | |||
| availableQty != null && availableQty <= BigDecimal.ZERO -> "insufficient_stock" | |||
| else -> "available" | |||
| } | |||
| @@ -920,8 +920,8 @@ open class PickOrderService( | |||
| val lotAvailability = when { | |||
| isExpired -> "expired" | |||
| sol?.status == "rejected" -> "rejected" | |||
| availableQty <= zero -> "insufficient_stock" | |||
| ill.status?.value == "unavailable" -> "status_unavailable" | |||
| availableQty <= zero -> "insufficient_stock" | |||
| 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", | |||
| 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 | |||
| val newItemId = newIll.inventoryLot?.item?.id | |||
| @@ -4578,8 +4589,8 @@ println("DEBUG sol polIds in linesResults: " + linesResults.mapNotNull { it["sto | |||
| lotAvailability = when { | |||
| isExpired -> "expired" | |||
| stockOutLine?.status == "rejected" -> "rejected" | |||
| availableQty <= zero -> "insufficient_stock" | |||
| illEntity.status?.value == "unavailable" -> "status_unavailable" | |||
| availableQty <= zero -> "insufficient_stock" | |||
| else -> "available" | |||
| }, | |||
| processingStatus = when { | |||
| @@ -137,7 +137,7 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long | |||
| SELECT ill FROM InventoryLotLine ill | |||
| JOIN ill.inventoryLot il | |||
| WHERE il.expiryDate < :today | |||
| AND ill.inQty != ill.outQty | |||
| AND coalesce(ill.inQty, 0) <> coalesce(ill.outQty, 0) | |||
| AND ill.deleted = false | |||
| ORDER BY il.expiryDate ASC | |||
| """) | |||
| @@ -83,19 +83,34 @@ open class InventoryLotLineService( | |||
| 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 { | |||
| val inventoryLotLine = | |||
| request.id?.let { inventoryLotLineRepository.findById(it).getOrNull() } ?: InventoryLotLine() | |||
| val inventoryLot = request.inventoryLotId?.let { inventoryLotRepository.findById(it).getOrNull() } | |||
| val warehouse = request.warehouseId?.let { warehouseRepository.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 qtyStatus = when (remainingQty > BigDecimal(0)) { | |||
| true -> InventoryLotLineStatus.AVAILABLE | |||
| else -> InventoryLotLineStatus.UNAVAILABLE | |||
| } | |||
| println("status: ${request.status}") | |||
| println("status123: ${status?.value}") | |||
| @@ -107,11 +122,7 @@ open class InventoryLotLineService( | |||
| outQty = request.outQty | |||
| holdQty = request.holdQty | |||
| 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 | |||
| } | |||
| @@ -1012,6 +1012,17 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta | |||
| 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 | |||
| stockOutLine.inventoryLotLine = resolved | |||
| stockOutLine.item = stockOutLine.item ?: resolved.inventoryLot?.item | |||
| @@ -1487,19 +1498,29 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { | |||
| open fun createStockOut(request: StockOutRequest): StockOutLine { | |||
| 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 currentOutQty = this.outQty ?: BigDecimal.ZERO | |||
| val newOutQty = currentOutQty + BigDecimal.valueOf(request.qty) | |||
| val newOutQty = currentOutQty + qtyBd | |||
| 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) | |||
| // 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 | |||
| val currentUser = SecurityUtils.getUser().orElseThrow() | |||
| @@ -1512,8 +1533,6 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { | |||
| val savedStockOut = stockOutRepository.save(stockOut) | |||
| // 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 stockOutLine = StockOutLine().apply { | |||
| @@ -1917,10 +1936,17 @@ fun applyStockOutLineDelta( | |||
| if (isIssuePosting) { | |||
| val latestLotLine = inventoryLotLineRepository.findById(lotLine.id!!).orElse(null) | |||
| if (latestLotLine != null) { | |||
| val prevStatus = latestLotLine.status | |||
| val currentHoldQty = latestLotLine.holdQty ?: BigDecimal.ZERO | |||
| val currentOutQty = latestLotLine.outQty ?: BigDecimal.ZERO | |||
| latestLotLine.holdQty = currentHoldQty.subtract(deltaQty).coerceAtLeast(BigDecimal.ZERO) | |||
| latestLotLine.outQty = currentOutQty.add(deltaQty) | |||
| latestLotLine.status = inventoryLotLineService.deriveInventoryLotLineStatus( | |||
| prevStatus, | |||
| latestLotLine.inQty, | |||
| latestLotLine.outQty, | |||
| latestLotLine.holdQty | |||
| ) | |||
| latestLotLine.modified = eventTime | |||
| if (!operator.isNullOrBlank()) { | |||
| latestLotLine.modifiedBy = operator | |||