| @@ -166,7 +166,12 @@ open class JobOrderService( | |||||
| val inventoriesMap = if (allItemIds.isNotEmpty()) { | val inventoriesMap = if (allItemIds.isNotEmpty()) { | ||||
| inventoryRepository.findInventoryInfoByItemIdInAndDeletedIsFalse(allItemIds) | inventoryRepository.findInventoryInfoByItemIdInAndDeletedIsFalse(allItemIds) | ||||
| .associateBy { it.itemId } | |||||
| .mapNotNull { info -> | |||||
| val itemId = info.itemId ?: return@mapNotNull null | |||||
| val stockUomId = info.stockUomId ?: return@mapNotNull null | |||||
| (itemId to stockUomId) to info | |||||
| } | |||||
| .toMap() | |||||
| } else { | } else { | ||||
| emptyMap() | emptyMap() | ||||
| } | } | ||||
| @@ -228,7 +233,7 @@ open class JobOrderService( | |||||
| private fun calculateStockCounts( | private fun calculateStockCounts( | ||||
| jobOrder: JobOrder, | jobOrder: JobOrder, | ||||
| inventoriesMap: Map<Long?, com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo> | |||||
| inventoriesMap: Map<Pair<Long, Long>, com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo> | |||||
| ): Pair<Int, Int> { | ): Pair<Int, Int> { | ||||
| // 过滤掉 consumables 和 CMB 类型的物料 | // 过滤掉 consumables 和 CMB 类型的物料 | ||||
| val nonConsumablesJobms = jobOrder.jobms.filter { jobm -> | val nonConsumablesJobms = jobOrder.jobms.filter { jobm -> | ||||
| @@ -243,36 +248,22 @@ open class JobOrderService( | |||||
| var sufficientCount = 0 | var sufficientCount = 0 | ||||
| var insufficientCount = 0 | var insufficientCount = 0 | ||||
| //println("=== JobOrderService.calculateStockCounts for JobOrder: ${jobOrder.code} ===") | |||||
| nonConsumablesJobms.forEach { jobm -> | nonConsumablesJobms.forEach { jobm -> | ||||
| val itemId = jobm.item?.id | val itemId = jobm.item?.id | ||||
| val itemCode = jobm.item?.code ?: "N/A" | |||||
| val itemName = jobm.item?.name ?: "N/A" | |||||
| if (itemId != null) { | if (itemId != null) { | ||||
| val inventory = inventoriesMap[itemId] | |||||
| val availableQty = if (inventory != null) { | |||||
| inventory.availableQty ?: ( | |||||
| (inventory.onHandQty ?: BigDecimal.ZERO) - | |||||
| (inventory.onHoldQty ?: BigDecimal.ZERO) - | |||||
| (inventory.unavailableQty ?: BigDecimal.ZERO) | |||||
| ) | |||||
| } else { | |||||
| BigDecimal.ZERO | |||||
| } | |||||
| val jobmUomId = jobm.uom?.id | |||||
| val inventory = resolveInventoryForJobm(itemId, jobmUomId, inventoriesMap) | |||||
| val availableQty = inventory?.availableQty | |||||
| ?: (inventory?.let { | |||||
| (it.onHandQty ?: BigDecimal.ZERO) - (it.unavailableQty ?: BigDecimal.ZERO) | |||||
| } ?: BigDecimal.ZERO) | |||||
| // ✅ 获取 reqQty 和 availableQty 的单位信息 | |||||
| val reqQty = jobm.reqQty ?: BigDecimal.ZERO | val reqQty = jobm.reqQty ?: BigDecimal.ZERO | ||||
| val reqUomId = jobm.uom?.id ?: 0L | val reqUomId = jobm.uom?.id ?: 0L | ||||
| val reqUomName = jobm.uom?.udfudesc ?: "N/A" | |||||
| // ✅ 修复:使用 itemUomService 获取 stockUomId(与 ProductProcessService 保持一致) | |||||
| val stockUnitItemUom = itemUomService.findStockUnitByItemId(itemId) | val stockUnitItemUom = itemUomService.findStockUnitByItemId(itemId) | ||||
| val stockUomId = stockUnitItemUom?.uom?.id ?: 0L | |||||
| val stockUomName = stockUnitItemUom?.uom?.udfudesc ?: "N/A" | |||||
| val availableUomId = inventory?.stockUomId ?: stockUnitItemUom?.uom?.id ?: 0L | |||||
| // ✅ 转换为 base unit 进行比较(与 ProductProcessService 保持一致) | |||||
| val baseReqQtyResult = if (reqUomId > 0 && reqQty > BigDecimal.ZERO) { | val baseReqQtyResult = if (reqUomId > 0 && reqQty > BigDecimal.ZERO) { | ||||
| try { | try { | ||||
| itemUomService.convertUomByItem( | itemUomService.convertUomByItem( | ||||
| @@ -291,13 +282,13 @@ open class JobOrderService( | |||||
| null | null | ||||
| } | } | ||||
| val baseAvailableQtyResult = if (stockUomId > 0 && availableQty > BigDecimal.ZERO) { | |||||
| val baseAvailableQtyResult = if (availableUomId > 0 && availableQty > BigDecimal.ZERO) { | |||||
| try { | try { | ||||
| itemUomService.convertUomByItem( | itemUomService.convertUomByItem( | ||||
| ConvertUomByItemRequest( | ConvertUomByItemRequest( | ||||
| itemId = itemId, | itemId = itemId, | ||||
| qty = availableQty, | qty = availableQty, | ||||
| uomId = stockUomId, | |||||
| uomId = availableUomId, | |||||
| targetUnit = "baseUnit" | targetUnit = "baseUnit" | ||||
| ) | ) | ||||
| ) | ) | ||||
| @@ -311,27 +302,32 @@ open class JobOrderService( | |||||
| val baseReqQty = baseReqQtyResult?.newQty ?: BigDecimal.ZERO | val baseReqQty = baseReqQtyResult?.newQty ?: BigDecimal.ZERO | ||||
| val baseAvailableQty = baseAvailableQtyResult?.newQty ?: BigDecimal.ZERO | val baseAvailableQty = baseAvailableQtyResult?.newQty ?: BigDecimal.ZERO | ||||
| val baseUomName = baseReqQtyResult?.udfudesc ?: baseAvailableQtyResult?.udfudesc ?: "N/A" | |||||
| // ✅ 使用 base unit 进行比较 | |||||
| if (baseAvailableQty >= baseReqQty) { | if (baseAvailableQty >= baseReqQty) { | ||||
| sufficientCount++ | sufficientCount++ | ||||
| //println("✅ SUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)") | |||||
| } else { | } else { | ||||
| insufficientCount++ | insufficientCount++ | ||||
| //println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)") | |||||
| } | } | ||||
| } else { | } else { | ||||
| // 如果没有 itemId,视为不足 | |||||
| insufficientCount++ | insufficientCount++ | ||||
| //println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - No itemId") | |||||
| } | } | ||||
| } | } | ||||
| //println("=== Result: sufficient=$sufficientCount, insufficient=$insufficientCount ===") | |||||
| return Pair(sufficientCount, insufficientCount) | return Pair(sufficientCount, insufficientCount) | ||||
| } | } | ||||
| private fun resolveInventoryForJobm( | |||||
| itemId: Long, | |||||
| jobmUomId: Long?, | |||||
| inventoriesMap: Map<Pair<Long, Long>, com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo>, | |||||
| ): com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo? { | |||||
| if (jobmUomId != null) { | |||||
| inventoriesMap[itemId to jobmUomId]?.let { return it } | |||||
| } | |||||
| val stockUnitUomId = itemUomService.findStockUnitByItemId(itemId)?.uom?.id ?: return null | |||||
| return inventoriesMap[itemId to stockUnitUomId] | |||||
| } | |||||
| open fun jobOrderDetail(id: Long): JobOrderDetail { | open fun jobOrderDetail(id: Long): JobOrderDetail { | ||||
| val sqlResult = jobOrderRepository.findJobOrderDetailById(id) ?: throw NoSuchElementException(); | val sqlResult = jobOrderRepository.findJobOrderDetailById(id) ?: throw NoSuchElementException(); | ||||
| @@ -168,8 +168,8 @@ open class PSService( | |||||
| /** Set or clear coffee_or_tea for itemCode + systemType (coffee / tea / lemon). */ | /** Set or clear coffee_or_tea for itemCode + systemType (coffee / tea / lemon). */ | ||||
| /** | /** | ||||
| * Recalculate [inventory.onHandQty] / hold / unavailable from [inventory_lot_line] for FG BOM items. | |||||
| * Same aggregation as pick-issue manual inventory sync. | |||||
| * Recalculate [inventory.onHandQty] / unavailable from [inventory_lot_line] for FG BOM items. | |||||
| * Per (itemId, stockUomId) bucket. Does not write onHoldQty. | |||||
| */ | */ | ||||
| fun refreshInventoryOnHandForFgBomItems(): Int { | fun refreshInventoryOnHandForFgBomItems(): Int { | ||||
| val sql = """ | val sql = """ | ||||
| @@ -182,24 +182,28 @@ open class PSService( | |||||
| LEFT JOIN ( | LEFT JOIN ( | ||||
| SELECT | SELECT | ||||
| il.itemId, | il.itemId, | ||||
| iu.uomId AS stockUomId, | |||||
| SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) AS totalOnHandQty, | SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) AS totalOnHandQty, | ||||
| SUM(COALESCE(ill.holdQty, 0)) AS totalOnHoldQty, | |||||
| SUM(CASE | SUM(CASE | ||||
| WHEN ill.status = 'unavailable' | |||||
| THEN COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0) | |||||
| WHEN LOWER(ill.status) = 'unavailable' | |||||
| THEN COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) | |||||
| ELSE 0 | ELSE 0 | ||||
| END) AS totalUnavailableQty | END) AS totalUnavailableQty | ||||
| FROM inventory_lot_line ill | FROM inventory_lot_line ill | ||||
| INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId AND il.deleted = 0 | |||||
| WHERE ill.deleted = 0 | |||||
| GROUP BY il.itemId | |||||
| INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId AND IFNULL(il.deleted, 0) = 0 | |||||
| INNER JOIN item_uom iu ON iu.id = ill.stockItemUomId AND IFNULL(iu.deleted, 0) = 0 | |||||
| WHERE IFNULL(ill.deleted, 0) = 0 | |||||
| GROUP BY il.itemId, iu.uomId | |||||
| ) calc ON calc.itemId = i.itemId | ) calc ON calc.itemId = i.itemId | ||||
| AND ( | |||||
| i.stockUomId = calc.stockUomId | |||||
| OR (i.stockUomId IS NULL AND i.uomId = calc.stockUomId) | |||||
| ) | |||||
| SET | SET | ||||
| i.onHandQty = COALESCE(calc.totalOnHandQty, 0), | i.onHandQty = COALESCE(calc.totalOnHandQty, 0), | ||||
| i.onHoldQty = COALESCE(calc.totalOnHoldQty, 0), | |||||
| i.unavailableQty = COALESCE(calc.totalUnavailableQty, 0), | i.unavailableQty = COALESCE(calc.totalUnavailableQty, 0), | ||||
| i.status = IF( | i.status = IF( | ||||
| COALESCE(calc.totalOnHandQty, 0) - COALESCE(calc.totalOnHoldQty, 0) - COALESCE(calc.totalUnavailableQty, 0) > 0, | |||||
| COALESCE(calc.totalOnHandQty, 0) - COALESCE(calc.totalUnavailableQty, 0) > 0, | |||||
| 'available', | 'available', | ||||
| 'unavailable' | 'unavailable' | ||||
| ), | ), | ||||
| @@ -27,14 +27,25 @@ interface ProductionScheduleLineRepository : AbstractRepository<ProductionSchedu | |||||
| bmi.code, | bmi.code, | ||||
| bmi.name, | bmi.name, | ||||
| bmi.`type`, | bmi.`type`, | ||||
| coalesce(i.onHandQty, 0) - coalesce(i.onHoldQty, 0) - coalesce(i.unavailableQty, 0) as availableQty, | |||||
| coalesce(i.onHandQty, 0) - coalesce(i.unavailableQty, 0) as availableQty, | |||||
| ceil(coalesce(bm.qty, 0) * pp.proportion) as demandQty | ceil(coalesce(bm.qty, 0) * pp.proportion) as demandQty | ||||
| from production_schedule_line psl | from production_schedule_line psl | ||||
| left join prod_prop pp on pp.pslId = psl.id | left join prod_prop pp on pp.pslId = psl.id | ||||
| left join bom b on b.itemId = psl.itemId | left join bom b on b.itemId = psl.itemId | ||||
| left join bom_material bm on bm.bomId = b.id | left join bom_material bm on bm.bomId = b.id | ||||
| left join items bmi on bmi.id = bm.itemId | left join items bmi on bmi.id = bm.itemId | ||||
| left join inventory i on i.itemId = bmi.id | |||||
| left join inventory i on i.id = ( | |||||
| select i2.id | |||||
| from inventory i2 | |||||
| where i2.itemId = bmi.id | |||||
| and ifnull(i2.deleted, 0) = 0 | |||||
| and ( | |||||
| i2.stockUomId = coalesce(bm.stockUnit, bm.uomId) | |||||
| or (i2.stockUomId is null and i2.uomId = coalesce(bm.stockUnit, bm.uomId)) | |||||
| ) | |||||
| order by case when i2.stockUomId is null then 1 else 0 end, i2.id | |||||
| limit 1 | |||||
| ) | |||||
| where psl.id = :id and bmi.id is not null | where psl.id = :id and bmi.id is not null | ||||
| group by psl.id, bm.id, i.id, pp.proportion | group by psl.id, bm.id, i.id, pp.proportion | ||||
| """) | """) | ||||
| @@ -171,29 +171,29 @@ open class ProductionScheduleService( | |||||
| val proportion = BigDecimal.ONE // BigDecimal(line.prodQty).divide(bm.bom?.outputQty ?: BigDecimal.ONE, 5, RoundingMode.HALF_UP) | val proportion = BigDecimal.ONE // BigDecimal(line.prodQty).divide(bm.bom?.outputQty ?: BigDecimal.ONE, 5, RoundingMode.HALF_UP) | ||||
| val demandQty = bm.qty?.times(proportion) ?: zero | val demandQty = bm.qty?.times(proportion) ?: zero | ||||
| val saleUnit = bm.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | val saleUnit = bm.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | ||||
| val materialStockUomId = bm.stockUnit?.toLong() | |||||
| ?: bm.uom?.id | |||||
| ?: bm.item?.id?.let { itemUomService.findStockUnitByItemId(it)?.uom?.id } | |||||
| RoughProdScheduleLineBomMaterialInfoByFg( | RoughProdScheduleLineBomMaterialInfoByFg( | ||||
| id = bm.id, | id = bm.id, | ||||
| code = bm.item?.code, | code = bm.item?.code, | ||||
| name = bm.item?.name, | name = bm.item?.name, | ||||
| type = bm.item?.type, | type = bm.item?.type, | ||||
| availableQty = bm.item?.inventories?.sumOf { | |||||
| (it.onHandQty ?: zero) - (it.onHoldQty ?: zero) - (it.unavailableQty ?: zero) | |||||
| }, | |||||
| availableQty = availableQtyWithoutHold(bm.item?.inventories, materialStockUomId, zero), | |||||
| demandQty = demandQty, | demandQty = demandQty, | ||||
| uomName = saleUnit?.uom?.udfudesc | uomName = saleUnit?.uom?.udfudesc | ||||
| ) | ) | ||||
| } | } | ||||
| val saleUnit = line.item.id?.let { itemUomService.findSalesUnitByItemId(it) } | val saleUnit = line.item.id?.let { itemUomService.findSalesUnitByItemId(it) } | ||||
| val fgStockUomId = line.item.id?.let { itemUomService.findStockUnitByItemId(it)?.uom?.id } | |||||
| RoughProdScheduleLineInfoByFg( | RoughProdScheduleLineInfoByFg( | ||||
| id = line.id, | id = line.id, | ||||
| code = line.item.code, | code = line.item.code, | ||||
| name = line.item.name, | name = line.item.name, | ||||
| type = line.item.type, | type = line.item.type, | ||||
| availableQty = line.item.inventories.sumOf { | |||||
| (it.onHandQty ?: zero) - (it.onHoldQty ?: zero) - (it.unavailableQty ?: zero) | |||||
| }, | |||||
| availableQty = availableQtyWithoutHold(line.item.inventories, fgStockUomId, zero), | |||||
| prodQty = BigDecimal(line.prodQty), | prodQty = BigDecimal(line.prodQty), | ||||
| lastMonthAvgSales = BigDecimal(line.lastMonthAvgSales), | lastMonthAvgSales = BigDecimal(line.lastMonthAvgSales), | ||||
| estCloseBal = BigDecimal(line.estCloseBal), | estCloseBal = BigDecimal(line.estCloseBal), | ||||
| @@ -258,15 +258,16 @@ open class ProductionScheduleService( | |||||
| val proportion = BigDecimal.ONE //BigDecimal(line.prodQty).divide(bm.bom?.outputQty ?: BigDecimal.ONE, 5, RoundingMode.HALF_UP) | val proportion = BigDecimal.ONE //BigDecimal(line.prodQty).divide(bm.bom?.outputQty ?: BigDecimal.ONE, 5, RoundingMode.HALF_UP) | ||||
| val demandQty = bm.qty?.times(proportion) ?: zero | val demandQty = bm.qty?.times(proportion) ?: zero | ||||
| val saleUnit = bm.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | val saleUnit = bm.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | ||||
| val materialStockUomId = bm.stockUnit?.toLong() | |||||
| ?: bm.uom?.id | |||||
| ?: bm.item?.id?.let { itemUomService.findStockUnitByItemId(it)?.uom?.id } | |||||
| RoughProdScheduleLineInfoByBomByDate( | RoughProdScheduleLineInfoByBomByDate( | ||||
| id = bm.item?.id, | id = bm.item?.id, | ||||
| code = bm.item?.code, | code = bm.item?.code, | ||||
| name = bm.item?.name, | name = bm.item?.name, | ||||
| type = bm.item?.type, | type = bm.item?.type, | ||||
| availableQty = bm.item?.inventories?.sumOf { | |||||
| (it.onHandQty ?: zero) - (it.onHoldQty ?: zero) - (it.unavailableQty ?: zero) | |||||
| } ?: zero, | |||||
| availableQty = availableQtyWithoutHold(bm.item?.inventories, materialStockUomId, zero), | |||||
| demandQty = demandQty, | demandQty = demandQty, | ||||
| assignDate = line.assignDate, | assignDate = line.assignDate, | ||||
| uomName = saleUnit?.uom?.udfudesc | uomName = saleUnit?.uom?.udfudesc | ||||
| @@ -724,7 +725,21 @@ open class ProductionScheduleService( | |||||
| FROM | FROM | ||||
| bom | bom | ||||
| LEFT JOIN items ON bom.itemId = items.id | LEFT JOIN items ON bom.itemId = items.id | ||||
| LEFT JOIN inventory ON items.id = inventory.itemId | |||||
| LEFT JOIN item_uom iu_stock ON iu_stock.itemId = items.id | |||||
| AND iu_stock.stockUnit = 1 | |||||
| AND IFNULL(iu_stock.deleted, 0) = 0 | |||||
| LEFT JOIN inventory ON inventory.id = ( | |||||
| SELECT i2.id | |||||
| FROM inventory i2 | |||||
| WHERE i2.itemId = items.id | |||||
| AND IFNULL(i2.deleted, 0) = 0 | |||||
| AND ( | |||||
| i2.stockUomId = iu_stock.uomId | |||||
| OR (i2.stockUomId IS NULL AND i2.uomId = iu_stock.uomId) | |||||
| ) | |||||
| ORDER BY CASE WHEN i2.stockUomId IS NULL THEN 1 ELSE 0 END, i2.id | |||||
| LIMIT 1 | |||||
| ) | |||||
| left join item_fake_onhand on items.code = item_fake_onhand.itemCode | left join item_fake_onhand on items.code = item_fake_onhand.itemCode | ||||
| WHERE bom.deleted = 0 and bom.description = 'FG' and bom.status = 'active' | WHERE bom.deleted = 0 and bom.description = 'FG' and bom.status = 'active' | ||||
| -- and bom.itemId != 16771 | -- and bom.itemId != 16771 | ||||
| @@ -1821,7 +1836,18 @@ open class ProductionScheduleService( | |||||
| left join bom on it.id = bom.itemId | left join bom on it.id = bom.itemId | ||||
| left join bom_material bm on bom.id = bm.bomId | left join bom_material bm on bom.id = bm.bomId | ||||
| left join items itm on bm.itemId = itm.id | left join items itm on bm.itemId = itm.id | ||||
| left join inventory iv on itm.id = iv.itemId | |||||
| left join inventory iv on iv.id = ( | |||||
| select i2.id | |||||
| from inventory i2 | |||||
| where i2.itemId = itm.id | |||||
| and ifnull(i2.deleted, 0) = 0 | |||||
| and ( | |||||
| i2.stockUomId = ius.uomId | |||||
| or (i2.stockUomId is null and i2.uomId = ius.uomId) | |||||
| ) | |||||
| order by case when i2.stockUomId is null then 1 else 0 end, i2.id | |||||
| limit 1 | |||||
| ) | |||||
| left join item_uom ius on itm.id = ius.itemId and ius.stockUnit = 1 | left join item_uom ius on itm.id = ius.itemId and ius.stockUnit = 1 | ||||
| left join uom_conversion ucs on ius.uomId = ucs.id | left join uom_conversion ucs on ius.uomId = ucs.id | ||||
| @@ -1893,7 +1919,18 @@ open class ProductionScheduleService( | |||||
| LEFT JOIN item_uom ium18 ON ium18.itemId = itm.id AND ium18.uomId = lsu.uomIdM18 AND ium18.deleted = 0 | LEFT JOIN item_uom ium18 ON ium18.itemId = itm.id AND ium18.uomId = lsu.uomIdM18 AND ium18.deleted = 0 | ||||
| LEFT JOIN uom_conversion uomM18 ON uomM18.id = ium18.uomId | LEFT JOIN uom_conversion uomM18 ON uomM18.id = ium18.uomId | ||||
| JOIN item_uom itsm ON itm.id = itsm.itemId and itsm.stockUnit = 1 | JOIN item_uom itsm ON itm.id = itsm.itemId and itsm.stockUnit = 1 | ||||
| LEFT JOIN inventory iv ON itm.id = iv.itemId | |||||
| LEFT JOIN inventory iv ON iv.id = ( | |||||
| SELECT i2.id | |||||
| FROM inventory i2 | |||||
| WHERE i2.itemId = itm.id | |||||
| AND IFNULL(i2.deleted, 0) = 0 | |||||
| AND ( | |||||
| i2.stockUomId = itsm.uomId | |||||
| OR (i2.stockUomId IS NULL AND i2.uomId = itsm.uomId) | |||||
| ) | |||||
| ORDER BY CASE WHEN i2.stockUomId IS NULL THEN 1 ELSE 0 END, i2.id | |||||
| LIMIT 1 | |||||
| ) | |||||
| WHERE DATE(ps.produceAt) >= DATE_ADD(:fromDate, INTERVAL 1 DAY) | WHERE DATE(ps.produceAt) >= DATE_ADD(:fromDate, INTERVAL 1 DAY) | ||||
| AND DATE(ps.produceAt) < DATE_ADD(:fromDate, INTERVAL 8 DAY) | AND DATE(ps.produceAt) < DATE_ADD(:fromDate, INTERVAL 8 DAY) | ||||
| AND ps.id = ( | AND ps.id = ( | ||||
| @@ -1968,5 +2005,16 @@ open class ProductionScheduleService( | |||||
| // Optional: log the action (if you have logging setup) | // Optional: log the action (if you have logging setup) | ||||
| // logger.info("Cleared all production schedules with produceAt >= today") | // logger.info("Cleared all production schedules with produceAt >= today") | ||||
| } | } | ||||
| private fun availableQtyWithoutHold( | |||||
| inventories: List<Inventory>?, | |||||
| stockUomId: Long?, | |||||
| zero: BigDecimal, | |||||
| ): BigDecimal { | |||||
| if (inventories.isNullOrEmpty() || stockUomId == null) return zero | |||||
| val bucket = inventories.firstOrNull { inv -> | |||||
| inv.stockUom?.id == stockUomId || (inv.stockUom == null && inv.uom?.id == stockUomId) | |||||
| } ?: return zero | |||||
| return (bucket.onHandQty ?: zero) - (bucket.unavailableQty ?: zero) | |||||
| } | |||||
| } | } | ||||
| @@ -2313,71 +2313,6 @@ private fun updateLotLineAfterIssue(lotLineId: Long, qty: BigDecimal, isMissItem | |||||
| lotLine.modifiedBy = "system" | lotLine.modifiedBy = "system" | ||||
| // 修复:使用 saveAndFlush 确保立即提交到数据库,触发触发器 | // 修复:使用 saveAndFlush 确保立即提交到数据库,触发触发器 | ||||
| inventoryLotLineRepository.saveAndFlush(lotLine) | inventoryLotLineRepository.saveAndFlush(lotLine) | ||||
| updateInventoryAfterLotLineChange(lotLine) | |||||
| } | |||||
| } | |||||
| private fun updateInventoryAfterLotLineChange(lotLine: InventoryLotLine) { | |||||
| try { | |||||
| val item = lotLine.inventoryLot?.item ?: return | |||||
| val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return | |||||
| // 使用 SQL 查询计算所有相关 lot lines 的总和 | |||||
| val sql = """ | |||||
| SELECT | |||||
| SUM(COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) as totalOnHandQty, | |||||
| SUM(COALESCE(ill.holdQty, 0)) as totalOnHoldQty, | |||||
| SUM(CASE | |||||
| WHEN ill.status = 'unavailable' | |||||
| THEN COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0) | |||||
| ELSE 0 | |||||
| END) as totalUnavailableQty | |||||
| FROM inventory_lot_line ill | |||||
| INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId | |||||
| WHERE il.itemId = :itemId | |||||
| AND ill.deleted = 0 | |||||
| """.trimIndent() | |||||
| val result = jdbcDao.queryForMap(sql, mapOf("itemId" to item.id!!)).orElse(null) | |||||
| if (result != null) { | |||||
| val totalOnHandQty = (result["totalOnHandQty"] as? Number)?.let { | |||||
| BigDecimal(it.toString()) | |||||
| } ?: BigDecimal.ZERO | |||||
| val totalOnHoldQty = (result["totalOnHoldQty"] as? Number)?.let { | |||||
| BigDecimal(it.toString()) | |||||
| } ?: BigDecimal.ZERO | |||||
| val totalUnavailableQty = (result["totalUnavailableQty"] as? Number)?.let { | |||||
| BigDecimal(it.toString()) | |||||
| } ?: BigDecimal.ZERO | |||||
| // 更新 inventory | |||||
| inventory.onHandQty = totalOnHandQty | |||||
| inventory.onHoldQty = totalOnHoldQty | |||||
| inventory.unavailableQty = totalUnavailableQty | |||||
| inventory.status = if (totalOnHandQty.subtract(totalOnHoldQty).subtract(totalUnavailableQty) > BigDecimal.ZERO) { | |||||
| "available" | |||||
| } else { | |||||
| "unavailable" | |||||
| } | |||||
| inventory.modified = LocalDateTime.now() | |||||
| inventory.modifiedBy = "system" | |||||
| inventoryRepository.saveAndFlush(inventory) | |||||
| println("=== MANUALLY UPDATED INVENTORY ===") | |||||
| println("Item ID: ${item.id}") | |||||
| println("Total OnHandQty: ${totalOnHandQty}") | |||||
| println("Total OnHoldQty: ${totalOnHoldQty}") | |||||
| println("Total UnavailableQty: ${totalUnavailableQty}") | |||||
| println("Status: ${inventory.status}") | |||||
| println("==================================") | |||||
| } | |||||
| } catch (e: Exception) { | |||||
| println("Error updating inventory manually: ${e.message}") | |||||
| e.printStackTrace() | |||||
| } | } | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 46 | v1.0.1 | 2026-08-05 */ | /** FP-MTMS Version Checklist | Functions Ref. No. 46 | v1.0.1 | 2026-08-05 */ | ||||
| @@ -2768,7 +2703,6 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse | |||||
| lotLine.modified = LocalDateTime.now() | lotLine.modified = LocalDateTime.now() | ||||
| lotLine.modifiedBy = "system" | lotLine.modifiedBy = "system" | ||||
| inventoryLotLineRepository.saveAndFlush(lotLine) | inventoryLotLineRepository.saveAndFlush(lotLine) | ||||
| updateInventoryAfterLotLineChange(lotLine) | |||||
| println("✅ Reset lot ${request.lotId}: issueQty=0, status=AVAILABLE") | println("✅ Reset lot ${request.lotId}: issueQty=0, status=AVAILABLE") | ||||
| } | } | ||||
| val rejectedLines = stockOutLineRepository | val rejectedLines = stockOutLineRepository | ||||
| @@ -31,6 +31,7 @@ import com.ffii.fpsms.modules.stock.entity.StockOutRepository | |||||
| import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | ||||
| import com.ffii.fpsms.modules.stock.entity.projection.CurrentInventoryItemInfo | import com.ffii.fpsms.modules.stock.entity.projection.CurrentInventoryItemInfo | ||||
| import com.ffii.fpsms.modules.stock.service.InventoryLotLineService | import com.ffii.fpsms.modules.stock.service.InventoryLotLineService | ||||
| import com.ffii.fpsms.modules.stock.entity.projection.InventoryLotLineInfo | |||||
| import com.ffii.fpsms.modules.stock.service.InventoryService | import com.ffii.fpsms.modules.stock.service.InventoryService | ||||
| import com.ffii.fpsms.modules.stock.service.StockOutLineService | import com.ffii.fpsms.modules.stock.service.StockOutLineService | ||||
| import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService | import com.ffii.fpsms.modules.stock.service.SuggestedPickLotService | ||||
| @@ -215,28 +216,17 @@ open class PickOrderService( | |||||
| val today = LocalDate.now() | val today = LocalDate.now() | ||||
| val zero = BigDecimal.ZERO | val zero = BigDecimal.ZERO | ||||
| val inventories = if (itemIds.isNotEmpty()) { | |||||
| inventoryLotLineService | |||||
| .allInventoryLotLinesByItemIdIn(itemIds) | |||||
| .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | |||||
| .filter { (it.inQty ?: zero).minus(it.outQty ?: zero).minus(it.holdQty ?: zero) > zero } | |||||
| .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) } | |||||
| .groupBy { it.item?.id } | |||||
| } else { | |||||
| emptyMap() | |||||
| } | |||||
| val inventories = pickableLotLinesByItemAndStockUom(itemIds, today, zero) | |||||
| // Build response (removed suggestions) | // Build response (removed suggestions) | ||||
| val pickOrderInfos = fullPickOrders.map { po -> | val pickOrderInfos = fullPickOrders.map { po -> | ||||
| val pickOrderLineInfos = po.pickOrderLines.map { pol -> | val pickOrderLineInfos = po.pickOrderLines.map { pol -> | ||||
| val inventory = pol.item?.id?.let { inventories[it] } | |||||
| GetPickOrderLineInfo( | GetPickOrderLineInfo( | ||||
| id = pol.id, | id = pol.id, | ||||
| itemId = pol.item?.id, | itemId = pol.item?.id, | ||||
| itemCode = pol.item?.code, | itemCode = pol.item?.code, | ||||
| itemName = pol.item?.name, | itemName = pol.item?.name, | ||||
| availableQty = inventory?.sumOf { i -> (i.availableQty ?: zero) }, | |||||
| availableQty = pickLineAvailableQty(pol.item?.id, pol.uom?.id, inventories, zero), | |||||
| requiredQty = pol.qty, | requiredQty = pol.qty, | ||||
| uomCode = pol.uom?.code, | uomCode = pol.uom?.code, | ||||
| uomDesc = pol.uom?.udfudesc, | uomDesc = pol.uom?.udfudesc, | ||||
| @@ -660,7 +650,7 @@ open class PickOrderService( | |||||
| // Get Inventory Data | // Get Inventory Data | ||||
| val requiredItems = pos | val requiredItems = pos | ||||
| .flatMap { it.pickOrderLines } | .flatMap { it.pickOrderLines } | ||||
| .groupBy { it.item?.id } | |||||
| .groupBy { it.item?.id to it.uom?.id } | |||||
| .map { (key, value) -> | .map { (key, value) -> | ||||
| key to object : CurrentInventoryItemInfo { | key to object : CurrentInventoryItemInfo { | ||||
| override val id: Long? = value[0].item?.id | override val id: Long? = value[0].item?.id | ||||
| @@ -671,42 +661,25 @@ open class PickOrderService( | |||||
| override val requiredQty: BigDecimal = value.sumOf { it.qty ?: zero } | override val requiredQty: BigDecimal = value.sumOf { it.qty ?: zero } | ||||
| } | } | ||||
| } // itemId - requiredQty | |||||
| } // itemId+uom - requiredQty | |||||
| val itemIds = requiredItems.mapNotNull { it.first } | |||||
| // val inventories = inventoryLotLineRepository.findCurrentInventoryByItems(itemIds) | |||||
| // val inventories = inventoryService.allInventoriesByItemIds(itemIds) | |||||
| val inventories = inventoryLotLineService | |||||
| .allInventoryLotLinesByItemIdIn(itemIds) | |||||
| .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | |||||
| .filter { (it.inQty ?: zero).minus(it.outQty ?: zero).minus(it.holdQty ?: zero) > zero } | |||||
| .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) } | |||||
| .sortedBy { it.expiryDate } | |||||
| .groupBy { it.item?.id } | |||||
| val itemIds = requiredItems.mapNotNull { it.first.first } | |||||
| val inventories = pickableLotLinesByItemAndStockUom(itemIds, today, zero) | |||||
| // Pick Orders | // Pick Orders | ||||
| val releasePickOrderInfos = pos | val releasePickOrderInfos = pos | ||||
| .map { po -> | .map { po -> | ||||
| val releasePickOrderLineInfos = po.pickOrderLines.map { pol -> | val releasePickOrderLineInfos = po.pickOrderLines.map { pol -> | ||||
| // if (pol.item?.id != null && pol.item!!.id!! > 0) { | |||||
| val inventory = pol.item?.id?.let { inventories[it] } | |||||
| val itemUom = pol.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | |||||
| // val inventory = inventories.find { it.itemId == pol.item?.id } | |||||
| // Return | |||||
| ReleasePickOrderLineInfo( | ReleasePickOrderLineInfo( | ||||
| id = pol.id, | id = pol.id, | ||||
| itemId = pol.item?.id, | itemId = pol.item?.id, | ||||
| itemCode = pol.item?.code, | itemCode = pol.item?.code, | ||||
| itemName = pol.item?.name, | itemName = pol.item?.name, | ||||
| // availableQty = inventory?.availableQty, | |||||
| availableQty = inventory?.sumOf { i -> (i.availableQty ?: zero) }, | |||||
| // availableQty = inventory?.sumOf { i -> (i.availableQty ?: zero) * (itemUom?.ratioN ?: one) * (itemUom?.ratioD ?: one) }, | |||||
| availableQty = pickLineAvailableQty(pol.item?.id, pol.uom?.id, inventories, zero), | |||||
| requiredQty = pol.qty, | requiredQty = pol.qty, | ||||
| uomCode = pol.uom?.code, | uomCode = pol.uom?.code, | ||||
| uomDesc = pol.uom?.udfudesc, | uomDesc = pol.uom?.udfudesc, | ||||
| ) | ) | ||||
| // } | |||||
| } | } | ||||
| // Return | // Return | ||||
| @@ -725,16 +698,12 @@ open class PickOrderService( | |||||
| // val inventory = inventories | // val inventory = inventories | ||||
| // .find { it.itemId == item.first } | // .find { it.itemId == item.first } | ||||
| val inventory = item.first?.let { inventories[it] } | |||||
| val itemUom = item.first?.let { itemUomService.findSalesUnitByItemId(it) } | |||||
| val inventory = item.first.let { (itemId, uomId) -> | |||||
| if (itemId != null && uomId != null) inventories[itemId to uomId] else null | |||||
| } | |||||
| item.second.let { | item.second.let { | ||||
| // it.availableQty = inventory?.availableQty | |||||
| it.availableQty = inventory?.sumOf { i -> i.availableQty ?: zero } | |||||
| // it.availableQty = inventory?.sumOf { i -> (i.availableQty ?: zero) * (itemUom?.ratioN ?: one) * (itemUom?.ratioD ?: one) } | |||||
| // return | |||||
| it.availableQty = inventory?.sumOf { i -> (i.inQty ?: zero) - (i.outQty ?: zero) } | |||||
| it | it | ||||
| } | } | ||||
| } | } | ||||
| @@ -759,7 +728,7 @@ open class PickOrderService( | |||||
| // Get Inventory Data | // Get Inventory Data | ||||
| val requiredItems = pos | val requiredItems = pos | ||||
| .flatMap { it.pickOrderLines } | .flatMap { it.pickOrderLines } | ||||
| .groupBy { it.item?.id } | |||||
| .groupBy { it.item?.id to it.uom?.id } | |||||
| .map { (key, value) -> | .map { (key, value) -> | ||||
| key to object : CurrentInventoryItemInfo { | key to object : CurrentInventoryItemInfo { | ||||
| override val id: Long? = value[0].item?.id | override val id: Long? = value[0].item?.id | ||||
| @@ -769,16 +738,10 @@ open class PickOrderService( | |||||
| override var availableQty: BigDecimal? = zero | override var availableQty: BigDecimal? = zero | ||||
| override val requiredQty: BigDecimal = value.sumOf { it.qty ?: zero } | override val requiredQty: BigDecimal = value.sumOf { it.qty ?: zero } | ||||
| } | } | ||||
| } // itemId - requiredQty | |||||
| } | |||||
| val itemIds = requiredItems.mapNotNull { it.first } | |||||
| val inventories = inventoryLotLineService | |||||
| .allInventoryLotLinesByItemIdIn(itemIds) | |||||
| .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | |||||
| .filter { (it.inQty ?: zero).minus(it.outQty ?: zero).minus(it.holdQty ?: zero) > zero } | |||||
| .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) } | |||||
| .sortedBy { it.expiryDate } | |||||
| .groupBy { it.item?.id } | |||||
| val itemIds = requiredItems.mapNotNull { it.first.first } | |||||
| val inventories = pickableLotLinesByItemAndStockUom(itemIds, today, zero) | |||||
| val suggestions = | val suggestions = | ||||
| suggestedPickLotService.suggestionForPickOrders(SuggestedPickLotForPoRequest(pickOrders = pos)) | suggestedPickLotService.suggestionForPickOrders(SuggestedPickLotForPoRequest(pickOrders = pos)) | ||||
| @@ -793,24 +756,10 @@ open class PickOrderService( | |||||
| emptyMap() | emptyMap() | ||||
| } | } | ||||
| // Pre-calculate available quantities for each item ONCE | |||||
| val itemAvailableQtyMap = mutableMapOf<Long, BigDecimal>() | |||||
| itemIds.forEach { itemId -> | |||||
| val inventory = inventories[itemId] | |||||
| val totalAvailableQty = inventory?.sumOf { i -> | |||||
| val inQty = i.inQty ?: zero | |||||
| val outQty = i.outQty ?: zero | |||||
| val holdQty = i.holdQty ?: zero | |||||
| inQty.minus(outQty).minus(holdQty) | |||||
| } ?: zero | |||||
| itemAvailableQtyMap[itemId] = totalAvailableQty | |||||
| } | |||||
| val releasePickOrderLineInfos = pos | val releasePickOrderLineInfos = pos | ||||
| .map { po -> | .map { po -> | ||||
| val releasePickOrderLineInfos = po.pickOrderLines.map { pol -> | val releasePickOrderLineInfos = po.pickOrderLines.map { pol -> | ||||
| val itemId = pol.item?.id | |||||
| val availableQty = itemId?.let { itemAvailableQtyMap[it] } ?: zero | |||||
| val availableQty = pickLineAvailableQty(pol.item?.id, pol.uom?.id, inventories, zero) | |||||
| // 取得這一行的所有 stock_out_line | // 取得這一行的所有 stock_out_line | ||||
| val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList() | val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList() | ||||
| @@ -871,16 +820,11 @@ open class PickOrderService( | |||||
| } | } | ||||
| // Items | // Items | ||||
| val currentInventoryInfos = requiredItems.map { item -> | val currentInventoryInfos = requiredItems.map { item -> | ||||
| val inventory = item.first?.let { inventories[it] } | |||||
| val itemUom = item.first?.let { itemUomService.findSalesUnitByItemId(it) } | |||||
| val inventory = item.first.let { (itemId, uomId) -> | |||||
| if (itemId != null && uomId != null) inventories[itemId to uomId] else null | |||||
| } | |||||
| item.second.let { | item.second.let { | ||||
| val convertedAvailableQty = inventory?.sumOf { i -> | |||||
| val baseQty = (i.availableQty ?: zero) | |||||
| baseQty | |||||
| } | |||||
| it.availableQty = convertedAvailableQty | |||||
| it.availableQty = inventory?.sumOf { i -> (i.inQty ?: zero) - (i.outQty ?: zero) } | |||||
| it | it | ||||
| } | } | ||||
| } | } | ||||
| @@ -2344,19 +2288,16 @@ open fun checkAndCompletePickOrderByConsoCode(consoCode: String): MessageRespons | |||||
| -- Calculate available quantity from inventory | -- Calculate available quantity from inventory | ||||
| COALESCE(( | COALESCE(( | ||||
| SELECT SUM( | |||||
| COALESCE(inv.onHandQty, 0) | |||||
| - COALESCE(inv.onHoldQty, 0) | |||||
| - COALESCE(inv.unavailableQty, 0) | |||||
| ) | |||||
| SELECT COALESCE(inv.onHandQty, 0) - COALESCE(inv.unavailableQty, 0) | |||||
| FROM fpsmsdb.inventory inv | FROM fpsmsdb.inventory inv | ||||
| JOIN fpsmsdb.item_uom iu | |||||
| ON iu.itemId = inv.itemId | |||||
| AND iu.uomId = inv.uomId | |||||
| AND iu.baseUnit = 1 | |||||
| AND iu.deleted = false | |||||
| WHERE inv.itemId = i.id | WHERE inv.itemId = i.id | ||||
| AND inv.deleted = false | AND inv.deleted = false | ||||
| AND ( | |||||
| inv.stockUomId = pol.uomId | |||||
| OR (inv.stockUomId IS NULL AND inv.uomId = pol.uomId) | |||||
| ) | |||||
| ORDER BY CASE WHEN inv.stockUomId IS NULL THEN 1 ELSE 0 END, inv.id | |||||
| LIMIT 1 | |||||
| ), 0) as availableQty, | ), 0) as availableQty, | ||||
| -- Check if all stock out lines for this pick order line are completed | -- Check if all stock out lines for this pick order line are completed | ||||
| @@ -5201,5 +5142,36 @@ open fun getAllPickOrderLotsWithDetailsHierarchical(userId: Long): Map<String, A | |||||
| ) | ) | ||||
| } | } | ||||
| private fun pickableLotLinesByItemAndStockUom( | |||||
| itemIds: List<Long>, | |||||
| today: LocalDate, | |||||
| zero: BigDecimal, | |||||
| ): Map<Pair<Long, Long>, List<InventoryLotLineInfo>> { | |||||
| if (itemIds.isEmpty()) return emptyMap() | |||||
| return inventoryLotLineService | |||||
| .allInventoryLotLinesByItemIdIn(itemIds) | |||||
| .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | |||||
| .filter { ((it.inQty ?: zero) - (it.outQty ?: zero)) > zero } | |||||
| .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) } | |||||
| .mapNotNull { line -> | |||||
| val itemId = line.item?.id ?: return@mapNotNull null | |||||
| val stockUomId = line.uomId ?: return@mapNotNull null | |||||
| (itemId to stockUomId) to line | |||||
| } | |||||
| .groupBy({ it.first }, { it.second }) | |||||
| } | |||||
| private fun pickLineAvailableQty( | |||||
| itemId: Long?, | |||||
| polUomId: Long?, | |||||
| lotsByBucket: Map<Pair<Long, Long>, List<InventoryLotLineInfo>>, | |||||
| zero: BigDecimal, | |||||
| ): BigDecimal { | |||||
| if (itemId == null || polUomId == null) return zero | |||||
| return lotsByBucket[itemId to polUomId] | |||||
| ?.sumOf { (it.inQty ?: zero) - (it.outQty ?: zero) } | |||||
| ?: zero | |||||
| } | |||||
| } | } | ||||
| @@ -35,7 +35,6 @@ import java.util.Optional | |||||
| import kotlin.jvm.optionals.getOrNull | 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.web.model.QrCodeAnalysisRequest | 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.QrCodeAnalysisResponse | ||||
| import com.ffii.fpsms.modules.stock.web.model.ScannedLotInfo | import com.ffii.fpsms.modules.stock.web.model.ScannedLotInfo | ||||
| @@ -58,7 +57,6 @@ open class InventoryLotLineService( | |||||
| private val warehouseRepository: WarehouseRepository, | private val warehouseRepository: WarehouseRepository, | ||||
| private val itemUomRespository: ItemUomRespository, | private val itemUomRespository: ItemUomRespository, | ||||
| private val stockInLineRepository: StockInLineRepository, | private val stockInLineRepository: StockInLineRepository, | ||||
| private val inventoryRepository: InventoryRepository, | |||||
| private val printerService: PrinterService, | private val printerService: PrinterService, | ||||
| @Lazy | @Lazy | ||||
| private val jobOrderService: JobOrderService | private val jobOrderService: JobOrderService | ||||
| @@ -150,11 +148,15 @@ open class InventoryLotLineService( | |||||
| } | } | ||||
| } | } | ||||
| open fun saveInventoryLotLine(request: SaveInventoryLotLineRequest): InventoryLotLine { | open fun saveInventoryLotLine(request: SaveInventoryLotLineRequest): InventoryLotLine { | ||||
| val inventoryLotLine = | |||||
| request.id?.let { inventoryLotLineRepository.findById(it).getOrNull() } ?: InventoryLotLine() | |||||
| val existing = request.id?.let { inventoryLotLineRepository.findById(it).getOrNull() } | |||||
| val inventoryLotLine = existing ?: 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() } | ||||
| ?: existing?.stockUom | |||||
| if (stockUom == null) { | |||||
| throw IllegalArgumentException("stockItemUomId is required for inventory_lot_line") | |||||
| } | |||||
| val status = request.status?.let { _status -> InventoryLotLineStatus.entries.find { it.value == _status } } | val status = request.status?.let { _status -> InventoryLotLineStatus.entries.find { it.value == _status } } | ||||
| inventoryLotLine.apply { | inventoryLotLine.apply { | ||||
| @@ -191,9 +193,6 @@ open class InventoryLotLineService( | |||||
| val updatedLotLine = saveInventoryLotLine(updateRequest) | val updatedLotLine = saveInventoryLotLine(updateRequest) | ||||
| // ADD THIS: Update inventory table after lot line status change | |||||
| updateInventoryTable(updatedLotLine) | |||||
| return MessageResponse( | return MessageResponse( | ||||
| id = updatedLotLine.id, | id = updatedLotLine.id, | ||||
| name = updatedLotLine.id.toString(), | name = updatedLotLine.id.toString(), | ||||
| @@ -204,45 +203,6 @@ open class InventoryLotLineService( | |||||
| ) | ) | ||||
| } | } | ||||
| // ADD THIS: New method to update inventory table | |||||
| private fun updateInventoryTable(inventoryLotLine: InventoryLotLine) { | |||||
| try { | |||||
| // Get the item ID from the inventory lot | |||||
| val itemId = inventoryLotLine.inventoryLot?.item?.id | |||||
| if (itemId == null) { | |||||
| println("Cannot update inventory table: itemId is null for lot line ${inventoryLotLine.id}") | |||||
| return | |||||
| } | |||||
| // Calculate onHoldQty (sum of holdQty from available lots only) | |||||
| val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE) | |||||
| .sumOf { it.holdQty ?: BigDecimal.ZERO } | |||||
| // Calculate unavailableQty (sum of inQty from unavailable lots only) | |||||
| val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.UNAVAILABLE) | |||||
| .sumOf { | |||||
| val inQty = it.inQty ?: BigDecimal.ZERO | |||||
| val outQty = it.outQty ?: BigDecimal.ZERO | |||||
| val remainingQty = inQty.minus(outQty) | |||||
| remainingQty | |||||
| } | |||||
| // Update the inventory table | |||||
| val inventory = inventoryRepository.findByItemId(itemId).orElse(null) | |||||
| if (inventory != null) { | |||||
| inventory.onHoldQty = onHoldQty | |||||
| inventory.unavailableQty = unavailableQty | |||||
| inventoryRepository.save(inventory) | |||||
| println("Updated inventory for item $itemId: onHoldQty=$onHoldQty, unavailableQty=$unavailableQty") | |||||
| } else { | |||||
| println("Inventory not found for item $itemId") | |||||
| } | |||||
| } catch (e: Exception) { | |||||
| println("Error updating inventory table for lot line ${inventoryLotLine.id}: ${e.message}") | |||||
| e.printStackTrace() | |||||
| } | |||||
| } | |||||
| @Throws(IOException::class) | @Throws(IOException::class) | ||||
| @Transactional | @Transactional | ||||
| open fun exportStockInLineQrcode(request: LotLineToQrcode): Map<String, Any> { | open fun exportStockInLineQrcode(request: LotLineToQrcode): Map<String, Any> { | ||||
| @@ -338,6 +338,9 @@ open class InventoryService( | |||||
| val warehouse = warehouseRepository.findAll().find { it.code == request.warehouseCode }!! | val warehouse = warehouseRepository.findAll().find { it.code == request.warehouseCode }!! | ||||
| // val salesUnit = itemUomService.findSalesUnitByItemId(itemId = inventoryLot.item!!.id!!) | // val salesUnit = itemUomService.findSalesUnitByItemId(itemId = inventoryLot.item!!.id!!) | ||||
| val stockUnit = itemUomService.findStockUnitByItemId(itemId = inventoryLot.item!!.id!!) | val stockUnit = itemUomService.findStockUnitByItemId(itemId = inventoryLot.item!!.id!!) | ||||
| ?: throw IllegalArgumentException( | |||||
| "stockItemUomId is required: no stockUnit item_uom for itemId=${inventoryLot.item!!.id}", | |||||
| ) | |||||
| // ChangeList #8: lot-line stockItemUomId must be set so trigger writes inventory.stockUomId. | // ChangeList #8: lot-line stockItemUomId must be set so trigger writes inventory.stockUomId. | ||||
| InventoryLotLine().apply { | InventoryLotLine().apply { | ||||
| this.inventoryLot = inventoryLot | this.inventoryLot = inventoryLot | ||||
| @@ -538,6 +538,9 @@ open class StockInLineService( | |||||
| this.inQty = convertedBaseQty | this.inQty = convertedBaseQty | ||||
| this.status = InventoryLotLineStatus.AVAILABLE | this.status = InventoryLotLineStatus.AVAILABLE | ||||
| this.stockUom = stockItemUom | this.stockUom = stockItemUom | ||||
| ?: throw IllegalArgumentException( | |||||
| "stockItemUomId is required: no stockUnit item_uom for itemId=${request.itemId}", | |||||
| ) | |||||
| } | } | ||||
| saveLines.add(inventoryLotLine) | saveLines.add(inventoryLotLine) | ||||
| } | } | ||||
| @@ -43,7 +43,6 @@ import com.ffii.fpsms.modules.pickOrder.service.PickOrderService | |||||
| import com.ffii.fpsms.modules.master.service.ItemUomService | import com.ffii.fpsms.modules.master.service.ItemUomService | ||||
| import com.ffii.fpsms.modules.common.SecurityUtils | import com.ffii.fpsms.modules.common.SecurityUtils | ||||
| import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository | import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryRepository | |||||
| import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository | import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssueRepository | ||||
| import java.time.LocalTime | import java.time.LocalTime | ||||
| import com.ffii.fpsms.modules.stock.entity.StockInLineRepository | import com.ffii.fpsms.modules.stock.entity.StockInLineRepository | ||||
| @@ -78,7 +77,6 @@ private val inventoryLotLineService: InventoryLotLineService, | |||||
| private val bagService: BagService, | private val bagService: BagService, | ||||
| private val pickOrderService: PickOrderService, | private val pickOrderService: PickOrderService, | ||||
| private val stockLedgerRepository: StockLedgerRepository, | private val stockLedgerRepository: StockLedgerRepository, | ||||
| private val inventoryRepository: InventoryRepository, | |||||
| private val pickExecutionIssueRepository: PickExecutionIssueRepository, | private val pickExecutionIssueRepository: PickExecutionIssueRepository, | ||||
| private val itemUomService: ItemUomService, | private val itemUomService: ItemUomService, | ||||
| private val inventoryBucketResolver: InventoryBucketResolver, | private val inventoryBucketResolver: InventoryBucketResolver, | ||||
| @@ -266,13 +264,6 @@ fun handleQc(stockOutLine: StockOutLine, request: UpdateStockOutLineRequest): Li | |||||
| this.holdQty = this.holdQty!!.minus((request.qty.div(ratio)).toBigDecimal()) | this.holdQty = this.holdQty!!.minus((request.qty.div(ratio)).toBigDecimal()) | ||||
| } | } | ||||
| inventoryLotLineRepository.save(targetLotLineEntry) | inventoryLotLineRepository.save(targetLotLineEntry) | ||||
| // update inventory | |||||
| val inventory = inventoryRepository.findByItemId(request.itemId).orElseThrow() | |||||
| val inventoryEntry = inventory.apply { | |||||
| this.onHandQty = this.onHandQty!!.minus((request.qty.div(ratio)).toBigDecimal()) | |||||
| this.onHoldQty = this.onHoldQty!!.minus((request.qty.div(ratio)).toBigDecimal()) | |||||
| } | |||||
| inventoryRepository.save(inventoryEntry) | |||||
| return listOf(stockOutLine, newStockOutLine) | return listOf(stockOutLine, newStockOutLine) | ||||
| } | } | ||||
| @Transactional | @Transactional | ||||
| @@ -817,10 +808,6 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { | |||||
| val savedLotLine = inventoryLotLineRepository.save(inventoryLotLine) | val savedLotLine = inventoryLotLineRepository.save(inventoryLotLine) | ||||
| println("Saved lot line: ${savedLotLine.id}, status: ${savedLotLine.status}, holdQty: ${savedLotLine.holdQty}") | println("Saved lot line: ${savedLotLine.id}, status: ${savedLotLine.status}, holdQty: ${savedLotLine.holdQty}") | ||||
| // Step 3: Update inventory table | |||||
| println("Updating inventory table...") | |||||
| updateInventoryTableAfterLotRejection(inventoryLotLine) | |||||
| // Step 4: Trigger resuggest | // Step 4: Trigger resuggest | ||||
| val pickOrderLine = stockOutLine.pickOrderLine | val pickOrderLine = stockOutLine.pickOrderLine | ||||
| if (pickOrderLine?.pickOrder?.id != null) { | if (pickOrderLine?.pickOrder?.id != null) { | ||||
| @@ -839,51 +826,6 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { | |||||
| } | } | ||||
| } | } | ||||
| // ADD THIS: Update inventory table after lot rejection | |||||
| private fun updateInventoryTableAfterLotRejection(inventoryLotLine: InventoryLotLine) { | |||||
| try { | |||||
| println("=== UPDATING INVENTORY TABLE ===") | |||||
| val itemId = inventoryLotLine.inventoryLot?.item?.id | |||||
| println("Item ID: $itemId") | |||||
| if (itemId != null) { | |||||
| // Calculate onHoldQty (sum of holdQty from available lots only) | |||||
| val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE) | |||||
| .sumOf { it.holdQty ?: BigDecimal.ZERO } | |||||
| // Calculate unavailableQty (sum of inQty from unavailable lots only) | |||||
| val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId,InventoryLotLineStatus.UNAVAILABLE) | |||||
| .sumOf { | |||||
| val inQty = it.inQty ?: BigDecimal.ZERO | |||||
| val outQty = it.outQty ?: BigDecimal.ZERO | |||||
| val remainingQty = inQty.minus(outQty) | |||||
| remainingQty | |||||
| } | |||||
| println("Calculated onHoldQty: $onHoldQty") | |||||
| println("Calculated unavailableQty: $unavailableQty") | |||||
| // Update the inventory table | |||||
| val inventory = inventoryRepository.findByItemId(itemId).orElse(null) | |||||
| if (inventory != null) { | |||||
| println("Found inventory record: ${inventory.id}") | |||||
| println("Current inventory - onHoldQty: ${inventory.onHoldQty}, unavailableQty: ${inventory.unavailableQty}") | |||||
| inventory.onHoldQty = onHoldQty | |||||
| inventory.unavailableQty = unavailableQty | |||||
| val savedInventory = inventoryRepository.save(inventory) | |||||
| println("Updated inventory - onHoldQty: ${savedInventory.onHoldQty}, unavailableQty: ${savedInventory.unavailableQty}") | |||||
| } else { | |||||
| println("No inventory record found for item $itemId") | |||||
| } | |||||
| } | |||||
| } catch (e: Exception) { | |||||
| println("Error updating inventory table after lot rejection: ${e.message}") | |||||
| e.printStackTrace() | |||||
| } | |||||
| } | |||||
| @Transactional(rollbackFor = [Exception::class]) | @Transactional(rollbackFor = [Exception::class]) | ||||
| open fun batchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | open fun batchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | ||||
| val startTime = System.currentTimeMillis() | val startTime = System.currentTimeMillis() | ||||
| @@ -955,14 +897,6 @@ open fun batchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | |||||
| this.holdQty = (this.holdQty ?: zero) - baseQty | this.holdQty = (this.holdQty ?: zero) - baseQty | ||||
| } | } | ||||
| inventoryLotLineRepository.save(lotLine) | inventoryLotLineRepository.save(lotLine) | ||||
| // 更新 inventory | |||||
| val inv = inventoryRepository.findByItemId(lotLine.inventoryLot?.item?.id!!).orElseThrow() | |||||
| inv.apply { | |||||
| this.onHandQty = (this.onHandQty ?: zero) - baseQty | |||||
| this.onHoldQty = (this.onHoldQty ?: zero) - baseQty | |||||
| } | |||||
| inventoryRepository.save(inv) | |||||
| } | } | ||||
| processedIds += line.stockOutLineId | processedIds += line.stockOutLineId | ||||
| @@ -2156,7 +2090,7 @@ fun applyStockOutLineDelta( | |||||
| val postingType = (typeOverride ?: savedSol.type ?: "").trim().lowercase() | val postingType = (typeOverride ?: savedSol.type ?: "").trim().lowercase() | ||||
| val isIssuePosting = postingType == "miss" || postingType == "bad" || postingType == "expiry" | val isIssuePosting = postingType == "miss" || postingType == "bad" || postingType == "expiry" | ||||
| // 2) inventory_lot_line + inventory | |||||
| // 2) inventory_lot_line; inventory onHand/unavailable is owned by the lot-line trigger | |||||
| if (!skipInventoryWrite) { | if (!skipInventoryWrite) { | ||||
| val lotLine = savedSol.inventoryLotLine | val lotLine = savedSol.inventoryLotLine | ||||
| if (lotLine != null) { | if (lotLine != null) { | ||||
| @@ -2196,19 +2130,6 @@ fun applyStockOutLineDelta( | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| val itemId = savedSol.item?.id | |||||
| if (itemId != null) { | |||||
| val inv = inventoryBucketResolver.findInventoryBucket(itemId, savedSol.inventoryLotLine) | |||||
| if (inv != null) { | |||||
| val zero = BigDecimal.ZERO | |||||
| inv.onHandQty = (inv.onHandQty ?: zero).minus(deltaQty) | |||||
| if (!isIssuePosting) { | |||||
| inv.onHoldQty = (inv.onHoldQty ?: zero).minus(deltaQty) | |||||
| } | |||||
| inventoryRepository.save(inv) | |||||
| } | |||||
| } | |||||
| } | } | ||||
| // 3) stock_ledger (same ledger shape as createStockLedgerForStockOut / completeStockOutAfterLotLineSave) | // 3) stock_ledger (same ledger shape as createStockLedgerForStockOut / completeStockOutAfterLotLineSave) | ||||
| @@ -65,7 +65,8 @@ open class StockTakeRecordService( | |||||
| val stockLedgerRepository: StockLedgerRepository, | val stockLedgerRepository: StockLedgerRepository, | ||||
| val inventoryRepository: InventoryRepository, | val inventoryRepository: InventoryRepository, | ||||
| val uomConversionRepository: UomConversionRepository, | val uomConversionRepository: UomConversionRepository, | ||||
| val itemsRepository: ItemsRepository | |||||
| val itemsRepository: ItemsRepository, | |||||
| val inventoryBucketResolver: InventoryBucketResolver, | |||||
| ) { | ) { | ||||
| private val logger: Logger = LoggerFactory.getLogger(StockTakeRecordService::class.java) | private val logger: Logger = LoggerFactory.getLogger(StockTakeRecordService::class.java) | ||||
| @@ -2125,7 +2126,7 @@ open fun batchSaveApproverStockTakeRecordsByIds( | |||||
| "batchSaveApproverStockTakeRecordsByIds cache build completed: lotLinePairs={}, lots={}, inventories={}, stockTakeLines={}, cacheBuildMs={}", | "batchSaveApproverStockTakeRecordsByIds cache build completed: lotLinePairs={}, lots={}, inventories={}, stockTakeLines={}, cacheBuildMs={}", | ||||
| adjustmentCache.inventoryLotLineByWarehouseLot.size, | adjustmentCache.inventoryLotLineByWarehouseLot.size, | ||||
| adjustmentCache.inventoryLotById.size, | adjustmentCache.inventoryLotById.size, | ||||
| adjustmentCache.inventoryByItemId.size, | |||||
| adjustmentCache.inventoryByItemAndStockUom.size, | |||||
| adjustmentCache.stockTakeLineByRecordId.size, | adjustmentCache.stockTakeLineByRecordId.size, | ||||
| elapsedMs(cacheBuildStartNs) | elapsedMs(cacheBuildStartNs) | ||||
| ) | ) | ||||
| @@ -2213,7 +2214,7 @@ open fun batchSaveApproverStockTakeRecordsByIds( | |||||
| val runtimeCache = BatchAdjustmentRuntimeCache( | val runtimeCache = BatchAdjustmentRuntimeCache( | ||||
| stockOutByStockTakeId = mutableMapOf(), | stockOutByStockTakeId = mutableMapOf(), | ||||
| stockInByStockTakeId = mutableMapOf(), | stockInByStockTakeId = mutableMapOf(), | ||||
| runningLedgerBalanceByItemId = mutableMapOf() | |||||
| runningLedgerBalanceByBucket = mutableMapOf() | |||||
| ) | ) | ||||
| val adjustmentContext = StockTakeAdjustmentBatchContext() | val adjustmentContext = StockTakeAdjustmentBatchContext() | ||||
| var varianceCount = 0 | var varianceCount = 0 | ||||
| @@ -2278,7 +2279,7 @@ open fun batchSaveApproverStockTakeRecordsByIds( | |||||
| "batchSaveApproverStockTakeRecordsByIds runtime cache stats: stockOutHeads={}, stockInHeads={}, ledgerItems={}, batchedStockTakeLines={}, batchedOutLines={}, batchedInLines={}, batchedInventoryLotLines={}, batchedLedgers={}", | "batchSaveApproverStockTakeRecordsByIds runtime cache stats: stockOutHeads={}, stockInHeads={}, ledgerItems={}, batchedStockTakeLines={}, batchedOutLines={}, batchedInLines={}, batchedInventoryLotLines={}, batchedLedgers={}", | ||||
| runtimeCache.stockOutByStockTakeId.size, | runtimeCache.stockOutByStockTakeId.size, | ||||
| runtimeCache.stockInByStockTakeId.size, | runtimeCache.stockInByStockTakeId.size, | ||||
| runtimeCache.runningLedgerBalanceByItemId.size, | |||||
| runtimeCache.runningLedgerBalanceByBucket.size, | |||||
| adjustmentContext.stockTakeLineByRecordId.size, | adjustmentContext.stockTakeLineByRecordId.size, | ||||
| adjustmentContext.stockOutLines.size, | adjustmentContext.stockOutLines.size, | ||||
| adjustmentContext.stockInLines.size, | adjustmentContext.stockInLines.size, | ||||
| @@ -2398,9 +2399,7 @@ private fun applyVarianceAdjustment( | |||||
| ?: throw IllegalArgumentException("Inventory lot not found") | ?: throw IllegalArgumentException("Inventory lot not found") | ||||
| val itemId = inventoryLot.item?.id ?: throw IllegalArgumentException("Item ID not found") | val itemId = inventoryLot.item?.id ?: throw IllegalArgumentException("Item ID not found") | ||||
| val inventory = cache?.inventoryByItemId?.get(itemId) | |||||
| ?: inventoryRepository.findByItemId(itemId).orElse(null) | |||||
| ?: throw IllegalArgumentException("Inventory not found for item") | |||||
| val inventory = resolveInventoryForStockTake(itemId, inventoryLotLine, cache) | |||||
| // 1. 更新開輪預建的 StockTakeLine,或舊資料無預建時新建 | // 1. 更新開輪預建的 StockTakeLine,或舊資料無預建時新建 | ||||
| val stockTakeLine = stockTakeRecord.id?.let { rid -> | val stockTakeLine = stockTakeRecord.id?.let { rid -> | ||||
| @@ -2481,11 +2480,9 @@ private fun applyVarianceAdjustment( | |||||
| // 避免同一批多筆盤虧時每筆都用同一個 inventory.onHandQty 導致 balance 錯誤。 | // 避免同一批多筆盤虧時每筆都用同一個 inventory.onHandQty 導致 balance 錯誤。 | ||||
| val itemIdForLedger = inventoryLot.item?.id | val itemIdForLedger = inventoryLot.item?.id | ||||
| ?: throw IllegalArgumentException("Item ID not found for stock take ledger") | ?: throw IllegalArgumentException("Item ID not found for stock take ledger") | ||||
| val previousBalance = runtimeCache?.runningLedgerBalanceByItemId?.get(itemIdForLedger) | |||||
| ?: run { | |||||
| val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemIdForLedger) | |||||
| latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() | |||||
| } | |||||
| val ledgerKey = stockTakeLedgerBucketKey(itemIdForLedger, latestLine, inventory) | |||||
| val previousBalance = runtimeCache?.runningLedgerBalanceByBucket?.get(ledgerKey) | |||||
| ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() | |||||
| val newBalance = previousBalance - qtyToRemove.toDouble() | val newBalance = previousBalance - qtyToRemove.toDouble() | ||||
| // FP-MTMS Version Checklist | Functions Ref. No. 46 | v1.0.1 | 2026-08-05 | // FP-MTMS Version Checklist | Functions Ref. No. 46 | v1.0.1 | 2026-08-05 | ||||
| @@ -2519,7 +2516,7 @@ private fun applyVarianceAdjustment( | |||||
| } else { | } else { | ||||
| stockLedgerRepository.save(stockLedger) | stockLedgerRepository.save(stockLedger) | ||||
| } | } | ||||
| runtimeCache?.runningLedgerBalanceByItemId?.put(itemIdForLedger, newBalance) | |||||
| runtimeCache?.runningLedgerBalanceByBucket?.put(ledgerKey, newBalance) | |||||
| val newOutQty = (latestLine.outQty ?: zero).add(qtyToRemove) | val newOutQty = (latestLine.outQty ?: zero).add(qtyToRemove) | ||||
| latestLine.outQty = newOutQty | latestLine.outQty = newOutQty | ||||
| @@ -2619,11 +2616,9 @@ private fun applyVarianceAdjustment( | |||||
| val itemIdForLedger = inventoryLot.item?.id | val itemIdForLedger = inventoryLot.item?.id | ||||
| ?: throw IllegalArgumentException("Item ID not found for stock take ledger (in)") | ?: throw IllegalArgumentException("Item ID not found for stock take ledger (in)") | ||||
| val previousBalance = runtimeCache?.runningLedgerBalanceByItemId?.get(itemIdForLedger) | |||||
| ?: run { | |||||
| val latestLedger = stockLedgerRepository.findFirstByItemIdAndDeletedFalseOrderByDateDescIdDesc(itemIdForLedger) | |||||
| latestLedger?.balance ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() | |||||
| } | |||||
| val ledgerKey = stockTakeLedgerBucketKey(itemIdForLedger, latestLine, inventory) | |||||
| val previousBalance = runtimeCache?.runningLedgerBalanceByBucket?.get(ledgerKey) | |||||
| ?: (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() | |||||
| val newBalance = previousBalance + plusQty.toDouble() | val newBalance = previousBalance + plusQty.toDouble() | ||||
| // latestLine.inQty already includes plusQty above — snapshot live lot line after move. | // latestLine.inQty already includes plusQty above — snapshot live lot line after move. | ||||
| @@ -2656,21 +2651,46 @@ private fun applyVarianceAdjustment( | |||||
| } else { | } else { | ||||
| stockLedgerRepository.save(stockLedger) | stockLedgerRepository.save(stockLedger) | ||||
| } | } | ||||
| runtimeCache?.runningLedgerBalanceByItemId?.put(itemIdForLedger, newBalance) | |||||
| runtimeCache?.runningLedgerBalanceByBucket?.put(ledgerKey, newBalance) | |||||
| } | } | ||||
| } | } | ||||
| private fun resolveInventoryForStockTake( | |||||
| itemId: Long, | |||||
| lotLine: InventoryLotLine, | |||||
| cache: BatchAdjustmentCache?, | |||||
| ): com.ffii.fpsms.modules.stock.entity.Inventory { | |||||
| val stockUomId = inventoryBucketResolver.resolveStockUomId(lotLine) | |||||
| if (stockUomId != null) { | |||||
| cache?.inventoryByItemAndStockUom?.get(itemId to stockUomId)?.let { return it } | |||||
| inventoryBucketResolver.findInventoryBucket(itemId, stockUomId)?.let { return it } | |||||
| } | |||||
| inventoryBucketResolver.findInventoryBucket(itemId, lotLine)?.let { return it } | |||||
| throw IllegalArgumentException("Inventory not found for item $itemId stock UOM $stockUomId") | |||||
| } | |||||
| private fun stockTakeLedgerBucketKey( | |||||
| itemId: Long, | |||||
| lotLine: InventoryLotLine, | |||||
| inventory: com.ffii.fpsms.modules.stock.entity.Inventory, | |||||
| ): Pair<Long, Long> { | |||||
| val stockUomId = inventoryBucketResolver.resolveStockUomId(lotLine) | |||||
| ?: inventory.stockUom?.id | |||||
| ?: 0L | |||||
| return itemId to stockUomId | |||||
| } | |||||
| private data class BatchAdjustmentCache( | private data class BatchAdjustmentCache( | ||||
| val inventoryLotLineByWarehouseLot: Map<Pair<Long, Long>, InventoryLotLine>, | val inventoryLotLineByWarehouseLot: Map<Pair<Long, Long>, InventoryLotLine>, | ||||
| val inventoryLotById: Map<Long, InventoryLot>, | val inventoryLotById: Map<Long, InventoryLot>, | ||||
| val inventoryByItemId: Map<Long, com.ffii.fpsms.modules.stock.entity.Inventory>, | |||||
| val inventoryByItemAndStockUom: Map<Pair<Long, Long>, com.ffii.fpsms.modules.stock.entity.Inventory>, | |||||
| val stockTakeLineByRecordId: Map<Long, StockTakeLine> | val stockTakeLineByRecordId: Map<Long, StockTakeLine> | ||||
| ) | ) | ||||
| private data class BatchAdjustmentRuntimeCache( | private data class BatchAdjustmentRuntimeCache( | ||||
| val stockOutByStockTakeId: MutableMap<Long, StockOut>, | val stockOutByStockTakeId: MutableMap<Long, StockOut>, | ||||
| val stockInByStockTakeId: MutableMap<Long, StockIn>, | val stockInByStockTakeId: MutableMap<Long, StockIn>, | ||||
| val runningLedgerBalanceByItemId: MutableMap<Long, Double> | |||||
| val runningLedgerBalanceByBucket: MutableMap<Pair<Long, Long>, Double> | |||||
| ) | ) | ||||
| private data class StockTakeAdjustmentBatchContext( | private data class StockTakeAdjustmentBatchContext( | ||||
| @@ -2725,10 +2745,15 @@ private fun buildBatchAdjustmentCache(records: List<StockTakeRecord>): BatchAdju | |||||
| emptyMap() | emptyMap() | ||||
| } | } | ||||
| val itemIds = inventoryLotById.values.mapNotNull { it.item?.id }.distinct() | val itemIds = inventoryLotById.values.mapNotNull { it.item?.id }.distinct() | ||||
| val inventoryByItemId = | |||||
| val inventoryByItemAndStockUom = | |||||
| if (itemIds.isNotEmpty()) { | if (itemIds.isNotEmpty()) { | ||||
| inventoryRepository.findAllByItemIdInAndDeletedIsFalse(itemIds) | inventoryRepository.findAllByItemIdInAndDeletedIsFalse(itemIds) | ||||
| .groupBy { it.item?.id ?: 0L } | |||||
| .mapNotNull { inv -> | |||||
| val itemId = inv.item?.id ?: return@mapNotNull null | |||||
| val stockUomId = inv.stockUom?.id ?: inv.uom?.id ?: return@mapNotNull null | |||||
| (itemId to stockUomId) to inv | |||||
| } | |||||
| .groupBy({ it.first }, { it.second }) | |||||
| .mapValues { (_, list) -> list.minByOrNull { it.id ?: Long.MAX_VALUE }!! } | .mapValues { (_, list) -> list.minByOrNull { it.id ?: Long.MAX_VALUE }!! } | ||||
| } else { | } else { | ||||
| emptyMap() | emptyMap() | ||||
| @@ -2745,7 +2770,7 @@ private fun buildBatchAdjustmentCache(records: List<StockTakeRecord>): BatchAdju | |||||
| return BatchAdjustmentCache( | return BatchAdjustmentCache( | ||||
| inventoryLotLineByWarehouseLot = inventoryLotLineByWarehouseLot, | inventoryLotLineByWarehouseLot = inventoryLotLineByWarehouseLot, | ||||
| inventoryLotById = inventoryLotById, | inventoryLotById = inventoryLotById, | ||||
| inventoryByItemId = inventoryByItemId, | |||||
| inventoryByItemAndStockUom = inventoryByItemAndStockUom, | |||||
| stockTakeLineByRecordId = stockTakeLineByRecordId | stockTakeLineByRecordId = stockTakeLineByRecordId | ||||
| ) | ) | ||||
| } | } | ||||
| @@ -28,8 +28,7 @@ import kotlin.jvm.optionals.getOrNull | |||||
| import org.springframework.transaction.annotation.Transactional | import org.springframework.transaction.annotation.Transactional | ||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | import com.ffii.fpsms.modules.master.web.models.MessageResponse | ||||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | ||||
| import java.math.RoundingMode | |||||
| import com.ffii.fpsms.modules.stock.entity.InventoryRepository | |||||
| import java.math.RoundingMode | |||||
| import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus | import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus | ||||
| import com.ffii.fpsms.modules.stock.web.model.PickAnotherLotRequest | import com.ffii.fpsms.modules.stock.web.model.PickAnotherLotRequest | ||||
| import com.ffii.fpsms.modules.stock.service.InventoryLotLineService | import com.ffii.fpsms.modules.stock.service.InventoryLotLineService | ||||
| @@ -54,7 +53,6 @@ open class SuggestedPickLotService( | |||||
| val itemUomService: ItemUomService, | val itemUomService: ItemUomService, | ||||
| val pickExecutionIssueRepository: PickExecutionIssueRepository, // 添加逗号 | val pickExecutionIssueRepository: PickExecutionIssueRepository, // 添加逗号 | ||||
| val pickOrderRepository: PickOrderRepository, | val pickOrderRepository: PickOrderRepository, | ||||
| val inventoryRepository: InventoryRepository, | |||||
| val failInventoryLotLineRepository: FailInventoryLotLineRepository, | val failInventoryLotLineRepository: FailInventoryLotLineRepository, | ||||
| val stockOutRepository: StockOutRepository, | val stockOutRepository: StockOutRepository, | ||||
| val itemRepository: ItemsRepository, | val itemRepository: ItemsRepository, | ||||
| @@ -1073,38 +1071,6 @@ val basePickOrders = (listOf(pickOrder) + allCompetingPickOrders).distinctBy { i | |||||
| } | } | ||||
| } | } | ||||
| // ✅ 优化:批量更新 inventory table(收集所有 item IDs,只更新一次) | |||||
| val allItemIdsToUpdate = allPickOrdersToResuggest | |||||
| .flatMap { it.pickOrderLines } | |||||
| .mapNotNull { it.item?.id } | |||||
| .distinct() | |||||
| if (allItemIdsToUpdate.isNotEmpty()) { | |||||
| println("=== Batch updating inventory table for ${allItemIdsToUpdate.size} items ===") | |||||
| allItemIdsToUpdate.forEach { itemId -> | |||||
| // Calculate onHoldQty for ALL pick orders that use this item | |||||
| // ✅ FIX: Use enum directly, not .value | |||||
| val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE) | |||||
| .sumOf { it.holdQty ?: BigDecimal.ZERO } | |||||
| // ✅ FIX: Use enum directly, not .value | |||||
| val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.UNAVAILABLE) | |||||
| .sumOf { | |||||
| val inQty = it.inQty ?: BigDecimal.ZERO | |||||
| val outQty = it.outQty ?: BigDecimal.ZERO | |||||
| val remainingQty = inQty.minus(outQty) | |||||
| remainingQty | |||||
| } | |||||
| val inventory = inventoryRepository.findByItemId(itemId).orElse(null) | |||||
| if (inventory != null) { | |||||
| inventory.onHoldQty = onHoldQty | |||||
| inventory.unavailableQty = unavailableQty | |||||
| inventoryRepository.save(inventory) | |||||
| } | |||||
| } | |||||
| println("✅ Batch updated inventory table for ${allItemIdsToUpdate.size} items") | |||||
| } | |||||
| println("=== RESUGGEST DEBUG END ===") | println("=== RESUGGEST DEBUG END ===") | ||||
| return MessageResponse( | return MessageResponse( | ||||
| @@ -1345,40 +1311,6 @@ private fun generateOptimalSuggestionsForAllPickOrders( | |||||
| return suggestions | return suggestions | ||||
| } | } | ||||
| private fun updateInventoryTableAfterResuggest(pickOrder: PickOrder) { | |||||
| try { | |||||
| // Get all item IDs from the pick order | |||||
| val itemIds = pickOrder.pickOrderLines.mapNotNull { it.item?.id }.distinct() | |||||
| itemIds.forEach { itemId -> | |||||
| // FIX: Calculate onHoldQty for ALL pick orders that use this item, not just the current one | |||||
| val onHoldQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.AVAILABLE) | |||||
| .sumOf { it.holdQty ?: BigDecimal.ZERO } | |||||
| // FIX: Use enum method instead of string method | |||||
| val unavailableQty = inventoryLotLineRepository.findAllByInventoryLotItemIdAndStatus(itemId, InventoryLotLineStatus.UNAVAILABLE) | |||||
| .sumOf { | |||||
| val inQty = it.inQty ?: BigDecimal.ZERO | |||||
| val outQty = it.outQty ?: BigDecimal.ZERO | |||||
| val remainingQty = inQty.minus(outQty) | |||||
| remainingQty | |||||
| } | |||||
| // Update the inventory table | |||||
| val inventory = inventoryRepository.findByItemId(itemId).orElse(null) | |||||
| if (inventory != null) { | |||||
| inventory.onHoldQty = onHoldQty | |||||
| inventory.unavailableQty = unavailableQty | |||||
| inventoryRepository.save(inventory) | |||||
| println("Updated inventory for item $itemId: onHoldQty=$onHoldQty, unavailableQty=$unavailableQty") | |||||
| } | |||||
| } | |||||
| } catch (e: Exception) { | |||||
| println("Error updating inventory table after resuggest: ${e.message}") | |||||
| e.printStackTrace() | |||||
| } | |||||
| } | |||||
| private fun findAllSuggestionsForPickOrder(pickOrderId: Long): List<SuggestedPickLot> { | private fun findAllSuggestionsForPickOrder(pickOrderId: Long): List<SuggestedPickLot> { | ||||
| val pickOrderLines = pickOrderRepository.findById(pickOrderId) | val pickOrderLines = pickOrderRepository.findById(pickOrderId) | ||||
| .orElseThrow() | .orElseThrow() | ||||
| @@ -1802,27 +1734,9 @@ private fun handleMissingItem(inventoryLotLine: InventoryLotLine, failQty: BigDe | |||||
| // 处理损坏物品的情况 | // 处理损坏物品的情况 | ||||
| private fun handleBrokenItem(inventoryLotLine: InventoryLotLine, failQty: BigDecimal) { | private fun handleBrokenItem(inventoryLotLine: InventoryLotLine, failQty: BigDecimal) { | ||||
| try { | try { | ||||
| // 1. 将库存批次行标记为不可用 | |||||
| inventoryLotLine.status = InventoryLotLineStatus.UNAVAILABLE | inventoryLotLine.status = InventoryLotLineStatus.UNAVAILABLE | ||||
| // 2. 更新库存表的不可用数量 | |||||
| val itemId = inventoryLotLine.inventoryLot?.item?.id | |||||
| if (itemId != null) { | |||||
| val inventory = inventoryRepository.findByItemId(itemId).orElse(null) | |||||
| if (inventory != null) { | |||||
| val currentUnavailableQty = inventory.unavailableQty ?: BigDecimal.ZERO | |||||
| inventory.unavailableQty = currentUnavailableQty.plus(failQty) | |||||
| inventoryRepository.save(inventory) | |||||
| println("Broken item handled: Updated unavailableQty by $failQty for item $itemId") | |||||
| } | |||||
| } | |||||
| // 3. 保存库存批次行的状态变更 | |||||
| inventoryLotLineRepository.save(inventoryLotLine) | inventoryLotLineRepository.save(inventoryLotLine) | ||||
| println("Broken item handled: Marked lot line ${inventoryLotLine.id} as unavailable") | println("Broken item handled: Marked lot line ${inventoryLotLine.id} as unavailable") | ||||
| } catch (e: Exception) { | } catch (e: Exception) { | ||||
| println("Error handling broken item: ${e.message}") | println("Error handling broken item: ${e.message}") | ||||
| throw e | throw e | ||||