Handle user put DO (m18 PO) sync in m18 sync page PO tab; make the fg/semi fg report count by date rather than by month, created a button to make qc accept faster, added item code/name in search criteria of PO, make 2 new version of PO Item label printout (can set 1 to print the old version in setting)production
| @@ -1,18 +1,21 @@ | |||
| package com.ffii.core.utils | |||
| import com.google.zxing.BarcodeFormat | |||
| import com.google.zxing.EncodeHintType | |||
| import java.awt.image.BufferedImage | |||
| import com.google.zxing.client.j2se.MatrixToImageWriter; | |||
| import com.google.zxing.common.BitMatrix; | |||
| import com.google.zxing.qrcode.QRCodeWriter; | |||
| import com.google.zxing.client.j2se.MatrixToImageWriter | |||
| import com.google.zxing.qrcode.QRCodeWriter | |||
| open class QrCodeUtil { | |||
| companion object { | |||
| fun generateQRCodeImage(barcodeText: String?): BufferedImage { | |||
| val barcodeWriter = QRCodeWriter() | |||
| val bitMatrix: BitMatrix = barcodeWriter.encode(barcodeText, BarcodeFormat.QR_CODE, 250, 250) | |||
| val img: BufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix) | |||
| return img | |||
| return generateQRCodeImage(barcodeText, 250, 4) | |||
| } | |||
| fun generateQRCodeImage(barcodeText: String?, size: Int, margin: Int): BufferedImage { | |||
| val hints = hashMapOf<EncodeHintType, Any>(EncodeHintType.MARGIN to margin) | |||
| val bitMatrix = QRCodeWriter().encode(barcodeText, BarcodeFormat.QR_CODE, size, size, hints) | |||
| return MatrixToImageWriter.toBufferedImage(bitMatrix) | |||
| } | |||
| } | |||
| } | |||
| @@ -89,14 +89,8 @@ open class M18DeliveryOrderService( | |||
| "" | |||
| } | |||
| // Shop PO | |||
| val shopPoBuyers = commonUtils.listToString(listOf(m18Config.BEID_TOA), "beId=equal=", "=or=") | |||
| val shopPoSupplier = commonUtils.listToString( | |||
| shopService.findM18VendorIdsByCodeRegexp(m18Config.SHOP_PO_SUPPLIER), | |||
| "venId=equal=", | |||
| "=or=" | |||
| ) | |||
| var shopPoConds = "(${shopPoBuyers})=and=(${shopPoSupplier})" | |||
| // Shop PO — same BE/supplier scope as [buildShopPoScopeConds] | |||
| var shopPoConds = buildShopPoScopeConds() | |||
| if (request.modifiedDateFrom != null && request.modifiedDateTo != null) { | |||
| shopPoConds += "=and=(${lastDateConds})" | |||
| @@ -160,9 +154,24 @@ open class M18DeliveryOrderService( | |||
| ) | |||
| } | |||
| /** | |||
| * Shop PO (MTMS DO) BE/supplier scope used by scheduler and by-code sync: | |||
| * beId = TOA and venId in [M18Config.SHOP_PO_SUPPLIER] (e.g. P06/P07). | |||
| */ | |||
| private fun buildShopPoScopeConds(): String { | |||
| val shopPoBuyers = commonUtils.listToString(listOf(m18Config.BEID_TOA), "beId=equal=", "=or=") | |||
| val shopPoSupplier = commonUtils.listToString( | |||
| shopService.findM18VendorIdsByCodeRegexp(m18Config.SHOP_PO_SUPPLIER), | |||
| "venId=equal=", | |||
| "=or=" | |||
| ) | |||
| return "(${shopPoBuyers})=and=(${shopPoSupplier})" | |||
| } | |||
| /** | |||
| * Sync a single M18 shop PO / delivery order by document [code], same search pattern as | |||
| * [com.ffii.fpsms.m18.service.M18PurchaseOrderService.savePurchaseOrderByCode]. | |||
| * [com.ffii.fpsms.m18.service.M18PurchaseOrderService.savePurchaseOrderByCode], including | |||
| * the scheduler shop-PO BE/supplier filters (rejects material POs). | |||
| * | |||
| * @param isExtraSync when true, persist local `delivery_order.isExtra=true` (manual DO(加單) sync). | |||
| * No M18-side "加單" filtering is used. | |||
| @@ -173,15 +182,16 @@ open class M18DeliveryOrderService( | |||
| isExtraSync: Boolean = false, | |||
| newOnly: Boolean = false, | |||
| ): SyncResult { | |||
| if (newOnly && deliveryOrderRepository.existsByCodeAndDeletedIsFalse(code)) { | |||
| val trimmed = code.trim() | |||
| if (newOnly && deliveryOrderRepository.existsByCodeAndDeletedIsFalse(trimmed)) { | |||
| return SyncResult( | |||
| totalProcessed = 1, | |||
| totalSuccess = 0, | |||
| totalFail = 0, | |||
| query = "skipped (newOnly=true): delivery_order.code already exists: $code", | |||
| query = "skipped (newOnly=true): delivery_order.code already exists: $trimmed", | |||
| ) | |||
| } | |||
| val conds = "(code=equal=$code)" | |||
| val conds = "(code=equal=$trimmed)=and=${buildShopPoScopeConds()}" | |||
| val searchRequest = M18PurchaseOrderListRequest( | |||
| stSearch = "po", | |||
| params = null, | |||
| @@ -228,6 +238,7 @@ open class M18DeliveryOrderService( | |||
| val successList = mutableListOf<Long>() | |||
| val skippedList = mutableListOf<Long>() | |||
| val skippedNotPendingList = mutableListOf<String>() | |||
| val successDetailList = mutableListOf<Long>() | |||
| val failList = mutableListOf<Long>() | |||
| val failDetailList = mutableListOf<Long>() | |||
| @@ -291,6 +302,23 @@ open class M18DeliveryOrderService( | |||
| // logger.info("${doRefType}: Finding For Latest M18 Data Log...") | |||
| val latestDeliveryOrderLog = | |||
| m18DataLogService.findLatestM18DataLogWithSuccess(deliveryOrder.id, doRefType) | |||
| val existingDeliveryOrderForSync = | |||
| latestDeliveryOrderLog?.id?.let { deliveryOrderService.findByM18DataLogId(it) } | |||
| if (existingDeliveryOrderForSync != null && | |||
| existingDeliveryOrderForSync.deleted != true && | |||
| existingDeliveryOrderForSync.status != DeliveryOrderStatus.PENDING | |||
| ) { | |||
| val statusValue = existingDeliveryOrderForSync.status?.value ?: "unknown" | |||
| logger.info( | |||
| "${doRefType}: Skipping M18 sync — local DO id=${existingDeliveryOrderForSync.id} " + | |||
| "code=${existingDeliveryOrderForSync.code} status=$statusValue " + | |||
| "(only pending may be overwritten; receiving=已放單). M18 ID: ${deliveryOrder.id}" | |||
| ) | |||
| skippedNotPendingList.add( | |||
| "code=${existingDeliveryOrderForSync.code} status=$statusValue" | |||
| ) | |||
| return@forEach | |||
| } | |||
| //logger.info(latestDeliveryOrderLog.toString()) | |||
| // Save to m18_data_log table | |||
| // logger.info("${doRefType}: Saving for M18 Data Log...") | |||
| @@ -312,10 +340,9 @@ open class M18DeliveryOrderService( | |||
| // logger.info("${doRefType}: Saved M18 Data Log. ID: ${saveM18DeliveryOrderLog.id}") | |||
| try { | |||
| // Find the delivery_order if exist | |||
| // Find the delivery_order if exist (re-use lookup from pending guard above) | |||
| // logger.info("${doRefType}: Finding exising delivery order...") | |||
| val existingDeliveryOrder = | |||
| latestDeliveryOrderLog?.id?.let { deliveryOrderService.findByM18DataLogId(it) } | |||
| val existingDeliveryOrder = existingDeliveryOrderForSync | |||
| // logger.info("${doRefType}: Exising delivery order ID: ${existingDeliveryOrder?.id}") | |||
| // Save to delivery_order table | |||
| @@ -621,12 +648,17 @@ open class M18DeliveryOrderService( | |||
| logger.info("--------------------------------------------End - Saving M18 Delivery Order--------------------------------------------") | |||
| val skippedSuffix = if (skippedList.isNotEmpty()) " | skipped=${skippedList.size}" else "" | |||
| val skippedExistingSuffix = if (skippedList.isNotEmpty()) " | skipped=${skippedList.size}" else "" | |||
| val skippedNotPendingSuffix = if (skippedNotPendingList.isNotEmpty()) { | |||
| " | skipped (not pending, only pending may be overwritten): ${skippedNotPendingList.joinToString("; ")}" | |||
| } else { | |||
| "" | |||
| } | |||
| return SyncResult( | |||
| totalProcessed = successList.size + failList.size + skippedList.size, | |||
| totalProcessed = successList.size + failList.size + skippedList.size + skippedNotPendingList.size, | |||
| totalSuccess = successList.size, | |||
| totalFail = failList.size, | |||
| query = (deliveryOrdersWithType?.query ?: "") + skippedSuffix, | |||
| query = (deliveryOrdersWithType?.query ?: "") + skippedExistingSuffix + skippedNotPendingSuffix, | |||
| ) | |||
| } | |||
| } | |||
| @@ -97,15 +97,8 @@ open class M18PurchaseOrderService( | |||
| "" | |||
| } | |||
| // Material PO | |||
| val materialPoBuyers = | |||
| commonUtils.listToString(listOf(m18Config.BEID_PP, m18Config.BEID_PF), "beId=equal=", "=or=") | |||
| val materialPoSupplierNot = commonUtils.listToString( | |||
| shopService.findM18VendorIdsByCodeRegexp(m18Config.MATERIAL_PO_SUPPLIER_NOT), | |||
| "venId=unequal=", | |||
| "=or=" | |||
| ) | |||
| var materialPoConds = "(${materialPoBuyers})=and=(${materialPoSupplierNot}))" | |||
| // Material PO — same BE/supplier scope as [buildMaterialPoScopeConds] | |||
| var materialPoConds = buildMaterialPoScopeConds() | |||
| if (request.modifiedDateFrom != null && request.modifiedDateTo != null) { | |||
| materialPoConds += "=and=(${lastDateConds})" | |||
| @@ -233,11 +226,28 @@ open class M18PurchaseOrderService( | |||
| return if (beId?.toString() == m18Config.BEID_TOA) PurchaseOrderType.SHOP else PurchaseOrderType.MATERIAL | |||
| } | |||
| /** | |||
| * Material PO BE/supplier scope used by scheduler and by-code sync: | |||
| * beId in (PP, PF) and venId not in [M18Config.MATERIAL_PO_SUPPLIER_NOT] (e.g. P06/P07 shop suppliers). | |||
| */ | |||
| private fun buildMaterialPoScopeConds(): String { | |||
| val materialPoBuyers = | |||
| commonUtils.listToString(listOf(m18Config.BEID_PP, m18Config.BEID_PF), "beId=equal=", "=or=") | |||
| val materialPoSupplierNot = commonUtils.listToString( | |||
| shopService.findM18VendorIdsByCodeRegexp(m18Config.MATERIAL_PO_SUPPLIER_NOT), | |||
| "venId=unequal=", | |||
| "=or=" | |||
| ) | |||
| return "(${materialPoBuyers})=and=(${materialPoSupplierNot})" | |||
| } | |||
| open fun savePurchaseOrderByCode(code: String): SyncResult { | |||
| // Follow the scheduler's original approach: | |||
| // 1) use /search/search to find the PO id by code | |||
| // 2) then sync by PO id (this reuses the same create/update logic) | |||
| val conds = "(code=equal=$code)" | |||
| // Same approach as scheduler list sync, restricted to one document code: | |||
| // 1) /search/search with material-PO BE/supplier scope + code | |||
| // 2) sync by PO id (reuses create/update logic) | |||
| // Rejects shop POs (TOA / P06|P07) that belong to DO sync instead. | |||
| val trimmed = code.trim() | |||
| val conds = "(code=equal=$trimmed)=and=${buildMaterialPoScopeConds()}" | |||
| return saveFromPoListSearch(conds, searchPurchaseOrdersByConds(conds)) | |||
| } | |||
| @@ -357,6 +367,7 @@ open class M18PurchaseOrderService( | |||
| val successDetailList = mutableListOf<Long>() | |||
| val failList = mutableListOf<Long>() | |||
| val failDetailList = mutableListOf<Long>() | |||
| val skippedNotPendingList = mutableListOf<String>() | |||
| val affectedItemIds = mutableSetOf<Long>() | |||
| val uomByM18IdCache = mutableMapOf<Long, com.ffii.fpsms.modules.master.entity.ItemUom?>() | |||
| @@ -398,11 +409,15 @@ open class M18PurchaseOrderService( | |||
| if (existingPurchaseOrderForSync != null && | |||
| existingPurchaseOrderForSync.status != PurchaseOrderStatus.PENDING | |||
| ) { | |||
| val statusValue = existingPurchaseOrderForSync.status?.value ?: "unknown" | |||
| logger.info( | |||
| "${poRefType}: Skipping M18 sync — local PO id=${existingPurchaseOrderForSync.id} " + | |||
| "code=${existingPurchaseOrderForSync.code} status=${existingPurchaseOrderForSync.status?.value} " + | |||
| "code=${existingPurchaseOrderForSync.code} status=$statusValue " + | |||
| "(only pending may be overwritten). M18 ID: $m18PurchaseOrderId" | |||
| ) | |||
| skippedNotPendingList.add( | |||
| "code=${existingPurchaseOrderForSync.code} status=$statusValue" | |||
| ) | |||
| return@forEach | |||
| } | |||
| @@ -729,11 +744,18 @@ open class M18PurchaseOrderService( | |||
| logger.info("--------------------------------------------End - Saving M18 Purchase Order--------------------------------------------") | |||
| val baseQuery = purchaseOrdersWithType?.query ?: "" | |||
| val skippedSuffix = if (skippedNotPendingList.isNotEmpty()) { | |||
| " | skipped (not pending, only pending may be overwritten): ${skippedNotPendingList.joinToString("; ")}" | |||
| } else { | |||
| "" | |||
| } | |||
| return SyncResult( | |||
| totalProcessed = successList.size + failList.size, | |||
| totalProcessed = successList.size + failList.size + skippedNotPendingList.size, | |||
| totalSuccess = successList.size, | |||
| totalFail = failList.size, | |||
| query = purchaseOrdersWithType?.query ?: "" | |||
| query = baseQuery + skippedSuffix | |||
| ) | |||
| } | |||
| } | |||
| @@ -135,4 +135,7 @@ public abstract class SettingNames { | |||
| /** JSON: last laser TCP send where printer returned receive (job order no., lot, itemId/stockInLineId, etc.) */ | |||
| public static final String LASER_PRINT_LAST_RECEIVE_SUCCESS = "LASER_PRINT.lastReceiveSuccess"; | |||
| /** PO / stock-in item label: 1 = original, 2 = enlarged, 3 = smaller QR with a larger item name. */ | |||
| public static final String LABEL_PO_ITEM_VERSION = "LABEL.poItem.version"; | |||
| } | |||
| @@ -211,8 +211,19 @@ open fun getPoSummariesByIds(ids: List<Long>): List<PurchaseOrderSummary> { | |||
| sql.append(" group by po.id"); | |||
| sql.append(" order by po.orderDate desc") | |||
| sql.append(" ) r") | |||
| if (args.containsKey("itemDetail")){ | |||
| sql.append(" where r.itemDetail like :itemDetail "); | |||
| val outerFilters = mutableListOf<String>() | |||
| if (args.containsKey("itemDetail")) { | |||
| outerFilters.add("r.itemDetail like :itemDetail") | |||
| } | |||
| if (args.containsKey("itemCode")) { | |||
| outerFilters.add("r.itemCode like :itemCode") | |||
| } | |||
| if (args.containsKey("itemName")) { | |||
| outerFilters.add("r.itemName like :itemName") | |||
| } | |||
| if (outerFilters.isNotEmpty()) { | |||
| sql.append(" where ") | |||
| sql.append(outerFilters.joinToString(" AND ")) | |||
| } | |||
| return sql.toString() | |||
| } | |||
| @@ -47,6 +47,8 @@ class PurchaseOrderController( | |||
| val criteriaArgs = CriteriaArgsBuilder.withRequest(request) | |||
| .addStringLike("supplier") | |||
| .addStringLike("code") | |||
| .addStringLike("itemCode") | |||
| .addStringLike("itemName") | |||
| .addString("status") | |||
| .addBoolean("escalated") | |||
| .addDate("orderDate") | |||
| @@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.report.service | |||
| import org.springframework.stereotype.Service | |||
| import com.ffii.core.support.JdbcDao | |||
| import java.time.LocalDate | |||
| @Service | |||
| class SemiFGProductionAnalysisReportService( | |||
| @@ -199,6 +200,122 @@ class SemiFGProductionAnalysisReportService( | |||
| return jdbcDao.queryForList(sql, args) | |||
| } | |||
| /** | |||
| * Same filters as [searchSemiFGProductionAnalysisReport], one row per item and production date. | |||
| * Quantity is not pivoted; the Excel builder places each date in the requested range as its own column. | |||
| */ | |||
| fun searchSemiFGProductionAnalysisDaily( | |||
| stockCategory: String?, | |||
| stockSubCategory: String?, | |||
| itemCode: String?, | |||
| year: String?, | |||
| rangeStart: LocalDate, | |||
| rangeEnd: LocalDate, | |||
| ): List<Map<String, Any>> { | |||
| val args = mutableMapOf<String, Any>() | |||
| val stockCategorySql = if (!itemCode.isNullOrBlank()) { | |||
| "" | |||
| } else if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) { | |||
| buildMultiValueExactClause(stockCategory, "it.type", "semiSc", args) | |||
| } else { | |||
| "" | |||
| } | |||
| val stockSubCategorySql = buildMultiValueLikeClause(stockSubCategory, "ic.sub", "stockSubCategory", args) | |||
| val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "semiItem", args) | |||
| val yearSql = if (!year.isNullOrBlank() && year != "All") { | |||
| args["year"] = year | |||
| "AND YEAR(CASE WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate ELSE si.productionDate END) = :year" | |||
| } else { | |||
| "" | |||
| } | |||
| args["lastOutDateStart"] = rangeStart.toString() | |||
| args["lastOutDateEnd"] = rangeEnd.toString() | |||
| val lastOutDateStartSql = | |||
| "AND DATE(CASE WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate ELSE si.productionDate END) >= DATE(:lastOutDateStart)" | |||
| val lastOutDateEndSql = | |||
| "AND DATE(CASE WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate ELSE si.productionDate END) <= DATE(:lastOutDateEnd)" | |||
| val sql = """ | |||
| WITH qr_agg AS ( | |||
| SELECT | |||
| qr.stockInLineId, | |||
| MAX(CASE WHEN qr.qcPassed = 0 THEN 1 ELSE 0 END) AS qcFailed | |||
| FROM qc_result qr | |||
| WHERE qr.deleted = 0 | |||
| GROUP BY qr.stockInLineId | |||
| ), | |||
| base AS ( | |||
| SELECT | |||
| COALESCE(it.code, '') AS itemNo, | |||
| COALESCE(it.name, '') AS itemName, | |||
| COALESCE(ic.sub, '') AS stockSubCategory, | |||
| COALESCE(uc.udfudesc, '') AS unitOfMeasure, | |||
| DATE( | |||
| CASE | |||
| WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate | |||
| ELSE si.productionDate | |||
| END | |||
| ) AS prodDate, | |||
| si.id AS stockInLineId, | |||
| CASE WHEN COALESCE(qr_agg.qcFailed, 0) = 1 THEN 0 | |||
| ELSE COALESCE(si.acceptedQty, 0) | |||
| END AS linePutAwayQty | |||
| FROM stock_in_line si | |||
| INNER JOIN items it ON si.itemId = it.id | |||
| INNER JOIN bom b ON b.code = it.code AND b.deleted = false | |||
| LEFT JOIN inventory_lot il ON il.id = si.inventoryLotId AND il.deleted = false | |||
| LEFT JOIN qr_agg ON qr_agg.stockInLineId = si.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 si.deleted = false | |||
| AND si.productLotNo IS NOT NULL | |||
| AND TRIM(si.productLotNo) <> '' | |||
| AND ( | |||
| CASE | |||
| WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate | |||
| ELSE si.productionDate | |||
| END | |||
| ) IS NOT NULL | |||
| AND si.jobOrderId IS NOT NULL | |||
| AND (si.status IS NULL OR si.status <> 'Pending') | |||
| $stockCategorySql | |||
| $stockSubCategorySql | |||
| $itemCodeSql | |||
| $yearSql | |||
| $lastOutDateStartSql | |||
| $lastOutDateEndSql | |||
| ), | |||
| dedup AS ( | |||
| SELECT | |||
| itemNo, | |||
| itemName, | |||
| stockSubCategory, | |||
| unitOfMeasure, | |||
| prodDate, | |||
| stockInLineId, | |||
| MAX(linePutAwayQty) AS linePutAwayQty | |||
| FROM base | |||
| GROUP BY itemNo, itemName, stockSubCategory, unitOfMeasure, prodDate, stockInLineId | |||
| ) | |||
| SELECT | |||
| d.itemNo AS itemNo, | |||
| MAX(d.itemName) AS itemName, | |||
| MAX(d.unitOfMeasure) AS unitOfMeasure, | |||
| d.prodDate AS prodDate, | |||
| CAST(COALESCE(SUM(d.linePutAwayQty), 0) AS DECIMAL(18,2)) AS qty | |||
| FROM dedup d | |||
| GROUP BY d.itemNo, d.prodDate | |||
| HAVING COALESCE(SUM(d.linePutAwayQty), 0) > 0 | |||
| ORDER BY d.itemNo, d.prodDate | |||
| """.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"). | |||
| @@ -7,6 +7,7 @@ import java.io.ByteArrayOutputStream | |||
| import java.time.LocalDate | |||
| import java.time.LocalTime | |||
| import java.time.format.DateTimeFormatter | |||
| import java.time.temporal.ChronoUnit | |||
| import com.ffii.fpsms.modules.report.service.SemiFGProductionAnalysisReportService | |||
| import com.ffii.fpsms.modules.report.service.ReportService | |||
| import org.apache.poi.ss.usermodel.BorderStyle | |||
| @@ -173,6 +174,243 @@ class SemiFGProductionAnalysisReportController( | |||
| return ResponseEntity(excelBytes, headers, HttpStatus.OK) | |||
| } | |||
| @PostMapping("/print-semi-fg-production-analysis-excel-daily") | |||
| fun exportSemiFGProductionAnalysisReportExcelDaily( | |||
| @RequestBody(required = false) req: ReportItemFilterRequest?, | |||
| ): ResponseEntity<ByteArray> { | |||
| val r = req ?: ReportItemFilterRequest() | |||
| val start = parseReportDate(r.lastOutDateStart) | |||
| val end = parseReportDate(r.lastOutDateEnd) | |||
| if (start == null || end == null) { | |||
| return jsonError(HttpStatus.BAD_REQUEST, "請先輸入完成生產日期(由、至)") | |||
| } | |||
| if (end.isBefore(start)) { | |||
| return jsonError(HttpStatus.BAD_REQUEST, "完成生產日期「由」不可晚於「至」") | |||
| } | |||
| val dayCount = ChronoUnit.DAYS.between(start, end) + 1 | |||
| if (dayCount > MAX_DAILY_COLUMNS) { | |||
| return jsonError(HttpStatus.BAD_REQUEST, "日期範圍不可超過 $MAX_DAILY_COLUMNS 日") | |||
| } | |||
| val dbData = semiFGProductionAnalysisReportService.searchSemiFGProductionAnalysisDaily( | |||
| r.stockCategory, | |||
| r.stockSubCategory, | |||
| r.combinedItemCode(), | |||
| r.year, | |||
| start, | |||
| end, | |||
| ) | |||
| val dates = generateSequence(start) { previous -> | |||
| val next = previous.plusDays(1) | |||
| if (next.isAfter(end)) null else next | |||
| }.toList() | |||
| val excelBytes = createSemiFGProductionAnalysisDailyExcel( | |||
| dbData = dbData, | |||
| reportTitle = "成品/半成品生產分析報告", | |||
| dates = dates, | |||
| ) | |||
| val headers = HttpHeaders().apply { | |||
| contentType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") | |||
| setContentDispositionFormData("attachment", "SemiFGProductionAnalysisReport_Daily.xlsx") | |||
| set("filename", "SemiFGProductionAnalysisReport_Daily.xlsx") | |||
| } | |||
| return ResponseEntity(excelBytes, headers, HttpStatus.OK) | |||
| } | |||
| private fun createSemiFGProductionAnalysisDailyExcel( | |||
| dbData: List<Map<String, Any>>, | |||
| reportTitle: String, | |||
| dates: List<LocalDate>, | |||
| ): ByteArray { | |||
| val workbook = XSSFWorkbook() | |||
| val safeSheetName = WorkbookUtil.createSafeSheetName(reportTitle) | |||
| val sheet = workbook.createSheet(safeSheetName) | |||
| val dateFormat = DateTimeFormatter.ISO_LOCAL_DATE | |||
| val columnDateFormat = DateTimeFormatter.ofPattern( | |||
| if (dates.first().year == dates.last().year) "d/M" else "d/M/yy", | |||
| ) | |||
| val fixedColumns = 3 | |||
| val totalColumns = fixedColumns + dates.size + 1 | |||
| var rowIndex = 0 | |||
| val titleStyle = workbook.createCellStyle().apply { | |||
| alignment = HorizontalAlignment.CENTER | |||
| verticalAlignment = VerticalAlignment.CENTER | |||
| } | |||
| val titleFont = workbook.createFont().apply { | |||
| bold = true | |||
| fontHeightInPoints = 16 | |||
| } | |||
| titleStyle.setFont(titleFont) | |||
| val infoStyle = workbook.createCellStyle().apply { | |||
| alignment = HorizontalAlignment.LEFT | |||
| verticalAlignment = VerticalAlignment.CENTER | |||
| } | |||
| val headerStyle = workbook.createCellStyle().apply { | |||
| alignment = HorizontalAlignment.CENTER | |||
| verticalAlignment = VerticalAlignment.CENTER | |||
| fillForegroundColor = IndexedColors.GREY_25_PERCENT.index | |||
| fillPattern = FillPatternType.SOLID_FOREGROUND | |||
| borderTop = BorderStyle.THIN | |||
| borderBottom = BorderStyle.THIN | |||
| borderLeft = BorderStyle.THIN | |||
| borderRight = BorderStyle.THIN | |||
| wrapText = true | |||
| } | |||
| val headerFont = workbook.createFont().apply { bold = true } | |||
| headerStyle.setFont(headerFont) | |||
| val textStyle = workbook.createCellStyle().apply { | |||
| alignment = HorizontalAlignment.LEFT | |||
| verticalAlignment = VerticalAlignment.CENTER | |||
| borderTop = BorderStyle.THIN | |||
| borderBottom = BorderStyle.THIN | |||
| borderLeft = BorderStyle.THIN | |||
| borderRight = BorderStyle.THIN | |||
| } | |||
| val numberStyle = workbook.createCellStyle().apply { | |||
| alignment = HorizontalAlignment.RIGHT | |||
| verticalAlignment = VerticalAlignment.CENTER | |||
| borderTop = BorderStyle.THIN | |||
| borderBottom = BorderStyle.THIN | |||
| borderLeft = BorderStyle.THIN | |||
| borderRight = BorderStyle.THIN | |||
| dataFormat = workbook.creationHelper.createDataFormat().getFormat("#,##0") | |||
| } | |||
| val dashStyle = workbook.createCellStyle().apply { | |||
| alignment = HorizontalAlignment.RIGHT | |||
| verticalAlignment = VerticalAlignment.CENTER | |||
| borderTop = BorderStyle.THIN | |||
| borderBottom = BorderStyle.THIN | |||
| borderLeft = BorderStyle.THIN | |||
| borderRight = BorderStyle.THIN | |||
| } | |||
| val titleRow = sheet.createRow(rowIndex++) | |||
| titleRow.heightInPoints = 28f | |||
| val titleCell = titleRow.createCell(0) | |||
| titleCell.setCellValue(reportTitle) | |||
| titleCell.cellStyle = titleStyle | |||
| if (totalColumns > 1) { | |||
| sheet.addMergedRegion(CellRangeAddress(0, 0, 0, totalColumns - 1)) | |||
| } | |||
| val rangeLabel = "${dates.first().format(dateFormat)} 至 ${dates.last().format(dateFormat)}" | |||
| val infoRow = sheet.createRow(rowIndex++) | |||
| val infoCell = infoRow.createCell(0) | |||
| infoCell.setCellValue( | |||
| "報告日期:${LocalDate.now().format(dateFormat)} " + | |||
| "報告時間:${LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))} " + | |||
| "完成生產日期:$rangeLabel" | |||
| ) | |||
| infoCell.cellStyle = infoStyle | |||
| if (totalColumns > 1) { | |||
| sheet.addMergedRegion(CellRangeAddress(1, 1, 0, totalColumns - 1)) | |||
| } | |||
| rowIndex++ | |||
| val headerRowIndex = rowIndex | |||
| val headerRow = sheet.createRow(rowIndex++) | |||
| headerRow.heightInPoints = 32f | |||
| val headers = buildList { | |||
| add("貨品編號") | |||
| add("貨品名稱") | |||
| add("單位") | |||
| dates.forEach { add(it.format(columnDateFormat)) } | |||
| add("上架總計") | |||
| } | |||
| headers.forEachIndexed { index, name -> | |||
| val cell = headerRow.createCell(index) | |||
| cell.setCellValue(name) | |||
| cell.cellStyle = headerStyle | |||
| } | |||
| val items = pivotDailyRows(dbData) | |||
| if (items.isEmpty()) { | |||
| val dataRow = sheet.createRow(rowIndex++) | |||
| setTextCell(dataRow, 0, "-", textStyle) | |||
| setTextCell(dataRow, 1, "-", textStyle) | |||
| setTextCell(dataRow, 2, "-", textStyle) | |||
| for (col in fixedColumns until totalColumns) { | |||
| setDashCell(dataRow, col, dashStyle) | |||
| } | |||
| } else { | |||
| items.forEach { item -> | |||
| val dataRow = sheet.createRow(rowIndex++) | |||
| setTextCell(dataRow, 0, item.itemNo.ifBlank { "-" }, textStyle) | |||
| setTextCell(dataRow, 1, item.itemName.ifBlank { "-" }, textStyle) | |||
| setTextCell(dataRow, 2, item.unitOfMeasure.ifBlank { "-" }, textStyle) | |||
| var total = 0.0 | |||
| dates.forEachIndexed { index, date -> | |||
| val qty = item.qtyByDate[date] ?: 0.0 | |||
| total += qty | |||
| setNumberCell(dataRow, fixedColumns + index, qty, numberStyle, dashStyle) | |||
| } | |||
| setNumberCell(dataRow, totalColumns - 1, total, numberStyle, dashStyle) | |||
| } | |||
| } | |||
| sheet.createFreezePane(fixedColumns, headerRowIndex + 1) | |||
| sheet.setColumnWidth(0, 18 * 256) | |||
| sheet.setColumnWidth(1, 24 * 256) | |||
| sheet.setColumnWidth(2, 12 * 256) | |||
| for (col in fixedColumns until totalColumns) { | |||
| sheet.setColumnWidth(col, 12 * 256) | |||
| } | |||
| val output = ByteArrayOutputStream() | |||
| workbook.use { it.write(output) } | |||
| return output.toByteArray() | |||
| } | |||
| private fun pivotDailyRows(dbData: List<Map<String, Any>>): List<DailyItemQty> { | |||
| val byItem = linkedMapOf<String, DailyItemQty>() | |||
| dbData.forEach { row -> | |||
| val itemNo = row["itemNo"]?.toString()?.trim().orEmpty() | |||
| val prodDate = toLocalDate(row["prodDate"]) ?: return@forEach | |||
| val qty = parseNumericForExcelChart(row["qty"]) | |||
| val item = byItem.getOrPut(itemNo) { | |||
| DailyItemQty( | |||
| itemNo = itemNo, | |||
| itemName = row["itemName"]?.toString().orEmpty(), | |||
| unitOfMeasure = row["unitOfMeasure"]?.toString().orEmpty(), | |||
| qtyByDate = linkedMapOf(), | |||
| ) | |||
| } | |||
| item.qtyByDate[prodDate] = (item.qtyByDate[prodDate] ?: 0.0) + qty | |||
| } | |||
| return byItem.values.toList() | |||
| } | |||
| private fun parseReportDate(raw: String?): LocalDate? { | |||
| if (raw.isNullOrBlank()) return null | |||
| val normalized = raw.trim().replace("/", "-") | |||
| return runCatching { LocalDate.parse(normalized) }.getOrNull() | |||
| ?: runCatching { LocalDate.parse(normalized, DateTimeFormatter.ofPattern("dd-MM-yyyy")) }.getOrNull() | |||
| } | |||
| private fun toLocalDate(value: Any?): LocalDate? = when (value) { | |||
| is LocalDate -> value | |||
| is java.sql.Date -> value.toLocalDate() | |||
| is java.sql.Timestamp -> value.toLocalDateTime().toLocalDate() | |||
| is java.time.LocalDateTime -> value.toLocalDate() | |||
| is java.util.Date -> value.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate() | |||
| is String -> parseReportDate(value.take(10)) | |||
| else -> null | |||
| } | |||
| private fun jsonError(status: HttpStatus, message: String): ResponseEntity<ByteArray> { | |||
| val escaped = message.replace("\\", "\\\\").replace("\"", "\\\"") | |||
| return ResponseEntity.status(status) | |||
| .contentType(MediaType.APPLICATION_JSON) | |||
| .body("""{"message":"$escaped"}""".toByteArray(Charsets.UTF_8)) | |||
| } | |||
| private fun createSemiFGProductionAnalysisExcel( | |||
| dbData: List<Map<String, Any>>, | |||
| reportTitle: String, | |||
| @@ -517,4 +755,15 @@ class SemiFGProductionAnalysisReportController( | |||
| val itemCodesWithCategory = semiFGProductionAnalysisReportService.getSemiFGItemCodesWithCategory(stockCategory) | |||
| return ResponseEntity(itemCodesWithCategory, HttpStatus.OK) | |||
| } | |||
| private data class DailyItemQty( | |||
| val itemNo: String, | |||
| val itemName: String, | |||
| val unitOfMeasure: String, | |||
| val qtyByDate: MutableMap<LocalDate, Double>, | |||
| ) | |||
| companion object { | |||
| private const val MAX_DAILY_COLUMNS = 366 | |||
| } | |||
| } | |||
| @@ -2,7 +2,6 @@ package com.ffii.fpsms.modules.stock.service | |||
| import com.ffii.core.response.RecordsRes | |||
| import com.ffii.core.utils.PdfUtils | |||
| import com.ffii.core.utils.QrCodeUtil | |||
| import com.ffii.fpsms.modules.master.entity.ItemUomRespository | |||
| import com.ffii.fpsms.modules.master.entity.WarehouseRepository | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | |||
| @@ -44,6 +43,7 @@ import com.ffii.fpsms.modules.stock.web.model.WorkbenchItemLotsResponse | |||
| import com.ffii.fpsms.modules.jobOrder.service.JobOrderService | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.ExportFGStockInLabelRequest | |||
| import com.ffii.fpsms.modules.master.service.PrinterService | |||
| import com.ffii.fpsms.modules.settings.service.SettingsService | |||
| import com.ffii.fpsms.modules.stock.web.model.PrintLabelForInventoryLotLineRequest | |||
| import com.ffii.fpsms.modules.jobOrder.web.model.PrintFGStockInLabelRequest | |||
| import com.ffii.core.utils.ZebraPrinterUtil | |||
| @@ -60,6 +60,7 @@ open class InventoryLotLineService( | |||
| private val stockInLineRepository: StockInLineRepository, | |||
| private val inventoryRepository: InventoryRepository, | |||
| private val printerService: PrinterService, | |||
| private val settingsService: SettingsService, | |||
| @Lazy | |||
| private val jobOrderService: JobOrderService | |||
| ) { | |||
| @@ -247,7 +248,8 @@ open class InventoryLotLineService( | |||
| @Throws(IOException::class) | |||
| @Transactional | |||
| open fun exportStockInLineQrcode(request: LotLineToQrcode): Map<String, Any> { | |||
| val QRCODE_PDF = "qrCodeLabel/poItemPDF.jrxml" | |||
| val labelVersion = PoItemLabelTemplate.version(settingsService) | |||
| val QRCODE_PDF = PoItemLabelTemplate.path(labelVersion) | |||
| val resource = ClassPathResource(QRCODE_PDF) | |||
| if (!resource.exists()) { | |||
| println("here") | |||
| @@ -287,13 +289,18 @@ open class InventoryLotLineService( | |||
| field["acceptedQty"] = "%.2f".format(info.acceptedQty) | |||
| val stockItemUom = itemUomRespository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse(info.itemId) | |||
| field["uom"] = stockItemUom?.uom?.udfudesc ?: info.uom?.udfudesc?.toString() ?: "N/A" | |||
| val fullUom = stockItemUom?.uom?.udfudesc ?: info.uom?.udfudesc?.toString() ?: "N/A" | |||
| val shortUom = stockItemUom?.uom?.udfShortDesc ?: info.uom?.udfShortDesc | |||
| field["uom"] = fullUom | |||
| val (qtyLabel, qtyDetail) = PoItemLabelTemplate.qtyLines(field["acceptedQty"] as String, fullUom, shortUom) | |||
| field["qtyLabel"] = qtyLabel | |||
| field["qtyDetail"] = qtyDetail | |||
| field["productionDate"] = info.productionDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||
| field["expiryDate"] = info.expiryDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||
| field["lotNo"] = info.lotNo!! | |||
| field["supplier"] = info.supplier ?: "N/A" | |||
| val image = QrCodeUtil.generateQRCodeImage(qrCodeContent) | |||
| val image = PoItemLabelTemplate.qrImage(qrCodeContent, labelVersion) | |||
| field["qrCode"] = image | |||
| fields.add(field) | |||
| } | |||
| @@ -0,0 +1,63 @@ | |||
| package com.ffii.fpsms.modules.stock.service | |||
| import com.ffii.core.utils.QrCodeUtil | |||
| import com.ffii.fpsms.modules.common.SettingNames | |||
| import com.ffii.fpsms.modules.settings.service.SettingsService | |||
| import java.awt.image.BufferedImage | |||
| object PoItemLabelTemplate { | |||
| const val V1 = "qrCodeLabel/poItemPDF.jrxml" | |||
| const val V2 = "qrCodeLabel/poItemPDF_v2.jrxml" | |||
| const val V3 = "qrCodeLabel/poItemPDF_v3.jrxml" | |||
| fun version(settingsService: SettingsService): Int { | |||
| val raw = settingsService.findByName(SettingNames.LABEL_PO_ITEM_VERSION) | |||
| .map { it.value } | |||
| .orElse("1") | |||
| ?.trim() | |||
| return when (raw) { | |||
| "2" -> 2 | |||
| "3" -> 3 | |||
| else -> 1 | |||
| } | |||
| } | |||
| fun path(version: Int): String = when (version) { | |||
| 2 -> V2 | |||
| 3 -> V3 | |||
| else -> V1 | |||
| } | |||
| fun qrImage(content: String, version: Int): BufferedImage = | |||
| if (version == 1) QrCodeUtil.generateQRCodeImage(content) | |||
| else QrCodeUtil.generateQRCodeImage(content, 500, 1) | |||
| /** First line is qty + short unit. Second line is the full unit only when it differs. */ | |||
| fun qtyLines(qtyRaw: String?, fullRaw: String?, shortRaw: String?): Pair<String, String> { | |||
| val qty = stripTrailingZeros(qtyRaw) | |||
| val full = blankUnit(fullRaw) | |||
| val short = blankUnit(shortRaw) | |||
| val same = short.isEmpty() || full.isEmpty() || short.equals(full, ignoreCase = true) | |||
| if (same) { | |||
| val unit = short.ifEmpty { full } | |||
| val line = when { | |||
| unit.isEmpty() -> qty | |||
| unit.first().isDigit() -> listOf(qty, unit).filter { it.isNotEmpty() }.joinToString(" ") | |||
| else -> qty + unit | |||
| } | |||
| return line to "" | |||
| } | |||
| return (qty + short) to full | |||
| } | |||
| private fun blankUnit(raw: String?): String { | |||
| val text = raw?.trim().orEmpty() | |||
| return if (text.isEmpty() || text.equals("N/A", ignoreCase = true)) "" else text | |||
| } | |||
| private fun stripTrailingZeros(raw: String?): String { | |||
| val text = raw?.trim().orEmpty() | |||
| if (!text.contains('.')) return text | |||
| return text.replace(Regex("0+$"), "").removeSuffix(".") | |||
| } | |||
| } | |||
| @@ -3,7 +3,6 @@ package com.ffii.fpsms.modules.stock.service | |||
| import com.ffii.core.exception.BadRequestException | |||
| import com.ffii.core.support.AbstractBaseEntityService | |||
| import com.ffii.core.support.JdbcDao | |||
| import com.ffii.core.utils.QrCodeUtil | |||
| import com.ffii.fpsms.modules.common.CodeGenerator | |||
| import com.ffii.fpsms.modules.master.entity.ItemsRepository | |||
| import com.ffii.fpsms.modules.master.entity.QcItemRepository | |||
| @@ -33,6 +32,7 @@ import com.ffii.fpsms.modules.master.entity.WarehouseRepository | |||
| import com.ffii.fpsms.modules.master.service.ItemUomService | |||
| //import com.ffii.fpsms.modules.master.service.BomOutputQtyService | |||
| import com.ffii.fpsms.modules.master.service.PrinterService | |||
| import com.ffii.fpsms.modules.settings.service.SettingsService | |||
| import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder | |||
| import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLine | |||
| import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository | |||
| @@ -98,6 +98,7 @@ open class StockInLineService( | |||
| private val itemUomRepository: ItemUomRespository, | |||
| private val itemUomService: ItemUomService, | |||
| private val printerService: PrinterService, | |||
| private val settingsService: SettingsService, | |||
| private val stockTakeLineRepository: StockTakeLineRepository, | |||
| private val inventoryLotLineService: InventoryLotLineService, | |||
| private val deliveryOrderRepository: DeliveryOrderRepository, | |||
| @@ -1208,7 +1209,8 @@ open class StockInLineService( | |||
| @Throws(IOException::class) | |||
| @Transactional | |||
| open fun exportStockInLineQrcode(request: ExportQrCodeRequest): Map<String, Any> { | |||
| val QRCODE_PDF = "qrCodeLabel/poItemPDF.jrxml" | |||
| val labelVersion = PoItemLabelTemplate.version(settingsService) | |||
| val QRCODE_PDF = PoItemLabelTemplate.path(labelVersion) | |||
| val resource = ClassPathResource(QRCODE_PDF) | |||
| if (!resource.exists()) { | |||
| println("here") | |||
| @@ -1242,12 +1244,17 @@ open class StockInLineService( | |||
| field["poCode"] = info.poCode ?: "N/A" | |||
| field["itemType"] = info.itemType ?: "N/A" | |||
| field["acceptedQty"] = "%.2f".format(acceptedQty) | |||
| field["uom"] = stockItemUom?.uom?.udfudesc ?: info.uom?.udfudesc?.toString() ?: "N/A" | |||
| val fullUom = stockItemUom?.uom?.udfudesc ?: info.uom?.udfudesc?.toString() ?: "N/A" | |||
| val shortUom = stockItemUom?.uom?.udfShortDesc ?: info.uom?.udfShortDesc | |||
| field["uom"] = fullUom | |||
| val (qtyLabel, qtyDetail) = PoItemLabelTemplate.qtyLines(field["acceptedQty"] as String, fullUom, shortUom) | |||
| field["qtyLabel"] = qtyLabel | |||
| field["qtyDetail"] = qtyDetail | |||
| field["productionDate"] = info.productionDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||
| field["expiryDate"] = info.expiryDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | |||
| field["lotNo"] = info.lotNo ?: "N/A" | |||
| field["supplier"] = info.supplier ?: "N/A" | |||
| val image = QrCodeUtil.generateQRCodeImage(qrCodeContent) | |||
| val image = PoItemLabelTemplate.qrImage(qrCodeContent, labelVersion) | |||
| field["qrCode"] = image | |||
| fields.add(field) | |||
| } | |||
| @@ -3,9 +3,10 @@ fpsms: | |||
| monitoring: | |||
| enabled: true | |||
| # Session length in production; frontend can call /refresh-token before expiry to stay logged in. | |||
| # Session length in production; frontend refreshes via /refresh-token before expiry. | |||
| # Floor staff often work ~08:00–22:00 (~14h); 16h access covers one shift without mid-day kickout. | |||
| jwt: | |||
| expiration-minutes: 480 # 8 hours access token (was 30 min); increase if users need longer AFK | |||
| expiration-minutes: 960 # 16 hours access token (was 480 / 8h — users logged out ~16:30 after morning login) | |||
| refresh-expiration-days: 7 | |||
| # Required for stable JWT across restarts; set JWT_SECRET in the deployment environment (long random string). | |||
| secret: ${JWT_SECRET:fpsms-dev-jwt-signing-secret-change-for-production-use-long-random-JWT_SECRET} | |||
| @@ -113,8 +113,9 @@ spring: | |||
| dialect: | |||
| storage_engine: innodb | |||
| # JWT: access token expiry and refresh token expiry. Frontend should call /refresh-token before access token expires. | |||
| # JWT: access token expiry and refresh token expiry. Frontend calls /refresh-token before access token expires. | |||
| # Signing key must be STABLE across server restarts (do not use a random key per boot). Override with env JWT_SECRET in production. | |||
| # Prod overrides to 16h access (floor shifts ~08:00–22:00); see application-prod.yml. | |||
| jwt: | |||
| expiration-minutes: 14400 # access token: 10 days (default); override in application-prod for shorter session | |||
| refresh-expiration-days: 30 # refresh token validity (days) | |||
| @@ -0,0 +1,9 @@ | |||
| --liquibase formatted sql | |||
| --changeset fpsms:20261001_po_item_label_version | |||
| INSERT INTO `settings` (`name`, `value`, `category`, `type`) | |||
| SELECT 'LABEL.poItem.version', '1', 'LABEL', '1/2' | |||
| FROM DUAL | |||
| WHERE NOT EXISTS ( | |||
| SELECT 1 FROM `settings` WHERE `name` = 'LABEL.poItem.version' | |||
| ); | |||
| @@ -0,0 +1,7 @@ | |||
| --liquibase formatted sql | |||
| --changeset fpsms:20261001_po_item_label_version_allow_3 | |||
| UPDATE `settings` | |||
| SET `type` = '1/2/3' | |||
| WHERE `name` = 'LABEL.poItem.version' | |||
| AND `type` = '1/2'; | |||
| @@ -0,0 +1,172 @@ | |||
| <?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="poItemPDF" printOrder="Horizontal" pageWidth="425" pageHeight="283" orientation="Landscape" columnWidth="425" leftMargin="0" rightMargin="0" topMargin="0" bottomMargin="0" uuid="6cb85eb9-3573-42f3-a293-099db250aec0"> | |||
| <property name="com.jaspersoft.studio.unit." value="pixel"/> | |||
| <parameter name="lotNo" class="java.lang.String"/> | |||
| <parameter name="itemName" class="java.lang.String"/> | |||
| <parameter name="itemNo" class="java.lang.String"/> | |||
| <parameter name="poCode" class="java.lang.String"/> | |||
| <parameter name="qrCode" class="java.awt.Image"/> | |||
| <parameter name="acceptedQty" class="java.math.BigDecimal"/> | |||
| <parameter name="uom" class="java.lang.String"/> | |||
| <parameter name="isTransfer" class="java.lang.String"/> | |||
| <queryString> | |||
| <![CDATA[]]> | |||
| </queryString> | |||
| <field name="lotNo" class="java.lang.String"/> | |||
| <field name="itemName" class="java.lang.String"/> | |||
| <field name="itemNo" class="java.lang.String"/> | |||
| <field name="acceptedQty" class="java.lang.String"/> | |||
| <field name="uom" class="java.lang.String"/> | |||
| <field name="qtyLabel" class="java.lang.String"/> | |||
| <field name="qtyDetail" class="java.lang.String"/> | |||
| <field name="qrCode" class="java.awt.Image"/> | |||
| <field name="poCode" class="java.lang.String"/> | |||
| <field name="supplier" class="java.lang.String"/> | |||
| <field name="productionDate" class="java.lang.String"/> | |||
| <field name="expiryDate" class="java.lang.String"/> | |||
| <background> | |||
| <band splitType="Stretch"/> | |||
| </background> | |||
| <detail> | |||
| <band height="279" splitType="Prevent"> | |||
| <staticText> | |||
| <reportElement x="8" y="0" width="108" height="56" uuid="c62ac00a-c495-462c-93bc-97bdd3cdb395"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="18" isBold="true"/> | |||
| </textElement> | |||
| <text><![CDATA[收貨日期:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="116" y="0" width="301" height="56" uuid="74a5cfba-046d-42f4-92d7-113729514a6f"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="44" isBold="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{productionDate}]]></textFieldExpression> | |||
| </textField> | |||
| <staticText> | |||
| <reportElement x="8" y="56" width="108" height="56" uuid="32edcad1-23c1-4d41-8c9a-352d9e6ef377"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="18" isBold="true"/> | |||
| </textElement> | |||
| <text><![CDATA[到期日:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="116" y="56" width="301" height="56" uuid="97e4a9bb-fc84-4074-b523-ddf9eb9ed2d2"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="44" isBold="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{expiryDate}]]></textFieldExpression> | |||
| </textField> | |||
| <staticText> | |||
| <reportElement x="8" y="114" width="230" height="18" uuid="f5f145f1-123f-4fe5-90fa-8550b3d2eb63"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="10" isBold="true"/> | |||
| </textElement> | |||
| <text><![CDATA[批次編號:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="8" y="132" width="230" height="20" uuid="402f35fc-b11a-4bf1-982d-e12c622f38ea"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="18" isBold="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{lotNo}]]></textFieldExpression> | |||
| </textField> | |||
| <staticText> | |||
| <reportElement x="8" y="152" width="108" height="18" uuid="96d46fb1-939d-4f3e-b8f0-172b69b85e9f"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="10" isBold="true"/> | |||
| </textElement> | |||
| <text><![CDATA[物料編號:]]></text> | |||
| </staticText> | |||
| <staticText> | |||
| <reportElement x="120" y="152" width="118" height="18" uuid="53a2d7cf-bd50-4264-abee-346823cd49dd"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="10" isBold="true"/> | |||
| </textElement> | |||
| <text><![CDATA[數量:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="8" y="170" width="108" height="28" uuid="1f289946-b945-4f79-8515-212368b8be3b"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="24" isBold="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{itemNo}]]></textFieldExpression> | |||
| </textField> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="120" y="170" width="120" height="18" uuid="629d9af8-c73f-4614-ba9d-a582e2ed4a29"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="14" isBold="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{qtyLabel}]]></textFieldExpression> | |||
| </textField> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="120" y="188" width="120" height="12" uuid="7c1a0e44-6b2d-4a91-9f10-2d8e5b6c4a11"> | |||
| <printWhenExpression><![CDATA[$F{qtyDetail} != null && !$F{qtyDetail}.trim().isEmpty()]]></printWhenExpression> | |||
| </reportElement> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="8"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{qtyDetail}]]></textFieldExpression> | |||
| </textField> | |||
| <staticText> | |||
| <reportElement x="8" y="200" width="230" height="16" uuid="5c3f9b03-222c-4c94-ab58-6bc86d44c5ce"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="10" isBold="true"/> | |||
| </textElement> | |||
| <text><![CDATA[物料名稱:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="8" y="216" width="230" height="30" uuid="e04b094d-2d16-4060-b6d9-2e6033a673ab"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="32" isBold="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{itemName}]]></textFieldExpression> | |||
| </textField> | |||
| <image scaleImage="FillFrame" hAlign="Center" vAlign="Middle"> | |||
| <reportElement x="248" y="112" width="166" height="166" uuid="5dd35c73-973b-4f0d-9cad-13305a29a9fd"/> | |||
| <imageExpression><![CDATA[$F{qrCode}]]></imageExpression> | |||
| </image> | |||
| <staticText> | |||
| <reportElement x="8" y="248" width="42" height="14" uuid="c2041205-51f8-45bb-a1d1-e3de37eebed7"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="8"/> | |||
| </textElement> | |||
| <text><![CDATA[供應商:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="50" y="248" width="188" height="14" uuid="ab82ca39-8840-4471-b7c0-cdeec9cb5693"/> | |||
| <textElement verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="8"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{supplier}]]></textFieldExpression> | |||
| </textField> | |||
| <staticText> | |||
| <reportElement x="8" y="262" width="76" height="14" uuid="1b1efafd-049a-432e-93c4-9662273011f7"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="8"/> | |||
| </textElement> | |||
| <text><![CDATA[採購訂單編號:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="84" y="262" width="154" height="14" uuid="b805e20a-9a36-45c8-8255-23b6cc58f47c"/> | |||
| <textElement verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="8"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$P{poCode}]]></textFieldExpression> | |||
| </textField> | |||
| <textField> | |||
| <reportElement x="276" y="183" width="110" height="22" uuid="d66a64b0-3335-4846-bcb0-a67594520055"> | |||
| <printWhenExpression><![CDATA[$P{isTransfer} != null && !$P{isTransfer}.toString().trim().isEmpty()]]></printWhenExpression> | |||
| </reportElement> | |||
| <box> | |||
| <pen lineWidth="2.0"/> | |||
| </box> | |||
| <textElement textAlignment="Center" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="11" isBold="true" isUnderline="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$P{isTransfer}]]></textFieldExpression> | |||
| </textField> | |||
| </band> | |||
| </detail> | |||
| </jasperReport> | |||
| @@ -0,0 +1,172 @@ | |||
| <?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="poItemPDF_v3" printOrder="Horizontal" pageWidth="425" pageHeight="283" orientation="Landscape" columnWidth="425" leftMargin="0" rightMargin="0" topMargin="0" bottomMargin="0" uuid="6cb85eb9-3573-42f3-a293-099db250aec0"> | |||
| <property name="com.jaspersoft.studio.unit." value="pixel"/> | |||
| <parameter name="lotNo" class="java.lang.String"/> | |||
| <parameter name="itemName" class="java.lang.String"/> | |||
| <parameter name="itemNo" class="java.lang.String"/> | |||
| <parameter name="poCode" class="java.lang.String"/> | |||
| <parameter name="qrCode" class="java.awt.Image"/> | |||
| <parameter name="acceptedQty" class="java.math.BigDecimal"/> | |||
| <parameter name="uom" class="java.lang.String"/> | |||
| <parameter name="isTransfer" class="java.lang.String"/> | |||
| <queryString> | |||
| <![CDATA[]]> | |||
| </queryString> | |||
| <field name="lotNo" class="java.lang.String"/> | |||
| <field name="itemName" class="java.lang.String"/> | |||
| <field name="itemNo" class="java.lang.String"/> | |||
| <field name="acceptedQty" class="java.lang.String"/> | |||
| <field name="uom" class="java.lang.String"/> | |||
| <field name="qtyLabel" class="java.lang.String"/> | |||
| <field name="qtyDetail" class="java.lang.String"/> | |||
| <field name="qrCode" class="java.awt.Image"/> | |||
| <field name="poCode" class="java.lang.String"/> | |||
| <field name="supplier" class="java.lang.String"/> | |||
| <field name="productionDate" class="java.lang.String"/> | |||
| <field name="expiryDate" class="java.lang.String"/> | |||
| <background> | |||
| <band splitType="Stretch"/> | |||
| </background> | |||
| <detail> | |||
| <band height="279" splitType="Prevent"> | |||
| <staticText> | |||
| <reportElement x="8" y="0" width="108" height="56" uuid="c62ac00a-c495-462c-93bc-97bdd3cdb395"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="18" isBold="true"/> | |||
| </textElement> | |||
| <text><![CDATA[收貨日期:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="116" y="0" width="301" height="56" uuid="74a5cfba-046d-42f4-92d7-113729514a6f"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="44" isBold="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{productionDate}]]></textFieldExpression> | |||
| </textField> | |||
| <staticText> | |||
| <reportElement x="8" y="56" width="108" height="56" uuid="32edcad1-23c1-4d41-8c9a-352d9e6ef377"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="18" isBold="true"/> | |||
| </textElement> | |||
| <text><![CDATA[到期日:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="116" y="56" width="301" height="56" uuid="97e4a9bb-fc84-4074-b523-ddf9eb9ed2d2"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="44" isBold="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{expiryDate}]]></textFieldExpression> | |||
| </textField> | |||
| <staticText> | |||
| <reportElement x="8" y="114" width="286" height="16" uuid="f5f145f1-123f-4fe5-90fa-8550b3d2eb63"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="10" isBold="true"/> | |||
| </textElement> | |||
| <text><![CDATA[批次編號:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="8" y="130" width="286" height="20" uuid="402f35fc-b11a-4bf1-982d-e12c622f38ea"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="18" isBold="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{lotNo}]]></textFieldExpression> | |||
| </textField> | |||
| <staticText> | |||
| <reportElement x="8" y="152" width="140" height="16" uuid="96d46fb1-939d-4f3e-b8f0-172b69b85e9f"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="10" isBold="true"/> | |||
| </textElement> | |||
| <text><![CDATA[物料編號:]]></text> | |||
| </staticText> | |||
| <staticText> | |||
| <reportElement x="152" y="152" width="142" height="16" uuid="53a2d7cf-bd50-4264-abee-346823cd49dd"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="10" isBold="true"/> | |||
| </textElement> | |||
| <text><![CDATA[數量:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="8" y="168" width="140" height="26" uuid="1f289946-b945-4f79-8515-212368b8be3b"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="22" isBold="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{itemNo}]]></textFieldExpression> | |||
| </textField> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="152" y="168" width="142" height="16" uuid="629d9af8-c73f-4614-ba9d-a582e2ed4a29"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="14" isBold="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{qtyLabel}]]></textFieldExpression> | |||
| </textField> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="152" y="184" width="142" height="12" uuid="7c1a0e44-6b2d-4a91-9f10-2d8e5b6c4a11"> | |||
| <printWhenExpression><![CDATA[$F{qtyDetail} != null && !$F{qtyDetail}.trim().isEmpty()]]></printWhenExpression> | |||
| </reportElement> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="8"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{qtyDetail}]]></textFieldExpression> | |||
| </textField> | |||
| <staticText> | |||
| <reportElement x="8" y="200" width="290" height="18" uuid="5c3f9b03-222c-4c94-ab58-6bc86d44c5ce"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="10" isBold="true"/> | |||
| </textElement> | |||
| <text><![CDATA[物料名稱:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="8" y="218" width="290" height="58" uuid="e04b094d-2d16-4060-b6d9-2e6033a673ab"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="48" isBold="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{itemName}]]></textFieldExpression> | |||
| </textField> | |||
| <image scaleImage="FillFrame" hAlign="Center" vAlign="Middle"> | |||
| <reportElement x="308" y="116" width="108" height="108" uuid="5dd35c73-973b-4f0d-9cad-13305a29a9fd"/> | |||
| <imageExpression><![CDATA[$F{qrCode}]]></imageExpression> | |||
| </image> | |||
| <staticText> | |||
| <reportElement x="308" y="226" width="108" height="12" uuid="c2041205-51f8-45bb-a1d1-e3de37eebed7"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="8"/> | |||
| </textElement> | |||
| <text><![CDATA[供應商:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="308" y="238" width="108" height="14" uuid="ab82ca39-8840-4471-b7c0-cdeec9cb5693"/> | |||
| <textElement verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="8"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$F{supplier}]]></textFieldExpression> | |||
| </textField> | |||
| <staticText> | |||
| <reportElement x="308" y="252" width="108" height="12" uuid="1b1efafd-049a-432e-93c4-9662273011f7"/> | |||
| <textElement textAlignment="Left" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="8"/> | |||
| </textElement> | |||
| <text><![CDATA[採購訂單編號:]]></text> | |||
| </staticText> | |||
| <textField textAdjust="ScaleFont" isBlankWhenNull="true"> | |||
| <reportElement x="308" y="264" width="108" height="14" uuid="b805e20a-9a36-45c8-8255-23b6cc58f47c"/> | |||
| <textElement verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="8"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$P{poCode}]]></textFieldExpression> | |||
| </textField> | |||
| <textField> | |||
| <reportElement x="322" y="156" width="80" height="20" uuid="d66a64b0-3335-4846-bcb0-a67594520055"> | |||
| <printWhenExpression><![CDATA[$P{isTransfer} != null && !$P{isTransfer}.toString().trim().isEmpty()]]></printWhenExpression> | |||
| </reportElement> | |||
| <box> | |||
| <pen lineWidth="2.0"/> | |||
| </box> | |||
| <textElement textAlignment="Center" verticalAlignment="Middle"> | |||
| <font fontName="微軟正黑體" size="11" isBold="true" isUnderline="true"/> | |||
| </textElement> | |||
| <textFieldExpression><![CDATA[$P{isTransfer}]]></textFieldExpression> | |||
| </textField> | |||
| </band> | |||
| </detail> | |||
| </jasperReport> | |||