diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt index a5e7efc..fc13ca5 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt @@ -842,7 +842,7 @@ open class DeliveryOrderService( this.item = pickOrderLine.item this.status = StockOutLineStatus.PENDING.status this.qty = 0.0 - this.type = "Nor" + this.type = "NOR" } stockOutLineRepository.save(line) } @@ -1524,7 +1524,7 @@ open class DeliveryOrderService( StockOutLineStatus.PENDING.status // 有正常库存批次时使用 PENDING } this.qty = 0.0 - this.type = "Nor" + this.type = "NOR" } stockOutLineRepository.save(line) } diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt index 8bc7d13..b2a659c 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt @@ -159,7 +159,12 @@ interface JobOrderRepository : AbstractRepository { SELECT jo FROM JobOrder jo WHERE jo.deleted = false AND (:code IS NULL OR jo.code LIKE CONCAT('%', :code, '%')) - AND (:bomName IS NULL OR jo.bom.name LIKE CONCAT('%', :bomName, '%')) + AND ( + :bomName IS NULL + OR jo.bom.name LIKE CONCAT('%', :bomName, '%') + OR jo.bom.item.code LIKE CONCAT('%', :bomName, '%') + OR jo.bom.item.name LIKE CONCAT('%', :bomName, '%') + ) AND (:planStartFrom IS NULL OR jo.planStart >= :planStartFrom) AND (:planStartTo IS NULL OR jo.planStart <= :planStartTo) AND ( diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt index 38297db..fbbc950 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt @@ -673,7 +673,7 @@ open class JobOrderService( this.item = pickOrderLine.item this.status = StockOutLineStatus.PENDING.status this.qty = 0.0 - this.type = "Nor" + this.type = "NOR" } stockOutLineRepository.save(line) } diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt index a24e540..b5061d7 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt @@ -203,6 +203,48 @@ open class ItemUomService( return stockQty.setScale(0, RoundingMode.DOWN) } + /** + * Convert purchase qty -> stock qty and keep decimal precision. + * Used for PO flows where decimal quantity must be preserved. + */ + open fun convertPurchaseQtyToStockQtyPrecise(itemId: Long, purchaseQty: BigDecimal): BigDecimal { + val purchaseUnit = findPurchaseUnitByItemId(itemId) ?: return purchaseQty + val stockUnit = findStockUnitByItemId(itemId) ?: return purchaseQty + val one = BigDecimal.ONE + val calcScale = 10 + + val baseQty = purchaseQty + .multiply(purchaseUnit.ratioN ?: one) + .divide(purchaseUnit.ratioD ?: one, calcScale, RoundingMode.HALF_UP) + + val stockQty = baseQty + .multiply(stockUnit.ratioD ?: one) + .divide(stockUnit.ratioN ?: one, calcScale, RoundingMode.HALF_UP) + + return stockQty.setScale(2, RoundingMode.HALF_UP) + } + + /** + * Convert qty from a specific UOM -> stock qty and keep decimal precision. + * Used for PO (M18 UOM) conversion where round-down is not allowed. + */ + open fun convertQtyToStockQtyPrecise(itemId: Long, uomId: Long, sourceQty: BigDecimal): BigDecimal { + val itemUom = findFirstByItemIdAndUomId(itemId, uomId) ?: return sourceQty + val stockUnit = findStockUnitByItemId(itemId) ?: return BigDecimal.ZERO + val one = BigDecimal.ONE + val calcScale = 10 + + val baseQty = sourceQty + .multiply(itemUom.ratioN ?: one) + .divide(itemUom.ratioD ?: one, calcScale, RoundingMode.HALF_UP) + + val stockQty = baseQty + .multiply(stockUnit.ratioD ?: one) + .divide(stockUnit.ratioN ?: one, calcScale, RoundingMode.HALF_UP) + + return stockQty.setScale(2, RoundingMode.HALF_UP) + } + // See if need to update the response open fun saveItemUom(request: ItemUomRequest): ItemUom { val itemUom = request.m18Id?.let { itemUomRespository.findFirstByM18IdOrderByIdDesc(it) } 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 bffac12..c99a01f 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 @@ -1150,7 +1150,7 @@ open class PickOrderService( this.status = com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus.PENDING.status this.qty = 0.0 - this.type = "Nor" + this.type = "NOR" } stockOutLIneRepository.save(line) precreated++ @@ -2110,7 +2110,7 @@ open class PickOrderService( this.status = com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus.PENDING.status this.qty = 0.0 - this.type = "Nor" + this.type = "NOR" } stockOutLIneRepository.save(line) precreated++ diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt index 6eb7000..fffbf9d 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt @@ -269,7 +269,7 @@ open fun getPoSummariesByIds(ids: List): List { val qtyM18 = thisPol.qtyM18 val uomM18Id = thisPol.uomM18?.id val stockQtyFromM18 = if (itemId != null && qtyM18 != null && uomM18Id != null) { - itemUomService.convertQtyToStockQtyRoundDown(itemId, uomM18Id, qtyM18) + itemUomService.convertQtyToStockQtyPrecise(itemId, uomM18Id, qtyM18) } else { BigDecimal.ZERO } @@ -279,7 +279,7 @@ open fun getPoSummariesByIds(ids: List): List { stockUomDesc = iu?.uom?.udfudesc, stockQty = if (stockQtyFromM18 > BigDecimal.ZERO) stockQtyFromM18 else { // fallback to legacy behavior when M18 fields are missing - iu?.item?.id?.let { iId -> itemUomService.convertPurchaseQtyToStockQty(iId, (thisPol.qty ?: BigDecimal.ZERO)) } ?: BigDecimal.ZERO + iu?.item?.id?.let { iId -> itemUomService.convertPurchaseQtyToStockQtyPrecise(iId, (thisPol.qty ?: BigDecimal.ZERO)) } ?: BigDecimal.ZERO }, stockRatioN = iu?.ratioN, stockRatioD = iu?.ratioD, diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLine.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLine.kt index dfaf039..12c9030 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLine.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLine.kt @@ -63,7 +63,7 @@ open class StockInLine : BaseEntity() { open var acceptedQty: BigDecimal? = null @Column(name = "acceptedQtyM18") - open var acceptedQtyM18: Int? = null + open var acceptedQtyM18: BigDecimal? = null @Column(name = "price", precision = 14, scale = 2) open var price: BigDecimal? = null @@ -134,6 +134,6 @@ open class StockInLine : BaseEntity() { */ fun getReceivedQtyM18ForPol(): BigDecimal? = purchaseOrderLine?.stockInLines?.sumOf { sil -> - sil.acceptedQtyM18?.let { BigDecimal.valueOf(it.toLong()) } ?: BigDecimal.ZERO + sil.acceptedQtyM18 ?: BigDecimal.ZERO } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/StockInLineInfo.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/StockInLineInfo.kt index b38210b..ed4fb6c 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/StockInLineInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/StockInLineInfo.kt @@ -28,7 +28,7 @@ interface StockInLineInfo { val receivedQty: BigDecimal? val demandQty: BigDecimal? val acceptedQty: BigDecimal - @get:Value("#{target.acceptedQtyM18 != null ? new java.math.BigDecimal(target.acceptedQtyM18) : null}") + @get:Value("#{target.acceptedQtyM18}") val purchaseAcceptedQty: BigDecimal? @get:Value("#{target.purchaseOrderLine?.qtyM18}") val qty: BigDecimal? diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt index 84ff44c..407c6a5 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt @@ -352,67 +352,5 @@ open class InventoryService( } -// @Throws(IOException::class) -// open fun updateInventory(request: SaveInventoryRequest): MessageResponse { -// // out need id -// // in not necessary -// var reqQty = request.qty -// if (request.type === "out") reqQty *= -1 -// if (request.id !== null) { // old record -// val inventory = inventoryRepository.findById(request.id).orElseThrow() -// val newStatus = request.status ?: inventory.status -// val newExpiry = request.expiryDate ?: inventory.expiryDate -// // uom should not be changing -// // stock in line should not be changing -// // item id should not be changing -// inventory.apply { -// qty = inventory.qty!! + reqQty -// expiryDate = newExpiry -// status = newStatus -// } -// val savedInventory = inventoryRepository.saveAndFlush(inventory) -// return MessageResponse( -// id = savedInventory.id, -// code = savedInventory.lotNo, -// name = "savedInventory.item!!.name", -// type = savedInventory.status, -// message = "update success", -// errorPosition = null -// ) -// } else { // new record -// val inventory = Inventory() -// val item = itemsRepository.findById(request.itemId).orElseThrow() -// val from = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE) -// val to = LocalDate.now().plusDays(1).format(DateTimeFormatter.ISO_LOCAL_DATE) -//// val stockInLine = .... -// val args = mapOf( -// "from" to from, -// "to" to to, -// "itemId" to item.id -// ) -// val prefix = "LOT" -// val count = jdbcDao.queryForInt(INVENTORY_COUNT.toString(), args) -// val newLotNo = CodeGenerator.generateCode(prefix, item.id!!, count) -// val newExpiry = request.expiryDate -// inventory.apply { -//// this.stockInLine = stockInline -//// this.item = item -// stockInLine = 0 -// qty = reqQty -// lotNo = newLotNo -// expiryDate = newExpiry -// uomId = 0 -// // status default "pending" in db -// } -// val savedInventory = inventoryRepository.saveAndFlush(inventory) -// return MessageResponse( -// id = savedInventory.id, -// code = savedInventory.lotNo, -// name = "savedInventory.item!!.name", -// type = savedInventory.status, -// message = "save success", -// errorPosition = null -// ) -// } -// } + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt index dbca9ba..2bc46dd 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt @@ -254,20 +254,20 @@ open class StockInLineService( itemNo = item.code this.stockIn = stockIn // PO-origin: - // 1) store user-input qty in acceptedQtyM18 (M18 unit) - // 2) calculate stock acceptedQty by converting from M18 unit + // 1) keep user-input qty in acceptedQtyM18 (M18 unit, decimal allowed) + // 2) calculate stock acceptedQty by converting from M18 unit without round-down if (pol != null && item.id != null) { - acceptedQtyM18 = request.acceptedQty.toInt() + acceptedQtyM18 = request.acceptedQty val m18UomId = pol.uomM18?.id acceptedQty = if (m18UomId != null) { - itemUomService.convertQtyToStockQtyRoundDown( + itemUomService.convertQtyToStockQtyPrecise( item.id!!, m18UomId, request.acceptedQty ) } else { // fallback to legacy: treat request.acceptedQty as purchase unit qty - itemUomService.convertPurchaseQtyToStockQtyRoundDown( + itemUomService.convertPurchaseQtyToStockQtyPrecise( item.id!!, request.acceptedQty ) @@ -285,7 +285,7 @@ open class StockInLineService( val m18UomId = pol.uomM18?.id val qtyM18 = pol.qtyM18 this.demandQty = if (m18UomId != null && qtyM18 != null) { - itemUomService.convertQtyToStockQtyRoundDown( + itemUomService.convertQtyToStockQtyPrecise( item.id!!, m18UomId, qtyM18 @@ -293,14 +293,14 @@ open class StockInLineService( } else { // fallback to legacy: treat pol.qty as purchase unit qty pol.qty?.let { polQty -> - itemUomService.convertPurchaseQtyToStockQty(item.id!!, polQty) + itemUomService.convertPurchaseQtyToStockQtyPrecise(item.id!!, polQty) } } } dnNo = request.dnNo receiptDate = request.receiptDate?.atStartOfDay() ?: LocalDateTime.now() productLotNo = request.productLotNo - this.type = "Nor" + this.type = "NOR" status = StockInLineStatus.PENDING.status } if (jo != null) { @@ -598,7 +598,7 @@ open class StockInLineService( val pol = sil.purchaseOrderLine!! // For PO-origin GRN, M18 ant qty must use the M18 UOM and received M18 qty. val totalQtyM18 = silList.sumOf { - it.acceptedQtyM18?.let { qty -> BigDecimal.valueOf(qty.toLong()) } ?: BigDecimal.ZERO + it.acceptedQtyM18 ?: BigDecimal.ZERO } val unitIdFromDataLog = (pol.m18DataLog?.dataLog?.get("unitId") as? Number)?.toLong()?.toInt() val itemName = (sil.item?.name ?: pol.item?.name).orEmpty() // always non-null for M18 bDesc/bDesc_en @@ -790,7 +790,7 @@ open class StockInLineService( if (request.qcAccept != true && request.status != StockInLineStatus.RECEIVED.status) { val requestQty = request.acceptQty ?: request.acceptedQty this.acceptedQty = if (this.purchaseOrderLine != null && this.item?.id != null && requestQty != null) { - itemUomService.convertPurchaseQtyToStockQty(this.item!!.id!!, requestQty) + itemUomService.convertPurchaseQtyToStockQtyPrecise(this.item!!.id!!, requestQty) } else { requestQty ?: this.acceptedQty } @@ -804,7 +804,7 @@ open class StockInLineService( val requestQty = request.acceptQty ?: request.acceptedQty if (requestQty != null) { this.acceptedQty = if (this.purchaseOrderLine != null && this.item?.id != null) { - itemUomService.convertPurchaseQtyToStockQty(this.item!!.id!!, requestQty) + itemUomService.convertPurchaseQtyToStockQtyPrecise(this.item!!.id!!, requestQty) } else { requestQty } @@ -820,10 +820,10 @@ open class StockInLineService( val m18UomId = pol.uomM18?.id val qtyM18 = pol.qtyM18 this.demandQty = if (m18UomId != null && qtyM18 != null) { - itemUomService.convertQtyToStockQtyRoundDown(itemId, m18UomId, qtyM18) + itemUomService.convertQtyToStockQtyPrecise(itemId, m18UomId, qtyM18) } else if (pol.qty != null) { // fallback to legacy fields when M18 fields are missing - itemUomService.convertPurchaseQtyToStockQty(itemId, pol.qty!!) + itemUomService.convertPurchaseQtyToStockQtyPrecise(itemId, pol.qty!!) } else { this.demandQty } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt index 8c76b89..bf2264e 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt @@ -45,6 +45,7 @@ import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository import java.time.LocalTime import com.ffii.fpsms.modules.stock.entity.StockInLineRepository import com.ffii.fpsms.modules.stock.entity.SuggestPickLotRepository +import java.util.UUID @Service open class StockOutLineService( private val jdbcDao: JdbcDao, @@ -184,7 +185,7 @@ val existingStockOutLine = stockOutLineRepository.findByPickOrderLineIdAndInvent this.inventoryLotLine = inventoryLotLine this.pickOrderLine = pickOrderLine this.status = StockOutLineStatus.PENDING.status - this.type = "Nor" + this.type = "NOR" } val savedStockOutLine = saveAndFlush(stockOutLine) createStockLedgerForStockOut(savedStockOutLine) @@ -283,7 +284,7 @@ open fun createWithoutConso(request: CreateStockOutLineWithoutConsoRequest): Mes this.inventoryLotLine = inventoryLotLine this.pickOrderLine = updatedPickOrderLine this.status = StockOutLineStatus.PENDING.status - this.type = "Nor" + this.type = "NOR" } println("Created stockOutLine with qty: ${request.qty}") @@ -1167,7 +1168,9 @@ open fun updateStockOutLineStatusByQRCodeAndLotNo(request: UpdateStockOutLineSta @Transactional(rollbackFor = [Exception::class]) open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { val startTime = System.currentTimeMillis() + val traceId = "BATCH-${UUID.randomUUID().toString().substring(0, 8)}" println("=== BATCH SUBMIT START ===") + println("[$traceId] Batch submit request received") println("Start time: ${java.time.LocalDateTime.now()}") println("Request lines count: ${request.lines.size}") @@ -1208,8 +1211,9 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { // 3) Process each request line request.lines.forEach { line: QrPickSubmitLineRequest -> + val lineTrace = "$traceId|SOL=${line.stockOutLineId}" try { - println("Processing line: stockOutLineId=${line.stockOutLineId}, noLot=${line.noLot}") + println("[$lineTrace] Processing line, noLot=${line.noLot}") if (line.noLot) { // noLot branch @@ -1219,7 +1223,7 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { qty = 0.0 )) processedIds += line.stockOutLineId - println(" ✓ noLot item processed") + println("[$lineTrace] noLot processed (status->completed, qty=0)") return@forEach } @@ -1228,7 +1232,7 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { ?: throw IllegalStateException("StockOutLine ${line.stockOutLineId} not found") val currentStatus = stockOutLine.status?.trim()?.lowercase() if (currentStatus == "completed" || currentStatus == "complete") { - println(" Skipping already completed stockOutLineId=${line.stockOutLineId}") + println("[$lineTrace] Skip because current status is already completed") return@forEach } @@ -1236,19 +1240,19 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { val targetActual = line.actualPickQty ?: BigDecimal.ZERO val required = line.requiredQty ?: BigDecimal.ZERO - println(" Current qty: $currentActual, Target qty: $targetActual, Required: $required") + println("[$lineTrace] currentActual=$currentActual, targetActual=$targetActual, required=$required") // 计算增量(前端发送的是目标累计值) val submitQty = targetActual - currentActual - println(" Submit qty (increment): $submitQty") + println("[$lineTrace] submitQty(increment)=$submitQty") // 使用前端发送的状态,否则根据数量自动判断 val newStatus = line.stockOutLineStatus ?: if (targetActual >= required) "completed" else "partially_completed" if (submitQty <= BigDecimal.ZERO) { - println(" Submit qty <= 0, only update status for stockOutLineId=${line.stockOutLineId}") + println("[$lineTrace] submitQty<=0, only update status, skip inventory+ledger") updateStatus( UpdateStockOutLineStatusRequest( @@ -1274,6 +1278,7 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { skipInventoryWrite = true ) ) + println("[$lineTrace] stock_out_line qty/status updated with delta=$submitQty (inventory+ledger deferred)") // Inventory updates - 修复:使用增量数量 // ✅ 修复:如果 inventoryLotLineId 为 null,从 stock_out_line 中获取 @@ -1282,7 +1287,7 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { // 在 newBatchSubmit 方法中,修改这部分代码(大约在 1169-1185 行) if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { - println(" Updating inventory lot line ${actualInventoryLotLineId} with qty $submitQty") + println("[$lineTrace] Updating inventory lot line $actualInventoryLotLineId with qty=$submitQty") // ✅ 修复:在更新 inventory_lot_line 之前获取 inventory 的当前 onHandQty val item = stockOutLine.item @@ -1291,7 +1296,7 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { } val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() - println(" Inventory before update: onHandQty=$onHandQtyBeforeUpdate") + println("[$lineTrace] Inventory before update: onHandQty=$onHandQtyBeforeUpdate") inventoryLotLineService.updateInventoryLotLineQuantities( UpdateInventoryLotLineQuantitiesRequest( @@ -1303,7 +1308,7 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { if (submitQty > BigDecimal.ZERO) { // ✅ 修复:传入更新前的 onHandQty,让 createStockLedgerForPickDelta 使用它 - createStockLedgerForPickDelta(line.stockOutLineId, submitQty, onHandQtyBeforeUpdate) + createStockLedgerForPickDelta(line.stockOutLineId, submitQty, onHandQtyBeforeUpdate, lineTrace) } } else if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId == null) { // ✅ 修复:即使没有 inventoryLotLineId,也应该获取 inventory.onHandQty @@ -1313,8 +1318,8 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { } val onHandQtyBeforeUpdate = (inventoryBeforeUpdate?.onHandQty ?: BigDecimal.ZERO).toDouble() - println(" Warning: No inventoryLotLineId found, but creating stock ledger anyway for stockOutLineId ${line.stockOutLineId}") - createStockLedgerForPickDelta(line.stockOutLineId, submitQty, onHandQtyBeforeUpdate) + println("[$lineTrace] Warning: No inventoryLotLineId, still trying ledger creation") + createStockLedgerForPickDelta(line.stockOutLineId, submitQty, onHandQtyBeforeUpdate, lineTrace) } try { val stockOutLine = stockOutLines[line.stockOutLineId] @@ -1356,9 +1361,9 @@ if (submitQty > BigDecimal.ZERO && actualInventoryLotLineId != null) { // 不中断主流程,只记录错误 } processedIds += line.stockOutLineId - println(" ✓ Line processed successfully") + println("[$lineTrace] Line processed successfully") } catch (e: Exception) { - println(" ✗ Error processing line ${line.stockOutLineId}: ${e.message}") + println("[$lineTrace] Error processing line: ${e.message}") e.printStackTrace() errors += "stockOutLineId=${line.stockOutLineId}: ${e.message}" } @@ -1534,31 +1539,44 @@ affectedConsoCodes.forEach { consoCode -> private fun createStockLedgerForPickDelta( stockOutLineId: Long, deltaQty: BigDecimal, - onHandQtyBeforeUpdate: Double? = null // ✅ 新增参数:更新前的 onHandQty + onHandQtyBeforeUpdate: Double? = null, // ✅ 新增参数:更新前的 onHandQty + traceTag: String? = null ) { - if (deltaQty <= BigDecimal.ZERO) return + val tracePrefix = traceTag?.let { "[$it] " } ?: "[SOL=$stockOutLineId] " + if (deltaQty <= BigDecimal.ZERO) { + println("${tracePrefix}Skip ledger creation: deltaQty <= 0 (deltaQty=$deltaQty)") + return + } - val sol = stockOutLineRepository.findById(stockOutLineId).orElse(null) ?: return - val item = sol.item ?: return + val sol = stockOutLineRepository.findById(stockOutLineId).orElse(null) + if (sol == null) { + println("${tracePrefix}Skip ledger creation: stockOutLine not found") + return + } + val item = sol.item + if (item == null) { + println("${tracePrefix}Skip ledger creation: stockOutLine.item is null") + return + } val inventory = inventoryRepository.findAllByItemIdAndDeletedIsFalse(item.id!!) - .firstOrNull() ?: return - - // ✅ 修复:如果传入了 onHandQtyBeforeUpdate,使用它;否则回退到原来的逻辑 - val previousBalance = if (onHandQtyBeforeUpdate != null) { - // 使用更新前的 onHandQty 作为基础 - onHandQtyBeforeUpdate - } else { - // 回退逻辑:优先使用最新的 ledger balance,如果没有则使用 inventory.onHandQty - val latestLedger = stockLedgerRepository.findLatestByItemId(item.id!!).firstOrNull() - latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() + .firstOrNull() + if (inventory == null) { + println("${tracePrefix}Skip ledger creation: inventory not found by itemId=${item.id}") + return } + + val previousBalance = resolvePreviousBalance( + itemId = item.id!!, + inventory = inventory, + onHandQtyBeforeUpdate = onHandQtyBeforeUpdate + ) val newBalance = previousBalance - deltaQty.toDouble() - println(" Creating stock ledger: previousBalance=$previousBalance, deltaQty=$deltaQty, newBalance=$newBalance") + println("${tracePrefix}Creating ledger: previousBalance=$previousBalance, deltaQty=$deltaQty, newBalance=$newBalance, inventoryId=${inventory.id}") if (onHandQtyBeforeUpdate != null) { - println(" Using onHandQtyBeforeUpdate: $onHandQtyBeforeUpdate") + println("${tracePrefix}Using onHandQtyBeforeUpdate: $onHandQtyBeforeUpdate") } val ledger = StockLedger().apply { @@ -1576,6 +1594,19 @@ affectedConsoCodes.forEach { consoCode -> } stockLedgerRepository.saveAndFlush(ledger) + println("${tracePrefix}Ledger created successfully for stockOutLineId=$stockOutLineId") + } + + private fun resolvePreviousBalance( + itemId: Long, + inventory: Inventory, + onHandQtyBeforeUpdate: Double? = null + ): Double { + if (onHandQtyBeforeUpdate != null) { + return onHandQtyBeforeUpdate + } + val latestLedger = stockLedgerRepository.findLatestByItemId(itemId).firstOrNull() + return latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() } @Transactional(rollbackFor = [Exception::class]) @@ -1769,7 +1800,7 @@ open fun batchScan(request: com.ffii.fpsms.modules.stock.web.model.BatchScanRequ this.inventoryLotLine = inventoryLotLine this.pickOrderLine = pickOrderLine this.status = StockOutLineStatus.CHECKED.status - this.type = "Nor" + this.type = "NOR" this.startTime = LocalDateTime.now() } @@ -1933,9 +1964,10 @@ fun applyStockOutLineDelta( val item = savedSol.item ?: return savedSol val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return savedSol - // unified balance source: latest ledger first, fallback inventory.onHand - val latestLedger = stockLedgerRepository.findLatestByItemId(item.id!!).firstOrNull() - val previousBalance = latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() + val previousBalance = resolvePreviousBalance( + itemId = item.id!!, + inventory = inventory + ) val outQty = deltaQty.toDouble() val newBalance = previousBalance - outQty diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt index 6a93518..e64f238 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt @@ -493,7 +493,7 @@ open class SuggestedPickLotService( this.qty = 0.0 this.status = StockOutLineStatus.PENDING.status this.deleted = false - this.type = "Nor" + this.type = "NOR" } val savedStockOutLine = stockOutLIneRepository.save(stockOutLine) @@ -543,7 +543,7 @@ open class SuggestedPickLotService( this.inventoryLotLine = suggestedLotLine this.pickOrderLine = updatedPickOrderLine this.status = StockOutLineStatus.PENDING.status - this.type = "Nor" + this.type = "NOR" } val savedStockOutLine = stockOutLIneRepository.saveAndFlush(stockOutLine) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveEscalationLogResponse.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveEscalationLogResponse.kt index b1d924c..7039276 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveEscalationLogResponse.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/SaveEscalationLogResponse.kt @@ -14,3 +14,16 @@ data class SaveEscalationLogResponse( val status: String?, val reason: String?, ) +enum class LedgerFailFactor { + INVALID_DELTA_QTY, + STOCK_OUT_LINE_NOT_FOUND, + ITEM_NOT_FOUND, + INVENTORY_NOT_FOUND, + LEDGER_SAVE_EXCEPTION, + UNKNOWN +} + data class LedgerCreateResult( + val success: Boolean, + val failFactor: LedgerFailFactor? = null, + val message: String? = null +) \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20260327_01_Enson/01_alter_stock_take.sql b/src/main/resources/db/changelog/changes/20260327_01_Enson/01_alter_stock_take.sql new file mode 100644 index 0000000..e8e14bb --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260327_01_Enson/01_alter_stock_take.sql @@ -0,0 +1,8 @@ +-- liquibase formatted sql +-- changeset Enson:alter_stock_in_line_acceptedQtyM18 + +ALTER TABLE `fpsmsdb`.`stock_in_line` +MODIFY COLUMN `acceptedQtyM18` DECIMAL(10,2); + + +