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