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 | package com.ffii.core.utils | ||||
| import com.google.zxing.BarcodeFormat | import com.google.zxing.BarcodeFormat | ||||
| import com.google.zxing.EncodeHintType | |||||
| import java.awt.image.BufferedImage | 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 { | open class QrCodeUtil { | ||||
| companion object { | companion object { | ||||
| fun generateQRCodeImage(barcodeText: String?): BufferedImage { | 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) { | if (request.modifiedDateFrom != null && request.modifiedDateTo != null) { | ||||
| shopPoConds += "=and=(${lastDateConds})" | 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 | * 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). | * @param isExtraSync when true, persist local `delivery_order.isExtra=true` (manual DO(加單) sync). | ||||
| * No M18-side "加單" filtering is used. | * No M18-side "加單" filtering is used. | ||||
| @@ -173,15 +182,16 @@ open class M18DeliveryOrderService( | |||||
| isExtraSync: Boolean = false, | isExtraSync: Boolean = false, | ||||
| newOnly: Boolean = false, | newOnly: Boolean = false, | ||||
| ): SyncResult { | ): SyncResult { | ||||
| if (newOnly && deliveryOrderRepository.existsByCodeAndDeletedIsFalse(code)) { | |||||
| val trimmed = code.trim() | |||||
| if (newOnly && deliveryOrderRepository.existsByCodeAndDeletedIsFalse(trimmed)) { | |||||
| return SyncResult( | return SyncResult( | ||||
| totalProcessed = 1, | totalProcessed = 1, | ||||
| totalSuccess = 0, | totalSuccess = 0, | ||||
| totalFail = 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( | val searchRequest = M18PurchaseOrderListRequest( | ||||
| stSearch = "po", | stSearch = "po", | ||||
| params = null, | params = null, | ||||
| @@ -228,6 +238,7 @@ open class M18DeliveryOrderService( | |||||
| val successList = mutableListOf<Long>() | val successList = mutableListOf<Long>() | ||||
| val skippedList = mutableListOf<Long>() | val skippedList = mutableListOf<Long>() | ||||
| val skippedNotPendingList = mutableListOf<String>() | |||||
| val successDetailList = mutableListOf<Long>() | val successDetailList = mutableListOf<Long>() | ||||
| val failList = mutableListOf<Long>() | val failList = mutableListOf<Long>() | ||||
| val failDetailList = mutableListOf<Long>() | val failDetailList = mutableListOf<Long>() | ||||
| @@ -291,6 +302,23 @@ open class M18DeliveryOrderService( | |||||
| // logger.info("${doRefType}: Finding For Latest M18 Data Log...") | // logger.info("${doRefType}: Finding For Latest M18 Data Log...") | ||||
| val latestDeliveryOrderLog = | val latestDeliveryOrderLog = | ||||
| m18DataLogService.findLatestM18DataLogWithSuccess(deliveryOrder.id, doRefType) | 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()) | //logger.info(latestDeliveryOrderLog.toString()) | ||||
| // Save to m18_data_log table | // Save to m18_data_log table | ||||
| // logger.info("${doRefType}: Saving for M18 Data Log...") | // 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}") | // logger.info("${doRefType}: Saved M18 Data Log. ID: ${saveM18DeliveryOrderLog.id}") | ||||
| try { | 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...") | // 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}") | // logger.info("${doRefType}: Exising delivery order ID: ${existingDeliveryOrder?.id}") | ||||
| // Save to delivery_order table | // Save to delivery_order table | ||||
| @@ -621,12 +648,17 @@ open class M18DeliveryOrderService( | |||||
| logger.info("--------------------------------------------End - Saving M18 Delivery Order--------------------------------------------") | 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( | return SyncResult( | ||||
| totalProcessed = successList.size + failList.size + skippedList.size, | |||||
| totalProcessed = successList.size + failList.size + skippedList.size + skippedNotPendingList.size, | |||||
| totalSuccess = successList.size, | totalSuccess = successList.size, | ||||
| totalFail = failList.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) { | if (request.modifiedDateFrom != null && request.modifiedDateTo != null) { | ||||
| materialPoConds += "=and=(${lastDateConds})" | materialPoConds += "=and=(${lastDateConds})" | ||||
| @@ -233,11 +226,28 @@ open class M18PurchaseOrderService( | |||||
| return if (beId?.toString() == m18Config.BEID_TOA) PurchaseOrderType.SHOP else PurchaseOrderType.MATERIAL | 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 { | 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)) | return saveFromPoListSearch(conds, searchPurchaseOrdersByConds(conds)) | ||||
| } | } | ||||
| @@ -357,6 +367,7 @@ open class M18PurchaseOrderService( | |||||
| val successDetailList = mutableListOf<Long>() | val successDetailList = mutableListOf<Long>() | ||||
| val failList = mutableListOf<Long>() | val failList = mutableListOf<Long>() | ||||
| val failDetailList = mutableListOf<Long>() | val failDetailList = mutableListOf<Long>() | ||||
| val skippedNotPendingList = mutableListOf<String>() | |||||
| val affectedItemIds = mutableSetOf<Long>() | val affectedItemIds = mutableSetOf<Long>() | ||||
| val uomByM18IdCache = mutableMapOf<Long, com.ffii.fpsms.modules.master.entity.ItemUom?>() | val uomByM18IdCache = mutableMapOf<Long, com.ffii.fpsms.modules.master.entity.ItemUom?>() | ||||
| @@ -398,11 +409,15 @@ open class M18PurchaseOrderService( | |||||
| if (existingPurchaseOrderForSync != null && | if (existingPurchaseOrderForSync != null && | ||||
| existingPurchaseOrderForSync.status != PurchaseOrderStatus.PENDING | existingPurchaseOrderForSync.status != PurchaseOrderStatus.PENDING | ||||
| ) { | ) { | ||||
| val statusValue = existingPurchaseOrderForSync.status?.value ?: "unknown" | |||||
| logger.info( | logger.info( | ||||
| "${poRefType}: Skipping M18 sync — local PO id=${existingPurchaseOrderForSync.id} " + | "${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" | "(only pending may be overwritten). M18 ID: $m18PurchaseOrderId" | ||||
| ) | ) | ||||
| skippedNotPendingList.add( | |||||
| "code=${existingPurchaseOrderForSync.code} status=$statusValue" | |||||
| ) | |||||
| return@forEach | return@forEach | ||||
| } | } | ||||
| @@ -729,11 +744,18 @@ open class M18PurchaseOrderService( | |||||
| logger.info("--------------------------------------------End - Saving M18 Purchase Order--------------------------------------------") | 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( | return SyncResult( | ||||
| totalProcessed = successList.size + failList.size, | |||||
| totalProcessed = successList.size + failList.size + skippedNotPendingList.size, | |||||
| totalSuccess = successList.size, | totalSuccess = successList.size, | ||||
| totalFail = failList.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.) */ | /** 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"; | 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(" group by po.id"); | ||||
| sql.append(" order by po.orderDate desc") | sql.append(" order by po.orderDate desc") | ||||
| sql.append(" ) r") | 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() | return sql.toString() | ||||
| } | } | ||||
| @@ -47,6 +47,8 @@ class PurchaseOrderController( | |||||
| val criteriaArgs = CriteriaArgsBuilder.withRequest(request) | val criteriaArgs = CriteriaArgsBuilder.withRequest(request) | ||||
| .addStringLike("supplier") | .addStringLike("supplier") | ||||
| .addStringLike("code") | .addStringLike("code") | ||||
| .addStringLike("itemCode") | |||||
| .addStringLike("itemName") | |||||
| .addString("status") | .addString("status") | ||||
| .addBoolean("escalated") | .addBoolean("escalated") | ||||
| .addDate("orderDate") | .addDate("orderDate") | ||||
| @@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.report.service | |||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import com.ffii.core.support.JdbcDao | import com.ffii.core.support.JdbcDao | ||||
| import java.time.LocalDate | |||||
| @Service | @Service | ||||
| class SemiFGProductionAnalysisReportService( | class SemiFGProductionAnalysisReportService( | ||||
| @@ -199,6 +200,122 @@ class SemiFGProductionAnalysisReportService( | |||||
| return jdbcDao.queryForList(sql, args) | 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. | * Gets list of item codes (bom.code) with names based on stockCategory filter. | ||||
| * Supports multiple categories separated by comma (e.g., "FG,WIP"). | * 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.LocalDate | ||||
| import java.time.LocalTime | import java.time.LocalTime | ||||
| import java.time.format.DateTimeFormatter | 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.SemiFGProductionAnalysisReportService | ||||
| import com.ffii.fpsms.modules.report.service.ReportService | import com.ffii.fpsms.modules.report.service.ReportService | ||||
| import org.apache.poi.ss.usermodel.BorderStyle | import org.apache.poi.ss.usermodel.BorderStyle | ||||
| @@ -173,6 +174,243 @@ class SemiFGProductionAnalysisReportController( | |||||
| return ResponseEntity(excelBytes, headers, HttpStatus.OK) | 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( | private fun createSemiFGProductionAnalysisExcel( | ||||
| dbData: List<Map<String, Any>>, | dbData: List<Map<String, Any>>, | ||||
| reportTitle: String, | reportTitle: String, | ||||
| @@ -517,4 +755,15 @@ class SemiFGProductionAnalysisReportController( | |||||
| val itemCodesWithCategory = semiFGProductionAnalysisReportService.getSemiFGItemCodesWithCategory(stockCategory) | val itemCodesWithCategory = semiFGProductionAnalysisReportService.getSemiFGItemCodesWithCategory(stockCategory) | ||||
| return ResponseEntity(itemCodesWithCategory, HttpStatus.OK) | 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.response.RecordsRes | ||||
| import com.ffii.core.utils.PdfUtils | 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.ItemUomRespository | ||||
| import com.ffii.fpsms.modules.master.entity.WarehouseRepository | import com.ffii.fpsms.modules.master.entity.WarehouseRepository | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | 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.service.JobOrderService | ||||
| import com.ffii.fpsms.modules.jobOrder.web.model.ExportFGStockInLabelRequest | import com.ffii.fpsms.modules.jobOrder.web.model.ExportFGStockInLabelRequest | ||||
| import com.ffii.fpsms.modules.master.service.PrinterService | 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.stock.web.model.PrintLabelForInventoryLotLineRequest | ||||
| import com.ffii.fpsms.modules.jobOrder.web.model.PrintFGStockInLabelRequest | import com.ffii.fpsms.modules.jobOrder.web.model.PrintFGStockInLabelRequest | ||||
| import com.ffii.core.utils.ZebraPrinterUtil | import com.ffii.core.utils.ZebraPrinterUtil | ||||
| @@ -60,6 +60,7 @@ open class InventoryLotLineService( | |||||
| private val stockInLineRepository: StockInLineRepository, | private val stockInLineRepository: StockInLineRepository, | ||||
| private val inventoryRepository: InventoryRepository, | private val inventoryRepository: InventoryRepository, | ||||
| private val printerService: PrinterService, | private val printerService: PrinterService, | ||||
| private val settingsService: SettingsService, | |||||
| @Lazy | @Lazy | ||||
| private val jobOrderService: JobOrderService | private val jobOrderService: JobOrderService | ||||
| ) { | ) { | ||||
| @@ -247,7 +248,8 @@ open class InventoryLotLineService( | |||||
| @Throws(IOException::class) | @Throws(IOException::class) | ||||
| @Transactional | @Transactional | ||||
| open fun exportStockInLineQrcode(request: LotLineToQrcode): Map<String, Any> { | 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) | val resource = ClassPathResource(QRCODE_PDF) | ||||
| if (!resource.exists()) { | if (!resource.exists()) { | ||||
| println("here") | println("here") | ||||
| @@ -287,13 +289,18 @@ open class InventoryLotLineService( | |||||
| field["acceptedQty"] = "%.2f".format(info.acceptedQty) | field["acceptedQty"] = "%.2f".format(info.acceptedQty) | ||||
| val stockItemUom = itemUomRespository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse(info.itemId) | 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["productionDate"] = info.productionDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | ||||
| field["expiryDate"] = info.expiryDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | field["expiryDate"] = info.expiryDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | ||||
| field["lotNo"] = info.lotNo!! | field["lotNo"] = info.lotNo!! | ||||
| field["supplier"] = info.supplier ?: "N/A" | field["supplier"] = info.supplier ?: "N/A" | ||||
| val image = QrCodeUtil.generateQRCodeImage(qrCodeContent) | |||||
| val image = PoItemLabelTemplate.qrImage(qrCodeContent, labelVersion) | |||||
| field["qrCode"] = image | field["qrCode"] = image | ||||
| fields.add(field) | 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.exception.BadRequestException | ||||
| import com.ffii.core.support.AbstractBaseEntityService | import com.ffii.core.support.AbstractBaseEntityService | ||||
| import com.ffii.core.support.JdbcDao | import com.ffii.core.support.JdbcDao | ||||
| import com.ffii.core.utils.QrCodeUtil | |||||
| import com.ffii.fpsms.modules.common.CodeGenerator | import com.ffii.fpsms.modules.common.CodeGenerator | ||||
| import com.ffii.fpsms.modules.master.entity.ItemsRepository | import com.ffii.fpsms.modules.master.entity.ItemsRepository | ||||
| import com.ffii.fpsms.modules.master.entity.QcItemRepository | 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.ItemUomService | ||||
| //import com.ffii.fpsms.modules.master.service.BomOutputQtyService | //import com.ffii.fpsms.modules.master.service.BomOutputQtyService | ||||
| import com.ffii.fpsms.modules.master.service.PrinterService | 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.PurchaseOrder | ||||
| import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLine | import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLine | ||||
| import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository | import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository | ||||
| @@ -98,6 +98,7 @@ open class StockInLineService( | |||||
| private val itemUomRepository: ItemUomRespository, | private val itemUomRepository: ItemUomRespository, | ||||
| private val itemUomService: ItemUomService, | private val itemUomService: ItemUomService, | ||||
| private val printerService: PrinterService, | private val printerService: PrinterService, | ||||
| private val settingsService: SettingsService, | |||||
| private val stockTakeLineRepository: StockTakeLineRepository, | private val stockTakeLineRepository: StockTakeLineRepository, | ||||
| private val inventoryLotLineService: InventoryLotLineService, | private val inventoryLotLineService: InventoryLotLineService, | ||||
| private val deliveryOrderRepository: DeliveryOrderRepository, | private val deliveryOrderRepository: DeliveryOrderRepository, | ||||
| @@ -1208,7 +1209,8 @@ open class StockInLineService( | |||||
| @Throws(IOException::class) | @Throws(IOException::class) | ||||
| @Transactional | @Transactional | ||||
| open fun exportStockInLineQrcode(request: ExportQrCodeRequest): Map<String, Any> { | 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) | val resource = ClassPathResource(QRCODE_PDF) | ||||
| if (!resource.exists()) { | if (!resource.exists()) { | ||||
| println("here") | println("here") | ||||
| @@ -1242,12 +1244,17 @@ open class StockInLineService( | |||||
| field["poCode"] = info.poCode ?: "N/A" | field["poCode"] = info.poCode ?: "N/A" | ||||
| field["itemType"] = info.itemType ?: "N/A" | field["itemType"] = info.itemType ?: "N/A" | ||||
| field["acceptedQty"] = "%.2f".format(acceptedQty) | 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["productionDate"] = info.productionDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | ||||
| field["expiryDate"] = info.expiryDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | field["expiryDate"] = info.expiryDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: "" | ||||
| field["lotNo"] = info.lotNo ?: "N/A" | field["lotNo"] = info.lotNo ?: "N/A" | ||||
| field["supplier"] = info.supplier ?: "N/A" | field["supplier"] = info.supplier ?: "N/A" | ||||
| val image = QrCodeUtil.generateQRCodeImage(qrCodeContent) | |||||
| val image = PoItemLabelTemplate.qrImage(qrCodeContent, labelVersion) | |||||
| field["qrCode"] = image | field["qrCode"] = image | ||||
| fields.add(field) | fields.add(field) | ||||
| } | } | ||||
| @@ -3,9 +3,10 @@ fpsms: | |||||
| monitoring: | monitoring: | ||||
| enabled: true | 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: | 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 | refresh-expiration-days: 7 | ||||
| # Required for stable JWT across restarts; set JWT_SECRET in the deployment environment (long random string). | # 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} | secret: ${JWT_SECRET:fpsms-dev-jwt-signing-secret-change-for-production-use-long-random-JWT_SECRET} | ||||
| @@ -113,8 +113,9 @@ spring: | |||||
| dialect: | dialect: | ||||
| storage_engine: innodb | 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. | # 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: | jwt: | ||||
| expiration-minutes: 14400 # access token: 10 days (default); override in application-prod for shorter session | expiration-minutes: 14400 # access token: 10 days (default); override in application-prod for shorter session | ||||
| refresh-expiration-days: 30 # refresh token validity (days) | 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> | |||||