diff --git a/src/main/java/com/ffii/fpsms/modules/bag/service/bagService.kt b/src/main/java/com/ffii/fpsms/modules/bag/service/bagService.kt index 4ae1cc6..29b7ea8 100644 --- a/src/main/java/com/ffii/fpsms/modules/bag/service/bagService.kt +++ b/src/main/java/com/ffii/fpsms/modules/bag/service/bagService.kt @@ -15,6 +15,8 @@ import java.time.LocalDateTime import java.time.LocalDate import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository import com.ffii.fpsms.modules.productProcess.entity.ProductProcessRepository +import com.ffii.fpsms.modules.master.entity.ItemUomRespository +import java.math.BigDecimal @Service open class BagService( private val bagRepository: BagRepository, @@ -22,20 +24,34 @@ open class BagService( private val joBagConsumptionRepository: JoBagConsumptionRepository, private val inventoryLotRepository: InventoryLotRepository, private val jobOrderRepository: JobOrderRepository, - private val productProcessRepository: ProductProcessRepository + private val productProcessRepository: ProductProcessRepository, + private val itemUomRepository: ItemUomRespository, ) { open fun createBagLotLinesByBagId(request: CreateBagLotLineRequest): MessageResponse { val bag = bagRepository.findById(request.bagId).orElse(null) val lot = inventoryLotRepository.findByLotNoAndItemId(request.lotNo, request.itemId) + val BaseUnitOfMeasure= itemUomRepository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(request.itemId) + val baseRatioN = BaseUnitOfMeasure?.ratioN ?: BigDecimal.ONE + println("baseRatioN: $baseRatioN") + val baseRatioD = BaseUnitOfMeasure?.ratioD ?: BigDecimal.ONE + println("baseRatioD: $baseRatioD") val bagLotLine = BagLotLine().apply { this.bagId = bag?.id this.lotId = lot?.id this.lotNo = lot?.lotNo - this.startQty = request.stockQty + this.startQty = request.stockQty.toBigDecimal() + .multiply(baseRatioN) + .divide(baseRatioD) + .toInt() + println("startQty: $startQty") this.consumedQty = 0 this.stockOutLineId = request.stockOutLineId this.scrapQty = 0 - this.balanceQty = request.stockQty + this.balanceQty = request.stockQty.toBigDecimal() + .multiply(baseRatioN) + .divide(baseRatioD) + .toInt() + println("balanceQty: $balanceQty") } bagLotLineRepository.save(bagLotLine) bag.takenBagBalance = (bag.takenBagBalance ?: 0) + (bagLotLine.balanceQty ?: 0) diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderBomMaterialService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderBomMaterialService.kt index 30ed86f..1453d1f 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderBomMaterialService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderBomMaterialService.kt @@ -34,7 +34,7 @@ open class JobOrderBomMaterialService( joId = joId, itemId = bm.item?.id, //reqQty = (bm.qty?.times(proportion) ?: zero).setScale(0,RoundingMode.CEILING), - reqQty = (bm.saleQty?.times(proportion) ?: zero).setScale(0, RoundingMode.CEILING), + reqQty = (bm.stockQty?.times(proportion) ?: zero).setScale(0, RoundingMode.CEILING), uomId = stockUnit?.uom?.id ) } ?: listOf() diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt index 636ae65..5d558b9 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt @@ -68,6 +68,7 @@ import java.time.LocalDate import java.time.LocalDateTime import com.ffii.fpsms.modules.master.entity.BomMaterialRepository import com.ffii.fpsms.modules.master.service.ItemUomService +import com.ffii.fpsms.modules.master.web.models.ConvertUomByItemRequest @Service open class JobOrderService( val jobOrderRepository: JobOrderRepository, @@ -208,7 +209,6 @@ open class JobOrderService( return RecordsRes(records, total.toInt()); } - // 添加辅助方法计算库存统计 private fun calculateStockCounts( jobOrder: JobOrder, inventoriesMap: Map @@ -216,7 +216,7 @@ open class JobOrderService( // 过滤掉 consumables 和 CMB 类型的物料 val nonConsumablesJobms = jobOrder.jobms.filter { jobm -> val itemType = jobm.item?.type?.lowercase() - itemType != "consumables" && itemType != "cmb"&& itemType != "nm" + itemType != "consumables" && itemType != "consumable" && itemType != "cmb" && itemType != "nm" } if (nonConsumablesJobms.isEmpty()) { @@ -226,14 +226,16 @@ open class JobOrderService( var sufficientCount = 0 var insufficientCount = 0 + println("=== JobOrderService.calculateStockCounts for JobOrder: ${jobOrder.code} ===") + nonConsumablesJobms.forEach { jobm -> val itemId = jobm.item?.id - val reqQty = jobm.reqQty ?: BigDecimal.ZERO + val itemCode = jobm.item?.code ?: "N/A" + val itemName = jobm.item?.name ?: "N/A" if (itemId != null) { val inventory = inventoriesMap[itemId] val availableQty = if (inventory != null) { - // 使用 availableQty,如果没有则计算:onHandQty - onHoldQty - unavailableQty inventory.availableQty ?: ( (inventory.onHandQty ?: BigDecimal.ZERO) - (inventory.onHoldQty ?: BigDecimal.ZERO) - @@ -243,17 +245,74 @@ open class JobOrderService( BigDecimal.ZERO } - if (availableQty >= reqQty) { + // ✅ 获取 reqQty 和 availableQty 的单位信息 + val reqQty = jobm.reqQty ?: BigDecimal.ZERO + val reqUomId = jobm.uom?.id ?: 0L + val reqUomName = jobm.uom?.udfudesc ?: "N/A" + + // ✅ 修复:使用 itemUomService 获取 stockUomId(与 ProductProcessService 保持一致) + val stockUnitItemUom = itemUomService.findStockUnitByItemId(itemId) + val stockUomId = stockUnitItemUom?.uom?.id ?: 0L + val stockUomName = stockUnitItemUom?.uom?.udfudesc ?: "N/A" + + // ✅ 转换为 base unit 进行比较(与 ProductProcessService 保持一致) + val baseReqQtyResult = if (reqUomId > 0 && reqQty > BigDecimal.ZERO) { + try { + itemUomService.convertUomByItem( + ConvertUomByItemRequest( + itemId = itemId, + qty = reqQty, + uomId = reqUomId, + targetUnit = "baseUnit" + ) + ) + } catch (e: Exception) { + println("Error converting reqQty to base unit: ${e.message}") + null + } + } else { + null + } + + val baseAvailableQtyResult = if (stockUomId > 0 && availableQty > BigDecimal.ZERO) { + try { + itemUomService.convertUomByItem( + ConvertUomByItemRequest( + itemId = itemId, + qty = availableQty, + uomId = stockUomId, + targetUnit = "baseUnit" + ) + ) + } catch (e: Exception) { + println("Error converting availableQty to base unit: ${e.message}") + null + } + } else { + null + } + + val baseReqQty = baseReqQtyResult?.newQty ?: BigDecimal.ZERO + val baseAvailableQty = baseAvailableQtyResult?.newQty ?: BigDecimal.ZERO + val baseUomName = baseReqQtyResult?.udfudesc ?: baseAvailableQtyResult?.udfudesc ?: "N/A" + + // ✅ 使用 base unit 进行比较 + if (baseAvailableQty >= baseReqQty) { sufficientCount++ + println("✅ SUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)") } else { insufficientCount++ + println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)") } } else { // 如果没有 itemId,视为不足 insufficientCount++ + println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - No itemId") } } + println("=== Result: sufficient=$sufficientCount, insufficient=$insufficientCount ===") + return Pair(sufficientCount, insufficientCount) } open fun jobOrderDetail(id: Long): JobOrderDetail { @@ -454,16 +513,30 @@ open class JobOrderService( } // ✅ 使用 stockReqQty (bomMaterial.saleQty) 和 stockUom (bomMaterial.salesUnit) - val stockReqQty = jobm.reqQty ?: bomMaterial?.saleQty ?: BigDecimal.ZERO - val stockUomId = bomMaterial?.salesUnit?.id + val stockReqQty = jobm.reqQty ?: bomMaterial?.stockQty ?: BigDecimal.ZERO + val stockUomId = bomMaterial?.stockUnit?.toLong() ?: itemUomService.findStockUnitByItemId(itemId)?.uom?.id // Fallback: 从 Item 获取库存单位 - ?: jobm.uom?.id // 最后的 fallback + ?: jobm.uom?.id // 最后的 fallback SavePickOrderLineRequest( itemId = itemId, qty = stockReqQty, // ✅ 使用库存单位数量 uomId = stockUomId, // ✅ 使用库存单位 ) + } + if (pols.isEmpty()) { + + jo.status = JobOrderStatus.PROCESSING + jobOrderRepository.save(jo) + return MessageResponse( + id = jo.id, + code = jo.code, + name = jo.bom?.name, + type = null, + message = null, + errorPosition = null, + entity = mapOf("status" to jo.status?.value) + ) } val po = SavePickOrderRequest( joId = jo.id, diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt index f1e34f5..e53ad3d 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt @@ -419,4 +419,56 @@ open class PlasticBagPrinterService( println("Han's Laser TCP 連接已關閉") } } + + fun sendDataFlex6330Zpl(request: PrintRequest) { + Socket().use { socket -> + try { + // Connect with timeout + socket.connect(InetSocketAddress(request.printerIp, request.printerPort), 5000) + socket.soTimeout = 5000 // read timeout if expecting response (optional) + + val out = socket.getOutputStream() + + // Build ZPL dynamically + val zpl = buildString { + append("^XA\n") + append("^PW500\n") // Print width ~42mm @300dpi; adjust 400-630 based on head + append("^LL280\n") // Label height ~23mm; tune for your pouch + append("^PON\n") // Normal orientation + append("^CI28\n") // UTF-8 / extended char set for Chinese + + // Chinese product name / description (top) + append("^FO20,20^A@N,36,36,E:SIMSUN.FNT^FD${request.itemName}^FS\n") // Assumes font loaded + + // Item code + append("^FO20,80^A0N,32,32^FD${request.itemCode}^FS\n") + + // Expiry date + append("^FO20,120^A0N,28,28^FDEXP: ${request.expiryDate}^FS\n") + + // Lot / Batch No. + append("^FO20,160^A0N,28,28^FDLOT: ${request.lotNo}^FS\n") + + // QR code encoding lotNo (or combine: item|lot|exp) + // Position right side, mag 6 (~good size), model 2 + val qrData = request.lotNo // or "${request.itemCode}|${request.lotNo}|${request.expiryDate}" + append("^FO320,20^BQN,2,6^FDQA,$qrData^FS\n") // QA, prefix for alphanumeric mode + + append("^XZ\n") + } + + // Send as bytes (UTF-8 safe) + out.write(zpl.toByteArray(Charsets.UTF_8)) + out.flush() + + println("DataFlex 6330 ZPL: Print job sent to ${request.printerIp}:${request.printerPort}") + // Optional: read response if printer echoes anything (rare in raw ZPL) + // val response = socket.getInputStream().readBytes().decodeToString() + // println("Response: $response") + + } catch (e: Exception) { + throw RuntimeException("DataFlex 6330 ZPL communication failed: ${e.message}", e) + } + } + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt index 2b20ab6..1c15144 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt @@ -46,6 +46,7 @@ class PlasticBagPrinterController( response.outputStream.flush() } + /* @PostMapping("/print-dataflex") fun printDataFlex(@RequestBody request: PrintRequest): ResponseEntity { return try { @@ -54,6 +55,16 @@ class PlasticBagPrinterController( } catch (e: Exception) { ResponseEntity.status(500).body("Error: ${e.message}") } + }*/ + + @PostMapping("/print-dataflex") + fun printDataFlex6330Zpl(@RequestBody request: PrintRequest): ResponseEntity { + return try { + plasticBagPrinterService.sendDataFlex6330Zpl(request) + ResponseEntity.ok("DataFlex 6330 ZPL print command sent successfully to ${request.printerIp}:${request.printerPort}") + } catch (e: Exception) { + ResponseEntity.status(500).body("Error sending ZPL to DataFlex 6330: ${e.message}") + } } @PostMapping("/print-laser") diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/BomMaterial.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/BomMaterial.kt index 8ec8fbd..4fa0e57 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/BomMaterial.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/BomMaterial.kt @@ -52,7 +52,12 @@ open class BomMaterial : BaseEntity() { open var baseUnit: Integer? = null @Column(name = "baseUnitName", length = 100) open var baseUnitName: String? = null - + @Column(name = "stockQty", precision = 14, scale = 2) + open var stockQty: BigDecimal? = null + @Column(name = "stockUnit", nullable = false) + open var stockUnit: Integer? = null + @Column(name = "stockUnitName", length = 100) + open var stockUnitName: String? = null @NotNull @ManyToOne(optional = false) @JoinColumn(name = "bomId", nullable = false) diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt index 3e53450..092c4c9 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt @@ -150,23 +150,25 @@ open class BomService( var baseUnit: Integer? = null var baseUnitName: String? = null + var stockQty: BigDecimal? = null + var stockUnit: Integer? = null + var stockUnitName: String? = null + if (item?.id != null) { val itemId = item.id!! try { - // ---- 1) 获取 item 的真实 stock unit ---- - val stockItemUom = itemUomService.findStockUnitByItemId(itemId) - val itemStockUnit = stockItemUom?.uom + // ---- 1) 获取 item 的真实 sale unit ---- + val saleItemUom = itemUomService.findSalesUnitByItemId(itemId) + val itemSaleUnit = saleItemUom?.uom - // saleUnitId: 使用 item 的 stock unit uom id - saleUnitId = itemStockUnit?.id - - // saleUnitCode: 从 Excel 数据查找 uom_conversion,如果失败则使用 Excel 数据本身 + // ---- 2) 确定 sale unit(来自 Excel column 7)---- if (excelSalesUnit != null) { // Excel 找到了 uom_conversion + saleUnitId = excelSalesUnit.id saleUnitCode = excelSalesUnit.udfudesc - // 检查 Excel sales unit 与 item stock unit 是否匹配 - if (itemStockUnit != null && excelSalesUnit.id != itemStockUnit.id) { + // 检查 Excel sales unit 与 item sale unit 是否匹配 + if (itemSaleUnit != null && excelSalesUnit.id != itemSaleUnit.id) { bomMaterialImportIssues.add( BomMaterialImportIssue( bomId = bomId, @@ -174,7 +176,7 @@ open class BomService( itemId = item.id, itemCode = item.code, itemName = item.name, - reason = "Excel sales unit (${excelSalesUnit.code}/${excelSalesUnit.udfudesc}) does not match item stock unit (${itemStockUnit.code}/${itemStockUnit.udfudesc})", + reason = "Excel sales unit (${excelSalesUnit.code}/${excelSalesUnit.udfudesc}) does not match item sale unit (${itemSaleUnit.code}/${itemSaleUnit.udfudesc})", srcQty = excelSaleQty, srcUomCode = excelSalesUnit.code ) @@ -182,7 +184,7 @@ open class BomService( } } else { // Excel 没有找到 uom_conversion,使用 Excel 数据本身 - saleUnitCode = excelSalesUnitCode ?: itemStockUnit?.udfudesc + saleUnitCode = excelSalesUnitCode if (excelSalesUnitCode != null) { bomMaterialImportIssues.add( @@ -200,26 +202,85 @@ open class BomService( } } - // ---- 2) 从 saleQty + item 的真实 stock unit 转换为 baseQty ---- - if (excelSaleQty != null && itemStockUnit != null) { - val baseResult = itemUomService.convertUomByItem( - ConvertUomByItemRequest( - itemId = itemId, - qty = excelSaleQty, - uomId = itemStockUnit.id!!, - targetUnit = "baseUnit" + // ---- 3) 从 saleQty(sale unit)转换为 baseQty(base unit)---- + if (excelSaleQty != null && excelSalesUnit != null && excelSalesUnit.id != null) { + try { + // 先检查 item 是否有 base unit + val baseItemUom = itemUomService.findBaseUnitByItemId(itemId) + if (baseItemUom == null) { + bomMaterialImportIssues.add( + BomMaterialImportIssue( + bomId = bomId, + bomCode = bomCode, + itemId = item.id, + itemCode = item.code, + itemName = item.name, + reason = "Cannot convert saleQty to baseQty: item base unit not found", + srcQty = excelSaleQty, + srcUomCode = excelSalesUnit.code + ) + ) + } else { + val baseResult = itemUomService.convertUomByItem( + ConvertUomByItemRequest( + itemId = itemId, + qty = excelSaleQty, + uomId = excelSalesUnit.id!!, + targetUnit = "baseUnit" + ) + ) + baseQty = baseResult.newQty + + // 获取 base unit 信息 + baseUnit = baseItemUom.uom?.id?.toInt()?.let { Integer.valueOf(it) } as? Integer + baseUnitName = baseItemUom.uom?.udfudesc + + // 验证转换结果 + if (baseQty == null) { + bomMaterialImportIssues.add( + BomMaterialImportIssue( + bomId = bomId, + bomCode = bomCode, + itemId = item.id, + itemCode = item.code, + itemName = item.name, + reason = "Cannot convert saleQty to baseQty: conversion returned null", + srcQty = excelSaleQty, + srcUomCode = excelSalesUnit.code + ) + ) + } + } + } catch (e: IllegalArgumentException) { + bomMaterialImportIssues.add( + BomMaterialImportIssue( + bomId = bomId, + bomCode = bomCode, + itemId = item.id, + itemCode = item.code, + itemName = item.name, + reason = "Cannot convert saleQty to baseQty: ${e.message ?: "IllegalArgumentException"}", + srcQty = excelSaleQty, + srcUomCode = excelSalesUnit.code + ) ) - ) - baseQty = baseResult.newQty - } - - // ---- 3) 获取 item 的真实 base unit ---- - val baseItemUom = itemUomService.findBaseUnitByItemId(itemId) - baseUnit = baseItemUom?.uom?.id?.toInt()?.let { Integer.valueOf(it) } as? Integer - baseUnitName = baseItemUom?.uom?.udfudesc - - // 如果 baseQty 转换失败,记录问题 - if (baseQty == null && excelSaleQty != null && itemStockUnit != null) { + println("【BOM Import Warning】bomCode=$bomCode, item=${item.code} 转 baseQty 失败: ${e.message}") + } catch (e: Exception) { + bomMaterialImportIssues.add( + BomMaterialImportIssue( + bomId = bomId, + bomCode = bomCode, + itemId = item.id, + itemCode = item.code, + itemName = item.name, + reason = "Cannot convert saleQty to baseQty: ${e.javaClass.simpleName} - ${e.message ?: "Unknown error"}", + srcQty = excelSaleQty, + srcUomCode = excelSalesUnit.code + ) + ) + println("【BOM Import Error】bomCode=$bomCode, item=${item.code} 转 baseQty 失败: ${e.message}") + } + } else if (excelSaleQty != null && excelSalesUnit == null) { bomMaterialImportIssues.add( BomMaterialImportIssue( bomId = bomId, @@ -227,12 +288,92 @@ open class BomService( itemId = item.id, itemCode = item.code, itemName = item.name, - reason = "Cannot convert saleQty to baseQty: conversion failed", + reason = "Cannot convert saleQty to baseQty: Excel sales unit not found", srcQty = excelSaleQty, - srcUomCode = itemStockUnit.code + srcUomCode = excelSalesUnitCode ) ) - } else if (excelSaleQty != null && itemStockUnit == null) { + } + + // ---- 4) 从 saleQty(sale unit)转换为 stockQty(stock unit)---- + if (excelSaleQty != null && excelSalesUnit != null && excelSalesUnit.id != null) { + try { + // 先检查 item 是否有 stock unit + val stockItemUom = itemUomService.findStockUnitByItemId(itemId) + if (stockItemUom == null) { + bomMaterialImportIssues.add( + BomMaterialImportIssue( + bomId = bomId, + bomCode = bomCode, + itemId = item.id, + itemCode = item.code, + itemName = item.name, + reason = "Cannot convert saleQty to stockQty: item stock unit not found", + srcQty = excelSaleQty, + srcUomCode = excelSalesUnit.code + ) + ) + } else { + val stockResult = itemUomService.convertUomByItem( + ConvertUomByItemRequest( + itemId = itemId, + qty = excelSaleQty, + uomId = excelSalesUnit.id!!, + targetUnit = "stockUnit" + ) + ) + stockQty = stockResult.newQty + + // 获取 stock unit 信息 + stockUnit = stockItemUom.uom?.id?.toInt()?.let { Integer.valueOf(it) } as? Integer + stockUnitName = stockItemUom.uom?.udfudesc + + // 验证转换结果 + if (stockQty == null) { + bomMaterialImportIssues.add( + BomMaterialImportIssue( + bomId = bomId, + bomCode = bomCode, + itemId = item.id, + itemCode = item.code, + itemName = item.name, + reason = "Cannot convert saleQty to stockQty: conversion returned null", + srcQty = excelSaleQty, + srcUomCode = excelSalesUnit.code + ) + ) + } + } + } catch (e: IllegalArgumentException) { + bomMaterialImportIssues.add( + BomMaterialImportIssue( + bomId = bomId, + bomCode = bomCode, + itemId = item.id, + itemCode = item.code, + itemName = item.name, + reason = "Cannot convert saleQty to stockQty: ${e.message ?: "IllegalArgumentException"}", + srcQty = excelSaleQty, + srcUomCode = excelSalesUnit.code + ) + ) + println("【BOM Import Warning】bomCode=$bomCode, item=${item.code} 转 stockQty 失败: ${e.message}") + } catch (e: Exception) { + bomMaterialImportIssues.add( + BomMaterialImportIssue( + bomId = bomId, + bomCode = bomCode, + itemId = item.id, + itemCode = item.code, + itemName = item.name, + reason = "Cannot convert saleQty to stockQty: ${e.javaClass.simpleName} - ${e.message ?: "Unknown error"}", + srcQty = excelSaleQty, + srcUomCode = excelSalesUnit.code + ) + ) + println("【BOM Import Error】bomCode=$bomCode, item=${item.code} 转 stockQty 失败: ${e.message}") + } + } else if (excelSaleQty != null && excelSalesUnit == null) { bomMaterialImportIssues.add( BomMaterialImportIssue( bomId = bomId, @@ -240,13 +381,37 @@ open class BomService( itemId = item.id, itemCode = item.code, itemName = item.name, - reason = "Cannot convert saleQty to baseQty: item stock unit not found", + reason = "Cannot convert saleQty to stockQty: Excel sales unit not found", srcQty = excelSaleQty, - srcUomCode = null + srcUomCode = excelSalesUnitCode ) ) } + // 最终检查:如果 stockQty 仍然为 null,记录问题 + if (stockQty == null && excelSaleQty != null && excelSalesUnit != null && excelSalesUnit.id != null) { + // 检查是否已经记录过这个问题(避免重复) + val alreadyReported = bomMaterialImportIssues.any { issue -> + issue.itemId == item.id && + issue.reason.contains("stockQty", ignoreCase = true) && + issue.srcQty == excelSaleQty + } + if (!alreadyReported) { + bomMaterialImportIssues.add( + BomMaterialImportIssue( + bomId = bomId, + bomCode = bomCode, + itemId = item.id, + itemCode = item.code, + itemName = item.name, + reason = "Cannot convert saleQty to stockQty: conversion failed (final check)", + srcQty = excelSaleQty, + srcUomCode = excelSalesUnit.code + ) + ) + } + } + } catch (e: IllegalArgumentException) { bomMaterialImportIssues.add( BomMaterialImportIssue( @@ -301,15 +466,21 @@ open class BomService( this.uom = req.uom // BOM 原始 UOM(column 3,保留) this.uomName = req.uomName - // 新逻辑:使用 Excel column 6 的 saleQty 和 item stock unit + // 新逻辑:使用 Excel column 6 的 saleQty 和 column 7 的 sales unit this.saleQty = saleQty - this.salesUnit = item?.id?.let { itemUomService.findStockUnitByItemId(it)?.uom } + this.salesUnit = excelSalesUnit // 使用 Excel 的 sales unit,不是 item 的 stock unit this.salesUnitCode = saleUnitCode + // 从 sale unit 转换为 base unit this.baseQty = baseQty this.baseUnit = baseUnit this.baseUnitName = baseUnitName + // 从 sale unit 转换为 stock unit(新增) + this.stockQty = stockQty + this.stockUnit = stockUnit + this.stockUnitName = stockUnitName + this.bom = req.bom } return bomMaterialRepository.saveAndFlush(bomMaterial) @@ -586,8 +757,8 @@ open class BomService( // 创建 equipment equipment = Equipment().apply { - this.name = equipmentName // 完整值 XXX-YYY - this.code = secondPart + this.code = equipmentName // 完整值 XXX-YYY + this.name = secondPart this.description = firstPart // XXX 写入 description } equipment = equipmentRepository.saveAndFlush(equipment) diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt index 07cc9ec..c2f18d7 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt @@ -648,8 +648,8 @@ open class ProductionScheduleService( i.pendingJobQty, ((i.stockQty * 1.0) + ifnull(i.pendingJobQty, 0) ) / i.avgQtyLastMonth as daysLeft, - -- i.baseScore as priority, - 25 + 25 + markDark + markFloat + markDense + markAS + markTimeSequence + markComplexity as priority, + ifnull(i.baseScore, 0) as priority, + -- 25 + 25 + markDark + markFloat + markDense + markAS + markTimeSequence + markComplexity as priority, i.* FROM (SELECT @@ -665,7 +665,7 @@ open class ProductionScheduleService( do.deleted = 0 and dol.itemId = items.id -- AND MONTH(do.estimatedArrivalDate) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH)) - AND do.estimatedArrivalDate >= '2026-01-08' AND do.estimatedArrivalDate < '2026-01-18' + AND do.estimatedArrivalDate >= '2026-01-08' AND do.estimatedArrivalDate < '2026-01-31' GROUP BY do.estimatedArrivalDate) AS d) AS avgQtyLastMonth, (select sum(reqQty) from job_order where bomId = bom.id and status != 'completed') AS pendingJobQty, @@ -718,8 +718,9 @@ open class ProductionScheduleService( LEFT JOIN items ON bom.itemId = items.id LEFT JOIN inventory ON items.id = inventory.itemId left join item_fake_onhand on items.code = item_fake_onhand.itemCode - WHERE - bom.itemId != 16771) AS i + WHERE 1 + and bom.itemId != 16771 + ) AS i WHERE 1 and i.avgQtyLastMonth is not null and i.onHandQty is not null diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt index 695a2cc..15979b2 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt @@ -74,9 +74,23 @@ open class PickExecutionIssueService( @Value("\${pick.execution.auto-resuggest-on-rejection:false}") private val autoResuggestOnLotRejection: Boolean = false - @Transactional(rollbackFor = [Exception::class]) open fun recordPickExecutionIssue(request: PickExecutionIssueRequest): MessageResponse { try { + println("=== recordPickExecutionIssue: START ===") + println("Request details:") + println(" pickOrderId: ${request.pickOrderId}") + println(" pickOrderLineId: ${request.pickOrderLineId}") + println(" itemId: ${request.itemId}") + println(" itemCode: ${request.itemCode}") + println(" lotId: ${request.lotId}") + println(" lotNo: ${request.lotNo}") + println(" requiredQty: ${request.requiredQty}") + println(" actualPickQty: ${request.actualPickQty}") + println(" missQty: ${request.missQty}") + println(" badItemQty: ${request.badItemQty}") + println(" badReason: ${request.badReason}") + println(" issueCategory: ${request.issueCategory}") + println("========================================") // 1. 检查是否已经存在相同的 pick execution issue 记录 val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse( @@ -84,7 +98,14 @@ open class PickExecutionIssueService( request.lotId ?: 0L ) + println("Checking for existing issues...") + println(" Found ${existingIssues.size} existing issues") + existingIssues.forEachIndexed { index, issue -> + println(" Existing[$index]: id=${issue.id}, issueNo=${issue.issueNo}, handleStatus=${issue.handleStatus}, issueQty=${issue.issueQty}") + } + if (existingIssues.isNotEmpty()) { + println("❌ Duplicate issue found - returning DUPLICATE error") return MessageResponse( id = null, name = "Pick execution issue already exists", @@ -96,18 +117,24 @@ open class PickExecutionIssueService( } val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null) + println("Pick order: id=${pickOrder?.id}, code=${pickOrder?.code}, type=${pickOrder?.type?.value}") // 2. 获取 inventory_lot_line 并计算账面数量 (bookQty) val inventoryLotLine = request.lotId?.let { inventoryLotLineRepository.findById(it).orElse(null) } + println("Inventory lot line: id=${inventoryLotLine?.id}, lotNo=${inventoryLotLine?.inventoryLot?.lotNo}") + // 计算账面数量(创建 issue 时的快照) val bookQty = if (inventoryLotLine != null) { val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO val outQty = inventoryLotLine.outQty ?: BigDecimal.ZERO - inQty.subtract(outQty) // bookQty = inQty - outQty + val calculated = inQty.subtract(outQty) // bookQty = inQty - outQty + println(" BookQty calculation: inQty=$inQty, outQty=$outQty, bookQty=$calculated") + calculated } else { + println(" No inventory lot line found, bookQty=0") BigDecimal.ZERO } @@ -117,41 +144,53 @@ open class PickExecutionIssueService( val missQty = request.missQty ?: BigDecimal.ZERO val badItemQty = request.badItemQty ?: BigDecimal.ZERO val badReason = request.badReason ?: "quantity_problem" + + println("=== Quantity Summary ===") + println(" Required Qty: $requiredQty") + println(" Actual Pick Qty: $actualPickQty") + println(" Miss Qty: $missQty") + println(" Bad Item Qty: $badItemQty") + println(" Bad Reason: $badReason") + println(" Book Qty: $bookQty") + // 4. 计算 issueQty(实际的问题数量) val issueQty = when { // 情况1: 已拣完但有坏品 actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> { + println(" Case 1: actualPickQty == requiredQty && badItemQty > 0") + println(" issueQty = badItemQty = $badItemQty") badItemQty // issueQty = badItemQty } badReason == "package_problem" && badItemQty > BigDecimal.ZERO -> { + println(" Case 2: badReason == 'package_problem' && badItemQty > 0") + println(" issueQty = badItemQty = $badItemQty") badItemQty } - actualPickQty < requiredQty -> { + println(" Case 3: actualPickQty < requiredQty") val calculatedIssueQty = bookQty.subtract(actualPickQty) + println(" issueQty = bookQty - actualPickQty = $bookQty - $actualPickQty = $calculatedIssueQty") if (missQty > BigDecimal.ZERO && missQty > calculatedIssueQty) { - println("⚠️ Warning: User reported missQty (${missQty}) exceeds calculated issueQty (${calculatedIssueQty})") - println(" BookQty: ${bookQty}, ActualPickQty: ${actualPickQty}") + println("⚠️ Warning: User reported missQty ($missQty) exceeds calculated issueQty ($calculatedIssueQty)") + println(" BookQty: $bookQty, ActualPickQty: $actualPickQty") } calculatedIssueQty } - - else -> BigDecimal.ZERO + else -> { + println(" Case 4: Default case") + println(" issueQty = 0") + BigDecimal.ZERO + } } - println("=== PICK EXECUTION ISSUE PROCESSING ===") - println("Required Qty: ${requiredQty}") - println("Actual Pick Qty: ${actualPickQty}") - println("Miss Qty (Reported): ${missQty}") - println("Bad Item Qty: ${badItemQty}") - println("Book Qty (inQty - outQty): ${bookQty}") - println("Issue Qty (Calculated): ${issueQty}") - println("Bad Reason: ${request.badReason}") - println("Lot ID: ${request.lotId}") - println("Item ID: ${request.itemId}") + println("=== Final IssueQty Calculation ===") + println(" Calculated IssueQty: $issueQty") println("================================================") // 5. 创建 pick execution issue 记录 + val issueNo = generateIssueNo() + println("Generated issue number: $issueNo") + val pickExecutionIssue = PickExecutionIssue( id = null, pickOrderId = request.pickOrderId, @@ -159,7 +198,7 @@ open class PickExecutionIssueService( pickOrderCreateDate = request.pickOrderCreateDate, pickExecutionDate = request.pickExecutionDate ?: LocalDate.now(), pickOrderLineId = request.pickOrderLineId, - issueNo = generateIssueNo(), + issueNo = issueNo, joPickOrderId = pickOrder?.jobOrder?.id, doPickOrderId = if (pickOrder?.type?.value == "do") pickOrder.id else null, issueCategory = IssueCategory.valueOf( @@ -189,11 +228,18 @@ open class PickExecutionIssueService( modifiedBy = "system", deleted = false ) - + + println("Creating pick execution issue record...") val savedIssue = pickExecutionIssueRepository.save(pickExecutionIssue) - + println("✅ Issue record created successfully!") + println(" Issue ID: ${savedIssue.id}") + println(" Issue No: ${savedIssue.issueNo}") + println(" Handle Status: ${savedIssue.handleStatus}") + println(" Issue Qty: ${savedIssue.issueQty}") + // 6. NEW: Update inventory_lot_line.issueQty if (request.lotId != null && inventoryLotLine != null) { + println("Updating inventory_lot_line.issueQty...") // ✅ 修改:如果只有 missQty,不更新 issueQty val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO val missQty = request.missQty ?: BigDecimal.ZERO @@ -205,6 +251,9 @@ open class PickExecutionIssueService( val hasMissItemWithPartialPick = missQty > BigDecimal.ZERO && actualPickQty > BigDecimal.ZERO + println(" isMissItemOnly: $isMissItemOnly") + println(" hasMissItemWithPartialPick: $hasMissItemWithPartialPick") + if (!isMissItemOnly && !hasMissItemWithPartialPick) { // 只有非 miss item 的情况才更新 issueQty val currentIssueQty = inventoryLotLine.issueQty ?: BigDecimal.ZERO @@ -213,42 +262,45 @@ open class PickExecutionIssueService( inventoryLotLine.modified = LocalDateTime.now() inventoryLotLine.modifiedBy = "system" inventoryLotLineRepository.saveAndFlush(inventoryLotLine) - println("Updated inventory_lot_line ${request.lotId} issueQty: ${currentIssueQty} -> ${newIssueQty}") + println("✅ Updated inventory_lot_line ${request.lotId} issueQty: $currentIssueQty -> $newIssueQty") } else { - println("Skipped updating issueQty for miss item (lot ${request.lotId})") + println("⏭️ Skipped updating issueQty for miss item (lot ${request.lotId})") } } - // 7. 获取相关数据用于后续处理 val actualPickQtyForProcessing = request.actualPickQty ?: BigDecimal.ZERO val missQtyForProcessing = request.missQty ?: BigDecimal.ZERO val badItemQtyForProcessing = request.badItemQty ?: BigDecimal.ZERO val lotId = request.lotId val itemId = request.itemId - - println("=== PICK EXECUTION ISSUE PROCESSING (NEW LOGIC) ===") - println("Actual Pick Qty: ${actualPickQtyForProcessing}") - println("Miss Qty: ${missQtyForProcessing}") - println("Bad Item Qty: ${badItemQtyForProcessing}") + + println("=== Processing Logic Selection ===") + println("Actual Pick Qty: $actualPickQtyForProcessing") + println("Miss Qty: $missQtyForProcessing") + println("Bad Item Qty: $badItemQtyForProcessing") println("Bad Reason: ${request.badReason}") - println("Lot ID: ${lotId}") - println("Item ID: ${itemId}") + println("Lot ID: $lotId") + println("Item ID: $itemId") println("================================================") - + // 8. 新的统一处理逻辑(根据 badReason 决定处理方式) when { // 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0) actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> { + println("→ Handling: Miss Item Only") handleMissItemOnly(request, missQtyForProcessing) } // 情况2: 只有 bad item (badItemQty > 0, missQty = 0) badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> { + println("→ Handling: Bad Item Only") // NEW: Check bad reason if (request.badReason == "package_problem") { + println(" Bad reason is 'package_problem' - calling handleBadItemPackageProblem") handleBadItemPackageProblem(request, badItemQtyForProcessing) } else { + println(" Bad reason is 'quantity_problem' - calling handleBadItemOnly") // quantity_problem or default: handle as normal bad item handleBadItemOnly(request, badItemQtyForProcessing) } @@ -256,29 +308,34 @@ open class PickExecutionIssueService( // 情况3: 既有 miss item 又有 bad item missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> { + println("→ Handling: Both Miss and Bad Item") // NEW: Check bad reason if (request.badReason == "package_problem") { + println(" Bad reason is 'package_problem' - calling handleBothMissAndBadItemPackageProblem") handleBothMissAndBadItemPackageProblem(request, missQtyForProcessing, badItemQtyForProcessing) } else { + println(" Bad reason is 'quantity_problem' - calling handleBothMissAndBadItem") handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing) } } // 情况4: 有 miss item 的情况(无论 actualPickQty 是多少) missQtyForProcessing > BigDecimal.ZERO -> { + println("→ Handling: Miss Item With Partial Pick") handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing) } // 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item) actualPickQtyForProcessing > BigDecimal.ZERO -> { + println("→ Handling: Normal Pick") handleNormalPick(request, actualPickQtyForProcessing) } else -> { - println("Unknown case: actualPickQty=${actualPickQtyForProcessing}, missQty=${missQtyForProcessing}, badItemQty=${badItemQtyForProcessing}") + println("⚠️ Unknown case: actualPickQty=$actualPickQtyForProcessing, missQty=$missQtyForProcessing, badItemQty=$badItemQtyForProcessing") } } - + val pickOrderForCompletion = pickOrderRepository.findById(request.pickOrderId).orElse(null) val consoCode = pickOrderForCompletion?.consoCode @@ -300,7 +357,9 @@ open class PickExecutionIssueService( println("⚠️ Error checking pick order completion by pickOrderId: ${e.message}") } } - + + println("=== recordPickExecutionIssue: SUCCESS ===") + println("Issue ID: ${savedIssue.id}, Issue No: ${savedIssue.issueNo}") return MessageResponse( id = savedIssue.id, name = "Pick execution issue recorded successfully", @@ -309,9 +368,10 @@ open class PickExecutionIssueService( message = "Pick execution issue recorded successfully", errorPosition = null ) - + } catch (e: Exception) { - println("=== ERROR IN recordPickExecutionIssue ===") + println("=== recordPickExecutionIssue: ERROR ===") + println("Error: ${e.message}") e.printStackTrace() return MessageResponse( id = null, @@ -2585,10 +2645,11 @@ open fun getLotIssueDetails(lotId: Long, itemId: Long, issueType: String): LotIs ) } -// New: Submit with custom quantity -@Transactional(rollbackFor = [Exception::class]) open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse { try { + println("=== submitIssueWithQty: START ===") + println("Request: lotId=${request.lotId}, itemId=${request.itemId}, issueType=${request.issueType}, submitQty=${request.submitQty}, handler=${request.handler}") + // Find all issues for this lot and item val issues = if (request.issueType == "miss") { pickExecutionIssueRepository.findMissItemList(IssueCategory.lot_issue) @@ -2606,7 +2667,13 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse } } + println("Found ${issues.size} issues to process") + issues.forEachIndexed { index, issue -> + println(" Issue[$index]: id=${issue.id}, issueQty=${issue.issueQty}, handleStatus=${issue.handleStatus}") + } + if (issues.isEmpty()) { + println("❌ No issues found for this lot") return MessageResponse( id = null, name = "Error", @@ -2623,13 +2690,36 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse // Use custom quantity instead of sum val submitQty = request.submitQty - if (submitQty <= BigDecimal.ZERO) { + // ✅ 修改:允许提交0,0表示"标记为已处理但无需出库" + if (submitQty < BigDecimal.ZERO) { + println("❌ Submit quantity cannot be negative: $submitQty") return MessageResponse( id = null, name = "Error", code = "INVALID", type = "stock_issue", - message = "Submit quantity must be greater than 0", + message = "Submit quantity cannot be negative", + errorPosition = null + ) + } + + // ✅ 新增:如果提交数量为0,只标记issue为已处理,不创建stock_out_line + if (submitQty == BigDecimal.ZERO) { + println("ℹ️ Submit quantity is 0 - marking issues as handled without creating stock out") + + // Mark all issues as handled + issues.forEach { issue -> + println(" Marking issue ${issue.id} as handled by handler $handler") + markIssueHandled(issue, handler) + } + + println("✅ All issues marked as handled (no stock out created)") + return MessageResponse( + id = null, + name = "Success", + code = "SUCCESS", + type = "stock_issue", + message = "Issues marked as handled (no stock out - quantity is 0)", errorPosition = null ) } @@ -2649,14 +2739,17 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse firstIssue.issueRemark, handler ) + println("Created stock out header: id=${stockOut.id}, type=${if (isMissItem) "MISS_ITEM" else "BAD_ITEM"}") val pickOrderLine = firstIssue.pickOrderLineId?.let { pickOrderLineRepository.findById(it).orElse(null) } + println("Pick order line: id=${pickOrderLine?.id}") val lotLine = request.lotId.let { inventoryLotLineRepository.findById(it).orElse(null) } + println("Lot line: id=${lotLine?.id}, lotNo=${lotLine?.inventoryLot?.lotNo}") val item = itemsRepository.findById(request.itemId).orElse(null) ?: return MessageResponse( @@ -2667,6 +2760,7 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse message = "Item not found", errorPosition = null ) + println("Item: id=${item.id}, code=${item.code}") // Create stock_out_line with custom quantity val stockOutLine = StockOutLine().apply { @@ -2679,21 +2773,27 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse this.type = if (isMissItem) "Miss" else "Bad" } val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) + println("Created stock out line: id=${savedStockOutLine.id}, qty=${savedStockOutLine.qty}, status=${savedStockOutLine.status}") + if (!isMissItem && request.lotId != null) { val lotLineForReset = inventoryLotLineRepository.findById(request.lotId).orElse(null) if (lotLineForReset != null) { + val oldIssueQty = lotLineForReset.issueQty lotLineForReset.issueQty = BigDecimal.ZERO inventoryLotLineRepository.saveAndFlush(lotLineForReset) - println("✅ Reset issueQty to 0 for lot ${request.lotId} before bad item submission (submitIssueWithQty)") + println("✅ Reset issueQty for lot ${request.lotId}: $oldIssueQty -> 0") } } + // Update inventory_lot_line with custom quantity - pass isMissItem flag if (request.lotId != null) { + println("Updating lot line after issue: lotId=${request.lotId}, submitQty=$submitQty, isMissItem=$isMissItem") updateLotLineAfterIssue(request.lotId, submitQty, isMissItem) } // Mark all issues as handled issues.forEach { issue -> + println(" Marking issue ${issue.id} as handled by handler $handler") markIssueHandled(issue, handler) } @@ -2708,6 +2808,7 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, if (isMissItem) "Miss" else "Bad") + println("=== submitIssueWithQty: SUCCESS ===") return MessageResponse( id = stockOut.id, name = "Success", @@ -2717,6 +2818,9 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse errorPosition = null ) } catch (e: Exception) { + println("=== submitIssueWithQty: ERROR ===") + println("Error: ${e.message}") + e.printStackTrace() return MessageResponse( id = null, name = "Error", diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt index 74ea89b..deb791b 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/ProductProcessLineRepository.kt @@ -2,6 +2,8 @@ package com.ffii.fpsms.modules.productProcess.entity import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository +import org.springframework.data.jpa.repository.Query +import java.time.LocalDateTime @Repository interface ProductProcessLineRepository : JpaRepository { @@ -10,4 +12,22 @@ interface ProductProcessLineRepository : JpaRepository fun findByHandler_IdAndStartTimeIsNotNullAndEndTimeIsNull(handlerId: Long): List fun findByProductProcess_IdIn(ids: List): List + @Query("SELECT l FROM ProductProcessLine l LEFT JOIN FETCH l.equipment WHERE l.productProcess.id = :productProcessId") + fun findByProductProcess_IdWithEquipment(productProcessId: Long): List + + // 用於 Operator KPI:抓取在指定時間範圍內有重疊的所有行 + @Query( + """ + SELECT l FROM ProductProcessLine l + WHERE l.deleted = false + AND l.startTime IS NOT NULL + AND ( + (l.startTime <= :endOfDay AND (l.endTime IS NULL OR l.endTime >= :startOfDay)) + ) + """ + ) + fun findAllOverlappingWithDateRange(startOfDay: LocalDateTime, endOfDay: LocalDateTime): List + + // 用於 Equipment 狀態:查詢指定 equipmentDetailId 目前仍未完成的所有行 + fun findByEquipmentDetailIdAndDeletedFalseAndEndTimeIsNull(equipmentDetailId: Long): List } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt index f43dfa5..c1a2429 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/entity/projections/ProductProcessInfo.kt @@ -97,12 +97,12 @@ data class jobOrderLineInfo( val type: String?, val reqQty: Double?, - val baseReqQty: Int?, + val baseReqQty: Long?, val stockQty: Int?, val stockReqQty: Double?, - val baseStockQty: Int?, + val baseStockQty: Long?, val reqUom: String?, val reqBaseUom: String?, diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt index 859c088..6841cfa 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt @@ -11,12 +11,12 @@ import com.ffii.fpsms.modules.master.entity.EquipmentDetailRepository import com.ffii.fpsms.modules.productProcess.entity.projections.jobOrderLineInfo import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import java.time.LocalDate + import java.time.LocalDateTime import java.time.temporal.ChronoUnit import com.ffii.fpsms.modules.user.entity.UserRepository import com.ffii.fpsms.modules.user.entity.User -import java.time.format.DateTimeFormatter + import com.ffii.fpsms.modules.master.entity.BomRepository import com.ffii.fpsms.modules.master.entity.BomProcessRepository import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository @@ -50,6 +50,9 @@ import com.ffii.fpsms.modules.master.entity.UomConversionRepository import com.ffii.fpsms.modules.master.entity.ItemUomRespository import com.ffii.fpsms.modules.master.web.models.* import java.math.RoundingMode +import java.time.LocalDate +import java.time.format.DateTimeFormatter + @Service @Transactional open class ProductProcessService( @@ -692,7 +695,7 @@ val sufficientStockQty = bomMaterials // ✅ 获取 req UOM - 对于 reqQty,使用 bomMaterial.uom(BOM 的 UOM) // ✅ 对于 stockReqQty,使用 line.uom(库存单位,已按比例调整) val reqUomId = bomMaterial?.uom?.id ?: line.uom?.id ?: 0L // BOM 的 UOM - val stockReqUomId = line.uom?.id ?: bomMaterial?.salesUnit?.id ?: 0L // 库存单位 UOM + val stockReqUomId = line.uom?.id ?: bomMaterial?.stockUnit?.toLong() ?: 0L // 库存单位 UOM val reqUom = reqUomId.takeIf { it > 0 }?.let { uomConversionRepository.findByIdAndDeletedFalse(it) } val uomName = reqUom?.udfudesc @@ -706,7 +709,7 @@ val sufficientStockQty = bomMaterials println("=== Quantity Calculation for Item: ${line.item?.code} (id=$itemId) ===") println("JobOrderBomMaterial reqQty (adjusted, stock unit): $actualReqQty in UOM: ${stockReqUom?.udfudesc} (id=$stockReqUomId)") - println("BomMaterial qty (base): ${bomMaterial?.qty}, saleQty (base): ${bomMaterial?.saleQty}") + println("BomMaterial qty (base): ${bomMaterial?.qty}, stockQty (base): ${bomMaterial?.stockQty}") println("Original stockQty: $stockQtyValue in UOM: $stockUomNameForStock (id=$stockUomId)") val jobOrder = jobOrderRepository.findById(joid).orElse(null) @@ -721,8 +724,8 @@ val sufficientStockQty = bomMaterials // ✅ reqQty 使用 bomMaterial.qty * proportion(BOM 单位) val reqQtyInBomUnit = (bomMaterial?.qty?.times(proportion) ?: BigDecimal.ZERO) - // ✅ stockReqQty 使用 bomMaterial.saleQty * proportion(库存单位,已按比例调整) - val stockReqQtyInStockUnit = (bomMaterial?.saleQty?.times(proportion) ?: BigDecimal.ZERO) + // ✅ stockReqQty 使用 bomMaterial.stockQty * proportion(库存单位,已按比例调整) + val stockReqQtyInStockUnit = (bomMaterial?.stockQty?.times(proportion) ?: BigDecimal.ZERO) // ✅ Convert reqQty (BOM unit) to base unit val baseReqQtyResult = if (reqUomId > 0 && reqQtyInBomUnit > BigDecimal.ZERO) { @@ -848,7 +851,7 @@ val sufficientStockQty = bomMaterials baseReqQty = baseReqQty, stockQty = stockQty, - // ✅ stockReqQty:使用 bomMaterial.saleQty * proportion(库存单位,已按比例调整) + // ✅ stockReqQty:使用 bomMaterial.stockQty * proportion(库存单位,已按比例调整) stockReqQty = if (stockReqUomId in decimalUomIds) { stockReqQtyInStockUnit.toDouble() } else { @@ -1510,13 +1513,13 @@ val sufficientStockQty = bomMaterials productProcessLine.startTime = LocalDateTime.now() productProcessLineRepository.save(productProcessLine) } - if(allproductProcessLines.all { it.status == "Completed" }) { + if(allproductProcessLines.all { it.status == "Completed"|| it.status == "Pass" }) { updateProductProcessEndTime(productProcessId) updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED) val productProcess = productProcessRepository.findById(productProcessId).orElse(null) val jobOrder = jobOrderRepository.findById(productProcess?.jobOrder?.id?:0L).orElse(null) if(jobOrder != null) { - jobOrder.status = JobOrderStatus.PENDING_QC + jobOrder.status = JobOrderStatus.STORING jobOrderRepository.save(jobOrder) stockInLineService.create( @@ -1783,42 +1786,35 @@ val sufficientStockQty = bomMaterials ) } -open fun getJobProcessStatus(): List { - val productProcesses = productProcessRepository.findAllByDeletedIsFalse() - .filter { it.status != ProductProcessStatus.COMPLETED } + open fun getJobProcessStatus(date: LocalDate?): List { + val productProcesses = productProcessRepository.findAllByDeletedIsFalse() + .let { list -> if (date == null) list else list.filter { it.date == date } } + return productProcesses.mapNotNull { process -> val jobOrder = jobOrderRepository.findById(process.jobOrder?.id ?: 0L).orElse(null) - - // Filter out jobOrders in PLANNING status - if (jobOrder?.status == JobOrderStatus.PLANNING) { - return@mapNotNull null - } - + + // 仍然保留:PLANNING 不显示(如你也要显示,删掉这段) + if (jobOrder?.status == JobOrderStatus.PLANNING) return@mapNotNull null + val lines = productProcessLineRepository.findByProductProcess_Id(process.id ?: 0L) .sortedBy { it.seqNo } - val bom=bomRepository.findById(process.bom?.id ?: 0L).orElse(null) - // Calculate planEndTime based on first start time + remaining processing time val firstStartTime = lines.firstOrNull { it.startTime != null }?.startTime val calculatedPlanEndTime = if (firstStartTime != null) { - // Calculate total remaining processing time (in minutes) for unfinished processes var totalRemainingMinutes = 0L lines.forEach { line -> if (line.endTime == null) { - // Process is not finished, add its processing time totalRemainingMinutes += (line.processingTime ?: 0).toLong() totalRemainingMinutes += (line.setupTime ?: 0).toLong() totalRemainingMinutes += (line.changeoverTime ?: 0).toLong() } } - // Add remaining time to first start time firstStartTime.plusMinutes(totalRemainingMinutes) } else { - // No process has started yet, use original planEndTime jobOrder?.planEnd } - + JobProcessStatusResponse( jobOrderId = jobOrder?.id ?: 0L, jobOrderCode = jobOrder?.code ?: "", @@ -1829,29 +1825,19 @@ open fun getJobProcessStatus(): List { processes = (0 until 6).map { index -> if (index < lines.size) { val line = lines[index] - val equipmentDetailId = line.equipmentDetailId - - // Use line's own data instead of indexing into bomProcesses - val equipmentCode = when { - equipmentDetailId != null -> { - equipmentDetailRepository.findById(equipmentDetailId).orElse(null)?.code ?: "" - } - line.equipment?.code != null -> { - line.equipment?.code ?: "" - } - else -> { - // Safely access bomProcess - it might be deleted - try { - line.bomProcess?.equipment?.code ?: "" - } catch (e: jakarta.persistence.EntityNotFoundException) { - // BomProcess was deleted, fallback to equipmentType - "" - }.takeIf { it.isNotEmpty() } ?: (line.equipmentType ?: "") - } + + // equipment.description + equipment_detail.name + val equipmentName = try { line.bomProcess?.equipment?.name } catch (_: jakarta.persistence.EntityNotFoundException) { null } + + + val equipmentDetailName = line.equipmentDetailId?.let { id -> + equipmentDetailRepository.findById(id).orElse(null)?.name } - + ProcessStatusInfo( - equipmentCode = equipmentCode, + processName = line.name, // ✅ 新增:工序名称 + equipmentName = equipmentName, // ✅ 替代 equipmentCode + equipmentDetailName = equipmentDetailName, // ✅ 新增 startTime = line.startTime, endTime = line.endTime, processingTime = line.processingTime, @@ -1861,13 +1847,15 @@ open fun getJobProcessStatus(): List { ) } else { ProcessStatusInfo( + processName = null, + equipmentName = null, + equipmentDetailName = null, startTime = null, endTime = null, processingTime = null, setupTime = null, changeoverTime = null, - isRequired = false, - equipmentCode = null, + isRequired = false ) } } diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt index 0f6d22b..a3b241f 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/web/ProductProcessController.kt @@ -9,6 +9,9 @@ import org.springframework.data.domain.Pageable import org.springframework.web.bind.annotation.* import com.ffii.fpsms.modules.productProcess.entity.projections.ProductProcessInfo import com.ffii.fpsms.modules.master.web.models.MessageResponse +import java.time.LocalDate +import java.time.format.DateTimeFormatter + @RestController @RequestMapping("/product-process") class ProductProcessController( @@ -226,11 +229,29 @@ class ProductProcessController( return productProcessService.UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTime(request) } @GetMapping("/Demo/JobProcessStatus") - fun getJobProcessStatus(): List { - return productProcessService.getJobProcessStatus() + fun getJobProcessStatus(@RequestParam(required = false) date: String?): List { + val parsedDate = date?.takeIf { it.isNotBlank() }?.let { + LocalDate.parse(it, DateTimeFormatter.ISO_DATE) // yyyy-MM-dd + } + return productProcessService.getJobProcessStatus(parsedDate) } @PostMapping("/Demo/ProcessLine/delete/{lineId}") fun deleteProductProcessLine(@PathVariable lineId: Long): MessageResponse { return productProcessService.deleteProductProcessLine(lineId) } + + // ===== Dashboards ===== + + @GetMapping("/Demo/OperatorKpi") + fun getOperatorKpi(@RequestParam(required = false) date: String?): List { + val parsedDate = date?.takeIf { it.isNotBlank() }?.let { + LocalDate.parse(it, DateTimeFormatter.ISO_DATE) + } + return productProcessService.getOperatorKpi(parsedDate) + } + + @GetMapping("/Demo/EquipmentStatus") + fun getEquipmentStatus(): List { + return productProcessService.getEquipmentStatusByType() + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt b/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt index b251851..cd1494f 100644 --- a/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/productProcess/web/model/SaveProductProcessRequest.kt @@ -220,7 +220,9 @@ data class UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest( val changeoverTime: Int? ) data class ProcessStatusInfo( - val equipmentCode: String?, + val processName: String?, + val equipmentName: String?, + val equipmentDetailName: String?, val startTime: LocalDateTime?, val endTime: LocalDateTime?, val processingTime: Int?, @@ -235,6 +237,66 @@ data class JobProcessStatusResponse( val itemCode: String, val itemName: String, val planEndTime: LocalDateTime?, - val status: String, + val status: String, val processes: List +) + +// ===== Operator KPI Dashboard ===== + +data class OperatorKpiProcessInfo( + val jobOrderId: Long?, + val jobOrderCode: String?, + val productProcessId: Long?, + val productProcessLineId: Long?, + val processName: String?, + val equipmentName: String?, + val equipmentDetailName: String?, + val startTime: LocalDateTime?, + val endTime: LocalDateTime?, + val processingTime: Int?, + val itemCode: String?, + val itemName: String?, +) + +data class OperatorKpiResponse( + val operatorId: Long, + val operatorName: String?, + val staffNo: String?, + val totalProcessingMinutes: Long, + val totalJobOrderCount: Int, + val currentProcesses: List, +) + +// ===== Equipment Status Dashboard ===== + +data class EquipmentStatusProcessInfo( + val jobOrderId: Long?, + val jobOrderCode: String?, + val productProcessId: Long?, + val productProcessLineId: Long?, + val processName: String?, + val operatorName: String?, + val startTime: LocalDateTime?, + val processingTime: Int?, + +) + +data class EquipmentStatusPerDetail( + val equipmentDetailId: Long, + val equipmentDetailCode: String?, + val equipmentDetailName: String?, + val equipmentId: Long?, + val equipmentTypeName: String?, + val status: String, + val repairAndMaintenanceStatus: Boolean?, + val latestRepairAndMaintenanceDate: LocalDateTime?, + val lastRepairAndMaintenanceDate: LocalDateTime?, + val repairAndMaintenanceRemarks: String?, + val currentProcess: EquipmentStatusProcessInfo?, +) + +data class EquipmentStatusByTypeResponse( + val equipmentTypeId: Long, + val equipmentTypeName: String?, + val details: List, ) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt index c57fc55..55ef7d3 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt @@ -97,6 +97,30 @@ open class ReportService( return "AND (${conditions.joinToString(" OR ")})" } + /** + * Helper function to build SQL clause for comma-separated values with exact match. + * Supports multiple values like "val1, val2, val3" and generates OR conditions with =. + */ + private fun buildMultiValueExactClause( + paramValue: String?, + columnName: String, + paramPrefix: String, + args: MutableMap + ): String { + if (paramValue.isNullOrBlank()) return "" + + val values = paramValue.split(",").map { it.trim() }.filter { it.isNotBlank() } + if (values.isEmpty()) return "" + + val conditions = values.mapIndexed { index, value -> + val paramName = "${paramPrefix}_$index" + args[paramName] = value + "$columnName = :$paramName" + } + + return "AND (${conditions.joinToString(" OR ")})" + } + /** * Queries the database for Stock In Traceability Report data. * Joins stock_in_line, stock_in, items, item_category, qc_result, inventory_lot, inventory_lot_line, warehouse, and shop tables. @@ -106,6 +130,7 @@ open class ReportService( stockCategory: String?, stockSubCategory: String?, itemCode: String?, + year: String?, lastInDateStart: String?, lastInDateEnd: String? ): List> { @@ -115,6 +140,13 @@ open class ReportService( val stockSubCategorySql = buildMultiValueLikeClause(stockSubCategory, "ic.sub", "stockSubCategory", args) val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args) + val yearSql = if (!year.isNullOrBlank()) { + args["year"] = year + "AND YEAR(sil.receiptDate) = :year" + } else { + "" + } + val lastInDateStartSql = if (!lastInDateStart.isNullOrBlank()) { args["lastInDateStart"] = lastInDateStart "AND sil.receiptDate >= :lastInDateStart" @@ -168,6 +200,7 @@ open class ReportService( $stockCategorySql $stockSubCategorySql $itemCodeSql + $yearSql $lastInDateStartSql $lastInDateEndSql ORDER BY ic.sub, it.code, sil.lotNo @@ -176,6 +209,205 @@ open class ReportService( return jdbcDao.queryForList(sql, args) } + /** + * Queries the database for Semi FG Production Analysis Report data. + * Flow: + * 1. Filter bom by description (FG/WIP) to get bom.code values + * 2. Match bom.code with stock_ledger.itemCode + * 3. Aggregate stock_ledger data by month for each item based on inQty + * Supports comma-separated values for stockCategory, stockSubCategory, and itemCode. + */ + fun searchSemiFGProductionAnalysisReport( + stockCategory: String?, + stockSubCategory: String?, + itemCode: String?, + year: String?, + lastOutDateStart: String?, + lastOutDateEnd: String? + ): List> { + val args = mutableMapOf() + + // Filter by stockCategory from bom.description (FG/WIP) - this finds which bom.code values match + // Supports multiple categories separated by comma (e.g., "FG,WIP") + // If "All" is selected or contains "All", don't filter by description + val stockCategorySql = if (!itemCode.isNullOrBlank()) { + // When itemCode is provided, skip stockCategory filter + "" + } else if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) { + // Handle multiple categories (comma-separated) + val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" } + if (categories.isNotEmpty()) { + val conditions = categories.mapIndexed { index, cat -> + val paramName = "stockCategory_$index" + args[paramName] = cat + "b.description = :$paramName" + } + "AND (${conditions.joinToString(" OR ")})" + } else { + "" + } + } else { + "" + } + val stockSubCategorySql = buildMultiValueLikeClause(stockSubCategory, "ic.sub", "stockSubCategory", args) + // Filter by itemCode - match bom.code (user input should match bom.code, which then matches stock_ledger.itemCode) + val itemCodeSql = buildMultiValueExactClause(itemCode, "b.code", "itemCode", args) + + val yearSql = if (!year.isNullOrBlank()) { + args["year"] = year + "AND YEAR(sl.modified) = :year" + } else { + "" + } + + val lastOutDateStartSql = if (!lastOutDateStart.isNullOrBlank()) { + args["lastOutDateStart"] = lastOutDateStart + "AND DATE(sl.modified) >= :lastOutDateStart" + } else "" + + val lastOutDateEndSql = if (!lastOutDateEnd.isNullOrBlank()) { + args["lastOutDateEnd"] = lastOutDateEnd + "AND DATE(sl.modified) < :lastOutDateEnd" + } else "" + + val sql = """ + SELECT + COALESCE(ic.sub, '') as stockSubCategory, + COALESCE(sl.itemCode, '') as itemNo, + COALESCE(b.name, '') as itemName, + COALESCE(uc.code, '') as unitOfMeasure, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 1 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJan, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 2 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyFeb, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 3 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyMar, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 4 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyApr, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 5 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyMay, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 6 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJun, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 7 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyJul, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 8 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyAug, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 9 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtySep, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 10 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyOct, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 11 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyNov, + CAST(COALESCE(SUM(CASE WHEN MONTH(sl.modified) = 12 THEN sl.inQty ELSE 0 END), 0) AS DECIMAL(18,2)) as qtyDec, + CAST(COALESCE(SUM(sl.inQty), 0) AS CHAR) as totalProductionQty + FROM stock_ledger sl + INNER JOIN bom b ON sl.itemCode = b.code AND b.deleted = false + LEFT JOIN items it ON sl.itemId = it.id + LEFT JOIN item_category ic ON it.categoryId = ic.id + LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = true + LEFT JOIN uom_conversion uc ON iu.uomId = uc.id + WHERE sl.deleted = false + AND sl.inQty IS NOT NULL + AND sl.inQty > 0 + $stockCategorySql + $stockSubCategorySql + $itemCodeSql + $yearSql + $lastOutDateStartSql + $lastOutDateEndSql + GROUP BY sl.itemCode, ic.sub, it.id, b.name, uc.code, b.description + ORDER BY ic.sub, sl.itemCode + """.trimIndent() + + return jdbcDao.queryForList(sql, args) + } + + /** + * Gets list of item codes (bom.code) with names based on stockCategory filter. + * Supports multiple categories separated by comma (e.g., "FG,WIP"). + * If stockCategory is "All" or null, returns all codes. + * If stockCategory is "FG" or "WIP" or "FG,WIP", returns codes matching those descriptions. + * Returns a list of maps with "code" and "name" keys. + */ + fun getSemiFGItemCodes(stockCategory: String?): List> { + val args = mutableMapOf() + + val stockCategorySql = if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) { + // Handle multiple categories (comma-separated) + val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" } + if (categories.isNotEmpty()) { + val conditions = categories.mapIndexed { index, cat -> + val paramName = "stockCategory_$index" + args[paramName] = cat + "b.description = :$paramName" + } + "AND (${conditions.joinToString(" OR ")})" + } else { + "" + } + } else { + "" + } + + val sql = """ + SELECT DISTINCT b.code, COALESCE(b.name, '') as name + FROM bom b + WHERE b.deleted = false + AND b.code IS NOT NULL + AND b.code != '' + $stockCategorySql + ORDER BY b.code + """.trimIndent() + + val results = jdbcDao.queryForList(sql, args) + return results.mapNotNull { + val code = it["code"]?.toString() + val name = it["name"]?.toString() ?: "" + if (code != null) { + mapOf("code" to code, "name" to name) + } else { + null + } + } + } + + /** + * Gets list of item codes with their category (FG/WIP) and name based on stockCategory filter. + * Supports multiple categories separated by comma (e.g., "FG,WIP"). + * Returns a list of maps with "code", "category", and "name" keys. + */ + fun getSemiFGItemCodesWithCategory(stockCategory: String?): List> { + val args = mutableMapOf() + + val stockCategorySql = if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) { + // Handle multiple categories (comma-separated) + val categories = stockCategory.split(",").map { it.trim() }.filter { it.isNotBlank() && it != "All" } + if (categories.isNotEmpty()) { + val conditions = categories.mapIndexed { index, cat -> + val paramName = "stockCategory_$index" + args[paramName] = cat + "b.description = :$paramName" + } + "AND (${conditions.joinToString(" OR ")})" + } else { + "" + } + } else { + "" + } + + val sql = """ + SELECT DISTINCT b.code, COALESCE(b.description, '') as category, COALESCE(b.name, '') as name + FROM bom b + WHERE b.deleted = false + AND b.code IS NOT NULL + AND b.code != '' + $stockCategorySql + ORDER BY b.code + """.trimIndent() + + val results = jdbcDao.queryForList(sql, args) + return results.mapNotNull { + val code = it["code"]?.toString() + val category = it["category"]?.toString() ?: "" + val name = it["name"]?.toString() ?: "" + if (code != null) { + mapOf("code" to code, "category" to category, "name" to name) + } else { + null + } + } + } + /** * Compiles and fills a Jasper Report, returning the PDF as a ByteArray. */ diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt index ddfa12f..ceaf074 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt @@ -85,6 +85,7 @@ class ReportController( @RequestParam(required = false) stockCategory: String?, @RequestParam(required = false) stockSubCategory: String?, @RequestParam(required = false) itemCode: String?, + @RequestParam(required = false) year: String?, @RequestParam(required = false) lastInDateStart: String?, @RequestParam(required = false) lastInDateEnd: String? ): ResponseEntity { @@ -94,6 +95,7 @@ class ReportController( parameters["stockCategory"] = stockCategory ?: "All" parameters["stockSubCategory"] = stockSubCategory ?: "All" parameters["itemNo"] = itemCode ?: "All" + parameters["year"] = year ?: LocalDate.now().year.toString() parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) parameters["lastInDateStart"] = lastInDateStart ?: "" @@ -104,6 +106,7 @@ class ReportController( stockCategory, stockSubCategory, itemCode, + year, lastInDateStart, lastInDateEnd ) @@ -122,4 +125,68 @@ class ReportController( return ResponseEntity(pdfBytes, headers, HttpStatus.OK) } + + @GetMapping("/print-semi-fg-production-analysis") + fun generateSemiFGProductionAnalysisReport( + @RequestParam(required = false) stockCategory: String?, + @RequestParam(required = false) stockSubCategory: String?, + @RequestParam(required = false) itemCode: String?, + @RequestParam(required = false) year: String?, + @RequestParam(required = false) lastOutDateStart: String?, + @RequestParam(required = false) lastOutDateEnd: String? + ): ResponseEntity { + val parameters = mutableMapOf() + + // Set report header parameters + parameters["stockCategory"] = stockCategory ?: "All" + parameters["stockSubCategory"] = stockSubCategory ?: "All" + parameters["itemNo"] = itemCode ?: "All" + parameters["year"] = year ?: LocalDate.now().year.toString() + parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + parameters["lastOutDateStart"] = lastOutDateStart ?: "" + parameters["lastOutDateEnd"] = lastOutDateEnd ?: "" + parameters["deliveryPeriodStart"] = "" + parameters["deliveryPeriodEnd"] = "" + + // Query the DB to get a list of data + val dbData = reportService.searchSemiFGProductionAnalysisReport( + stockCategory, + stockSubCategory, + itemCode, + year, + lastOutDateStart, + lastOutDateEnd + ) + + val pdfBytes = reportService.createPdfResponse( + "/jasper/SemiFGProductionAnalysisReport.jrxml", + parameters, + dbData + ) + + val headers = HttpHeaders().apply { + contentType = MediaType.APPLICATION_PDF + setContentDispositionFormData("attachment", "SemiFGProductionAnalysisReport.pdf") + set("filename", "SemiFGProductionAnalysisReport.pdf") + } + + return ResponseEntity(pdfBytes, headers, HttpStatus.OK) + } + + @GetMapping("/semi-fg-item-codes") + fun getSemiFGItemCodes( + @RequestParam(required = false) stockCategory: String? + ): ResponseEntity>> { + val itemCodes = reportService.getSemiFGItemCodes(stockCategory) + return ResponseEntity(itemCodes, HttpStatus.OK) + } + + @GetMapping("/semi-fg-item-codes-with-category") + fun getSemiFGItemCodesWithCategory( + @RequestParam(required = false) stockCategory: String? + ): ResponseEntity>> { + val itemCodesWithCategory = reportService.getSemiFGItemCodesWithCategory(stockCategory) + return ResponseEntity(itemCodesWithCategory, HttpStatus.OK) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/StockInLineInfo.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/StockInLineInfo.kt index 7218193..dddbddf 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/StockInLineInfo.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/projection/StockInLineInfo.kt @@ -13,6 +13,8 @@ interface StockInLineInfo { val itemId: Long @get:Value("#{target.item?.name}") val itemName: String? + @get:Value("#{target.item?.LocationCode}") + val locationCode: String? val itemNo: String @get:Value("#{target.stockIn?.id}") val stockInId: Long @@ -74,4 +76,5 @@ interface PutAwayLineForSil { val putawayDate: LocalDateTime?; @get:Value("#{target.createdBy}") val putawayUser: String?; + } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt index 3519e4b..0346119 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/SuggestedPickLotService.kt @@ -310,7 +310,8 @@ open class SuggestedPickLotService( } // 规则 2:原有逻辑:跳过 3F - if (warehouseStoreId == "3F") { + val isJobOrder = pickOrder?.type?.value == "jo" || pickOrder?.type?.value == "jo" + if (warehouseStoreId == "3F" && !isJobOrder) { return@forEachIndexed } diff --git a/src/main/resources/db/changelog/changes/20260204_Enson/01_add_bom_materail.sql b/src/main/resources/db/changelog/changes/20260204_Enson/01_add_bom_materail.sql new file mode 100644 index 0000000..aec7cc7 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260204_Enson/01_add_bom_materail.sql @@ -0,0 +1,9 @@ +-- liquibase formatted sql +-- changeset KelvinY:add_baseScore_to_bom +-- preconditions onFail:MARK_RAN +-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'fpsmsdb' AND TABLE_NAME = 'bom_material' AND COLUMN_NAME = 'stockQty'; + +ALTER TABLE `fpsmsdb`.`bom_material` +ADD COLUMN `stockQty` DECIMAL(14, 2) NULL DEFAULT NULL AFTER `salesUnitCode`, +ADD COLUMN `stockUnit` INTEGER NULL DEFAULT NULL AFTER `stockQty`, +ADD COLUMN `stockUnitName` VARCHAR(255) NULL DEFAULT NULL AFTER `stockUnit`; \ No newline at end of file diff --git a/src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml b/src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml new file mode 100644 index 0000000..37a0742 --- /dev/null +++ b/src/main/resources/jasper/SemiFGProductionAnalysisReport.jrxml @@ -0,0 +1,664 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +