| @@ -15,6 +15,8 @@ import java.time.LocalDateTime | |||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository | import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository | ||||
| import com.ffii.fpsms.modules.productProcess.entity.ProductProcessRepository | import com.ffii.fpsms.modules.productProcess.entity.ProductProcessRepository | ||||
| import com.ffii.fpsms.modules.master.entity.ItemUomRespository | |||||
| import java.math.BigDecimal | |||||
| @Service | @Service | ||||
| open class BagService( | open class BagService( | ||||
| private val bagRepository: BagRepository, | private val bagRepository: BagRepository, | ||||
| @@ -22,20 +24,34 @@ open class BagService( | |||||
| private val joBagConsumptionRepository: JoBagConsumptionRepository, | private val joBagConsumptionRepository: JoBagConsumptionRepository, | ||||
| private val inventoryLotRepository: InventoryLotRepository, | private val inventoryLotRepository: InventoryLotRepository, | ||||
| private val jobOrderRepository: JobOrderRepository, | private val jobOrderRepository: JobOrderRepository, | ||||
| private val productProcessRepository: ProductProcessRepository | |||||
| private val productProcessRepository: ProductProcessRepository, | |||||
| private val itemUomRepository: ItemUomRespository, | |||||
| ) { | ) { | ||||
| open fun createBagLotLinesByBagId(request: CreateBagLotLineRequest): MessageResponse { | open fun createBagLotLinesByBagId(request: CreateBagLotLineRequest): MessageResponse { | ||||
| val bag = bagRepository.findById(request.bagId).orElse(null) | val bag = bagRepository.findById(request.bagId).orElse(null) | ||||
| val lot = inventoryLotRepository.findByLotNoAndItemId(request.lotNo, request.itemId) | 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 { | val bagLotLine = BagLotLine().apply { | ||||
| this.bagId = bag?.id | this.bagId = bag?.id | ||||
| this.lotId = lot?.id | this.lotId = lot?.id | ||||
| this.lotNo = lot?.lotNo | 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.consumedQty = 0 | ||||
| this.stockOutLineId = request.stockOutLineId | this.stockOutLineId = request.stockOutLineId | ||||
| this.scrapQty = 0 | this.scrapQty = 0 | ||||
| this.balanceQty = request.stockQty | |||||
| this.balanceQty = request.stockQty.toBigDecimal() | |||||
| .multiply(baseRatioN) | |||||
| .divide(baseRatioD) | |||||
| .toInt() | |||||
| println("balanceQty: $balanceQty") | |||||
| } | } | ||||
| bagLotLineRepository.save(bagLotLine) | bagLotLineRepository.save(bagLotLine) | ||||
| bag.takenBagBalance = (bag.takenBagBalance ?: 0) + (bagLotLine.balanceQty ?: 0) | bag.takenBagBalance = (bag.takenBagBalance ?: 0) + (bagLotLine.balanceQty ?: 0) | ||||
| @@ -34,7 +34,7 @@ open class JobOrderBomMaterialService( | |||||
| joId = joId, | joId = joId, | ||||
| itemId = bm.item?.id, | itemId = bm.item?.id, | ||||
| //reqQty = (bm.qty?.times(proportion) ?: zero).setScale(0,RoundingMode.CEILING), | //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 | uomId = stockUnit?.uom?.id | ||||
| ) | ) | ||||
| } ?: listOf() | } ?: listOf() | ||||
| @@ -68,6 +68,7 @@ import java.time.LocalDate | |||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||
| import com.ffii.fpsms.modules.master.entity.BomMaterialRepository | import com.ffii.fpsms.modules.master.entity.BomMaterialRepository | ||||
| import com.ffii.fpsms.modules.master.service.ItemUomService | import com.ffii.fpsms.modules.master.service.ItemUomService | ||||
| import com.ffii.fpsms.modules.master.web.models.ConvertUomByItemRequest | |||||
| @Service | @Service | ||||
| open class JobOrderService( | open class JobOrderService( | ||||
| val jobOrderRepository: JobOrderRepository, | val jobOrderRepository: JobOrderRepository, | ||||
| @@ -208,7 +209,6 @@ open class JobOrderService( | |||||
| return RecordsRes<JobOrderInfoWithTypeName>(records, total.toInt()); | return RecordsRes<JobOrderInfoWithTypeName>(records, total.toInt()); | ||||
| } | } | ||||
| // 添加辅助方法计算库存统计 | |||||
| private fun calculateStockCounts( | private fun calculateStockCounts( | ||||
| jobOrder: JobOrder, | jobOrder: JobOrder, | ||||
| inventoriesMap: Map<Long?, com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo> | inventoriesMap: Map<Long?, com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo> | ||||
| @@ -216,7 +216,7 @@ open class JobOrderService( | |||||
| // 过滤掉 consumables 和 CMB 类型的物料 | // 过滤掉 consumables 和 CMB 类型的物料 | ||||
| val nonConsumablesJobms = jobOrder.jobms.filter { jobm -> | val nonConsumablesJobms = jobOrder.jobms.filter { jobm -> | ||||
| val itemType = jobm.item?.type?.lowercase() | val itemType = jobm.item?.type?.lowercase() | ||||
| itemType != "consumables" && itemType != "cmb"&& itemType != "nm" | |||||
| itemType != "consumables" && itemType != "consumable" && itemType != "cmb" && itemType != "nm" | |||||
| } | } | ||||
| if (nonConsumablesJobms.isEmpty()) { | if (nonConsumablesJobms.isEmpty()) { | ||||
| @@ -226,14 +226,16 @@ open class JobOrderService( | |||||
| var sufficientCount = 0 | var sufficientCount = 0 | ||||
| var insufficientCount = 0 | var insufficientCount = 0 | ||||
| println("=== JobOrderService.calculateStockCounts for JobOrder: ${jobOrder.code} ===") | |||||
| nonConsumablesJobms.forEach { jobm -> | nonConsumablesJobms.forEach { jobm -> | ||||
| val itemId = jobm.item?.id | val itemId = jobm.item?.id | ||||
| val reqQty = jobm.reqQty ?: BigDecimal.ZERO | |||||
| val itemCode = jobm.item?.code ?: "N/A" | |||||
| val itemName = jobm.item?.name ?: "N/A" | |||||
| if (itemId != null) { | if (itemId != null) { | ||||
| val inventory = inventoriesMap[itemId] | val inventory = inventoriesMap[itemId] | ||||
| val availableQty = if (inventory != null) { | val availableQty = if (inventory != null) { | ||||
| // 使用 availableQty,如果没有则计算:onHandQty - onHoldQty - unavailableQty | |||||
| inventory.availableQty ?: ( | inventory.availableQty ?: ( | ||||
| (inventory.onHandQty ?: BigDecimal.ZERO) - | (inventory.onHandQty ?: BigDecimal.ZERO) - | ||||
| (inventory.onHoldQty ?: BigDecimal.ZERO) - | (inventory.onHoldQty ?: BigDecimal.ZERO) - | ||||
| @@ -243,17 +245,74 @@ open class JobOrderService( | |||||
| BigDecimal.ZERO | 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++ | sufficientCount++ | ||||
| println("✅ SUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)") | |||||
| } else { | } else { | ||||
| insufficientCount++ | insufficientCount++ | ||||
| println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - reqQty: $reqQty ($reqUomName) = $baseReqQty ($baseUomName), availableQty: $availableQty ($stockUomName) = $baseAvailableQty ($baseUomName)") | |||||
| } | } | ||||
| } else { | } else { | ||||
| // 如果没有 itemId,视为不足 | // 如果没有 itemId,视为不足 | ||||
| insufficientCount++ | insufficientCount++ | ||||
| println("❌ INSUFFICIENT - Item: $itemCode ($itemName) - No itemId") | |||||
| } | } | ||||
| } | } | ||||
| println("=== Result: sufficient=$sufficientCount, insufficient=$insufficientCount ===") | |||||
| return Pair(sufficientCount, insufficientCount) | return Pair(sufficientCount, insufficientCount) | ||||
| } | } | ||||
| open fun jobOrderDetail(id: Long): JobOrderDetail { | open fun jobOrderDetail(id: Long): JobOrderDetail { | ||||
| @@ -454,16 +513,30 @@ open class JobOrderService( | |||||
| } | } | ||||
| // ✅ 使用 stockReqQty (bomMaterial.saleQty) 和 stockUom (bomMaterial.salesUnit) | // ✅ 使用 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 获取库存单位 | ?: itemUomService.findStockUnitByItemId(itemId)?.uom?.id // Fallback: 从 Item 获取库存单位 | ||||
| ?: jobm.uom?.id // 最后的 fallback | |||||
| ?: jobm.uom?.id // 最后的 fallback | |||||
| SavePickOrderLineRequest( | SavePickOrderLineRequest( | ||||
| itemId = itemId, | itemId = itemId, | ||||
| qty = stockReqQty, // ✅ 使用库存单位数量 | qty = stockReqQty, // ✅ 使用库存单位数量 | ||||
| uomId = stockUomId, // ✅ 使用库存单位 | 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( | val po = SavePickOrderRequest( | ||||
| joId = jo.id, | joId = jo.id, | ||||
| @@ -419,4 +419,56 @@ open class PlasticBagPrinterService( | |||||
| println("Han's Laser TCP 連接已關閉") | 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) | |||||
| } | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -46,6 +46,7 @@ class PlasticBagPrinterController( | |||||
| response.outputStream.flush() | response.outputStream.flush() | ||||
| } | } | ||||
| /* | |||||
| @PostMapping("/print-dataflex") | @PostMapping("/print-dataflex") | ||||
| fun printDataFlex(@RequestBody request: PrintRequest): ResponseEntity<String> { | fun printDataFlex(@RequestBody request: PrintRequest): ResponseEntity<String> { | ||||
| return try { | return try { | ||||
| @@ -54,6 +55,16 @@ class PlasticBagPrinterController( | |||||
| } catch (e: Exception) { | } catch (e: Exception) { | ||||
| ResponseEntity.status(500).body("Error: ${e.message}") | ResponseEntity.status(500).body("Error: ${e.message}") | ||||
| } | } | ||||
| }*/ | |||||
| @PostMapping("/print-dataflex") | |||||
| fun printDataFlex6330Zpl(@RequestBody request: PrintRequest): ResponseEntity<String> { | |||||
| 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") | @PostMapping("/print-laser") | ||||
| @@ -52,7 +52,12 @@ open class BomMaterial : BaseEntity<Long>() { | |||||
| open var baseUnit: Integer? = null | open var baseUnit: Integer? = null | ||||
| @Column(name = "baseUnitName", length = 100) | @Column(name = "baseUnitName", length = 100) | ||||
| open var baseUnitName: String? = null | 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 | @NotNull | ||||
| @ManyToOne(optional = false) | @ManyToOne(optional = false) | ||||
| @JoinColumn(name = "bomId", nullable = false) | @JoinColumn(name = "bomId", nullable = false) | ||||
| @@ -150,23 +150,25 @@ open class BomService( | |||||
| var baseUnit: Integer? = null | var baseUnit: Integer? = null | ||||
| var baseUnitName: String? = null | var baseUnitName: String? = null | ||||
| var stockQty: BigDecimal? = null | |||||
| var stockUnit: Integer? = null | |||||
| var stockUnitName: String? = null | |||||
| if (item?.id != null) { | if (item?.id != null) { | ||||
| val itemId = item.id!! | val itemId = item.id!! | ||||
| try { | 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) { | if (excelSalesUnit != null) { | ||||
| // Excel 找到了 uom_conversion | // Excel 找到了 uom_conversion | ||||
| saleUnitId = excelSalesUnit.id | |||||
| saleUnitCode = excelSalesUnit.udfudesc | 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( | bomMaterialImportIssues.add( | ||||
| BomMaterialImportIssue( | BomMaterialImportIssue( | ||||
| bomId = bomId, | bomId = bomId, | ||||
| @@ -174,7 +176,7 @@ open class BomService( | |||||
| itemId = item.id, | itemId = item.id, | ||||
| itemCode = item.code, | itemCode = item.code, | ||||
| itemName = item.name, | 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, | srcQty = excelSaleQty, | ||||
| srcUomCode = excelSalesUnit.code | srcUomCode = excelSalesUnit.code | ||||
| ) | ) | ||||
| @@ -182,7 +184,7 @@ open class BomService( | |||||
| } | } | ||||
| } else { | } else { | ||||
| // Excel 没有找到 uom_conversion,使用 Excel 数据本身 | // Excel 没有找到 uom_conversion,使用 Excel 数据本身 | ||||
| saleUnitCode = excelSalesUnitCode ?: itemStockUnit?.udfudesc | |||||
| saleUnitCode = excelSalesUnitCode | |||||
| if (excelSalesUnitCode != null) { | if (excelSalesUnitCode != null) { | ||||
| bomMaterialImportIssues.add( | 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( | bomMaterialImportIssues.add( | ||||
| BomMaterialImportIssue( | BomMaterialImportIssue( | ||||
| bomId = bomId, | bomId = bomId, | ||||
| @@ -227,12 +288,92 @@ open class BomService( | |||||
| itemId = item.id, | itemId = item.id, | ||||
| itemCode = item.code, | itemCode = item.code, | ||||
| itemName = item.name, | itemName = item.name, | ||||
| reason = "Cannot convert saleQty to baseQty: conversion failed", | |||||
| reason = "Cannot convert saleQty to baseQty: Excel sales unit not found", | |||||
| srcQty = excelSaleQty, | 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( | bomMaterialImportIssues.add( | ||||
| BomMaterialImportIssue( | BomMaterialImportIssue( | ||||
| bomId = bomId, | bomId = bomId, | ||||
| @@ -240,13 +381,37 @@ open class BomService( | |||||
| itemId = item.id, | itemId = item.id, | ||||
| itemCode = item.code, | itemCode = item.code, | ||||
| itemName = item.name, | 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, | 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) { | } catch (e: IllegalArgumentException) { | ||||
| bomMaterialImportIssues.add( | bomMaterialImportIssues.add( | ||||
| BomMaterialImportIssue( | BomMaterialImportIssue( | ||||
| @@ -301,15 +466,21 @@ open class BomService( | |||||
| this.uom = req.uom // BOM 原始 UOM(column 3,保留) | this.uom = req.uom // BOM 原始 UOM(column 3,保留) | ||||
| this.uomName = req.uomName | this.uomName = req.uomName | ||||
| // 新逻辑:使用 Excel column 6 的 saleQty 和 item stock unit | |||||
| // 新逻辑:使用 Excel column 6 的 saleQty 和 column 7 的 sales unit | |||||
| this.saleQty = saleQty | this.saleQty = saleQty | ||||
| this.salesUnit = item?.id?.let { itemUomService.findStockUnitByItemId(it)?.uom } | |||||
| this.salesUnit = excelSalesUnit // 使用 Excel 的 sales unit,不是 item 的 stock unit | |||||
| this.salesUnitCode = saleUnitCode | this.salesUnitCode = saleUnitCode | ||||
| // 从 sale unit 转换为 base unit | |||||
| this.baseQty = baseQty | this.baseQty = baseQty | ||||
| this.baseUnit = baseUnit | this.baseUnit = baseUnit | ||||
| this.baseUnitName = baseUnitName | this.baseUnitName = baseUnitName | ||||
| // 从 sale unit 转换为 stock unit(新增) | |||||
| this.stockQty = stockQty | |||||
| this.stockUnit = stockUnit | |||||
| this.stockUnitName = stockUnitName | |||||
| this.bom = req.bom | this.bom = req.bom | ||||
| } | } | ||||
| return bomMaterialRepository.saveAndFlush(bomMaterial) | return bomMaterialRepository.saveAndFlush(bomMaterial) | ||||
| @@ -586,8 +757,8 @@ open class BomService( | |||||
| // 创建 equipment | // 创建 equipment | ||||
| equipment = Equipment().apply { | equipment = Equipment().apply { | ||||
| this.name = equipmentName // 完整值 XXX-YYY | |||||
| this.code = secondPart | |||||
| this.code = equipmentName // 完整值 XXX-YYY | |||||
| this.name = secondPart | |||||
| this.description = firstPart // XXX 写入 description | this.description = firstPart // XXX 写入 description | ||||
| } | } | ||||
| equipment = equipmentRepository.saveAndFlush(equipment) | equipment = equipmentRepository.saveAndFlush(equipment) | ||||
| @@ -648,8 +648,8 @@ open class ProductionScheduleService( | |||||
| i.pendingJobQty, | i.pendingJobQty, | ||||
| ((i.stockQty * 1.0) + ifnull(i.pendingJobQty, 0) ) / i.avgQtyLastMonth as daysLeft, | ((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.* | i.* | ||||
| FROM | FROM | ||||
| (SELECT | (SELECT | ||||
| @@ -665,7 +665,7 @@ open class ProductionScheduleService( | |||||
| do.deleted = 0 and | do.deleted = 0 and | ||||
| dol.itemId = items.id | dol.itemId = items.id | ||||
| -- AND MONTH(do.estimatedArrivalDate) = MONTH(DATE_SUB(NOW(), INTERVAL 1 MONTH)) | -- 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, | GROUP BY do.estimatedArrivalDate) AS d) AS avgQtyLastMonth, | ||||
| (select sum(reqQty) from job_order where bomId = bom.id and status != 'completed') AS pendingJobQty, | (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 items ON bom.itemId = items.id | ||||
| LEFT JOIN inventory ON items.id = inventory.itemId | LEFT JOIN inventory ON items.id = inventory.itemId | ||||
| left join item_fake_onhand on items.code = item_fake_onhand.itemCode | left join item_fake_onhand on items.code = item_fake_onhand.itemCode | ||||
| WHERE | |||||
| bom.itemId != 16771) AS i | |||||
| WHERE 1 | |||||
| and bom.itemId != 16771 | |||||
| ) AS i | |||||
| WHERE 1 | WHERE 1 | ||||
| and i.avgQtyLastMonth is not null | and i.avgQtyLastMonth is not null | ||||
| and i.onHandQty is not null | and i.onHandQty is not null | ||||
| @@ -74,9 +74,23 @@ open class PickExecutionIssueService( | |||||
| @Value("\${pick.execution.auto-resuggest-on-rejection:false}") | @Value("\${pick.execution.auto-resuggest-on-rejection:false}") | ||||
| private val autoResuggestOnLotRejection: Boolean = false | private val autoResuggestOnLotRejection: Boolean = false | ||||
| @Transactional(rollbackFor = [Exception::class]) | |||||
| open fun recordPickExecutionIssue(request: PickExecutionIssueRequest): MessageResponse { | open fun recordPickExecutionIssue(request: PickExecutionIssueRequest): MessageResponse { | ||||
| try { | 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 记录 | // 1. 检查是否已经存在相同的 pick execution issue 记录 | ||||
| val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse( | val existingIssues = pickExecutionIssueRepository.findByPickOrderLineIdAndLotIdAndDeletedFalse( | ||||
| @@ -84,7 +98,14 @@ open class PickExecutionIssueService( | |||||
| request.lotId ?: 0L | 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()) { | if (existingIssues.isNotEmpty()) { | ||||
| println("❌ Duplicate issue found - returning DUPLICATE error") | |||||
| return MessageResponse( | return MessageResponse( | ||||
| id = null, | id = null, | ||||
| name = "Pick execution issue already exists", | name = "Pick execution issue already exists", | ||||
| @@ -96,18 +117,24 @@ open class PickExecutionIssueService( | |||||
| } | } | ||||
| val pickOrder = pickOrderRepository.findById(request.pickOrderId).orElse(null) | 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) | // 2. 获取 inventory_lot_line 并计算账面数量 (bookQty) | ||||
| val inventoryLotLine = request.lotId?.let { | val inventoryLotLine = request.lotId?.let { | ||||
| inventoryLotLineRepository.findById(it).orElse(null) | inventoryLotLineRepository.findById(it).orElse(null) | ||||
| } | } | ||||
| println("Inventory lot line: id=${inventoryLotLine?.id}, lotNo=${inventoryLotLine?.inventoryLot?.lotNo}") | |||||
| // 计算账面数量(创建 issue 时的快照) | // 计算账面数量(创建 issue 时的快照) | ||||
| val bookQty = if (inventoryLotLine != null) { | val bookQty = if (inventoryLotLine != null) { | ||||
| val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO | val inQty = inventoryLotLine.inQty ?: BigDecimal.ZERO | ||||
| val outQty = inventoryLotLine.outQty ?: 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 { | } else { | ||||
| println(" No inventory lot line found, bookQty=0") | |||||
| BigDecimal.ZERO | BigDecimal.ZERO | ||||
| } | } | ||||
| @@ -117,41 +144,53 @@ open class PickExecutionIssueService( | |||||
| val missQty = request.missQty ?: BigDecimal.ZERO | val missQty = request.missQty ?: BigDecimal.ZERO | ||||
| val badItemQty = request.badItemQty ?: BigDecimal.ZERO | val badItemQty = request.badItemQty ?: BigDecimal.ZERO | ||||
| val badReason = request.badReason ?: "quantity_problem" | 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(实际的问题数量) | // 4. 计算 issueQty(实际的问题数量) | ||||
| val issueQty = when { | val issueQty = when { | ||||
| // 情况1: 已拣完但有坏品 | // 情况1: 已拣完但有坏品 | ||||
| actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> { | actualPickQty == requiredQty && badItemQty > BigDecimal.ZERO -> { | ||||
| println(" Case 1: actualPickQty == requiredQty && badItemQty > 0") | |||||
| println(" issueQty = badItemQty = $badItemQty") | |||||
| badItemQty // issueQty = badItemQty | badItemQty // issueQty = badItemQty | ||||
| } | } | ||||
| badReason == "package_problem" && badItemQty > BigDecimal.ZERO -> { | badReason == "package_problem" && badItemQty > BigDecimal.ZERO -> { | ||||
| println(" Case 2: badReason == 'package_problem' && badItemQty > 0") | |||||
| println(" issueQty = badItemQty = $badItemQty") | |||||
| badItemQty | badItemQty | ||||
| } | } | ||||
| actualPickQty < requiredQty -> { | actualPickQty < requiredQty -> { | ||||
| println(" Case 3: actualPickQty < requiredQty") | |||||
| val calculatedIssueQty = bookQty.subtract(actualPickQty) | val calculatedIssueQty = bookQty.subtract(actualPickQty) | ||||
| println(" issueQty = bookQty - actualPickQty = $bookQty - $actualPickQty = $calculatedIssueQty") | |||||
| if (missQty > BigDecimal.ZERO && missQty > 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 | 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("================================================") | println("================================================") | ||||
| // 5. 创建 pick execution issue 记录 | // 5. 创建 pick execution issue 记录 | ||||
| val issueNo = generateIssueNo() | |||||
| println("Generated issue number: $issueNo") | |||||
| val pickExecutionIssue = PickExecutionIssue( | val pickExecutionIssue = PickExecutionIssue( | ||||
| id = null, | id = null, | ||||
| pickOrderId = request.pickOrderId, | pickOrderId = request.pickOrderId, | ||||
| @@ -159,7 +198,7 @@ open class PickExecutionIssueService( | |||||
| pickOrderCreateDate = request.pickOrderCreateDate, | pickOrderCreateDate = request.pickOrderCreateDate, | ||||
| pickExecutionDate = request.pickExecutionDate ?: LocalDate.now(), | pickExecutionDate = request.pickExecutionDate ?: LocalDate.now(), | ||||
| pickOrderLineId = request.pickOrderLineId, | pickOrderLineId = request.pickOrderLineId, | ||||
| issueNo = generateIssueNo(), | |||||
| issueNo = issueNo, | |||||
| joPickOrderId = pickOrder?.jobOrder?.id, | joPickOrderId = pickOrder?.jobOrder?.id, | ||||
| doPickOrderId = if (pickOrder?.type?.value == "do") pickOrder.id else null, | doPickOrderId = if (pickOrder?.type?.value == "do") pickOrder.id else null, | ||||
| issueCategory = IssueCategory.valueOf( | issueCategory = IssueCategory.valueOf( | ||||
| @@ -189,11 +228,18 @@ open class PickExecutionIssueService( | |||||
| modifiedBy = "system", | modifiedBy = "system", | ||||
| deleted = false | deleted = false | ||||
| ) | ) | ||||
| println("Creating pick execution issue record...") | |||||
| val savedIssue = pickExecutionIssueRepository.save(pickExecutionIssue) | 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 | // 6. NEW: Update inventory_lot_line.issueQty | ||||
| if (request.lotId != null && inventoryLotLine != null) { | if (request.lotId != null && inventoryLotLine != null) { | ||||
| println("Updating inventory_lot_line.issueQty...") | |||||
| // ✅ 修改:如果只有 missQty,不更新 issueQty | // ✅ 修改:如果只有 missQty,不更新 issueQty | ||||
| val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO | val actualPickQty = request.actualPickQty ?: BigDecimal.ZERO | ||||
| val missQty = request.missQty ?: BigDecimal.ZERO | val missQty = request.missQty ?: BigDecimal.ZERO | ||||
| @@ -205,6 +251,9 @@ open class PickExecutionIssueService( | |||||
| val hasMissItemWithPartialPick = missQty > BigDecimal.ZERO | val hasMissItemWithPartialPick = missQty > BigDecimal.ZERO | ||||
| && actualPickQty > BigDecimal.ZERO | && actualPickQty > BigDecimal.ZERO | ||||
| println(" isMissItemOnly: $isMissItemOnly") | |||||
| println(" hasMissItemWithPartialPick: $hasMissItemWithPartialPick") | |||||
| if (!isMissItemOnly && !hasMissItemWithPartialPick) { | if (!isMissItemOnly && !hasMissItemWithPartialPick) { | ||||
| // 只有非 miss item 的情况才更新 issueQty | // 只有非 miss item 的情况才更新 issueQty | ||||
| val currentIssueQty = inventoryLotLine.issueQty ?: BigDecimal.ZERO | val currentIssueQty = inventoryLotLine.issueQty ?: BigDecimal.ZERO | ||||
| @@ -213,42 +262,45 @@ open class PickExecutionIssueService( | |||||
| inventoryLotLine.modified = LocalDateTime.now() | inventoryLotLine.modified = LocalDateTime.now() | ||||
| inventoryLotLine.modifiedBy = "system" | inventoryLotLine.modifiedBy = "system" | ||||
| inventoryLotLineRepository.saveAndFlush(inventoryLotLine) | inventoryLotLineRepository.saveAndFlush(inventoryLotLine) | ||||
| println("Updated inventory_lot_line ${request.lotId} issueQty: ${currentIssueQty} -> ${newIssueQty}") | |||||
| println("✅ Updated inventory_lot_line ${request.lotId} issueQty: $currentIssueQty -> $newIssueQty") | |||||
| } else { | } else { | ||||
| println("Skipped updating issueQty for miss item (lot ${request.lotId})") | |||||
| println("⏭️ Skipped updating issueQty for miss item (lot ${request.lotId})") | |||||
| } | } | ||||
| } | } | ||||
| // 7. 获取相关数据用于后续处理 | // 7. 获取相关数据用于后续处理 | ||||
| val actualPickQtyForProcessing = request.actualPickQty ?: BigDecimal.ZERO | val actualPickQtyForProcessing = request.actualPickQty ?: BigDecimal.ZERO | ||||
| val missQtyForProcessing = request.missQty ?: BigDecimal.ZERO | val missQtyForProcessing = request.missQty ?: BigDecimal.ZERO | ||||
| val badItemQtyForProcessing = request.badItemQty ?: BigDecimal.ZERO | val badItemQtyForProcessing = request.badItemQty ?: BigDecimal.ZERO | ||||
| val lotId = request.lotId | val lotId = request.lotId | ||||
| val itemId = request.itemId | 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("Bad Reason: ${request.badReason}") | ||||
| println("Lot ID: ${lotId}") | |||||
| println("Item ID: ${itemId}") | |||||
| println("Lot ID: $lotId") | |||||
| println("Item ID: $itemId") | |||||
| println("================================================") | println("================================================") | ||||
| // 8. 新的统一处理逻辑(根据 badReason 决定处理方式) | // 8. 新的统一处理逻辑(根据 badReason 决定处理方式) | ||||
| when { | when { | ||||
| // 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0) | // 情况1: 只有 miss item (actualPickQty = 0, missQty > 0, badItemQty = 0) | ||||
| actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> { | actualPickQtyForProcessing == BigDecimal.ZERO && missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing == BigDecimal.ZERO -> { | ||||
| println("→ Handling: Miss Item Only") | |||||
| handleMissItemOnly(request, missQtyForProcessing) | handleMissItemOnly(request, missQtyForProcessing) | ||||
| } | } | ||||
| // 情况2: 只有 bad item (badItemQty > 0, missQty = 0) | // 情况2: 只有 bad item (badItemQty > 0, missQty = 0) | ||||
| badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> { | badItemQtyForProcessing > BigDecimal.ZERO && missQtyForProcessing == BigDecimal.ZERO -> { | ||||
| println("→ Handling: Bad Item Only") | |||||
| // NEW: Check bad reason | // NEW: Check bad reason | ||||
| if (request.badReason == "package_problem") { | if (request.badReason == "package_problem") { | ||||
| println(" Bad reason is 'package_problem' - calling handleBadItemPackageProblem") | |||||
| handleBadItemPackageProblem(request, badItemQtyForProcessing) | handleBadItemPackageProblem(request, badItemQtyForProcessing) | ||||
| } else { | } else { | ||||
| println(" Bad reason is 'quantity_problem' - calling handleBadItemOnly") | |||||
| // quantity_problem or default: handle as normal bad item | // quantity_problem or default: handle as normal bad item | ||||
| handleBadItemOnly(request, badItemQtyForProcessing) | handleBadItemOnly(request, badItemQtyForProcessing) | ||||
| } | } | ||||
| @@ -256,29 +308,34 @@ open class PickExecutionIssueService( | |||||
| // 情况3: 既有 miss item 又有 bad item | // 情况3: 既有 miss item 又有 bad item | ||||
| missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> { | missQtyForProcessing > BigDecimal.ZERO && badItemQtyForProcessing > BigDecimal.ZERO -> { | ||||
| println("→ Handling: Both Miss and Bad Item") | |||||
| // NEW: Check bad reason | // NEW: Check bad reason | ||||
| if (request.badReason == "package_problem") { | if (request.badReason == "package_problem") { | ||||
| println(" Bad reason is 'package_problem' - calling handleBothMissAndBadItemPackageProblem") | |||||
| handleBothMissAndBadItemPackageProblem(request, missQtyForProcessing, badItemQtyForProcessing) | handleBothMissAndBadItemPackageProblem(request, missQtyForProcessing, badItemQtyForProcessing) | ||||
| } else { | } else { | ||||
| println(" Bad reason is 'quantity_problem' - calling handleBothMissAndBadItem") | |||||
| handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing) | handleBothMissAndBadItem(request, missQtyForProcessing, badItemQtyForProcessing) | ||||
| } | } | ||||
| } | } | ||||
| // 情况4: 有 miss item 的情况(无论 actualPickQty 是多少) | // 情况4: 有 miss item 的情况(无论 actualPickQty 是多少) | ||||
| missQtyForProcessing > BigDecimal.ZERO -> { | missQtyForProcessing > BigDecimal.ZERO -> { | ||||
| println("→ Handling: Miss Item With Partial Pick") | |||||
| handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing) | handleMissItemWithPartialPick(request, actualPickQtyForProcessing, missQtyForProcessing) | ||||
| } | } | ||||
| // 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item) | // 情况5: 正常拣货 (actualPickQty > 0, 没有 miss 或 bad item) | ||||
| actualPickQtyForProcessing > BigDecimal.ZERO -> { | actualPickQtyForProcessing > BigDecimal.ZERO -> { | ||||
| println("→ Handling: Normal Pick") | |||||
| handleNormalPick(request, actualPickQtyForProcessing) | handleNormalPick(request, actualPickQtyForProcessing) | ||||
| } | } | ||||
| else -> { | 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 pickOrderForCompletion = pickOrderRepository.findById(request.pickOrderId).orElse(null) | ||||
| val consoCode = pickOrderForCompletion?.consoCode | val consoCode = pickOrderForCompletion?.consoCode | ||||
| @@ -300,7 +357,9 @@ open class PickExecutionIssueService( | |||||
| println("⚠️ Error checking pick order completion by pickOrderId: ${e.message}") | println("⚠️ Error checking pick order completion by pickOrderId: ${e.message}") | ||||
| } | } | ||||
| } | } | ||||
| println("=== recordPickExecutionIssue: SUCCESS ===") | |||||
| println("Issue ID: ${savedIssue.id}, Issue No: ${savedIssue.issueNo}") | |||||
| return MessageResponse( | return MessageResponse( | ||||
| id = savedIssue.id, | id = savedIssue.id, | ||||
| name = "Pick execution issue recorded successfully", | name = "Pick execution issue recorded successfully", | ||||
| @@ -309,9 +368,10 @@ open class PickExecutionIssueService( | |||||
| message = "Pick execution issue recorded successfully", | message = "Pick execution issue recorded successfully", | ||||
| errorPosition = null | errorPosition = null | ||||
| ) | ) | ||||
| } catch (e: Exception) { | } catch (e: Exception) { | ||||
| println("=== ERROR IN recordPickExecutionIssue ===") | |||||
| println("=== recordPickExecutionIssue: ERROR ===") | |||||
| println("Error: ${e.message}") | |||||
| e.printStackTrace() | e.printStackTrace() | ||||
| return MessageResponse( | return MessageResponse( | ||||
| id = null, | 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 { | open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse { | ||||
| try { | 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 | // Find all issues for this lot and item | ||||
| val issues = if (request.issueType == "miss") { | val issues = if (request.issueType == "miss") { | ||||
| pickExecutionIssueRepository.findMissItemList(IssueCategory.lot_issue) | 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()) { | if (issues.isEmpty()) { | ||||
| println("❌ No issues found for this lot") | |||||
| return MessageResponse( | return MessageResponse( | ||||
| id = null, | id = null, | ||||
| name = "Error", | name = "Error", | ||||
| @@ -2623,13 +2690,36 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse | |||||
| // Use custom quantity instead of sum | // Use custom quantity instead of sum | ||||
| val submitQty = request.submitQty | val submitQty = request.submitQty | ||||
| if (submitQty <= BigDecimal.ZERO) { | |||||
| // ✅ 修改:允许提交0,0表示"标记为已处理但无需出库" | |||||
| if (submitQty < BigDecimal.ZERO) { | |||||
| println("❌ Submit quantity cannot be negative: $submitQty") | |||||
| return MessageResponse( | return MessageResponse( | ||||
| id = null, | id = null, | ||||
| name = "Error", | name = "Error", | ||||
| code = "INVALID", | code = "INVALID", | ||||
| type = "stock_issue", | 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 | errorPosition = null | ||||
| ) | ) | ||||
| } | } | ||||
| @@ -2649,14 +2739,17 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse | |||||
| firstIssue.issueRemark, | firstIssue.issueRemark, | ||||
| handler | handler | ||||
| ) | ) | ||||
| println("Created stock out header: id=${stockOut.id}, type=${if (isMissItem) "MISS_ITEM" else "BAD_ITEM"}") | |||||
| val pickOrderLine = firstIssue.pickOrderLineId?.let { | val pickOrderLine = firstIssue.pickOrderLineId?.let { | ||||
| pickOrderLineRepository.findById(it).orElse(null) | pickOrderLineRepository.findById(it).orElse(null) | ||||
| } | } | ||||
| println("Pick order line: id=${pickOrderLine?.id}") | |||||
| val lotLine = request.lotId.let { | val lotLine = request.lotId.let { | ||||
| inventoryLotLineRepository.findById(it).orElse(null) | inventoryLotLineRepository.findById(it).orElse(null) | ||||
| } | } | ||||
| println("Lot line: id=${lotLine?.id}, lotNo=${lotLine?.inventoryLot?.lotNo}") | |||||
| val item = itemsRepository.findById(request.itemId).orElse(null) | val item = itemsRepository.findById(request.itemId).orElse(null) | ||||
| ?: return MessageResponse( | ?: return MessageResponse( | ||||
| @@ -2667,6 +2760,7 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse | |||||
| message = "Item not found", | message = "Item not found", | ||||
| errorPosition = null | errorPosition = null | ||||
| ) | ) | ||||
| println("Item: id=${item.id}, code=${item.code}") | |||||
| // Create stock_out_line with custom quantity | // Create stock_out_line with custom quantity | ||||
| val stockOutLine = StockOutLine().apply { | val stockOutLine = StockOutLine().apply { | ||||
| @@ -2679,21 +2773,27 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse | |||||
| this.type = if (isMissItem) "Miss" else "Bad" | this.type = if (isMissItem) "Miss" else "Bad" | ||||
| } | } | ||||
| val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) | val savedStockOutLine = stockOutLineRepository.saveAndFlush(stockOutLine) | ||||
| println("Created stock out line: id=${savedStockOutLine.id}, qty=${savedStockOutLine.qty}, status=${savedStockOutLine.status}") | |||||
| if (!isMissItem && request.lotId != null) { | if (!isMissItem && request.lotId != null) { | ||||
| val lotLineForReset = inventoryLotLineRepository.findById(request.lotId).orElse(null) | val lotLineForReset = inventoryLotLineRepository.findById(request.lotId).orElse(null) | ||||
| if (lotLineForReset != null) { | if (lotLineForReset != null) { | ||||
| val oldIssueQty = lotLineForReset.issueQty | |||||
| lotLineForReset.issueQty = BigDecimal.ZERO | lotLineForReset.issueQty = BigDecimal.ZERO | ||||
| inventoryLotLineRepository.saveAndFlush(lotLineForReset) | 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 | // Update inventory_lot_line with custom quantity - pass isMissItem flag | ||||
| if (request.lotId != null) { | if (request.lotId != null) { | ||||
| println("Updating lot line after issue: lotId=${request.lotId}, submitQty=$submitQty, isMissItem=$isMissItem") | |||||
| updateLotLineAfterIssue(request.lotId, submitQty, isMissItem) | updateLotLineAfterIssue(request.lotId, submitQty, isMissItem) | ||||
| } | } | ||||
| // Mark all issues as handled | // Mark all issues as handled | ||||
| issues.forEach { issue -> | issues.forEach { issue -> | ||||
| println(" Marking issue ${issue.id} as handled by handler $handler") | |||||
| markIssueHandled(issue, handler) | markIssueHandled(issue, handler) | ||||
| } | } | ||||
| @@ -2708,6 +2808,7 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse | |||||
| createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, if (isMissItem) "Miss" else "Bad") | createStockLedgerForStockOutWithBalance(savedStockOutLine, balance, if (isMissItem) "Miss" else "Bad") | ||||
| println("=== submitIssueWithQty: SUCCESS ===") | |||||
| return MessageResponse( | return MessageResponse( | ||||
| id = stockOut.id, | id = stockOut.id, | ||||
| name = "Success", | name = "Success", | ||||
| @@ -2717,6 +2818,9 @@ open fun submitIssueWithQty(request: SubmitIssueWithQtyRequest): MessageResponse | |||||
| errorPosition = null | errorPosition = null | ||||
| ) | ) | ||||
| } catch (e: Exception) { | } catch (e: Exception) { | ||||
| println("=== submitIssueWithQty: ERROR ===") | |||||
| println("Error: ${e.message}") | |||||
| e.printStackTrace() | |||||
| return MessageResponse( | return MessageResponse( | ||||
| id = null, | id = null, | ||||
| name = "Error", | name = "Error", | ||||
| @@ -2,6 +2,8 @@ package com.ffii.fpsms.modules.productProcess.entity | |||||
| import org.springframework.data.jpa.repository.JpaRepository | import org.springframework.data.jpa.repository.JpaRepository | ||||
| import org.springframework.stereotype.Repository | import org.springframework.stereotype.Repository | ||||
| import org.springframework.data.jpa.repository.Query | |||||
| import java.time.LocalDateTime | |||||
| @Repository | @Repository | ||||
| interface ProductProcessLineRepository : JpaRepository<ProductProcessLine, Long> { | interface ProductProcessLineRepository : JpaRepository<ProductProcessLine, Long> { | ||||
| @@ -10,4 +12,22 @@ interface ProductProcessLineRepository : JpaRepository<ProductProcessLine, Long> | |||||
| fun findByHandler_IdAndStartTimeIsNotNullAndEndTimeIsNull(handlerId: Long): List<ProductProcessLine> | fun findByHandler_IdAndStartTimeIsNotNullAndEndTimeIsNull(handlerId: Long): List<ProductProcessLine> | ||||
| fun findByProductProcess_IdIn(ids: List<Long>): List<ProductProcessLine> | fun findByProductProcess_IdIn(ids: List<Long>): List<ProductProcessLine> | ||||
| @Query("SELECT l FROM ProductProcessLine l LEFT JOIN FETCH l.equipment WHERE l.productProcess.id = :productProcessId") | |||||
| fun findByProductProcess_IdWithEquipment(productProcessId: Long): List<ProductProcessLine> | |||||
| // 用於 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<ProductProcessLine> | |||||
| // 用於 Equipment 狀態:查詢指定 equipmentDetailId 目前仍未完成的所有行 | |||||
| fun findByEquipmentDetailIdAndDeletedFalseAndEndTimeIsNull(equipmentDetailId: Long): List<ProductProcessLine> | |||||
| } | } | ||||
| @@ -97,12 +97,12 @@ data class jobOrderLineInfo( | |||||
| val type: String?, | val type: String?, | ||||
| val reqQty: Double?, | val reqQty: Double?, | ||||
| val baseReqQty: Int?, | |||||
| val baseReqQty: Long?, | |||||
| val stockQty: Int?, | val stockQty: Int?, | ||||
| val stockReqQty: Double?, | val stockReqQty: Double?, | ||||
| val baseStockQty: Int?, | |||||
| val baseStockQty: Long?, | |||||
| val reqUom: String?, | val reqUom: String?, | ||||
| val reqBaseUom: String?, | val reqBaseUom: String?, | ||||
| @@ -11,12 +11,12 @@ import com.ffii.fpsms.modules.master.entity.EquipmentDetailRepository | |||||
| import com.ffii.fpsms.modules.productProcess.entity.projections.jobOrderLineInfo | import com.ffii.fpsms.modules.productProcess.entity.projections.jobOrderLineInfo | ||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import org.springframework.transaction.annotation.Transactional | import org.springframework.transaction.annotation.Transactional | ||||
| import java.time.LocalDate | |||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||
| import java.time.temporal.ChronoUnit | import java.time.temporal.ChronoUnit | ||||
| import com.ffii.fpsms.modules.user.entity.UserRepository | import com.ffii.fpsms.modules.user.entity.UserRepository | ||||
| import com.ffii.fpsms.modules.user.entity.User | 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.BomRepository | ||||
| import com.ffii.fpsms.modules.master.entity.BomProcessRepository | import com.ffii.fpsms.modules.master.entity.BomProcessRepository | ||||
| import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository | 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.entity.ItemUomRespository | ||||
| import com.ffii.fpsms.modules.master.web.models.* | import com.ffii.fpsms.modules.master.web.models.* | ||||
| import java.math.RoundingMode | import java.math.RoundingMode | ||||
| import java.time.LocalDate | |||||
| import java.time.format.DateTimeFormatter | |||||
| @Service | @Service | ||||
| @Transactional | @Transactional | ||||
| open class ProductProcessService( | open class ProductProcessService( | ||||
| @@ -692,7 +695,7 @@ val sufficientStockQty = bomMaterials | |||||
| // ✅ 获取 req UOM - 对于 reqQty,使用 bomMaterial.uom(BOM 的 UOM) | // ✅ 获取 req UOM - 对于 reqQty,使用 bomMaterial.uom(BOM 的 UOM) | ||||
| // ✅ 对于 stockReqQty,使用 line.uom(库存单位,已按比例调整) | // ✅ 对于 stockReqQty,使用 line.uom(库存单位,已按比例调整) | ||||
| val reqUomId = bomMaterial?.uom?.id ?: line.uom?.id ?: 0L // BOM 的 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 reqUom = reqUomId.takeIf { it > 0 }?.let { uomConversionRepository.findByIdAndDeletedFalse(it) } | ||||
| val uomName = reqUom?.udfudesc | val uomName = reqUom?.udfudesc | ||||
| @@ -706,7 +709,7 @@ val sufficientStockQty = bomMaterials | |||||
| println("=== Quantity Calculation for Item: ${line.item?.code} (id=$itemId) ===") | println("=== Quantity Calculation for Item: ${line.item?.code} (id=$itemId) ===") | ||||
| println("JobOrderBomMaterial reqQty (adjusted, stock unit): $actualReqQty in UOM: ${stockReqUom?.udfudesc} (id=$stockReqUomId)") | 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)") | println("Original stockQty: $stockQtyValue in UOM: $stockUomNameForStock (id=$stockUomId)") | ||||
| val jobOrder = jobOrderRepository.findById(joid).orElse(null) | val jobOrder = jobOrderRepository.findById(joid).orElse(null) | ||||
| @@ -721,8 +724,8 @@ val sufficientStockQty = bomMaterials | |||||
| // ✅ reqQty 使用 bomMaterial.qty * proportion(BOM 单位) | // ✅ reqQty 使用 bomMaterial.qty * proportion(BOM 单位) | ||||
| val reqQtyInBomUnit = (bomMaterial?.qty?.times(proportion) ?: BigDecimal.ZERO) | 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 | // ✅ Convert reqQty (BOM unit) to base unit | ||||
| val baseReqQtyResult = if (reqUomId > 0 && reqQtyInBomUnit > BigDecimal.ZERO) { | val baseReqQtyResult = if (reqUomId > 0 && reqQtyInBomUnit > BigDecimal.ZERO) { | ||||
| @@ -848,7 +851,7 @@ val sufficientStockQty = bomMaterials | |||||
| baseReqQty = baseReqQty, | baseReqQty = baseReqQty, | ||||
| stockQty = stockQty, | stockQty = stockQty, | ||||
| // ✅ stockReqQty:使用 bomMaterial.saleQty * proportion(库存单位,已按比例调整) | |||||
| // ✅ stockReqQty:使用 bomMaterial.stockQty * proportion(库存单位,已按比例调整) | |||||
| stockReqQty = if (stockReqUomId in decimalUomIds) { | stockReqQty = if (stockReqUomId in decimalUomIds) { | ||||
| stockReqQtyInStockUnit.toDouble() | stockReqQtyInStockUnit.toDouble() | ||||
| } else { | } else { | ||||
| @@ -1510,13 +1513,13 @@ val sufficientStockQty = bomMaterials | |||||
| productProcessLine.startTime = LocalDateTime.now() | productProcessLine.startTime = LocalDateTime.now() | ||||
| productProcessLineRepository.save(productProcessLine) | productProcessLineRepository.save(productProcessLine) | ||||
| } | } | ||||
| if(allproductProcessLines.all { it.status == "Completed" }) { | |||||
| if(allproductProcessLines.all { it.status == "Completed"|| it.status == "Pass" }) { | |||||
| updateProductProcessEndTime(productProcessId) | updateProductProcessEndTime(productProcessId) | ||||
| updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED) | updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED) | ||||
| val productProcess = productProcessRepository.findById(productProcessId).orElse(null) | val productProcess = productProcessRepository.findById(productProcessId).orElse(null) | ||||
| val jobOrder = jobOrderRepository.findById(productProcess?.jobOrder?.id?:0L).orElse(null) | val jobOrder = jobOrderRepository.findById(productProcess?.jobOrder?.id?:0L).orElse(null) | ||||
| if(jobOrder != null) { | if(jobOrder != null) { | ||||
| jobOrder.status = JobOrderStatus.PENDING_QC | |||||
| jobOrder.status = JobOrderStatus.STORING | |||||
| jobOrderRepository.save(jobOrder) | jobOrderRepository.save(jobOrder) | ||||
| stockInLineService.create( | stockInLineService.create( | ||||
| @@ -1783,42 +1786,35 @@ val sufficientStockQty = bomMaterials | |||||
| ) | ) | ||||
| } | } | ||||
| open fun getJobProcessStatus(): List<JobProcessStatusResponse> { | |||||
| val productProcesses = productProcessRepository.findAllByDeletedIsFalse() | |||||
| .filter { it.status != ProductProcessStatus.COMPLETED } | |||||
| open fun getJobProcessStatus(date: LocalDate?): List<JobProcessStatusResponse> { | |||||
| val productProcesses = productProcessRepository.findAllByDeletedIsFalse() | |||||
| .let { list -> if (date == null) list else list.filter { it.date == date } } | |||||
| return productProcesses.mapNotNull { process -> | return productProcesses.mapNotNull { process -> | ||||
| val jobOrder = jobOrderRepository.findById(process.jobOrder?.id ?: 0L).orElse(null) | 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) | val lines = productProcessLineRepository.findByProductProcess_Id(process.id ?: 0L) | ||||
| .sortedBy { it.seqNo } | .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 firstStartTime = lines.firstOrNull { it.startTime != null }?.startTime | ||||
| val calculatedPlanEndTime = if (firstStartTime != null) { | val calculatedPlanEndTime = if (firstStartTime != null) { | ||||
| // Calculate total remaining processing time (in minutes) for unfinished processes | |||||
| var totalRemainingMinutes = 0L | var totalRemainingMinutes = 0L | ||||
| lines.forEach { line -> | lines.forEach { line -> | ||||
| if (line.endTime == null) { | if (line.endTime == null) { | ||||
| // Process is not finished, add its processing time | |||||
| totalRemainingMinutes += (line.processingTime ?: 0).toLong() | totalRemainingMinutes += (line.processingTime ?: 0).toLong() | ||||
| totalRemainingMinutes += (line.setupTime ?: 0).toLong() | totalRemainingMinutes += (line.setupTime ?: 0).toLong() | ||||
| totalRemainingMinutes += (line.changeoverTime ?: 0).toLong() | totalRemainingMinutes += (line.changeoverTime ?: 0).toLong() | ||||
| } | } | ||||
| } | } | ||||
| // Add remaining time to first start time | |||||
| firstStartTime.plusMinutes(totalRemainingMinutes) | firstStartTime.plusMinutes(totalRemainingMinutes) | ||||
| } else { | } else { | ||||
| // No process has started yet, use original planEndTime | |||||
| jobOrder?.planEnd | jobOrder?.planEnd | ||||
| } | } | ||||
| JobProcessStatusResponse( | JobProcessStatusResponse( | ||||
| jobOrderId = jobOrder?.id ?: 0L, | jobOrderId = jobOrder?.id ?: 0L, | ||||
| jobOrderCode = jobOrder?.code ?: "", | jobOrderCode = jobOrder?.code ?: "", | ||||
| @@ -1829,29 +1825,19 @@ open fun getJobProcessStatus(): List<JobProcessStatusResponse> { | |||||
| processes = (0 until 6).map { index -> | processes = (0 until 6).map { index -> | ||||
| if (index < lines.size) { | if (index < lines.size) { | ||||
| val line = lines[index] | 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( | ProcessStatusInfo( | ||||
| equipmentCode = equipmentCode, | |||||
| processName = line.name, // ✅ 新增:工序名称 | |||||
| equipmentName = equipmentName, // ✅ 替代 equipmentCode | |||||
| equipmentDetailName = equipmentDetailName, // ✅ 新增 | |||||
| startTime = line.startTime, | startTime = line.startTime, | ||||
| endTime = line.endTime, | endTime = line.endTime, | ||||
| processingTime = line.processingTime, | processingTime = line.processingTime, | ||||
| @@ -1861,13 +1847,15 @@ open fun getJobProcessStatus(): List<JobProcessStatusResponse> { | |||||
| ) | ) | ||||
| } else { | } else { | ||||
| ProcessStatusInfo( | ProcessStatusInfo( | ||||
| processName = null, | |||||
| equipmentName = null, | |||||
| equipmentDetailName = null, | |||||
| startTime = null, | startTime = null, | ||||
| endTime = null, | endTime = null, | ||||
| processingTime = null, | processingTime = null, | ||||
| setupTime = null, | setupTime = null, | ||||
| changeoverTime = null, | changeoverTime = null, | ||||
| isRequired = false, | |||||
| equipmentCode = null, | |||||
| isRequired = false | |||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| @@ -9,6 +9,9 @@ import org.springframework.data.domain.Pageable | |||||
| import org.springframework.web.bind.annotation.* | import org.springframework.web.bind.annotation.* | ||||
| import com.ffii.fpsms.modules.productProcess.entity.projections.ProductProcessInfo | import com.ffii.fpsms.modules.productProcess.entity.projections.ProductProcessInfo | ||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | import com.ffii.fpsms.modules.master.web.models.MessageResponse | ||||
| import java.time.LocalDate | |||||
| import java.time.format.DateTimeFormatter | |||||
| @RestController | @RestController | ||||
| @RequestMapping("/product-process") | @RequestMapping("/product-process") | ||||
| class ProductProcessController( | class ProductProcessController( | ||||
| @@ -226,11 +229,29 @@ class ProductProcessController( | |||||
| return productProcessService.UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTime(request) | return productProcessService.UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTime(request) | ||||
| } | } | ||||
| @GetMapping("/Demo/JobProcessStatus") | @GetMapping("/Demo/JobProcessStatus") | ||||
| fun getJobProcessStatus(): List<JobProcessStatusResponse> { | |||||
| return productProcessService.getJobProcessStatus() | |||||
| fun getJobProcessStatus(@RequestParam(required = false) date: String?): List<JobProcessStatusResponse> { | |||||
| 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}") | @PostMapping("/Demo/ProcessLine/delete/{lineId}") | ||||
| fun deleteProductProcessLine(@PathVariable lineId: Long): MessageResponse { | fun deleteProductProcessLine(@PathVariable lineId: Long): MessageResponse { | ||||
| return productProcessService.deleteProductProcessLine(lineId) | return productProcessService.deleteProductProcessLine(lineId) | ||||
| } | } | ||||
| // ===== Dashboards ===== | |||||
| @GetMapping("/Demo/OperatorKpi") | |||||
| fun getOperatorKpi(@RequestParam(required = false) date: String?): List<OperatorKpiResponse> { | |||||
| val parsedDate = date?.takeIf { it.isNotBlank() }?.let { | |||||
| LocalDate.parse(it, DateTimeFormatter.ISO_DATE) | |||||
| } | |||||
| return productProcessService.getOperatorKpi(parsedDate) | |||||
| } | |||||
| @GetMapping("/Demo/EquipmentStatus") | |||||
| fun getEquipmentStatus(): List<EquipmentStatusByTypeResponse> { | |||||
| return productProcessService.getEquipmentStatusByType() | |||||
| } | |||||
| } | } | ||||
| @@ -220,7 +220,9 @@ data class UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest( | |||||
| val changeoverTime: Int? | val changeoverTime: Int? | ||||
| ) | ) | ||||
| data class ProcessStatusInfo( | data class ProcessStatusInfo( | ||||
| val equipmentCode: String?, | |||||
| val processName: String?, | |||||
| val equipmentName: String?, | |||||
| val equipmentDetailName: String?, | |||||
| val startTime: LocalDateTime?, | val startTime: LocalDateTime?, | ||||
| val endTime: LocalDateTime?, | val endTime: LocalDateTime?, | ||||
| val processingTime: Int?, | val processingTime: Int?, | ||||
| @@ -235,6 +237,66 @@ data class JobProcessStatusResponse( | |||||
| val itemCode: String, | val itemCode: String, | ||||
| val itemName: String, | val itemName: String, | ||||
| val planEndTime: LocalDateTime?, | val planEndTime: LocalDateTime?, | ||||
| val status: String, | |||||
| val status: String, | |||||
| val processes: List<ProcessStatusInfo> | val processes: List<ProcessStatusInfo> | ||||
| ) | |||||
| // ===== 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<OperatorKpiProcessInfo>, | |||||
| ) | |||||
| // ===== 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<EquipmentStatusPerDetail>, | |||||
| ) | ) | ||||
| @@ -97,6 +97,30 @@ open class ReportService( | |||||
| return "AND (${conditions.joinToString(" OR ")})" | 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, Any> | |||||
| ): 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. | * 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. | * 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?, | stockCategory: String?, | ||||
| stockSubCategory: String?, | stockSubCategory: String?, | ||||
| itemCode: String?, | itemCode: String?, | ||||
| year: String?, | |||||
| lastInDateStart: String?, | lastInDateStart: String?, | ||||
| lastInDateEnd: String? | lastInDateEnd: String? | ||||
| ): List<Map<String, Any>> { | ): List<Map<String, Any>> { | ||||
| @@ -115,6 +140,13 @@ open class ReportService( | |||||
| val stockSubCategorySql = buildMultiValueLikeClause(stockSubCategory, "ic.sub", "stockSubCategory", args) | val stockSubCategorySql = buildMultiValueLikeClause(stockSubCategory, "ic.sub", "stockSubCategory", args) | ||||
| val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", 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()) { | val lastInDateStartSql = if (!lastInDateStart.isNullOrBlank()) { | ||||
| args["lastInDateStart"] = lastInDateStart | args["lastInDateStart"] = lastInDateStart | ||||
| "AND sil.receiptDate >= :lastInDateStart" | "AND sil.receiptDate >= :lastInDateStart" | ||||
| @@ -168,6 +200,7 @@ open class ReportService( | |||||
| $stockCategorySql | $stockCategorySql | ||||
| $stockSubCategorySql | $stockSubCategorySql | ||||
| $itemCodeSql | $itemCodeSql | ||||
| $yearSql | |||||
| $lastInDateStartSql | $lastInDateStartSql | ||||
| $lastInDateEndSql | $lastInDateEndSql | ||||
| ORDER BY ic.sub, it.code, sil.lotNo | ORDER BY ic.sub, it.code, sil.lotNo | ||||
| @@ -176,6 +209,205 @@ open class ReportService( | |||||
| return jdbcDao.queryForList(sql, args) | 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<Map<String, Any>> { | |||||
| val args = mutableMapOf<String, Any>() | |||||
| // 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<Map<String, String>> { | |||||
| val args = mutableMapOf<String, Any>() | |||||
| 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<Map<String, String>> { | |||||
| val args = mutableMapOf<String, Any>() | |||||
| 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. | * Compiles and fills a Jasper Report, returning the PDF as a ByteArray. | ||||
| */ | */ | ||||
| @@ -85,6 +85,7 @@ class ReportController( | |||||
| @RequestParam(required = false) stockCategory: String?, | @RequestParam(required = false) stockCategory: String?, | ||||
| @RequestParam(required = false) stockSubCategory: String?, | @RequestParam(required = false) stockSubCategory: String?, | ||||
| @RequestParam(required = false) itemCode: String?, | @RequestParam(required = false) itemCode: String?, | ||||
| @RequestParam(required = false) year: String?, | |||||
| @RequestParam(required = false) lastInDateStart: String?, | @RequestParam(required = false) lastInDateStart: String?, | ||||
| @RequestParam(required = false) lastInDateEnd: String? | @RequestParam(required = false) lastInDateEnd: String? | ||||
| ): ResponseEntity<ByteArray> { | ): ResponseEntity<ByteArray> { | ||||
| @@ -94,6 +95,7 @@ class ReportController( | |||||
| parameters["stockCategory"] = stockCategory ?: "All" | parameters["stockCategory"] = stockCategory ?: "All" | ||||
| parameters["stockSubCategory"] = stockSubCategory ?: "All" | parameters["stockSubCategory"] = stockSubCategory ?: "All" | ||||
| parameters["itemNo"] = itemCode ?: "All" | parameters["itemNo"] = itemCode ?: "All" | ||||
| parameters["year"] = year ?: LocalDate.now().year.toString() | |||||
| parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) | parameters["reportDate"] = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) | ||||
| parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) | parameters["reportTime"] = LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) | ||||
| parameters["lastInDateStart"] = lastInDateStart ?: "" | parameters["lastInDateStart"] = lastInDateStart ?: "" | ||||
| @@ -104,6 +106,7 @@ class ReportController( | |||||
| stockCategory, | stockCategory, | ||||
| stockSubCategory, | stockSubCategory, | ||||
| itemCode, | itemCode, | ||||
| year, | |||||
| lastInDateStart, | lastInDateStart, | ||||
| lastInDateEnd | lastInDateEnd | ||||
| ) | ) | ||||
| @@ -122,4 +125,68 @@ class ReportController( | |||||
| return ResponseEntity(pdfBytes, headers, HttpStatus.OK) | 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<ByteArray> { | |||||
| val parameters = mutableMapOf<String, Any>() | |||||
| // 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<List<Map<String, String>>> { | |||||
| 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<List<Map<String, String>>> { | |||||
| val itemCodesWithCategory = reportService.getSemiFGItemCodesWithCategory(stockCategory) | |||||
| return ResponseEntity(itemCodesWithCategory, HttpStatus.OK) | |||||
| } | |||||
| } | } | ||||
| @@ -13,6 +13,8 @@ interface StockInLineInfo { | |||||
| val itemId: Long | val itemId: Long | ||||
| @get:Value("#{target.item?.name}") | @get:Value("#{target.item?.name}") | ||||
| val itemName: String? | val itemName: String? | ||||
| @get:Value("#{target.item?.LocationCode}") | |||||
| val locationCode: String? | |||||
| val itemNo: String | val itemNo: String | ||||
| @get:Value("#{target.stockIn?.id}") | @get:Value("#{target.stockIn?.id}") | ||||
| val stockInId: Long | val stockInId: Long | ||||
| @@ -74,4 +76,5 @@ interface PutAwayLineForSil { | |||||
| val putawayDate: LocalDateTime?; | val putawayDate: LocalDateTime?; | ||||
| @get:Value("#{target.createdBy}") | @get:Value("#{target.createdBy}") | ||||
| val putawayUser: String?; | val putawayUser: String?; | ||||
| } | } | ||||
| @@ -310,7 +310,8 @@ open class SuggestedPickLotService( | |||||
| } | } | ||||
| // 规则 2:原有逻辑:跳过 3F | // 规则 2:原有逻辑:跳过 3F | ||||
| if (warehouseStoreId == "3F") { | |||||
| val isJobOrder = pickOrder?.type?.value == "jo" || pickOrder?.type?.value == "jo" | |||||
| if (warehouseStoreId == "3F" && !isJobOrder) { | |||||
| return@forEachIndexed | return@forEachIndexed | ||||
| } | } | ||||
| @@ -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`; | |||||
| @@ -0,0 +1,664 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <!-- Created with Jaspersoft Studio version 6.21.3.final using JasperReports Library version 6.21.3-4a3078d20785ebe464f18037d738d12fc98c13cf --> | |||||
| <jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="SemiFGProductionAnalysisReport" pageWidth="842" pageHeight="595" orientation="Landscape" columnWidth="802" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="055685d6-c02e-403a-8511-48e9df2752d3"> | |||||
| <property name="com.jaspersoft.studio.unit." value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.pageWidth" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.topMargin" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.bottomMargin" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.leftMargin" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.rightMargin" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.columnWidth" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.columnSpacing" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/> | |||||
| <property name="com.jaspersoft.studio.data.sql.tables" value=""/> | |||||
| <parameter name="stockSubCategory" class="java.lang.String"> | |||||
| <defaultValueExpression><![CDATA["stockSubCategory"]]></defaultValueExpression> | |||||
| </parameter> | |||||
| <parameter name="stockCategory" class="java.lang.String"> | |||||
| <defaultValueExpression><![CDATA["stockCategory"]]></defaultValueExpression> | |||||
| </parameter> | |||||
| <parameter name="itemNo" class="java.lang.String"> | |||||
| <defaultValueExpression><![CDATA["itemCode"]]></defaultValueExpression> | |||||
| </parameter> | |||||
| <parameter name="year" class="java.lang.String"/> | |||||
| <parameter name="reportDate" class="java.lang.String"/> | |||||
| <parameter name="reportTime" class="java.lang.String"/> | |||||
| <parameter name="deliveryPeriodStart" class="java.lang.String"/> | |||||
| <parameter name="deliveryPeriodEnd" class="java.lang.String"/> | |||||
| <parameter name="lastOutDateStart" class="java.lang.String"> | |||||
| <parameterDescription><![CDATA["lastOutDateStart"]]></parameterDescription> | |||||
| </parameter> | |||||
| <parameter name="lastOutDateEnd" class="java.lang.String"> | |||||
| <parameterDescription><![CDATA["lastOutDateStart"]]></parameterDescription> | |||||
| </parameter> | |||||
| <queryString> | |||||
| <![CDATA[select * from fpsmsdb.items , fpsmsdb.stock_out_line]]> | |||||
| </queryString> | |||||
| <field name="id" class="java.lang.Integer"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="id"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="id"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="items"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="modified" class="java.time.LocalDateTime"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="modified"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="modified"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="items"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="itemNo" class="java.lang.String"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="code"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="code"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="items"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="itemName" class="java.lang.String"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="name"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="name"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="items"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="description" class="java.lang.String"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="description"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="description"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="items"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="remarks" class="java.lang.String"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="remarks"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="remarks"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="items"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="type" class="java.lang.String"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="type"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="type"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="items"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="stockSubCategory" class="java.lang.Integer"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="categoryId"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="categoryId"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="items"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qcCategoryId" class="java.lang.Integer"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qcCategoryId"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qcCategoryId"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="items"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="uomId" class="java.lang.Integer"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="uomId"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="uomId"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="items"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="itemId" class="java.lang.Integer"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="itemId"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="itemId"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qty" class="java.math.BigDecimal"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="stockOutId" class="java.lang.Integer"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="stockOutId"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="stockOutId"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="pickOrderLineId" class="java.lang.Integer"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="pickOrderLineId"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="pickOrderLineId"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="status" class="java.lang.String"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="status"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="status"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="pickTime" class="java.time.LocalDateTime"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="pickTime"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="pickTime"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qtyJan" class="java.math.BigDecimal"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qtyFeb" class="java.math.BigDecimal"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qtyMar" class="java.math.BigDecimal"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qtyApr" class="java.math.BigDecimal"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qtyMay" class="java.math.BigDecimal"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qtyJun" class="java.math.BigDecimal"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qtyJul" class="java.math.BigDecimal"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qtyAug" class="java.math.BigDecimal"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qtySep" class="java.math.BigDecimal"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qtyOct" class="java.math.BigDecimal"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qtyNov" class="java.math.BigDecimal"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="qtyDec" class="java.math.BigDecimal"> | |||||
| <property name="com.jaspersoft.studio.field.name" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.label" value="qty"/> | |||||
| <property name="com.jaspersoft.studio.field.tree.path" value="stock_out_line"/> | |||||
| <fieldDescription><![CDATA[]]></fieldDescription> | |||||
| </field> | |||||
| <field name="unitOfMeasure" class="java.lang.String"/> | |||||
| <field name="totalProductionQty" class="java.lang.String"/> | |||||
| <group name="Group1"> | |||||
| <groupExpression><![CDATA[$F{itemNo}]]></groupExpression> | |||||
| <groupHeader> | |||||
| <band height="92"> | |||||
| <staticText> | |||||
| <reportElement x="0" y="10" width="81" height="20" uuid="09bcfab9-9520-48dd-b851-3c00951cc550"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="12"/> | |||||
| </textElement> | |||||
| <text><![CDATA[貨品編號:]]></text> | |||||
| </staticText> | |||||
| <textField> | |||||
| <reportElement x="81" y="10" width="119" height="20" uuid="6f905a5d-cf6b-4d62-8433-ff3d570829ae"/> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="12"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{itemNo}]]></textFieldExpression> | |||||
| </textField> | |||||
| <staticText> | |||||
| <reportElement x="280" y="10" width="80" height="20" uuid="f0d35ecc-bc8d-4f5e-a94c-5cc6e5cde51e"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Right" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="12"/> | |||||
| </textElement> | |||||
| <text><![CDATA[貨品名稱:]]></text> | |||||
| </staticText> | |||||
| <textField> | |||||
| <reportElement x="360" y="10" width="130" height="20" uuid="11265d83-f36f-4cf2-ae3f-60d504226ab0"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="12"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{itemName}]]></textFieldExpression> | |||||
| </textField> | |||||
| <staticText> | |||||
| <reportElement x="610" y="10" width="41" height="20" uuid="a9054f9d-f217-4daa-a35c-076fe16a2df6"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Right" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="12"/> | |||||
| </textElement> | |||||
| <text><![CDATA[單位:]]></text> | |||||
| </staticText> | |||||
| <textField> | |||||
| <reportElement x="651" y="10" width="129" height="20" uuid="1a6c5348-668b-40cd-b8c7-a7b53c5dbcbe"> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="12"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{unitOfMeasure}]]></textFieldExpression> | |||||
| </textField> | |||||
| <staticText> | |||||
| <reportElement x="0" y="40" width="56" height="20" uuid="e7a7df0c-bb6b-48da-9612-f9ca6b11f845"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <text><![CDATA[一月]]></text> | |||||
| </staticText> | |||||
| <staticText> | |||||
| <reportElement x="224" y="40" width="56" height="20" uuid="1502529a-e0bf-4fff-aa34-7b4cff72ddf0"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <text><![CDATA[五月]]></text> | |||||
| </staticText> | |||||
| <staticText> | |||||
| <reportElement x="56" y="40" width="56" height="20" uuid="afd73da5-fb15-4768-ae44-ccc1f57123af"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle" markup="html"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <text><![CDATA[二月]]></text> | |||||
| </staticText> | |||||
| <staticText> | |||||
| <reportElement x="560" y="40" width="56" height="20" uuid="741b9a24-b91b-4ccb-aa7b-2eee600e7994"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <text><![CDATA[十一月]]></text> | |||||
| </staticText> | |||||
| <staticText> | |||||
| <reportElement x="392" y="40" width="56" height="20" uuid="b2935034-a5b9-4508-aa4a-0029704f8a3b"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <text><![CDATA[八月]]></text> | |||||
| </staticText> | |||||
| <staticText> | |||||
| <reportElement x="112" y="40" width="56" height="20" uuid="7d81e729-e12d-4f2e-a857-6224163177bd"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <text><![CDATA[三月]]></text> | |||||
| </staticText> | |||||
| <staticText> | |||||
| <reportElement x="168" y="40" width="56" height="20" uuid="cc73619f-39f9-4165-a8d7-e4679704b7ee"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <text><![CDATA[四月]]></text> | |||||
| </staticText> | |||||
| <staticText> | |||||
| <reportElement x="336" y="40" width="56" height="20" uuid="7af699eb-2dc9-4f4a-861c-0e54c284fbbd"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <text><![CDATA[七月]]></text> | |||||
| </staticText> | |||||
| <staticText> | |||||
| <reportElement x="280" y="40" width="56" height="20" uuid="0fbf45b7-f56b-4e19-a130-1adcb46f294d"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <text><![CDATA[六月]]></text> | |||||
| </staticText> | |||||
| <staticText> | |||||
| <reportElement x="616" y="40" width="74" height="20" uuid="3403153f-8bc9-4108-b3e2-45af3fd95305"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <text><![CDATA[十二月]]></text> | |||||
| </staticText> | |||||
| <staticText> | |||||
| <reportElement x="448" y="40" width="56" height="20" uuid="40912a1d-624d-496f-999c-41cd3210a441"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <text><![CDATA[九月]]></text> | |||||
| </staticText> | |||||
| <staticText> | |||||
| <reportElement x="504" y="40" width="56" height="20" uuid="0db0531d-3805-4d02-be13-746e6b97fd14"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <text><![CDATA[十月]]></text> | |||||
| </staticText> | |||||
| <textField> | |||||
| <reportElement x="690" y="62" width="90" height="20" uuid="803f19df-9c30-4b63-a930-69955daf442e"> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{totalProductionQty}]]></textFieldExpression> | |||||
| </textField> | |||||
| <line> | |||||
| <reportElement x="0" y="89" width="799" height="1" uuid="72083934-812f-42b9-b29b-b73d98fe6925"> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| </line> | |||||
| <line> | |||||
| <reportElement x="0" y="90" width="799" height="1" uuid="f0436daf-da7a-42fd-8514-c5c70716792d"> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| </line> | |||||
| <staticText> | |||||
| <reportElement x="690" y="40" width="90" height="20" uuid="4de75182-a992-4144-a36b-e49a56fbe89f"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <text><![CDATA[總和]]></text> | |||||
| </staticText> | |||||
| </band> | |||||
| </groupHeader> | |||||
| </group> | |||||
| <pageHeader> | |||||
| <band height="140" splitType="Stretch"> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| <textField evaluationTime="Report"> | |||||
| <reportElement x="770" y="0" width="30" height="23" uuid="6e9ed0a2-e1e6-4533-a786-f086b868a84c"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Center" verticalAlignment="Middle"/> | |||||
| <textFieldExpression><![CDATA[$V{PAGE_NUMBER}]]></textFieldExpression> | |||||
| </textField> | |||||
| <staticText> | |||||
| <reportElement x="690" y="0" width="30" height="23" uuid="5a9e0fa9-418d-4838-9f82-e2f336e5bce7"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Center" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="12"/> | |||||
| </textElement> | |||||
| <text><![CDATA[頁數]]></text> | |||||
| </staticText> | |||||
| <staticText> | |||||
| <reportElement x="750" y="0" width="30" height="23" uuid="26d3d09c-48aa-4870-822a-c403e7faddfa"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Center" verticalAlignment="Justified"> | |||||
| <font fontName="微軟正黑體" size="12"/> | |||||
| </textElement> | |||||
| <text><![CDATA[/]]></text> | |||||
| </staticText> | |||||
| <textField> | |||||
| <reportElement x="730" y="0" width="30" height="23" uuid="0c758a26-1c7f-484d-919e-d215917e9216"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Center" verticalAlignment="Middle"/> | |||||
| <textFieldExpression><![CDATA[$V{PAGE_NUMBER}]]></textFieldExpression> | |||||
| </textField> | |||||
| <staticText> | |||||
| <reportElement x="0" y="60" width="90" height="23" uuid="0daddd8b-ca61-42af-9c2b-cbf11b7d7dac"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="16" isBold="true"/> | |||||
| </textElement> | |||||
| <text><![CDATA[報告日期:]]></text> | |||||
| </staticText> | |||||
| <textField> | |||||
| <reportElement x="651" y="90" width="148" height="23" uuid="53b66bc3-4925-4340-add0-b4f2069a76c1"/> | |||||
| <textElement textAlignment="Right" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="16" isBold="true"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$P{year}]]></textFieldExpression> | |||||
| </textField> | |||||
| <staticText> | |||||
| <reportElement x="560" y="60" width="90" height="23" uuid="e2be43a3-3570-4a4e-a671-d96f872a5ad7"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Right" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="16" isBold="true"/> | |||||
| </textElement> | |||||
| <text><![CDATA[報告時間:]]></text> | |||||
| </staticText> | |||||
| <staticText> | |||||
| <reportElement x="560" y="90" width="90" height="23" uuid="29ed871d-3417-4978-9799-964389143451"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Right" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="16" isBold="true"/> | |||||
| </textElement> | |||||
| <text><![CDATA[年份:]]></text> | |||||
| </staticText> | |||||
| <textField> | |||||
| <reportElement x="651" y="60" width="148" height="23" uuid="cb1fbb06-e953-40e5-bdbe-5fc219f7c884"/> | |||||
| <textElement textAlignment="Right" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="16" isBold="true"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$P{reportTime}]]></textFieldExpression> | |||||
| </textField> | |||||
| <textField> | |||||
| <reportElement x="90" y="60" width="190" height="23" uuid="d398cf17-318c-4c16-a8bf-6f064e911965"/> | |||||
| <textElement> | |||||
| <font fontName="微軟正黑體" size="16" isBold="true"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$P{reportDate}]]></textFieldExpression> | |||||
| </textField> | |||||
| <line> | |||||
| <reportElement x="0" y="129" width="799" height="1" uuid="a7fde15d-ebf2-4516-88ab-fb01689a414e"> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| </line> | |||||
| <line> | |||||
| <reportElement x="0" y="130" width="799" height="1" uuid="9e9ba4e3-e369-4180-b01b-1b581e8fa00d"> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| </line> | |||||
| <staticText> | |||||
| <reportElement x="280" y="10" width="210" height="39" uuid="7451001b-6d5a-438c-82db-2b9e687d9a27"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Center" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="16" isBold="true"/> | |||||
| </textElement> | |||||
| <text><![CDATA[成品/半成品生產分析報告]]></text> | |||||
| </staticText> | |||||
| <textField> | |||||
| <reportElement x="90" y="90" width="336" height="23" uuid="b51949e1-c40a-4db0-a799-4243555893fd"> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||||
| <font fontName="微軟正黑體" size="12" isBold="true"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$P{lastOutDateStart} + " 到 " + $P{lastOutDateEnd}]]></textFieldExpression> | |||||
| </textField> | |||||
| <staticText> | |||||
| <reportElement x="0" y="90" width="90" height="23" uuid="fb09a559-f5fa-4e56-a891-87c600a2745a"> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="12" isBold="true"/> | |||||
| </textElement> | |||||
| <text><![CDATA[完成生產日期:]]></text> | |||||
| </staticText> | |||||
| </band> | |||||
| </pageHeader> | |||||
| <detail> | |||||
| <band height="22" splitType="Stretch"> | |||||
| <textField> | |||||
| <reportElement x="280" y="-30" width="56" height="20" uuid="3c77d2e4-afbf-4d52-9ee0-c90174f35809"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{qtyJun}]]></textFieldExpression> | |||||
| </textField> | |||||
| <textField> | |||||
| <reportElement x="336" y="-30" width="56" height="20" uuid="b7f7d359-7a9a-4d8f-8f0d-3a579fd92af6"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{qtyJul}]]></textFieldExpression> | |||||
| </textField> | |||||
| <textField> | |||||
| <reportElement x="0" y="-30" width="56" height="20" uuid="09cd34d3-ca81-4e96-8e62-9e2b5309b748"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{qtyJan}]]></textFieldExpression> | |||||
| </textField> | |||||
| <textField> | |||||
| <reportElement x="560" y="-30" width="56" height="20" uuid="30c01289-b963-4063-be9f-42a87e0e37d1"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{qtyNov}]]></textFieldExpression> | |||||
| </textField> | |||||
| <textField> | |||||
| <reportElement x="616" y="-30" width="74" height="20" uuid="b4f317e7-6c8f-4f37-b78d-00005559e398"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{qtyDec}]]></textFieldExpression> | |||||
| </textField> | |||||
| <textField> | |||||
| <reportElement x="392" y="-30" width="56" height="20" uuid="a84b08f0-123f-40ff-988b-7f9ac62cec09"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{qtyAug}]]></textFieldExpression> | |||||
| </textField> | |||||
| <textField> | |||||
| <reportElement x="448" y="-30" width="56" height="20" uuid="b4a7d70f-ab5a-4303-b6a8-70f76fda74df"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{qtySep}]]></textFieldExpression> | |||||
| </textField> | |||||
| <textField> | |||||
| <reportElement x="504" y="-30" width="56" height="20" uuid="3661155a-e86a-4fe7-9753-2f3893aed786"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{qtyOct}]]></textFieldExpression> | |||||
| </textField> | |||||
| <textField> | |||||
| <reportElement x="112" y="-30" width="56" height="20" uuid="6393dfd5-1fc6-4ee0-8044-9b2bae5d5019"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{qtyMar}]]></textFieldExpression> | |||||
| </textField> | |||||
| <textField> | |||||
| <reportElement x="168" y="-30" width="56" height="20" uuid="e4f89d0a-4dc5-4408-a99e-51f5458c77ac"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{qtyApr}]]></textFieldExpression> | |||||
| </textField> | |||||
| <textField> | |||||
| <reportElement x="224" y="-30" width="56" height="20" uuid="c2b581c3-979e-4450-b077-5017c7f485b0"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{qtyMay}]]></textFieldExpression> | |||||
| </textField> | |||||
| <textField> | |||||
| <reportElement x="56" y="-30" width="56" height="20" uuid="0153c92a-ff58-457d-9ece-bd61f349530b"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| </reportElement> | |||||
| <textElement textAlignment="Left" verticalAlignment="Top"> | |||||
| <font fontName="微軟正黑體" size="10"/> | |||||
| </textElement> | |||||
| <textFieldExpression><![CDATA[$F{qtyFeb}]]></textFieldExpression> | |||||
| </textField> | |||||
| </band> | |||||
| </detail> | |||||
| </jasperReport> | |||||