| @@ -654,6 +654,7 @@ open class PickOrderService( | |||||
| println(pos) | println(pos) | ||||
| val groupsByPickOrderId = pickOrderGroupRepository.findByPickOrderIdInAndDeletedIsFalse(ids) | val groupsByPickOrderId = pickOrderGroupRepository.findByPickOrderIdInAndDeletedIsFalse(ids) | ||||
| .groupBy { it.pickOrderId } | .groupBy { it.pickOrderId } | ||||
| // Get Inventory Data | // Get Inventory Data | ||||
| val requiredItems = pos | val requiredItems = pos | ||||
| .flatMap { it.pickOrderLines } | .flatMap { it.pickOrderLines } | ||||
| @@ -666,13 +667,10 @@ open class PickOrderService( | |||||
| override val uomDesc: String? = value[0].uom?.udfudesc | override val uomDesc: String? = value[0].uom?.udfudesc | ||||
| 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 | } // itemId - requiredQty | ||||
| val itemIds = requiredItems.mapNotNull { it.first } | val itemIds = requiredItems.mapNotNull { it.first } | ||||
| // val inventories = inventoryLotLineRepository.findCurrentInventoryByItems(itemIds) | |||||
| // val inventories = inventoryService.allInventoriesByItemIds(itemIds) | |||||
| val inventories = inventoryLotLineService | val inventories = inventoryLotLineService | ||||
| .allInventoryLotLinesByItemIdIn(itemIds) | .allInventoryLotLinesByItemIdIn(itemIds) | ||||
| .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | ||||
| @@ -680,37 +678,71 @@ open class PickOrderService( | |||||
| .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) } | .filter { it.expiryDate.isAfter(today) || it.expiryDate.isEqual(today) } | ||||
| .sortedBy { it.expiryDate } | .sortedBy { it.expiryDate } | ||||
| .groupBy { it.item?.id } | .groupBy { it.item?.id } | ||||
| val suggestions = | val suggestions = | ||||
| suggestedPickLotService.suggestionForPickOrders(SuggestedPickLotForPoRequest(pickOrders = pos)) | suggestedPickLotService.suggestionForPickOrders(SuggestedPickLotForPoRequest(pickOrders = pos)) | ||||
| // ✅ Get stock out line data for each pick order line | |||||
| val pickOrderLineIds = pos.flatMap { it.pickOrderLines }.mapNotNull { it.id } | |||||
| val stockOutLinesByPickOrderLineId = if (pickOrderLineIds.isNotEmpty()) { | |||||
| pickOrderLineIds.associateWith { pickOrderLineId -> | |||||
| stockOutLIneRepository.findAllByPickOrderLineIdAndDeletedFalse(pickOrderLineId) | |||||
| } | |||||
| } else { | |||||
| emptyMap() | |||||
| } | |||||
| // 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 inventory = pol.item?.id?.let { inventories[it] } | ||||
| val itemUom = pol.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | val itemUom = pol.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | ||||
| // val inventory = inventories.find { it.itemId == pol.item?.id } | |||||
| // ✅ Fix: Convert availableQty from base units to sales units | |||||
| val convertedAvailableQty = inventory?.sumOf { i -> | |||||
| val inQty = i.inQty ?: zero | |||||
| val outQty = i.outQty ?: zero | |||||
| val holdQty = i.holdQty ?: zero | |||||
| val baseQty = inQty.minus(outQty).minus(holdQty) | |||||
| println("Item ID: ${i.item?.id}, InQty: $inQty, OutQty: $outQty, HoldQty: $holdQty, BaseQty: $baseQty") | |||||
| // Apply unit conversion if needed | |||||
| val itemUom = pol.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | |||||
| val ratioN = itemUom?.ratioN | |||||
| val ratioD = itemUom?.ratioD | |||||
| println("Item ID: ${pol.item?.id}, RatioN: $ratioN, RatioD: $ratioD") | |||||
| val convertedQty: BigDecimal = if (ratioN != null && ratioD != null && ratioD != zero) { | |||||
| baseQty.divide(ratioN.divide(ratioD, 10, java.math.RoundingMode.HALF_UP), 2, java.math.RoundingMode.HALF_UP) | |||||
| } else { | |||||
| baseQty | |||||
| } | |||||
| // Ensure the lambda returns BigDecimal | |||||
| convertedQty | |||||
| } | |||||
| // ✅ Calculate total picked quantity from stock out lines | |||||
| val stockOutLines = stockOutLinesByPickOrderLineId[pol.id] ?: emptyList() | |||||
| val totalPickedQty = stockOutLines.sumOf { it.qty ?: zero } | |||||
| // Return | // Return | ||||
| 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?.availableQty, | |||||
| availableQty = inventory?.sumOf { i -> (i.availableQty ?: zero) }, | |||||
| // availableQty = inventory?.sumOf { i -> (i.availableQty ?: zero) * (itemUom?.ratioN ?: one) * (itemUom?.ratioD ?: one) }, | |||||
| availableQty = convertedAvailableQty, // ✅ Use converted quantity | |||||
| requiredQty = pol.qty, | requiredQty = pol.qty, | ||||
| uomCode = pol.uom?.code, | uomCode = pol.uom?.code, | ||||
| uomDesc = pol.uom?.udfudesc, | uomDesc = pol.uom?.udfudesc, | ||||
| suggestedList = suggestions.suggestedList.filter { it.pickOrderLine?.id == pol.id } | |||||
| suggestedList = suggestions.suggestedList.filter { it.pickOrderLine?.id == pol.id }, | |||||
| // ✅ Add picked quantity data | |||||
| pickedQty = totalPickedQty | |||||
| ) | ) | ||||
| // } | |||||
| } | } | ||||
| val groupName = po.id?.let { pickOrderId -> | val groupName = po.id?.let { pickOrderId -> | ||||
| groupsByPickOrderId[pickOrderId]?.firstOrNull()?.name | |||||
| } ?: "No Group" | |||||
| groupsByPickOrderId[pickOrderId]?.firstOrNull()?.name | |||||
| } ?: "No Group" | |||||
| // Return | // Return | ||||
| GetPickOrderInfo( | GetPickOrderInfo( | ||||
| id = po.id, | id = po.id, | ||||
| @@ -723,34 +755,37 @@ open class PickOrderService( | |||||
| pickOrderLines = releasePickOrderLineInfos | pickOrderLines = releasePickOrderLineInfos | ||||
| ) | ) | ||||
| } | } | ||||
| // Items | // Items | ||||
| val currentInventoryInfos = requiredItems.map { item -> | val currentInventoryInfos = requiredItems.map { item -> | ||||
| // val inventory = inventories | |||||
| // .find { it.itemId == item.first } | |||||
| val inventory = item.first?.let { inventories[it] } | val inventory = item.first?.let { inventories[it] } | ||||
| val itemUom = item.first?.let { itemUomService.findSalesUnitByItemId(it) } | val itemUom = item.first?.let { itemUomService.findSalesUnitByItemId(it) } | ||||
| 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 | |||||
| val convertedAvailableQty = inventory?.sumOf { i -> | |||||
| val baseQty = (i.availableQty ?: zero) | |||||
| val ratioN = itemUom?.ratioN | |||||
| val ratioD = itemUom?.ratioD | |||||
| if (ratioN != null && ratioD != null && ratioD != zero) { | |||||
| baseQty.divide(ratioN.divide(ratioD, 10, java.math.RoundingMode.HALF_UP), 2, java.math.RoundingMode.HALF_UP) | |||||
| } else { | |||||
| baseQty | |||||
| } | |||||
| } | |||||
| it.availableQty = convertedAvailableQty | |||||
| it | it | ||||
| } | } | ||||
| } | } | ||||
| val consoCode = pos.firstOrNull()?.consoCode | |||||
| return GetPickOrderInfoResponse( | return GetPickOrderInfoResponse( | ||||
| // consoCode = consoCode, | |||||
| consoCode = consoCode, | |||||
| pickOrders = releasePickOrderInfos, | pickOrders = releasePickOrderInfos, | ||||
| items = currentInventoryInfos, | items = currentInventoryInfos, | ||||
| ) | ) | ||||
| } | } | ||||
| open fun getAllPickOrdersInfo(): GetPickOrderInfoResponse { | open fun getAllPickOrdersInfo(): GetPickOrderInfoResponse { | ||||
| // 使用现有的查询方法获取所有 Pick Orders,然后在内存中过滤 | // 使用现有的查询方法获取所有 Pick Orders,然后在内存中过滤 | ||||
| val allPickOrders = pickOrderRepository.findAll() | val allPickOrders = pickOrderRepository.findAll() | ||||
| @@ -763,6 +798,7 @@ open class PickOrderService( | |||||
| // 如果没有任何已发布的 Pick Orders,返回空结果 | // 如果没有任何已发布的 Pick Orders,返回空结果 | ||||
| if (releasedPickOrderIds.isEmpty()) { | if (releasedPickOrderIds.isEmpty()) { | ||||
| return GetPickOrderInfoResponse( | return GetPickOrderInfoResponse( | ||||
| consoCode = null, | |||||
| pickOrders = emptyList(), | pickOrders = emptyList(), | ||||
| items = emptyList() | items = emptyList() | ||||
| ) | ) | ||||
| @@ -779,150 +815,72 @@ open class PickOrderService( | |||||
| println("pickOrderLineId: $pickOrderLineId") | println("pickOrderLineId: $pickOrderLineId") | ||||
| println("today: $today") | println("today: $today") | ||||
| // 检查具体的数量字段值 | |||||
| val quantityCheckSql = """ | |||||
| SELECT | |||||
| spl.id as suggestedPickLotId, | |||||
| ill.id as lotLineId, | |||||
| ill.inQty, | |||||
| ill.outQty, | |||||
| ill.holdQty, | |||||
| COALESCE(ill.inQty, 0) as inQtyCoalesced, | |||||
| COALESCE(ill.outQty, 0) as outQtyCoalesced, | |||||
| COALESCE(ill.holdQty, 0) as holdQtyCoalesced, | |||||
| (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQtyCalculated | |||||
| FROM fpsmsdb.suggested_pick_lot spl | |||||
| JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId | |||||
| WHERE spl.pickOrderLineId = :pickOrderLineId | |||||
| AND ill.status = 'available' | |||||
| """.trimIndent() | |||||
| val quantityCheckResult = jdbcDao.queryForList(quantityCheckSql, mapOf("pickOrderLineId" to pickOrderLineId)) | |||||
| println("Quantity check result:") | |||||
| quantityCheckResult.forEach { row -> | |||||
| println("Row: $row") | |||||
| // ✅ 重新添加:首先检查是否需要 resuggest | |||||
| val needsResuggest = checkIfNeedsResuggest(pickOrderLineId) | |||||
| if (needsResuggest) { | |||||
| println("⚠️ NEEDS RESUGGEST: Auto-triggering resuggest for pick order line ID: $pickOrderLineId") | |||||
| try { | |||||
| // 获取 pick order ID | |||||
| val pickOrderId = getPickOrderIdFromPickOrderLineId(pickOrderLineId) | |||||
| if (pickOrderId != null) { | |||||
| suggestedPickLotService.resuggestPickOrder(pickOrderId) | |||||
| println("✅ Resuggest completed successfully for pick order ID: $pickOrderId") | |||||
| } else { | |||||
| println("❌ Could not find pick order ID for pick order line ID: $pickOrderLineId") | |||||
| } | |||||
| } catch (e: Exception) { | |||||
| println("❌ Error during auto-resuggest: ${e.message}") | |||||
| } | |||||
| } else { | |||||
| println("✅ No resuggest needed for pick order line ID: $pickOrderLineId") | |||||
| } | } | ||||
| // 检查日期条件 | |||||
| val dateCheckSql = """ | |||||
| val sql = """ | |||||
| SELECT | SELECT | ||||
| spl.id as suggestedPickLotId, | |||||
| ill.id as lotLineId, | |||||
| ill.id as lotId, | |||||
| il.lotNo, | il.lotNo, | ||||
| il.expiryDate, | il.expiryDate, | ||||
| :today as today, | |||||
| il.expiryDate >= :today as isNotExpired, | |||||
| il.expiryDate IS NULL as isNullExpiry | |||||
| FROM fpsmsdb.suggested_pick_lot spl | |||||
| JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId | |||||
| JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId | |||||
| WHERE spl.pickOrderLineId = :pickOrderLineId | |||||
| AND ill.status = 'available' | |||||
| AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 | |||||
| """.trimIndent() | |||||
| val dateCheckResult = | |||||
| jdbcDao.queryForList(dateCheckSql, mapOf("pickOrderLineId" to pickOrderLineId, "today" to today)) | |||||
| println("Date check result:") | |||||
| dateCheckResult.forEach { row -> | |||||
| println("Row: $row") | |||||
| } | |||||
| // 检查 warehouse JOIN | |||||
| val warehouseCheckSql = """ | |||||
| SELECT | |||||
| spl.id as suggestedPickLotId, | |||||
| ill.id as lotLineId, | |||||
| ill.warehouseId, | |||||
| w.id as warehouseId, | |||||
| w.name as warehouseName | |||||
| FROM fpsmsdb.suggested_pick_lot spl | |||||
| JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId | |||||
| JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId | |||||
| JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId | |||||
| WHERE spl.pickOrderLineId = :pickOrderLineId | |||||
| AND ill.status = 'available' | |||||
| AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 | |||||
| AND (il.expiryDate IS NULL OR il.expiryDate >= :today) | |||||
| """.trimIndent() | |||||
| val warehouseCheckResult = | |||||
| jdbcDao.queryForList(warehouseCheckSql, mapOf("pickOrderLineId" to pickOrderLineId, "today" to today)) | |||||
| println("Warehouse check result count: ${warehouseCheckResult.size}") | |||||
| warehouseCheckResult.forEach { row -> | |||||
| println("Warehouse Row: $row") | |||||
| } | |||||
| // 检查 uom_conversion JOIN - 使用 LEFT JOIN 并检查 stockItemUomId | |||||
| val uomCheckSql = """ | |||||
| SELECT | |||||
| w.name as location, | |||||
| COALESCE(uc.udfudesc, 'N/A') as stockUnit, | |||||
| -- ✅ 修复:availableQty 应该显示实际可用数量(保持小数) | |||||
| CASE | |||||
| WHEN sales_iu.ratioN IS NOT NULL AND sales_iu.ratioD IS NOT NULL AND sales_iu.ratioD != 0 THEN | |||||
| (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) / (sales_iu.ratioN / sales_iu.ratioD) | |||||
| ELSE | |||||
| (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) | |||||
| END as availableQty, | |||||
| -- ✅ 修复:requiredQty 直接使用 spl.qty,因为它已经是销售单位 | |||||
| spl.qty as requiredQty, | |||||
| COALESCE(sol.qty, 0) as actualPickQty, | |||||
| spl.id as suggestedPickLotId, | spl.id as suggestedPickLotId, | ||||
| ill.id as lotLineId, | |||||
| ill.stockItemUomId, | |||||
| uc.id as uomId, | |||||
| uc.code as uomCode | |||||
| ill.status as lotStatus, | |||||
| sol.id as stockOutLineId, | |||||
| sol.status as stockOutLineStatus, | |||||
| sol.qty as stockOutLineQty, | |||||
| spl.suggestedLotLineId as debugSuggestedLotLineId, | |||||
| ill.inventoryLotId as debugInventoryLotId, | |||||
| CASE | |||||
| WHEN ill.status != 'available' THEN 'unavailable' | |||||
| WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 'expired' | |||||
| WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) < (spl.qty * (sales_iu.ratioN / sales_iu.ratioD)) THEN 'insufficient_stock' | |||||
| ELSE 'available' | |||||
| END as lotAvailability | |||||
| FROM fpsmsdb.suggested_pick_lot spl | FROM fpsmsdb.suggested_pick_lot spl | ||||
| JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId | JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId | ||||
| JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId | JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId | ||||
| JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId | |||||
| LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = ill.stockItemUomId | |||||
| LEFT JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId | |||||
| LEFT JOIN fpsmsdb.item_uom sales_iu ON sales_iu.itemId = il.itemId AND sales_iu.salesUnit = true AND sales_iu.deleted = false | |||||
| LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = sales_iu.uomId | |||||
| LEFT JOIN fpsmsdb.stock_out_line sol ON sol.pickOrderLineId = spl.pickOrderLineId AND sol.inventoryLotLineId = spl.suggestedLotLineId | |||||
| WHERE spl.pickOrderLineId = :pickOrderLineId | WHERE spl.pickOrderLineId = :pickOrderLineId | ||||
| AND ill.status = 'available' | |||||
| AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) > 0 | |||||
| AND (il.expiryDate IS NULL OR il.expiryDate >= :today) | |||||
| ORDER BY il.expiryDate ASC, il.lotNo ASC | |||||
| """.trimIndent() | """.trimIndent() | ||||
| val uomCheckResult = | |||||
| jdbcDao.queryForList(uomCheckSql, mapOf("pickOrderLineId" to pickOrderLineId, "today" to today)) | |||||
| println("UOM check result count: ${uomCheckResult.size}") | |||||
| uomCheckResult.forEach { row -> | |||||
| println("UOM Row: $row") | |||||
| } | |||||
| println("🔍 Executing SQL for lot details: $sql") | |||||
| println("🔍 With parameters: pickOrderLineId = $pickOrderLineId") | |||||
| // 修改查询,通过 item_uom 表获取 UOM 信息 | |||||
| val sql = """ | |||||
| SELECT | |||||
| ill.id as lotId, | |||||
| il.lotNo, | |||||
| il.expiryDate, | |||||
| w.name as location, | |||||
| COALESCE(uc.udfudesc, 'N/A') as stockUnit, | |||||
| (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as availableQty, | |||||
| spl.qty as requiredQty, | |||||
| COALESCE(sol.qty, 0) as actualPickQty, | |||||
| spl.id as suggestedPickLotId, | |||||
| ill.status as lotStatus, | |||||
| CASE | |||||
| WHEN ill.status != 'available' THEN 'status_unavailable' | |||||
| WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < :today) THEN 'expired' | |||||
| WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) < spl.qty THEN 'insufficient_stock' | |||||
| ELSE 'available' | |||||
| END as lotAvailability | |||||
| FROM fpsmsdb.suggested_pick_lot spl | |||||
| JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId | |||||
| JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId | |||||
| JOIN fpsmsdb.warehouse w ON w.id = ill.warehouseId | |||||
| LEFT JOIN fpsmsdb.item_uom iu ON iu.id = ill.stockItemUomId | |||||
| LEFT JOIN fpsmsdb.uom_conversion uc ON uc.id = iu.uomId | |||||
| LEFT JOIN fpsmsdb.stock_out_line sol ON sol.inventoryLotLineId = ill.id | |||||
| AND sol.pickOrderLineId = spl.pickOrderLineId | |||||
| WHERE spl.pickOrderLineId = :pickOrderLineId | |||||
| ORDER BY | |||||
| CASE lotAvailability | |||||
| WHEN 'available' THEN 1 | |||||
| WHEN 'insufficient_stock' THEN 2 | |||||
| WHEN 'expired' THEN 3 | |||||
| WHEN 'status_unavailable' THEN 4 | |||||
| END, | |||||
| il.expiryDate ASC | |||||
| """.trimIndent() | |||||
| val params = mapOf( | |||||
| "pickOrderLineId" to pickOrderLineId, | |||||
| "today" to today | |||||
| ) | |||||
| val result = jdbcDao.queryForList(sql, mapOf("pickOrderLineId" to pickOrderLineId)) | |||||
| val result = jdbcDao.queryForList(sql, params) | |||||
| println("Final result count: ${result.size}") | println("Final result count: ${result.size}") | ||||
| result.forEach { row -> | result.forEach { row -> | ||||
| println("Final Row: $row") | println("Final Row: $row") | ||||
| @@ -931,7 +889,6 @@ open class PickOrderService( | |||||
| return result | return result | ||||
| } | } | ||||
| @Transactional(rollbackFor = [java.lang.Exception::class]) | @Transactional(rollbackFor = [java.lang.Exception::class]) | ||||
| open fun releaseConsoPickOrderAction(request: ReleaseConsoPickOrderRequest): ReleasePickOrderInfoResponse { | open fun releaseConsoPickOrderAction(request: ReleaseConsoPickOrderRequest): ReleasePickOrderInfoResponse { | ||||
| val zero = BigDecimal.ZERO | val zero = BigDecimal.ZERO | ||||
| @@ -993,7 +950,6 @@ open class PickOrderService( | |||||
| val releasedBy = SecurityUtils.getUser().getOrNull() | val releasedBy = SecurityUtils.getUser().getOrNull() | ||||
| val user = userService.find(assignTo).orElse(null) | val user = userService.find(assignTo).orElse(null) | ||||
| val pickOrders = pickOrderRepository.findAllByIdInAndStatus(pickOrderIds, PickOrderStatus.ASSIGNED) | val pickOrders = pickOrderRepository.findAllByIdInAndStatus(pickOrderIds, PickOrderStatus.ASSIGNED) | ||||
| if (pickOrders.isEmpty()) { | if (pickOrders.isEmpty()) { | ||||
| @@ -1007,36 +963,37 @@ open class PickOrderService( | |||||
| ) | ) | ||||
| } | } | ||||
| // ← GENERATE CONSOCODE EARLY AND SAVE IMMEDIATELY | |||||
| val newConsoCode = assignConsoCode() | |||||
| val currUser = SecurityUtils.getUser().orElseThrow() | |||||
| // Create and save StockOut immediately to prevent duplicate consoCodes | |||||
| val stockOut = StockOut().apply { | |||||
| this.type = "job" | |||||
| this.consoPickOrderCode = newConsoCode | |||||
| this.status = StockOutStatus.PENDING.status | |||||
| this.handler = currUser.id | |||||
| } | |||||
| val savedStockOut = stockOutRepository.saveAndFlush(stockOut) // ← FLUSH to commit immediately | |||||
| // Update pick orders | |||||
| pickOrders.forEach { pickOrder -> | pickOrders.forEach { pickOrder -> | ||||
| pickOrder.apply { | pickOrder.apply { | ||||
| this.releasedBy = releasedBy | this.releasedBy = releasedBy | ||||
| status = PickOrderStatus.RELEASED | status = PickOrderStatus.RELEASED | ||||
| this.assignTo = user | this.assignTo = user | ||||
| this.consoCode = newConsoCode // ← Also assign consoCode to pick orders | |||||
| } | } | ||||
| } | } | ||||
| val suggestions = suggestedPickLotService.suggestionForPickOrders( | val suggestions = suggestedPickLotService.suggestionForPickOrders( | ||||
| SuggestedPickLotForPoRequest(pickOrders = pickOrders) | SuggestedPickLotForPoRequest(pickOrders = pickOrders) | ||||
| ) | ) | ||||
| val currUser = SecurityUtils.getUser().orElseThrow() | |||||
| val stockOut = StockOut().apply { | |||||
| this.type = "job" | |||||
| this.consoPickOrderCode = null // 单个 pick orders 没有 consoCode | |||||
| this.status = StockOutStatus.PENDING.status | |||||
| this.handler = currUser.id | |||||
| } | |||||
| stockOutRepository.save(stockOut) | |||||
| val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList) | val saveSuggestedPickLots = suggestedPickLotService.saveAll(suggestions.suggestedList) | ||||
| pickOrderRepository.saveAll(pickOrders) | pickOrderRepository.saveAll(pickOrders) | ||||
| val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | ||||
| saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | saveSuggestedPickLots.mapNotNull { it.suggestedLotLine?.id } | ||||
| ) | ) | ||||
| @@ -1059,7 +1016,8 @@ open class PickOrderService( | |||||
| code = "SUCCESS", | code = "SUCCESS", | ||||
| type = "pickorder", | type = "pickorder", | ||||
| message = "Pick orders released successfully with inventory management", | message = "Pick orders released successfully with inventory management", | ||||
| errorPosition = null | |||||
| errorPosition = null, | |||||
| entity = mapOf("consoCode" to newConsoCode) // ← Return the generated consoCode | |||||
| ) | ) | ||||
| } catch (e: Exception) { | } catch (e: Exception) { | ||||
| @@ -1228,4 +1186,139 @@ open class PickOrderService( | |||||
| errorPosition = "", | errorPosition = "", | ||||
| ) | ) | ||||
| } | } | ||||
| // ✅ 添加缺失的方法:从 pick order line ID 获取 pick order ID | |||||
| private fun getPickOrderIdFromPickOrderLineId(pickOrderLineId: Long): Long? { | |||||
| val sql = """ | |||||
| SELECT po.id as pickOrderId | |||||
| FROM fpsmsdb.pick_order_line pol | |||||
| JOIN fpsmsdb.pick_order po ON po.id = pol.poId | |||||
| WHERE pol.id = :pickOrderLineId | |||||
| """.trimIndent() | |||||
| val result = jdbcDao.queryForList(sql, mapOf("pickOrderLineId" to pickOrderLineId)) | |||||
| return if (result.isNotEmpty()) { | |||||
| (result[0]["pickOrderId"] as Number).toLong() | |||||
| } else { | |||||
| null | |||||
| } | |||||
| } | |||||
| // ✅ 添加检查是否需要 resuggest 的方法 | |||||
| private fun checkIfNeedsResuggest(pickOrderLineId: Long): Boolean { | |||||
| println("🔍 checkIfNeedsResuggest called with pickOrderLineId: $pickOrderLineId") | |||||
| // ✅ 首先执行一个调试查询来查看实际的数值 | |||||
| val debugSql = """ | |||||
| SELECT | |||||
| spl.id as suggestedPickLotId, | |||||
| spl.qty as suggestedQty, | |||||
| ill.inQty, | |||||
| ill.outQty, | |||||
| ill.holdQty, | |||||
| (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) as baseAvailableQty, | |||||
| sales_iu.ratioN, | |||||
| sales_iu.ratioD, | |||||
| CASE | |||||
| WHEN sales_iu.ratioN IS NOT NULL AND sales_iu.ratioD IS NOT NULL AND sales_iu.ratioD != 0 | |||||
| THEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) / (sales_iu.ratioN / sales_iu.ratioD) | |||||
| ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) | |||||
| END as convertedAvailableQty | |||||
| FROM fpsmsdb.suggested_pick_lot spl | |||||
| JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId | |||||
| JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId | |||||
| LEFT JOIN fpsmsdb.item_uom sales_iu ON sales_iu.itemId = il.itemId AND sales_iu.salesUnit = true AND sales_iu.deleted = false | |||||
| WHERE spl.pickOrderLineId = :pickOrderLineId | |||||
| """.trimIndent() | |||||
| println("🔍 Debug SQL: $debugSql") | |||||
| val debugResult = jdbcDao.queryForList(debugSql, mapOf("pickOrderLineId" to pickOrderLineId)) | |||||
| println("🔍 Debug result:") | |||||
| debugResult.forEach { row -> | |||||
| println(" SuggestedPickLotId: ${row["suggestedPickLotId"]}") | |||||
| println(" SuggestedQty: ${row["suggestedQty"]}") | |||||
| println(" InQty: ${row["inQty"]}") | |||||
| println(" OutQty: ${row["outQty"]}") | |||||
| println(" HoldQty: ${row["holdQty"]}") | |||||
| println(" BaseAvailableQty: ${row["baseAvailableQty"]}") | |||||
| println(" RatioN: ${row["ratioN"]}") | |||||
| println(" RatioD: ${row["ratioD"]}") | |||||
| println(" ConvertedAvailableQty: ${row["convertedAvailableQty"]}") | |||||
| // ✅ 修复:正确处理 Number 类型转换 | |||||
| val convertedAvailableQty = (row["convertedAvailableQty"] as Number).toDouble() | |||||
| val suggestedQty = (row["suggestedQty"] as Number).toDouble() | |||||
| println(" Is Insufficient: ${convertedAvailableQty < suggestedQty}") | |||||
| println(" ---") | |||||
| } | |||||
| val checkSql = """ | |||||
| SELECT | |||||
| COUNT(*) as totalSuggestions, | |||||
| COUNT(CASE WHEN ill.status != 'available' THEN 1 END) as unavailableLots, | |||||
| COUNT(CASE WHEN (il.expiryDate IS NOT NULL AND il.expiryDate < CURDATE()) THEN 1 END) as expiredLots, | |||||
| -- ✅ 修复:使用销售单位进行比较 | |||||
| COUNT(CASE WHEN | |||||
| CASE | |||||
| WHEN sales_iu.ratioN IS NOT NULL AND sales_iu.ratioD IS NOT NULL AND sales_iu.ratioD != 0 | |||||
| THEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) / (sales_iu.ratioN / sales_iu.ratioD) | |||||
| ELSE (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0) - COALESCE(ill.holdQty, 0)) | |||||
| END < spl.qty | |||||
| THEN 1 END) as insufficientStockLots, | |||||
| COUNT(CASE WHEN sol.status IN ('rejected', 'lot-change', 'determine1') THEN 1 END) as problematicStockOutLines, | |||||
| -- ✅ 检查单位不一致问题 | |||||
| COUNT(CASE WHEN spl.qty > pol.qty * 100 THEN 1 END) as unitMismatchLots | |||||
| FROM fpsmsdb.suggested_pick_lot spl | |||||
| JOIN fpsmsdb.inventory_lot_line ill ON ill.id = spl.suggestedLotLineId | |||||
| JOIN fpsmsdb.inventory_lot il ON il.id = ill.inventoryLotId | |||||
| JOIN fpsmsdb.pick_order_line pol ON pol.id = spl.pickOrderLineId | |||||
| -- ✅ 添加销售单位转换 | |||||
| LEFT JOIN fpsmsdb.item_uom sales_iu ON sales_iu.itemId = il.itemId AND sales_iu.salesUnit = true AND sales_iu.deleted = false | |||||
| LEFT JOIN fpsmsdb.stock_out_line sol ON sol.inventoryLotLineId = ill.id | |||||
| AND sol.pickOrderLineId = spl.pickOrderLineId | |||||
| WHERE spl.pickOrderLineId = :pickOrderLineId | |||||
| """.trimIndent() | |||||
| println("🔍 Executing SQL: $checkSql") | |||||
| println("🔍 With parameters: pickOrderLineId = $pickOrderLineId") | |||||
| val result = jdbcDao.queryForList(checkSql, mapOf("pickOrderLineId" to pickOrderLineId)) | |||||
| println("🔍 SQL result size: ${result.size}") | |||||
| if (result.isNotEmpty()) { | |||||
| val row = result[0] | |||||
| val totalSuggestions = (row["totalSuggestions"] as Number).toInt() | |||||
| val unavailableLots = (row["unavailableLots"] as Number).toInt() | |||||
| val expiredLots = (row["expiredLots"] as Number).toInt() | |||||
| val insufficientStockLots = (row["insufficientStockLots"] as Number).toInt() | |||||
| val problematicStockOutLines = (row["problematicStockOutLines"] as Number).toInt() | |||||
| val unitMismatchLots = (row["unitMismatchLots"] as Number).toInt() | |||||
| println("=== Resuggest Check ===") | |||||
| println("Total suggestions: $totalSuggestions") | |||||
| println("Unavailable lots: $unavailableLots") | |||||
| println("Expired lots: $expiredLots") | |||||
| println("Insufficient stock lots: $insufficientStockLots") | |||||
| println("Problematic stock out lines: $problematicStockOutLines") | |||||
| println("Unit mismatch lots: $unitMismatchLots") | |||||
| // 检查是否需要 resuggest 的条件 | |||||
| val needsResuggest = ( | |||||
| unavailableLots > 0 || | |||||
| expiredLots > 0 || | |||||
| insufficientStockLots > 0 || | |||||
| problematicStockOutLines > 0 || | |||||
| unitMismatchLots > 0 || // ✅ 单位不一致检查 | |||||
| totalSuggestions == 0 // 没有建议也需要 resuggest | |||||
| ) | |||||
| println("Needs resuggest: $needsResuggest") | |||||
| return needsResuggest | |||||
| } else { | |||||
| println("🔍 No results returned from SQL query") | |||||
| } | |||||
| return false | |||||
| } | |||||
| } | } | ||||
| @@ -13,7 +13,8 @@ data class ReleasePickOrderInfoResponse( | |||||
| ) | ) | ||||
| data class GetPickOrderInfoResponse( | data class GetPickOrderInfoResponse( | ||||
| // val consoCode: String, | |||||
| val consoCode: String?, | |||||
| val pickOrders: List<GetPickOrderInfo>, | val pickOrders: List<GetPickOrderInfo>, | ||||
| val items: List<CurrentInventoryItemInfo> | val items: List<CurrentInventoryItemInfo> | ||||
| ) | ) | ||||
| @@ -60,6 +61,7 @@ data class GetPickOrderLineInfo( | |||||
| val uomCode: String?, | val uomCode: String?, | ||||
| val uomDesc: String?, | val uomDesc: String?, | ||||
| val suggestedList: List<SuggestedPickLot>?, | val suggestedList: List<SuggestedPickLot>?, | ||||
| val pickedQty: BigDecimal?=BigDecimal.ZERO, | |||||
| ) | ) | ||||
| // Final Response - Conso Pick Order Detail | // Final Response - Conso Pick Order Detail | ||||
| @@ -16,4 +16,8 @@ interface StockOutLIneRepository: AbstractRepository<StockOutLine, Long> { | |||||
| fun findStockOutLineInfoById(id: Long): StockOutLineInfo | fun findStockOutLineInfoById(id: Long): StockOutLineInfo | ||||
| fun findAllByStockOutId(stockOutId: Long): List<StockOutLine> | fun findAllByStockOutId(stockOutId: Long): List<StockOutLine> | ||||
| fun findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||||
| pickOrderLineId: Long, | |||||
| inventoryLotLineId: Long | |||||
| ): List<StockOutLine> | |||||
| } | } | ||||
| @@ -7,4 +7,8 @@ import org.springframework.stereotype.Repository | |||||
| @Repository | @Repository | ||||
| interface SuggestPickLotRepository : AbstractRepository<SuggestedPickLot, Long> { | interface SuggestPickLotRepository : AbstractRepository<SuggestedPickLot, Long> { | ||||
| fun findAllByPickOrderLineIn(lines: List<PickOrderLine>): List<SuggestedPickLot> | fun findAllByPickOrderLineIn(lines: List<PickOrderLine>): List<SuggestedPickLot> | ||||
| fun findAllByPickOrderLineIdIn(pickOrderLineIds: List<Long>): List<SuggestedPickLot> | |||||
| fun findAllByPickOrderLineId(pickOrderLineId: Long): List<SuggestedPickLot> | |||||
| } | } | ||||
| @@ -13,6 +13,8 @@ import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | |||||
| import com.ffii.fpsms.modules.stock.entity.projection.InventoryLotLineInfo | import com.ffii.fpsms.modules.stock.entity.projection.InventoryLotLineInfo | ||||
| import com.ffii.fpsms.modules.stock.entity.projection.LotLineToQrcode | import com.ffii.fpsms.modules.stock.entity.projection.LotLineToQrcode | ||||
| import com.ffii.fpsms.modules.stock.web.InventoryLotLineController | import com.ffii.fpsms.modules.stock.web.InventoryLotLineController | ||||
| import com.ffii.fpsms.modules.stock.web.model.UpdateInventoryLotLineStatusRequest | |||||
| import com.ffii.fpsms.modules.stock.web.model.ExportQrCodeRequest | import com.ffii.fpsms.modules.stock.web.model.ExportQrCodeRequest | ||||
| import com.ffii.fpsms.modules.stock.web.model.SaveInventoryLotLineRequest | import com.ffii.fpsms.modules.stock.web.model.SaveInventoryLotLineRequest | ||||
| import com.ffii.fpsms.modules.stock.web.model.SearchInventoryLotLineInfoRequest | import com.ffii.fpsms.modules.stock.web.model.SearchInventoryLotLineInfoRequest | ||||
| @@ -29,7 +31,7 @@ import java.math.BigDecimal | |||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||
| import java.util.Optional | import java.util.Optional | ||||
| import kotlin.jvm.optionals.getOrNull | import kotlin.jvm.optionals.getOrNull | ||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||||
| @Service | @Service | ||||
| open class InventoryLotLineService( | open class InventoryLotLineService( | ||||
| private val inventoryLotLineRepository: InventoryLotLineRepository, | private val inventoryLotLineRepository: InventoryLotLineRepository, | ||||
| @@ -87,7 +89,45 @@ open class InventoryLotLineService( | |||||
| return inventoryLotLineRepository.save(inventoryLotLine) | return inventoryLotLineRepository.save(inventoryLotLine) | ||||
| } | } | ||||
| @Transactional | |||||
| open fun updateInventoryLotLineStatus(request: UpdateInventoryLotLineStatusRequest): MessageResponse { | |||||
| try { | |||||
| val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() | |||||
| // 使用现有的 saveInventoryLotLine 逻辑 | |||||
| val updateRequest = SaveInventoryLotLineRequest( | |||||
| id = inventoryLotLine.id, | |||||
| inventoryLotId = inventoryLotLine.inventoryLot?.id, | |||||
| warehouseId = inventoryLotLine.warehouse?.id, | |||||
| stockUomId = inventoryLotLine.stockUom?.id, | |||||
| inQty = inventoryLotLine.inQty, | |||||
| outQty = inventoryLotLine.outQty, | |||||
| holdQty = inventoryLotLine.holdQty, | |||||
| status = request.status, // 新的状态 | |||||
| remarks = inventoryLotLine.remarks | |||||
| ) | |||||
| val updatedInventoryLotLine = saveInventoryLotLine(updateRequest) | |||||
| return MessageResponse( | |||||
| id = updatedInventoryLotLine.id, | |||||
| name = "Inventory lot line status updated", | |||||
| code = "SUCCESS", | |||||
| type = "inventory_lot_line", | |||||
| message = "Status updated to ${request.status}", | |||||
| errorPosition = null | |||||
| ) | |||||
| } catch (e: Exception) { | |||||
| return MessageResponse( | |||||
| id = null, | |||||
| name = "Failed to update inventory lot line status", | |||||
| code = "ERROR", | |||||
| type = "inventory_lot_line", | |||||
| message = "Error: ${e.message}", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| } | |||||
| @Throws(IOException::class) | @Throws(IOException::class) | ||||
| @Transactional | @Transactional | ||||
| open fun exportStockInLineQrcode(request: LotLineToQrcode): Map<String, Any> { | open fun exportStockInLineQrcode(request: LotLineToQrcode): Map<String, Any> { | ||||
| @@ -66,6 +66,23 @@ open class StockOutLineService( | |||||
| open fun create(request: CreateStockOutLineRequest): MessageResponse { | open fun create(request: CreateStockOutLineRequest): MessageResponse { | ||||
| // pick flow step 1 | // pick flow step 1 | ||||
| // println(request.pickOrderLineId) | // println(request.pickOrderLineId) | ||||
| val existingStockOutLine = stockOutLineRepository.findByPickOrderLineIdAndInventoryLotLineIdAndDeletedFalse( | |||||
| request.pickOrderLineId, | |||||
| request.inventoryLotLineId | |||||
| ) | |||||
| if (existingStockOutLine.isNotEmpty()) { | |||||
| // ✅ 如果已存在,返回 null 表示不需要创建 | |||||
| return MessageResponse( | |||||
| id = null, | |||||
| name = "Stock out line already exists", | |||||
| code = "EXISTS", | |||||
| type = "stock_out_line", | |||||
| message = "Stock out line already exists for this pick order line and inventory lot line", | |||||
| errorPosition = null, | |||||
| entity = null, | |||||
| ) | |||||
| } | |||||
| val stockOut = stockOutRepository.findByConsoPickOrderCode(request.consoCode).orElseThrow() | val stockOut = stockOutRepository.findByConsoPickOrderCode(request.consoCode).orElseThrow() | ||||
| val pickOrderLine = pickOrderLineRepository.saveAndFlush( | val pickOrderLine = pickOrderLineRepository.saveAndFlush( | ||||
| pickOrderLineRepository.findById(request.pickOrderLineId).orElseThrow() | pickOrderLineRepository.findById(request.pickOrderLineId).orElseThrow() | ||||
| @@ -97,48 +114,146 @@ open class StockOutLineService( | |||||
| entity = mappedSavedStockOutLine, | entity = mappedSavedStockOutLine, | ||||
| ) | ) | ||||
| } | } | ||||
| @Transactional | |||||
| fun handleQc(stockOutLine: StockOutLine, request: UpdateStockOutLineRequest): List<StockOutLine?> { | |||||
| var newStockOutLine: StockOutLine? = null | |||||
| if (request.qty < stockOutLine.qty!!) { | |||||
| newStockOutLine = StockOutLine().apply { | |||||
| this.pickOrderLine = stockOutLine.pickOrderLine | |||||
| this.stockOut = stockOutLine.stockOut | |||||
| this.item = stockOutLine.item | |||||
| this.qty = stockOutLine.qty!! - request.qty | |||||
| this.status = StockOutLineStatus.DETERMINE1.status // escalated | |||||
| } | |||||
| } | |||||
| val inventoryLotLine = if (request.inventoryLotLineId != null) | |||||
| inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() | |||||
| else null | |||||
| stockOutLine.apply { | |||||
| this.inventoryLotLine = inventoryLotLine ?: stockOutLine.inventoryLotLine | |||||
| this.qty = request.qty | |||||
| this.status = StockOutLineStatus.COMPLETE.status // complete | |||||
| } | |||||
| // update inventory lot line | |||||
| val zero = BigDecimal.ZERO | |||||
| val one = BigDecimal.ONE | |||||
| val targetLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId!!).orElseThrow() | |||||
| val salesUnit = inventoryLotLine?.inventoryLot?.item?.id?.let {_itemId -> itemUomRespository.findByItemIdAndSalesUnitIsTrueAndDeletedIsFalse(_itemId) } | |||||
| val ratio = (salesUnit?.ratioN ?: zero).divide(salesUnit?.ratioD ?: one).toDouble() | |||||
| val targetLotLineEntry = targetLotLine.apply { | |||||
| this.outQty = (this.outQty?: BigDecimal.ZERO) + (request.qty.div(ratio)).toBigDecimal() | |||||
| this.holdQty = this.holdQty!!.minus((request.qty.div(ratio)).toBigDecimal()) | |||||
| } | |||||
| 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) | |||||
| } | |||||
| @Transactional | @Transactional | ||||
| fun handleQc(stockOutLine: StockOutLine, request: UpdateStockOutLineRequest): List<StockOutLine?> { | |||||
| var newStockOutLine: StockOutLine? = null | |||||
| if (request.qty < stockOutLine.qty!!) { | |||||
| newStockOutLine = StockOutLine().apply { | |||||
| this.pickOrderLine = stockOutLine.pickOrderLine | |||||
| this.stockOut = stockOutLine.stockOut | |||||
| this.item = stockOutLine.item | |||||
| this.qty = stockOutLine.qty!! - request.qty | |||||
| this.status = StockOutLineStatus.DETERMINE1.status // escalated | |||||
| } | |||||
| open fun createWithoutConso(request: CreateStockOutLineWithoutConsoRequest): MessageResponse { | |||||
| try { | |||||
| // ✅ Get stockOutId from pickOrderLineId with detailed error | |||||
| val stockOutId = getStockOutIdFromPickOrderLine(request.pickOrderLineId) | |||||
| println("Found stockOutId: $stockOutId for pickOrderLineId: ${request.pickOrderLineId}") | |||||
| val stockOut = stockOutRepository.findById(stockOutId).orElseThrow { | |||||
| IllegalArgumentException("StockOut not found with ID: $stockOutId") | |||||
| } | } | ||||
| val inventoryLotLine = if (request.inventoryLotLineId != null) | |||||
| inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() | |||||
| else null | |||||
| stockOutLine.apply { | |||||
| this.inventoryLotLine = inventoryLotLine ?: stockOutLine.inventoryLotLine | |||||
| this.qty = request.qty | |||||
| this.status = StockOutLineStatus.COMPLETE.status // complete | |||||
| println("Found stockOut: ${stockOut.id} with consoCode: ${stockOut.consoPickOrderCode}") | |||||
| val pickOrderLine = pickOrderLineRepository.findById(request.pickOrderLineId).orElseThrow { | |||||
| IllegalArgumentException("PickOrderLine not found with ID: ${request.pickOrderLineId}") | |||||
| } | } | ||||
| // update inventory lot line | |||||
| val zero = BigDecimal.ZERO | |||||
| val one = BigDecimal.ONE | |||||
| val targetLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId!!).orElseThrow() | |||||
| val salesUnit = inventoryLotLine?.inventoryLot?.item?.id?.let {_itemId -> itemUomRespository.findByItemIdAndSalesUnitIsTrueAndDeletedIsFalse(_itemId) } | |||||
| val ratio = (salesUnit?.ratioN ?: zero).divide(salesUnit?.ratioD ?: one).toDouble() | |||||
| val targetLotLineEntry = targetLotLine.apply { | |||||
| this.outQty = (this.outQty?: BigDecimal.ZERO) + (request.qty.div(ratio)).toBigDecimal() | |||||
| this.holdQty = this.holdQty!!.minus((request.qty.div(ratio)).toBigDecimal()) | |||||
| println("Found pickOrderLine: ${pickOrderLine.id}") | |||||
| val updatedPickOrderLine = pickOrderLineRepository.saveAndFlush( | |||||
| pickOrderLine.apply { | |||||
| this.status = PickOrderLineStatus.PICKING | |||||
| } | |||||
| ) | |||||
| println("Updated pickOrderLine status to: ${updatedPickOrderLine.status}") | |||||
| val item = itemRepository.findById(updatedPickOrderLine.item!!.id!!).orElseThrow { | |||||
| IllegalArgumentException("Item not found with ID: ${updatedPickOrderLine.item!!.id}") | |||||
| } | } | ||||
| 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()) | |||||
| println("Found item: ${item.id} - ${item.code}") | |||||
| val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow { | |||||
| IllegalArgumentException("InventoryLotLine not found with ID: ${request.inventoryLotLineId}") | |||||
| } | } | ||||
| inventoryRepository.save(inventoryEntry) | |||||
| return listOf(stockOutLine, newStockOutLine) | |||||
| println("Found inventoryLotLine: ${inventoryLotLine.id}") | |||||
| val stockOutLine = StockOutLine() | |||||
| .apply { | |||||
| this.item = item | |||||
| this.qty = request.qty | |||||
| this.stockOut = stockOut | |||||
| this.inventoryLotLine = inventoryLotLine | |||||
| this.pickOrderLine = updatedPickOrderLine | |||||
| this.status = StockOutLineStatus.PENDING.status | |||||
| } | |||||
| println("Created stockOutLine with qty: ${request.qty}") | |||||
| val savedStockOutLine = saveAndFlush(stockOutLine) | |||||
| println("Saved stockOutLine with ID: ${savedStockOutLine.id}") | |||||
| val mappedSavedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!) | |||||
| println("Mapped stockOutLine info: $mappedSavedStockOutLine") | |||||
| return MessageResponse( | |||||
| id = savedStockOutLine.id, | |||||
| name = savedStockOutLine.inventoryLotLine!!.inventoryLot!!.lotNo, | |||||
| code = savedStockOutLine.stockOut!!.consoPickOrderCode, | |||||
| type = savedStockOutLine.status, | |||||
| message = "success", | |||||
| errorPosition = null, | |||||
| entity = mappedSavedStockOutLine, | |||||
| ) | |||||
| } catch (e: Exception) { | |||||
| println("Error in createWithoutConso: ${e.message}") | |||||
| e.printStackTrace() | |||||
| throw e | |||||
| } | |||||
| } | |||||
| // ✅ Update helper method with detailed error messages | |||||
| private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { | |||||
| println("Getting stockOutId for pickOrderLineId: $pickOrderLineId") | |||||
| // ✅ Fixed: Use poId instead of pick_order_id | |||||
| val sql = """ | |||||
| SELECT so.id as stockOutId, so.consoPickOrderCode, po.consoCode | |||||
| FROM stock_out so | |||||
| JOIN pick_order po ON po.consoCode = so.consoPickOrderCode | |||||
| JOIN pick_order_line pol ON pol.poId = po.id -- ✅ Fixed: Use poId | |||||
| WHERE pol.id = :pickOrderLineId | |||||
| """.trimIndent() | |||||
| val result = jdbcDao.queryForList(sql, mapOf("pickOrderLineId" to pickOrderLineId)) | |||||
| println("SQL result: $result") | |||||
| if (result.isEmpty()) { | |||||
| throw IllegalArgumentException("No StockOut found for pickOrderLineId: $pickOrderLineId. Check if pick order line exists and has associated stock out.") | |||||
| } | } | ||||
| val stockOutId = result[0]["stockOutId"] as? Long | |||||
| val consoPickOrderCode = result[0]["consoPickOrderCode"] as? String | |||||
| val consoCode = result[0]["consoCode"] as? String | |||||
| println("Found stockOutId: $stockOutId, consoPickOrderCode: $consoPickOrderCode, consoCode: $consoCode") | |||||
| if (stockOutId == null) { | |||||
| throw IllegalArgumentException("StockOut ID is null for pickOrderLineId: $pickOrderLineId. ConsoCode: $consoCode, ConsoPickOrderCode: $consoPickOrderCode") | |||||
| } | |||||
| return stockOutId | |||||
| } | |||||
| @Transactional | @Transactional | ||||
| fun handleLotChangeApprovalOrReject(stockOutLine: StockOutLine, request: UpdateStockOutLineRequest): List<StockOutLine?> { | fun handleLotChangeApprovalOrReject(stockOutLine: StockOutLine, request: UpdateStockOutLineRequest): List<StockOutLine?> { | ||||
| /** | /** | ||||
| @@ -244,4 +359,44 @@ open class StockOutLineService( | |||||
| entity = lineInfoList, | entity = lineInfoList, | ||||
| ) | ) | ||||
| } | } | ||||
| @Transactional | |||||
| open fun updateStatus(request: UpdateStockOutLineStatusRequest): MessageResponse { | |||||
| try { | |||||
| val stockOutLine = stockOutLineRepository.findById(request.id).orElseThrow { | |||||
| IllegalArgumentException("StockOutLine not found with ID: ${request.id}") | |||||
| } | |||||
| println("Updating StockOutLine ID: ${request.id}") | |||||
| println("Current status: ${stockOutLine.status}") | |||||
| println("New status: ${request.status}") | |||||
| // Update status | |||||
| stockOutLine.status = request.status | |||||
| // Update quantity if provided | |||||
| if (request.qty != null) { | |||||
| stockOutLine.qty = request.qty | |||||
| } | |||||
| val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) | |||||
| println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}") | |||||
| val mappedSavedStockOutLine = stockOutLineRepository.findStockOutLineInfoById(savedStockOutLine.id!!) | |||||
| return MessageResponse( | |||||
| id = savedStockOutLine.id, | |||||
| name = savedStockOutLine.inventoryLotLine!!.inventoryLot!!.lotNo, | |||||
| code = savedStockOutLine.stockOut!!.consoPickOrderCode, | |||||
| type = savedStockOutLine.status, | |||||
| message = "Stock out line status updated successfully", | |||||
| errorPosition = null, | |||||
| entity = mappedSavedStockOutLine, | |||||
| ) | |||||
| } catch (e: Exception) { | |||||
| println("Error updating stock out line status: ${e.message}") | |||||
| e.printStackTrace() | |||||
| throw e | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -17,7 +17,10 @@ import java.math.BigDecimal | |||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import kotlin.jvm.optionals.getOrDefault | import kotlin.jvm.optionals.getOrDefault | ||||
| import kotlin.jvm.optionals.getOrNull | import kotlin.jvm.optionals.getOrNull | ||||
| import org.springframework.transaction.annotation.Transactional | |||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||||
| import com.ffii.fpsms.modules.pickOrder.entity.PickOrderRepository | |||||
| import java.math.RoundingMode | |||||
| @Service | @Service | ||||
| open class SuggestedPickLotService( | open class SuggestedPickLotService( | ||||
| val suggestedPickLotRepository: SuggestPickLotRepository, | val suggestedPickLotRepository: SuggestPickLotRepository, | ||||
| @@ -26,6 +29,7 @@ open class SuggestedPickLotService( | |||||
| val pickOrderLineRepository: PickOrderLineRepository, | val pickOrderLineRepository: PickOrderLineRepository, | ||||
| val inventoryLotLineService: InventoryLotLineService, | val inventoryLotLineService: InventoryLotLineService, | ||||
| val itemUomService: ItemUomService, | val itemUomService: ItemUomService, | ||||
| val pickOrderRepository: PickOrderRepository | |||||
| ) { | ) { | ||||
| // Calculation Available Qty / Remaining Qty | // Calculation Available Qty / Remaining Qty | ||||
| open fun calculateRemainingQtyForInfo(inventoryLotLine: InventoryLotLineInfo?): BigDecimal { | open fun calculateRemainingQtyForInfo(inventoryLotLine: InventoryLotLineInfo?): BigDecimal { | ||||
| @@ -84,22 +88,28 @@ open class SuggestedPickLotService( | |||||
| pols.forEach { line -> | pols.forEach { line -> | ||||
| val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | val salesUnit = line.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | ||||
| val lotLines = availableInventoryLotLines[line.item?.id].orEmpty() | val lotLines = availableInventoryLotLines[line.item?.id].orEmpty() | ||||
| val ratio = (salesUnit?.ratioN ?: zero).divide(salesUnit?.ratioD ?: one) | |||||
| var remainingQty = (line.qty ?: zero).multiply(ratio) | |||||
| println("remaining1 $remainingQty") | |||||
| val ratio = (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP) | |||||
| // ✅ 修复:remainingQty 应该是销售单位,不需要乘以 ratio | |||||
| var remainingQty = line.qty ?: zero | |||||
| println("remaining1 $remainingQty (sales units)") | |||||
| val updatedLotLines = mutableListOf<InventoryLotLineInfo>() | val updatedLotLines = mutableListOf<InventoryLotLineInfo>() | ||||
| lotLines.forEachIndexed { index, lotLine -> | lotLines.forEachIndexed { index, lotLine -> | ||||
| if (remainingQty <= zero) return@forEachIndexed | if (remainingQty <= zero) return@forEachIndexed | ||||
| println("calculateRemainingQtyForInfo(lotLine) ${calculateRemainingQtyForInfo(lotLine)}") | println("calculateRemainingQtyForInfo(lotLine) ${calculateRemainingQtyForInfo(lotLine)}") | ||||
| val availableQty = calculateRemainingQtyForInfo(lotLine) | |||||
| .multiply(ratio) | |||||
| .minus(holdQtyMap[lotLine.id] ?: zero) | |||||
| // ✅ 修复:计算可用数量,转换为销售单位 | |||||
| val availableQtyInBaseUnits = calculateRemainingQtyForInfo(lotLine) | |||||
| val holdQtyInBaseUnits = holdQtyMap[lotLine.id] ?: zero | |||||
| val availableQtyInSalesUnits = availableQtyInBaseUnits | |||||
| .minus(holdQtyInBaseUnits) | |||||
| .divide(ratio, 2, RoundingMode.HALF_UP) | |||||
| println("holdQtyMap[lotLine.id] ?: zero ${holdQtyMap[lotLine.id] ?: zero}") | println("holdQtyMap[lotLine.id] ?: zero ${holdQtyMap[lotLine.id] ?: zero}") | ||||
| if (availableQty <= zero) { | |||||
| if (availableQtyInSalesUnits <= zero) { | |||||
| updatedLotLines += lotLine | updatedLotLines += lotLine | ||||
| return@forEachIndexed | return@forEachIndexed | ||||
| } | } | ||||
| @@ -107,27 +117,30 @@ open class SuggestedPickLotService( | |||||
| val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() } | val inventoryLotLine = lotLine.id?.let { inventoryLotLineService.findById(it).getOrNull() } | ||||
| val originalHoldQty = inventoryLotLine?.holdQty | val originalHoldQty = inventoryLotLine?.holdQty | ||||
| // Update Qty | |||||
| val assignQty = minOf(availableQty, remainingQty) | |||||
| remainingQty = remainingQty.minus(assignQty) | |||||
| holdQtyMap[lotLine.id] = (holdQtyMap[lotLine.id] ?: zero).plus(assignQty) | |||||
| // lotLine.holdQty = lotLine.holdQty?.plus(assignQty) | |||||
| // ✅ 修复:在销售单位中计算分配数量 | |||||
| val assignQtyInSalesUnits = minOf(availableQtyInSalesUnits, remainingQty) | |||||
| remainingQty = remainingQty.minus(assignQtyInSalesUnits) | |||||
| // ✅ 修复:将销售单位转换为基础单位来更新 holdQty | |||||
| val assignQtyInBaseUnits = assignQtyInSalesUnits.multiply(ratio) | |||||
| holdQtyMap[lotLine.id] = (holdQtyMap[lotLine.id] ?: zero).plus(assignQtyInBaseUnits) | |||||
| suggestedList += SuggestedPickLot().apply { | suggestedList += SuggestedPickLot().apply { | ||||
| type = SuggestedPickLotType.PICK_ORDER | type = SuggestedPickLotType.PICK_ORDER | ||||
| suggestedLotLine = inventoryLotLine | suggestedLotLine = inventoryLotLine | ||||
| pickOrderLine = line | pickOrderLine = line | ||||
| qty = assignQty | |||||
| qty = assignQtyInSalesUnits // ✅ 保存销售单位 | |||||
| } | } | ||||
| } | } | ||||
| // if still have remainingQty | // if still have remainingQty | ||||
| println("remaining2 $remainingQty") | |||||
| println("remaining2 $remainingQty (sales units)") | |||||
| if (remainingQty > zero) { | if (remainingQty > zero) { | ||||
| suggestedList += SuggestedPickLot().apply { | suggestedList += SuggestedPickLot().apply { | ||||
| type = SuggestedPickLotType.PICK_ORDER | type = SuggestedPickLotType.PICK_ORDER | ||||
| suggestedLotLine = null | suggestedLotLine = null | ||||
| pickOrderLine = line | pickOrderLine = line | ||||
| qty = remainingQty | |||||
| qty = remainingQty // ✅ 保存销售单位 | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -184,4 +197,305 @@ open class SuggestedPickLotService( | |||||
| open fun saveAll(request: List<SuggestedPickLot>): List<SuggestedPickLot> { | open fun saveAll(request: List<SuggestedPickLot>): List<SuggestedPickLot> { | ||||
| return suggestedPickLotRepository.saveAllAndFlush(request) | return suggestedPickLotRepository.saveAllAndFlush(request) | ||||
| } | } | ||||
| } | |||||
| @Transactional(rollbackFor = [Exception::class]) | |||||
| open fun resuggestPickOrder(pickOrderId: Long): MessageResponse { | |||||
| try { | |||||
| val pickOrder = pickOrderRepository.findById(pickOrderId).orElseThrow() | |||||
| // Step 1: Find ALL suggestions for this pick order (not just problematic ones) | |||||
| val allSuggestions = findAllSuggestionsForPickOrder(pickOrderId) | |||||
| if (allSuggestions.isEmpty()) { | |||||
| return MessageResponse( | |||||
| id = pickOrderId, | |||||
| name = "No suggestions found", | |||||
| code = "SUCCESS", | |||||
| type = "resuggest", | |||||
| message = "No suggestions to resuggest", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| // Step 2: Calculate total excess quantity from problematic suggestions | |||||
| val problematicSuggestions = findProblematicSuggestions(pickOrderId) | |||||
| val totalExcessQty = problematicSuggestions.sumOf { suggestion -> | |||||
| val pickOrderLine = pickOrder.pickOrderLines.find { it.id == suggestion.pickOrderLine?.id } | |||||
| val inventoryLotLine = suggestion.suggestedLotLine | |||||
| if (pickOrderLine == null || inventoryLotLine == null) { | |||||
| BigDecimal.ZERO | |||||
| } else { | |||||
| val salesUnit = pickOrderLine.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | |||||
| val ratio = (salesUnit?.ratioN ?: BigDecimal.ONE).divide(salesUnit?.ratioD ?: BigDecimal.ONE, 10, RoundingMode.HALF_UP) | |||||
| val availableQtyInBaseUnits = (inventoryLotLine.inQty ?: BigDecimal.ZERO) | |||||
| .minus(inventoryLotLine.outQty ?: BigDecimal.ZERO) | |||||
| .minus(inventoryLotLine.holdQty ?: BigDecimal.ZERO) | |||||
| val suggestionQtyInBaseUnits = (suggestion.qty ?: BigDecimal.ZERO).multiply(ratio) | |||||
| val excessQtyInBaseUnits = suggestionQtyInBaseUnits.minus(availableQtyInBaseUnits) | |||||
| if (excessQtyInBaseUnits > BigDecimal.ZERO) { | |||||
| excessQtyInBaseUnits | |||||
| } else { | |||||
| BigDecimal.ZERO | |||||
| } | |||||
| } | |||||
| } | |||||
| // Step 3: Remove ALL existing suggestions and reset holdQty | |||||
| val affectedLotLineIds = allSuggestions.mapNotNull { it.suggestedLotLine?.id }.distinct() | |||||
| val inventoryLotLines = inventoryLotLineRepository.findAllByIdIn(affectedLotLineIds) | |||||
| inventoryLotLines.forEach { lotLine -> | |||||
| // ✅ FIX: Reset holdQty to 0 for lots being resuggested | |||||
| lotLine.holdQty = BigDecimal.ZERO | |||||
| } | |||||
| inventoryLotLineRepository.saveAll(inventoryLotLines) | |||||
| // Delete ALL suggestions for this pick order | |||||
| suggestedPickLotRepository.deleteAllById(allSuggestions.mapNotNull { it.id }) | |||||
| // Step 4: Generate completely new suggestions following FEFO | |||||
| val newSuggestions = generateCorrectSuggestionsWithExistingHolds(pickOrder) | |||||
| // Step 5: Save new suggestions and update holdQty | |||||
| val savedSuggestions = suggestedPickLotRepository.saveAll(newSuggestions) | |||||
| val newInventoryLotLines = inventoryLotLineRepository.findAllByIdIn( | |||||
| savedSuggestions.mapNotNull { it.suggestedLotLine?.id } | |||||
| ) | |||||
| /* | |||||
| savedSuggestions.forEach { suggestion -> | |||||
| val lotLine = newInventoryLotLines.find { it.id == suggestion.suggestedLotLine?.id } | |||||
| lotLine?.let { | |||||
| it.holdQty = (it.holdQty ?: BigDecimal.ZERO).plus(suggestion.qty ?: BigDecimal.ZERO) | |||||
| } | |||||
| } | |||||
| inventoryLotLineRepository.saveAll(newInventoryLotLines) | |||||
| */ | |||||
| return MessageResponse( | |||||
| id = pickOrderId, | |||||
| name = "Pick order resuggested successfully", | |||||
| code = "SUCCESS", | |||||
| type = "resuggest", | |||||
| message = "Redistributed ${allSuggestions.size} suggestions following FEFO order", | |||||
| errorPosition = null | |||||
| ) | |||||
| } catch (e: Exception) { | |||||
| return MessageResponse( | |||||
| id = pickOrderId, | |||||
| name = "Failed to resuggest pick order", | |||||
| code = "ERROR", | |||||
| type = "resuggest", | |||||
| message = "Error: ${e.message}", | |||||
| errorPosition = null | |||||
| ) | |||||
| } | |||||
| } | |||||
| private fun findAllSuggestionsForPickOrder(pickOrderId: Long): List<SuggestedPickLot> { | |||||
| val pickOrderLines = pickOrderRepository.findById(pickOrderId) | |||||
| .orElseThrow() | |||||
| .pickOrderLines | |||||
| val pickOrderLineIds = pickOrderLines.mapNotNull { it.id } | |||||
| return suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) | |||||
| } | |||||
| private fun generateSuggestionsForExcessQuantity(pickOrder: PickOrder, excessQtyInBaseUnits: BigDecimal): List<SuggestedPickLot> { | |||||
| val suggestions = mutableListOf<SuggestedPickLot>() | |||||
| val zero = BigDecimal.ZERO | |||||
| val one = BigDecimal.ONE | |||||
| pickOrder.pickOrderLines.forEach { orderLine -> | |||||
| val itemId = orderLine.item?.id ?: return@forEach | |||||
| // Get sales unit conversion ratio | |||||
| val salesUnit = itemUomService.findSalesUnitByItemId(itemId) | |||||
| val ratio = (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP) | |||||
| // Convert excess quantity to sales units | |||||
| val excessQtyInSalesUnits = excessQtyInBaseUnits.divide(ratio, 0, RoundingMode.HALF_UP) | |||||
| if (excessQtyInSalesUnits <= zero) return@forEach | |||||
| // Get available inventory lots for this item (FEFO order) | |||||
| val availableLots = inventoryLotLineService | |||||
| .allInventoryLotLinesByItemIdIn(listOf(itemId)) | |||||
| .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | |||||
| .sortedBy { it.expiryDate } | |||||
| var remainingQtyInBaseUnits = excessQtyInBaseUnits | |||||
| availableLots.forEach { lotInfo -> | |||||
| if (remainingQtyInBaseUnits <= zero) return@forEach | |||||
| val lot = lotInfo.id?.let { inventoryLotLineRepository.findById(it).orElse(null) } | |||||
| ?: return@forEach | |||||
| val totalQty = lot.inQty ?: zero | |||||
| val outQty = lot.outQty ?: zero | |||||
| val holdQty = lot.holdQty ?: zero | |||||
| val availableQtyInBaseUnits = totalQty.minus(outQty).minus(holdQty) | |||||
| if (availableQtyInBaseUnits <= zero) return@forEach | |||||
| val assignQtyInBaseUnits = minOf(availableQtyInBaseUnits, remainingQtyInBaseUnits) | |||||
| remainingQtyInBaseUnits = remainingQtyInBaseUnits.minus(assignQtyInBaseUnits) | |||||
| val assignQtyInSalesUnits = assignQtyInBaseUnits.divide(ratio, 0, RoundingMode.DOWN) | |||||
| if (assignQtyInSalesUnits > zero) { | |||||
| suggestions.add(SuggestedPickLot().apply { | |||||
| type = SuggestedPickLotType.PICK_ORDER | |||||
| suggestedLotLine = lot | |||||
| pickOrderLine = orderLine | |||||
| qty = assignQtyInSalesUnits | |||||
| }) | |||||
| } | |||||
| } | |||||
| } | |||||
| return suggestions | |||||
| } | |||||
| private fun findProblematicSuggestions(pickOrderId: Long): List<SuggestedPickLot> { | |||||
| val pickOrderLines = pickOrderRepository.findById(pickOrderId) | |||||
| .orElseThrow() | |||||
| .pickOrderLines | |||||
| val pickOrderLineIds = pickOrderLines.mapNotNull { it.id } | |||||
| println("=== DEBUG: findProblematicSuggestions ===") | |||||
| println("Pick Order ID: $pickOrderId") | |||||
| println("Pick Order Line IDs: $pickOrderLineIds") | |||||
| val allSuggestions = suggestedPickLotRepository.findAllByPickOrderLineIdIn(pickOrderLineIds) | |||||
| println("Total suggestions found: ${allSuggestions.size}") | |||||
| val problematicSuggestions = allSuggestions.filter { suggestion -> | |||||
| val pickOrderLine = pickOrderLines.find { it.id == suggestion.pickOrderLine?.id } | |||||
| val inventoryLotLine = suggestion.suggestedLotLine | |||||
| if (pickOrderLine == null || inventoryLotLine == null) { | |||||
| println("Suggestion ${suggestion.id}: Missing pickOrderLine or inventoryLotLine") | |||||
| return@filter true | |||||
| } | |||||
| // Get sales unit conversion ratio | |||||
| val salesUnit = pickOrderLine.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } | |||||
| val ratio = (salesUnit?.ratioN ?: BigDecimal.ONE).divide(salesUnit?.ratioD ?: BigDecimal.ONE, 10, RoundingMode.HALF_UP) | |||||
| val ratioCheck = (suggestion.qty ?: BigDecimal.ZERO).divide(pickOrderLine.qty ?: BigDecimal.ONE, 2, RoundingMode.HALF_UP) | |||||
| // Calculate available qty in base units | |||||
| val availableQtyInBaseUnits = (inventoryLotLine.inQty ?: BigDecimal.ZERO) | |||||
| .minus(inventoryLotLine.outQty ?: BigDecimal.ZERO) | |||||
| .minus(inventoryLotLine.holdQty ?: BigDecimal.ZERO) | |||||
| // Convert available qty to sales units for comparison | |||||
| val availableQtyInSalesUnits = availableQtyInBaseUnits.divide(ratio, 2, RoundingMode.HALF_UP) | |||||
| // Convert suggestion qty to base units for comparison | |||||
| val suggestionQtyInBaseUnits = (suggestion.qty ?: BigDecimal.ZERO).multiply(ratio) | |||||
| println("Suggestion ${suggestion.id}: qty=${suggestion.qty} (sales), requiredQty=${pickOrderLine.qty} (sales), ratio=$ratioCheck") | |||||
| println(" - availableQtyInBaseUnits=$availableQtyInBaseUnits, availableQtyInSalesUnits=$availableQtyInSalesUnits") | |||||
| println(" - suggestionQtyInBaseUnits=$suggestionQtyInBaseUnits") | |||||
| // Flag as problematic if: | |||||
| val exceedsRatio = ratioCheck > BigDecimal("10") | |||||
| val exceedsAvailable = suggestionQtyInBaseUnits > availableQtyInBaseUnits | |||||
| println(" - exceedsRatio=$exceedsRatio, exceedsAvailable=$exceedsAvailable") | |||||
| val isProblematic = exceedsRatio || exceedsAvailable | |||||
| if (isProblematic) { | |||||
| println("Suggestion ${suggestion.id}: PROBLEMATIC - ratio=$ratioCheck, exceedsAvailable=$exceedsAvailable") | |||||
| } | |||||
| isProblematic | |||||
| } | |||||
| println("Problematic suggestions found: ${problematicSuggestions.size}") | |||||
| return problematicSuggestions | |||||
| } | |||||
| private fun generateCorrectSuggestionsWithExistingHolds(pickOrder: PickOrder): List<SuggestedPickLot> { | |||||
| val suggestions = mutableListOf<SuggestedPickLot>() | |||||
| val zero = BigDecimal.ZERO | |||||
| val one = BigDecimal.ONE | |||||
| pickOrder.pickOrderLines.forEach { orderLine -> | |||||
| val itemId = orderLine.item?.id ?: return@forEach | |||||
| val requiredQty = orderLine.qty ?: zero // This is in sales units | |||||
| println("=== DEBUG: generateCorrectSuggestionsWithExistingHolds ===") | |||||
| println("Item ID: $itemId, Required Qty: $requiredQty") | |||||
| // Get sales unit conversion ratio | |||||
| val salesUnit = itemUomService.findSalesUnitByItemId(itemId) | |||||
| val ratio = (salesUnit?.ratioN ?: one).divide(salesUnit?.ratioD ?: one, 10, RoundingMode.HALF_UP) | |||||
| println("Ratio: $ratio") | |||||
| // Get available inventory lots for this item (FEFO order) | |||||
| val availableLots = inventoryLotLineService | |||||
| .allInventoryLotLinesByItemIdIn(listOf(itemId)) | |||||
| .filter { it.status == InventoryLotLineStatus.AVAILABLE.value } | |||||
| .sortedBy { it.expiryDate } | |||||
| var remainingQtyInSalesUnits = requiredQty | |||||
| availableLots.forEach { lotInfo -> | |||||
| if (remainingQtyInSalesUnits <= zero) return@forEach | |||||
| // Get the actual InventoryLotLine entity | |||||
| val lot = lotInfo.id?.let { inventoryLotLineRepository.findById(it).orElse(null) } | |||||
| ?: return@forEach | |||||
| // ✅ FIX: Calculate available qty directly in sales units | |||||
| val totalQty = lot.inQty ?: zero | |||||
| val outQty = lot.outQty ?: zero | |||||
| val holdQty = lot.holdQty ?: zero | |||||
| val availableQtyInBaseUnits = totalQty.minus(outQty).minus(holdQty) | |||||
| // ✅ FIX: Convert to sales units with proper rounding | |||||
| val availableQtyInSalesUnits = availableQtyInBaseUnits.divide(ratio, 2, RoundingMode.HALF_UP) | |||||
| println("Lot ${lot.id}: availableQtyInBaseUnits=$availableQtyInBaseUnits, availableQtyInSalesUnits=$availableQtyInSalesUnits") | |||||
| if (availableQtyInSalesUnits <= zero) return@forEach | |||||
| // ✅ FIX: Take what we can from this lot (constrained by available AND remaining) | |||||
| val assignQtyInSalesUnits = minOf(availableQtyInSalesUnits, remainingQtyInSalesUnits) | |||||
| remainingQtyInSalesUnits = remainingQtyInSalesUnits.minus(assignQtyInSalesUnits) | |||||
| println("Lot ${lot.id}: assignQtyInSalesUnits=$assignQtyInSalesUnits, remainingQtyInSalesUnits=$remainingQtyInSalesUnits") | |||||
| if (assignQtyInSalesUnits > zero) { | |||||
| suggestions.add(SuggestedPickLot().apply { | |||||
| type = SuggestedPickLotType.PICK_ORDER | |||||
| suggestedLotLine = lot | |||||
| pickOrderLine = orderLine | |||||
| qty = assignQtyInSalesUnits // Store in sales units | |||||
| }) | |||||
| } | |||||
| } | |||||
| // If we still have remaining qty, create a suggestion with null lot (insufficient stock) | |||||
| if (remainingQtyInSalesUnits > zero) { | |||||
| println("Remaining Qty in Sales Units: $remainingQtyInSalesUnits") | |||||
| suggestions.add(SuggestedPickLot().apply { | |||||
| type = SuggestedPickLotType.PICK_ORDER | |||||
| suggestedLotLine = null // No lot available | |||||
| pickOrderLine = orderLine | |||||
| qty = remainingQtyInSalesUnits | |||||
| }) | |||||
| } | |||||
| } | |||||
| return suggestions | |||||
| } | |||||
| } | |||||
| @@ -21,6 +21,8 @@ import java.io.OutputStream | |||||
| import java.io.UnsupportedEncodingException | import java.io.UnsupportedEncodingException | ||||
| import java.math.BigDecimal | import java.math.BigDecimal | ||||
| import java.text.ParseException | import java.text.ParseException | ||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||||
| import com.ffii.fpsms.modules.stock.web.model.UpdateInventoryLotLineStatusRequest | |||||
| @RequestMapping("/inventoryLotLine") | @RequestMapping("/inventoryLotLine") | ||||
| @RestController | @RestController | ||||
| @@ -75,4 +77,8 @@ class InventoryLotLineController ( | |||||
| response.addHeader("filename", "${pdf["fileName"]}.pdf") | response.addHeader("filename", "${pdf["fileName"]}.pdf") | ||||
| out.write(JasperExportManager.exportReportToPdf(jasperPrint)); | out.write(JasperExportManager.exportReportToPdf(jasperPrint)); | ||||
| } | } | ||||
| @PostMapping("/updateStatus") | |||||
| fun updateInventoryLotLineStatus(@RequestBody request: UpdateInventoryLotLineStatusRequest): MessageResponse { | |||||
| return inventoryLotLineService.updateInventoryLotLineStatus(request) | |||||
| } | |||||
| } | } | ||||
| @@ -8,8 +8,10 @@ import com.ffii.fpsms.modules.stock.service.StockInLineService | |||||
| import com.ffii.fpsms.modules.stock.service.StockOutLineService | import com.ffii.fpsms.modules.stock.service.StockOutLineService | ||||
| import com.ffii.fpsms.modules.stock.web.model.CreateStockOutLineRequest | import com.ffii.fpsms.modules.stock.web.model.CreateStockOutLineRequest | ||||
| import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineRequest | import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineRequest | ||||
| import com.ffii.fpsms.modules.stock.web.model.UpdateStockOutLineStatusRequest | |||||
| import jakarta.validation.Valid | import jakarta.validation.Valid | ||||
| import org.springframework.web.bind.annotation.* | import org.springframework.web.bind.annotation.* | ||||
| import com.ffii.fpsms.modules.stock.web.model.CreateStockOutLineWithoutConsoRequest | |||||
| @RestController | @RestController | ||||
| @RequestMapping("/stockOutLine") | @RequestMapping("/stockOutLine") | ||||
| @@ -25,9 +27,18 @@ class StockOutLineController( | |||||
| fun create(@Valid @RequestBody request: CreateStockOutLineRequest): MessageResponse { | fun create(@Valid @RequestBody request: CreateStockOutLineRequest): MessageResponse { | ||||
| return stockOutLineService.create(request) | return stockOutLineService.create(request) | ||||
| } | } | ||||
| @PostMapping("/createWithoutConso") | |||||
| fun createWithoutConso(@Valid @RequestBody request: CreateStockOutLineWithoutConsoRequest): MessageResponse { | |||||
| return stockOutLineService.createWithoutConso(request) | |||||
| } | |||||
| @PostMapping("/update") | @PostMapping("/update") | ||||
| fun update(@Valid @RequestBody request: UpdateStockOutLineRequest): MessageResponse { | fun update(@Valid @RequestBody request: UpdateStockOutLineRequest): MessageResponse { | ||||
| println("triggering") | println("triggering") | ||||
| return stockOutLineService.update(request) | return stockOutLineService.update(request) | ||||
| } | } | ||||
| @PostMapping("/updateStatus") | |||||
| fun updateStatus(@Valid @RequestBody request: UpdateStockOutLineStatusRequest): MessageResponse { | |||||
| return stockOutLineService.updateStatus(request) | |||||
| } | |||||
| } | } | ||||
| @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.PathVariable | |||||
| import org.springframework.web.bind.annotation.PostMapping | import org.springframework.web.bind.annotation.PostMapping | ||||
| import org.springframework.web.bind.annotation.RequestMapping | import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RestController | import org.springframework.web.bind.annotation.RestController | ||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||||
| @RequestMapping("/suggestedPickLot") | @RequestMapping("/suggestedPickLot") | ||||
| @RestController | @RestController | ||||
| @@ -23,4 +24,9 @@ class SuggestedPickLotController( | |||||
| // val test1 = pickOrderRepository.findAllByConsoCode(conso).flatMap { it.pickOrderLines } | // val test1 = pickOrderRepository.findAllByConsoCode(conso).flatMap { it.pickOrderLines } | ||||
| // return suggestedPickLotService.suggestionForPickOrderLines(test1) | // return suggestedPickLotService.suggestionForPickOrderLines(test1) | ||||
| // } | // } | ||||
| @PostMapping("/resuggest/{pickOrderId}") | |||||
| fun resuggestPickOrder(@PathVariable pickOrderId: Long): MessageResponse { | |||||
| return suggestedPickLotService.resuggestPickOrder(pickOrderId) | |||||
| } | |||||
| } | } | ||||
| @@ -11,3 +11,7 @@ data class LotLineInfo( | |||||
| val remainingQty: BigDecimal, | val remainingQty: BigDecimal, | ||||
| val uom: String | val uom: String | ||||
| ) | ) | ||||
| data class UpdateInventoryLotLineStatusRequest( | |||||
| val inventoryLotLineId: Long, | |||||
| val status: String | |||||
| ) | |||||
| @@ -46,3 +46,15 @@ data class UpdateStockOutLineRequest( | |||||
| val pickTime: LocalDateTime?, | val pickTime: LocalDateTime?, | ||||
| // val pickerId: Long? | // val pickerId: Long? | ||||
| ) | ) | ||||
| data class CreateStockOutLineWithoutConsoRequest( | |||||
| val pickOrderLineId: Long, | |||||
| val inventoryLotLineId: Long, | |||||
| val stockOutId: Long, | |||||
| val qty: Double, | |||||
| ) | |||||
| data class UpdateStockOutLineStatusRequest( | |||||
| val id: Long, | |||||
| val status: String, | |||||
| val qty: Double? = null, | |||||
| val remarks: String? = null | |||||
| ) | |||||