From a8e47e482917d046bed334e95a7d0a9367e617d1 Mon Sep 17 00:00:00 2001 From: Fai Luk Date: Wed, 25 Mar 2026 22:26:27 +0800 Subject: [PATCH] refining the Purchase Order from m18 syn, onpack 2nd machine template files and zip export --- .../java/com/ffii/fpsms/m18/M18GrnRules.kt | 9 + .../m18/model/M18PurchaseOrderResponse.kt | 2 + .../m18/service/M18DeliveryOrderService.kt | 49 +++- .../fpsms/m18/service/M18MasterDataService.kt | 7 +- .../m18/service/M18PurchaseOrderService.kt | 3 +- .../ffii/fpsms/m18/web/M18TestController.kt | 5 + .../service/PlasticBagPrinterService.kt | 244 +++++++++++++++++- .../web/PlasticBagPrinterController.kt | 24 ++ .../master/entity/ItemUomRespository.kt | 3 + .../modules/master/service/ItemUomService.kt | 4 +- .../purchaseOrder/entity/PurchaseOrder.kt | 4 + .../service/PurchaseOrderService.kt | 1 + .../web/model/SavePurchaseOrderRequest.kt | 3 +- .../modules/report/service/ReportService.kt | 4 + .../stock/service/StockInLineService.kt | 6 + .../FGStockInLabel/FGStockInLabel.jrxml | 43 ++- ..._update_purchase_order_m18_created_uid.sql | 7 + .../20260325_onpack_qr_template_type.sql | 5 + .../changes/20260326_onpack_qr_text_rows.sql | 12 + .../29BF30F0E9FA4F800147936FC6B1DCFE.bmp | Bin 0 -> 10942 bytes .../33D5006F86DAFDE8BA743C4C55DEAEDA.bmp | Bin 0 -> 10302 bytes .../72CB65B12DF2E8E4A2698BB3DAE8155A.bmp | Bin 0 -> 10270 bytes .../731813F5BB5E82176C254DDAF620529A.bmp | Bin 0 -> 11102 bytes .../8CF3057D15F4C2931D361668CCCB66A9.bmp | Bin 0 -> 11022 bytes .../93628E5851758D32BCA25B00602F6D2E.bmp | Bin 0 -> 10942 bytes .../A49477813D8529AAE0442F3BCD81327A.bmp | Bin 0 -> 10142 bytes src/main/resources/onpack2030_2/PP1175.image | Bin 0 -> 4720 bytes src/main/resources/onpack2030_2/PP2236.image | Bin 0 -> 4720 bytes src/main/resources/onpack2030_2/PP2237.image | Bin 0 -> 4722 bytes src/main/resources/onpack2030_2/PP2238.image | Bin 0 -> 4722 bytes src/main/resources/onpack2030_2/PP2239.image | Bin 0 -> 4722 bytes src/main/resources/onpack2030_2/PP2336.image | Bin 0 -> 4722 bytes src/main/resources/onpack2030_2/PP2338.image | Bin 0 -> 4722 bytes src/main/resources/onpack2030_2/pp1175.job | Bin 0 -> 112 bytes src/main/resources/onpack2030_2/pp2236.job | Bin 0 -> 112 bytes src/main/resources/onpack2030_2/pp2237.job | Bin 0 -> 112 bytes src/main/resources/onpack2030_2/pp2238.job | Bin 0 -> 112 bytes src/main/resources/onpack2030_2/pp2239.job | Bin 0 -> 112 bytes src/main/resources/onpack2030_2/pp2336.job | Bin 0 -> 112 bytes src/main/resources/onpack2030_2/pp2338.job | Bin 0 -> 112 bytes 40 files changed, 400 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/m18/M18GrnRules.kt create mode 100644 src/main/resources/db/changelog/changes/20260324_02_fp/01_update_purchase_order_m18_created_uid.sql create mode 100644 src/main/resources/db/changelog/changes/20260325_onpack_qr_template_type.sql create mode 100644 src/main/resources/db/changelog/changes/20260326_onpack_qr_text_rows.sql create mode 100644 src/main/resources/onpack2030_2/29BF30F0E9FA4F800147936FC6B1DCFE.bmp create mode 100644 src/main/resources/onpack2030_2/33D5006F86DAFDE8BA743C4C55DEAEDA.bmp create mode 100644 src/main/resources/onpack2030_2/72CB65B12DF2E8E4A2698BB3DAE8155A.bmp create mode 100644 src/main/resources/onpack2030_2/731813F5BB5E82176C254DDAF620529A.bmp create mode 100644 src/main/resources/onpack2030_2/8CF3057D15F4C2931D361668CCCB66A9.bmp create mode 100644 src/main/resources/onpack2030_2/93628E5851758D32BCA25B00602F6D2E.bmp create mode 100644 src/main/resources/onpack2030_2/A49477813D8529AAE0442F3BCD81327A.bmp create mode 100644 src/main/resources/onpack2030_2/PP1175.image create mode 100644 src/main/resources/onpack2030_2/PP2236.image create mode 100644 src/main/resources/onpack2030_2/PP2237.image create mode 100644 src/main/resources/onpack2030_2/PP2238.image create mode 100644 src/main/resources/onpack2030_2/PP2239.image create mode 100644 src/main/resources/onpack2030_2/PP2336.image create mode 100644 src/main/resources/onpack2030_2/PP2338.image create mode 100644 src/main/resources/onpack2030_2/pp1175.job create mode 100644 src/main/resources/onpack2030_2/pp2236.job create mode 100644 src/main/resources/onpack2030_2/pp2237.job create mode 100644 src/main/resources/onpack2030_2/pp2238.job create mode 100644 src/main/resources/onpack2030_2/pp2239.job create mode 100644 src/main/resources/onpack2030_2/pp2336.job create mode 100644 src/main/resources/onpack2030_2/pp2338.job diff --git a/src/main/java/com/ffii/fpsms/m18/M18GrnRules.kt b/src/main/java/com/ffii/fpsms/m18/M18GrnRules.kt new file mode 100644 index 0000000..45cb98c --- /dev/null +++ b/src/main/java/com/ffii/fpsms/m18/M18GrnRules.kt @@ -0,0 +1,9 @@ +package com.ffii.fpsms.m18 + +/** + * M18 PO [createUid] is stored on [com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder.m18CreatedUId]. + * For these M18 user ids, FPSMS does not post GRN to M18 (see [com.ffii.fpsms.modules.stock.service.StockInLineService]). + */ +object M18GrnRules { + val SKIP_GRN_FOR_M18_CREATED_UIDS: Set = setOf(2569L, 2676L) +} diff --git a/src/main/java/com/ffii/fpsms/m18/model/M18PurchaseOrderResponse.kt b/src/main/java/com/ffii/fpsms/m18/model/M18PurchaseOrderResponse.kt index 65863f9..0c44b15 100644 --- a/src/main/java/com/ffii/fpsms/m18/model/M18PurchaseOrderResponse.kt +++ b/src/main/java/com/ffii/fpsms/m18/model/M18PurchaseOrderResponse.kt @@ -19,6 +19,8 @@ data class M18PurchaseOrderData ( data class M18PurchaseOrderMainPo ( val id: Long, val code: String, + /** M18 user id who created the PO (persisted in [com.ffii.fpsms.m18.entity.M18DataLog.dataLog] via sync reflection). */ + val createUid: Long? = null, /** Supplier Id */ val venId: Long, /** ETA */ diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt index 0ca9942..118691d 100644 --- a/src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt +++ b/src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt @@ -151,9 +151,54 @@ open class M18DeliveryOrderService( return deliveryOrder } - open fun saveDeliveryOrders(request: M18CommonRequest) : SyncResult{ - logger.info("--------------------------------------------Start - Saving M18 Delivery Order--------------------------------------------") + open fun saveDeliveryOrders(request: M18CommonRequest): SyncResult { val deliveryOrdersWithType = getDeliveryOrdersWithType(request) + return saveDeliveryOrdersWithPreparedList(deliveryOrdersWithType) + } + + /** + * Sync a single M18 shop PO / delivery order by document [code], same search pattern as + * [com.ffii.fpsms.m18.service.M18PurchaseOrderService.savePurchaseOrderByCode]. + */ + open fun saveDeliveryOrderByCode(code: String): SyncResult { + val searchRequest = M18PurchaseOrderListRequest( + stSearch = "po", + params = null, + conds = "(code=equal=$code)" + ) + val doListResponse = try { + apiCallerService.get( + M18_FETCH_PURCHASE_ORDER_LIST_API, + searchRequest + ).block() + } catch (e: Exception) { + logger.error("(Getting DO list By Code) Error on Function - ${e.stackTrace}") + logger.error(e.message) + null + } + + val doValues = doListResponse?.values + if (doValues.isNullOrEmpty()) { + return SyncResult( + totalProcessed = 1, + totalSuccess = 0, + totalFail = 1, + query = "code=equal=$code" + ) + } + + val prepared = M18PurchaseOrderListResponseWithType( + valuesWithType = mutableListOf(Pair(PurchaseOrderType.SHOP, doListResponse)), + query = "code=equal=$code" + ) + + return saveDeliveryOrdersWithPreparedList(prepared) + } + + private fun saveDeliveryOrdersWithPreparedList( + deliveryOrdersWithType: M18PurchaseOrderListResponseWithType? + ): SyncResult { + logger.info("--------------------------------------------Start - Saving M18 Delivery Order--------------------------------------------") val successList = mutableListOf() val successDetailList = mutableListOf() diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt index 5609426..28ba99c 100644 --- a/src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt +++ b/src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt @@ -253,6 +253,9 @@ open class M18MasterDataService( shouldSyncRatioFromCunitByLocalUomId(localBaseId, p.ratioN, p.ratioD) } == true) price?.forEach { + val endMillis = it.endDate + val endInstant = Instant.ofEpochMilli(endMillis) + val now = Instant.now() val localUomId = uomConversionService.findByM18Id(it.unitId)?.id val keepOriginalItemRatio = forceCunitForAllRows val useUnitRatios = forceCunitForAllRows @@ -278,7 +281,7 @@ open class M18MasterDataService( ratioN = if (useUnitRatios) (unitRatios?.ratioN ?: it.ratioN) else it.ratioN, itemRatioD = if (keepOriginalItemRatio) it.ratioD else null, itemRatioN = if (keepOriginalItemRatio) it.ratioN else null, - deleted = it.expired + deleted = it.expired || endInstant.isBefore(now) ) // logger.info("saved item id: ${savedItem.id}") @@ -414,7 +417,7 @@ open class M18MasterDataService( ratioN = if (useUnitRatios) (unitRatios?.ratioN ?: it.ratioN) else it.ratioN, itemRatioD = if (keepOriginalItemRatio) it.ratioD else null, itemRatioN = if (keepOriginalItemRatio) it.ratioN else null, - deleted = endInstant.isBefore(now) + deleted = it.expired || endInstant.isBefore(now) ) itemUomService.saveItemUom(itemUomRequest) diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt index ecc0e3f..4cc5237 100644 --- a/src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt +++ b/src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt @@ -356,7 +356,8 @@ open class M18PurchaseOrderService( status = PurchaseOrderStatus.PENDING.value, type = resolvePoTypeByBeId(mainpo.beId).value, m18DataLogId = saveM18PurchaseOrderLog.id, - m18BeId = mainpo.beId + m18BeId = mainpo.beId, + m18CreatedUId = mainpo.createUid, ) val savePurchaseOrderResponse = diff --git a/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt b/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt index aa8cf68..9999a79 100644 --- a/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt +++ b/src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt @@ -69,6 +69,11 @@ class M18TestController ( fun testSyncPoByCode(@RequestParam code: String): SyncResult { return m18PurchaseOrderService.savePurchaseOrderByCode(code) } + + @GetMapping("/test/do-by-code") + fun testSyncDoByCode(@RequestParam code: String): SyncResult { + return m18DeliveryOrderService.saveDeliveryOrderByCode(code) + } // --------------------------------------------- Scheduler --------------------------------------------- /// // @GetMapping("/schedule/po") // fun schedulePo(@RequestParam @Valid newCron: String) { diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt index 08eeb6f..ad9744b 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/PlasticBagPrinterService.kt @@ -31,6 +31,7 @@ import java.io.InputStreamReader import java.net.ConnectException import java.net.SocketTimeoutException import org.springframework.core.io.ClassPathResource +import org.slf4j.LoggerFactory // Data class to store bitmap bytes + width (for XML) data class BitmapResult(val bytes: ByteArray, val width: Int) @@ -41,6 +42,7 @@ open class PlasticBagPrinterService( private val jdbcDao: JdbcDao, private val stockInLineRepository: StockInLineRepository, ) { + private val logger = LoggerFactory.getLogger(javaClass) fun generatePrintJobBundle( itemCode: String, @@ -186,7 +188,7 @@ open class PlasticBagPrinterService( val packagingJobOrders = normalizedJobOrders.filter { it.jobOrderId in allowedJobOrderIds } require(packagingJobOrders.isNotEmpty()) { "No 包裝 process job orders found for export" } - val exportItems = packagingJobOrders + val exportItemsRaw = packagingJobOrders .groupBy { it.itemCode.trim().lowercase() } .mapNotNull { (codeLower, orders) -> val order = orders.firstOrNull() ?: return@mapNotNull null @@ -197,7 +199,13 @@ open class PlasticBagPrinterService( Triple(codeLower, itemId, stockInLineId) } - require(exportItems.isNotEmpty()) { "No OnPack QR files could be generated for the selected date" } + require(exportItemsRaw.isNotEmpty()) { "No OnPack QR files could be generated for the selected date" } + + val codesUpper = exportItemsRaw.map { it.first.uppercase() }.toSet() + val allowedBmpCodes = codesOnPackMatchingTemplateType(codesUpper, "bmp") + val exportItems = exportItemsRaw.filter { allowedBmpCodes.contains(it.first.uppercase()) } + + require(exportItems.isNotEmpty()) { "No OnPack QR (bmp) rows in onpack_qr for the selected job orders, or no matching templates" } val baos = ByteArrayOutputStream() ZipOutputStream(baos).use { zos -> @@ -228,6 +236,124 @@ open class PlasticBagPrinterService( return baos.toByteArray() } + /** + * OnPack2023 檸檬機: templates under classpath `onpack2030_2/{code}.image` with embedded QR (Static text). + * Only replaces `...` under `` with JSON payload. No separate .bmp in zip. + */ + fun generateOnPackQrTextZip(jobOrders: List): ByteArray { + val normalizedJobOrders = jobOrders + .map { + OnPackQrJobOrderRequest( + jobOrderId = it.jobOrderId, + itemCode = it.itemCode.trim(), + ) + } + .filter { it.jobOrderId > 0 && it.itemCode.isNotBlank() } + + require(normalizedJobOrders.isNotEmpty()) { "No job orders provided" } + + val requestedJobOrderIds = normalizedJobOrders.map { it.jobOrderId }.distinct() + val allowedRows = jdbcDao.queryForList( + """ + SELECT DISTINCT jo.id AS jobOrderId + FROM job_order jo + JOIN bom b ON b.id = jo.bomId + JOIN bom_process bp ON bp.bomId = b.id + JOIN process p ON p.id = bp.processId + WHERE jo.id IN (:jobOrderIds) + AND jo.deleted = 0 + AND p.name = :processName + """.trimIndent(), + mapOf( + "jobOrderIds" to requestedJobOrderIds, + "processName" to "包裝", + ) + ) + val allowedJobOrderIds = allowedRows + .mapNotNull { (it["jobOrderId"] as? Number)?.toLong() } + .toSet() + val packagingJobOrders = normalizedJobOrders.filter { it.jobOrderId in allowedJobOrderIds } + require(packagingJobOrders.isNotEmpty()) { "No 包裝 process job orders found for export" } + + val exportItemsRaw = packagingJobOrders + .groupBy { it.itemCode.trim().lowercase() } + .mapNotNull { (codeLower, orders) -> + val order = orders.firstOrNull() ?: return@mapNotNull null + val stockInLine = stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(order.jobOrderId) + ?: return@mapNotNull null + val itemId = stockInLine.item?.id ?: return@mapNotNull null + val stockInLineId = stockInLine.id ?: return@mapNotNull null + Triple(codeLower, itemId, stockInLineId) + } + + require(exportItemsRaw.isNotEmpty()) { "No OnPack QR files could be generated for the selected date" } + + val codesUpper = exportItemsRaw.map { it.first.uppercase() }.toSet() + val allowedTextCodes = codesOnPackMatchingTemplateType(codesUpper, "text") + val exportItems = exportItemsRaw.filter { allowedTextCodes.contains(it.first.uppercase()) } + + require(exportItems.isNotEmpty()) { "No OnPack QR (text) rows in onpack_qr for the selected job orders, or no matching templates" } + + val baos = ByteArrayOutputStream() + ZipOutputStream(baos).use { zos -> + val addedEntries = linkedSetOf() + exportItems.forEach { (codeLower, itemId, stockInLineId) -> + val imageTemplate = loadOnPack2030_2ImageTemplateOrNull(codeLower) ?: run { + logger.warn("OnPack text ZIP: missing classpath template onpack2030_2/{}.image", codeLower) + return@forEach + } + val imageFileName = "$codeLower.image" + val imageContent = withOnPackStaticQrText(codeLower, imageTemplate, itemId, stockInLineId) + if (addedEntries.add(imageFileName)) { + addToZip(zos, imageFileName, imageContent) + } + val decodedXmlForAssets = decodeOnPackImageTemplateForTextEdit(imageTemplate).first + extractLogoBmpFileNamesFromOnPackImageXml(decodedXmlForAssets).forEach { bmpName -> + if (!addedEntries.add(bmpName)) return@forEach + val bmpBytes = loadOnPack2030_2AssetOrNull(bmpName) ?: run { + logger.warn("OnPack text ZIP: missing classpath asset onpack2030_2/{}", bmpName) + return@forEach + } + addToZip(zos, bmpName, bmpBytes) + } + val jobFileName = "$codeLower.job" + loadOnPack2030_2AssetOrNull(jobFileName)?.let { jobBytes -> + if (addedEntries.add(jobFileName)) { + addToZip(zos, jobFileName, jobBytes) + } + } + } + require(addedEntries.isNotEmpty()) { "No OnPack text template files could be generated for the selected date" } + } + return baos.toByteArray() + } + + /** + * Returns uppercase item codes present in `onpack_qr` with the given [templateType] (`bmp` or `text`). + * Empty or NULL `template_type` is treated as `bmp` for backward compatibility. + */ + private fun codesOnPackMatchingTemplateType(codesUpper: Set, templateType: String): Set { + if (codesUpper.isEmpty()) return emptySet() + val normalizedType = templateType.trim().lowercase() + val sql = when (normalizedType) { + "bmp" -> """ + SELECT DISTINCT UPPER(TRIM(code)) AS code + FROM onpack_qr + WHERE UPPER(TRIM(code)) IN (:codes) + AND COALESCE(NULLIF(TRIM(template_type), ''), 'bmp') = 'bmp' + """.trimIndent() + "text" -> """ + SELECT DISTINCT UPPER(TRIM(code)) AS code + FROM onpack_qr + WHERE UPPER(TRIM(code)) IN (:codes) + AND LOWER(TRIM(template_type)) = 'text' + """.trimIndent() + else -> return emptySet() + } + val rows = jdbcDao.queryForList(sql, mapOf("codes" to codesUpper.toList())) + return rows.mapNotNull { it["code"]?.toString()?.trim()?.uppercase() }.toSet() + } + private fun loadOnPackImageTemplateOrNull(codeLower: String): ByteArray? { val resourcePath = "onpack2030/${codeLower}.image" val resource = ClassPathResource(resourcePath) @@ -235,6 +361,31 @@ open class PlasticBagPrinterService( return resource.inputStream.use { it.readBytes() } } + private fun loadOnPack2030_2ImageTemplateOrNull(codeLower: String): ByteArray? { + val resourcePath = "onpack2030_2/${codeLower}.image" + val resource = ClassPathResource(resourcePath) + if (!resource.exists()) return null + return resource.inputStream.use { it.readBytes() } + } + + private fun loadOnPack2030_2AssetOrNull(fileName: String): ByteArray? { + val safe = fileName.trim().replace(Regex("""[\\/]+"""), "").ifBlank { return null } + val resource = ClassPathResource("onpack2030_2/$safe") + if (!resource.exists()) return null + return resource.inputStream.use { it.readBytes() } + } + + /** Collect `xxx.bmp` inside each `...` block (decoded template XML). */ + private fun extractLogoBmpFileNamesFromOnPackImageXml(xml: String): Set { + val out = linkedSetOf() + Regex("""(?s)]*>[\s\S]*?""").findAll(xml).forEach { block -> + Regex("""([^<]+\.bmp)""", RegexOption.IGNORE_CASE).findAll(block.value).forEach { m -> + out.add(m.groupValues[1].trim()) + } + } + return out + } + private fun withOnPackLogo4Bmp(imageBytes: ByteArray, qrBmpFileName: String): ByteArray { // Use ISO-8859-1 one-byte mapping so all original bytes are preserved, // while replacing only ASCII XML fragment for LOGO_4 filename. @@ -246,6 +397,95 @@ open class PlasticBagPrinterService( return replaced.toByteArray(StandardCharsets.ISO_8859_1) } + /** + * `.image` templates from Windows tools are often UTF-16 LE with BOM; decoding as ISO-8859-1 breaks + * substring/regex matching (`hasNameQr=false` while XML is valid). + */ + private fun decodeOnPackImageTemplateForTextEdit(bytes: ByteArray): Pair ByteArray> { + val bomUtf16Le = byteArrayOf(0xFF.toByte(), 0xFE.toByte()) + val bomUtf16Be = byteArrayOf(0xFE.toByte(), 0xFF.toByte()) + val bomUtf8 = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()) + + when { + bytes.size >= 2 && bytes[0] == bomUtf16Le[0] && bytes[1] == bomUtf16Le[1] -> { + val body = bytes.copyOfRange(2, bytes.size) + val text = String(body, StandardCharsets.UTF_16LE) + return text to { s -> bomUtf16Le + s.toByteArray(StandardCharsets.UTF_16LE) } + } + bytes.size >= 2 && bytes[0] == bomUtf16Be[0] && bytes[1] == bomUtf16Be[1] -> { + val body = bytes.copyOfRange(2, bytes.size) + val text = String(body, StandardCharsets.UTF_16BE) + return text to { s -> bomUtf16Be + s.toByteArray(StandardCharsets.UTF_16BE) } + } + bytes.size >= 3 && bytes[0] == bomUtf8[0] && bytes[1] == bomUtf8[1] && bytes[2] == bomUtf8[2] -> { + val body = bytes.copyOfRange(3, bytes.size) + val text = String(body, StandardCharsets.UTF_8) + return text to { s -> bomUtf8 + s.toByteArray(StandardCharsets.UTF_8) } + } + // UTF-16 LE without BOM: "<" == 0x3C 0x00 + bytes.size >= 2 && bytes[0] == 0x3C.toByte() && bytes[1] == 0x00.toByte() -> { + val text = String(bytes, StandardCharsets.UTF_16LE) + return text to { s -> s.toByteArray(StandardCharsets.UTF_16LE) } + } + else -> { + val utf8 = String(bytes, StandardCharsets.UTF_8) + return utf8 to { s -> s.toByteArray(StandardCharsets.UTF_8) } + } + } + } + + private fun withOnPackStaticQrText(forCode: String, imageBytes: ByteArray, itemId: Long, stockInLineId: Long): ByteArray { + val payload = """{"itemId": $itemId, "stockInLineId": $stockInLineId}""" + val (oneByteText, encodeBack) = decodeOnPackImageTemplateForTextEdit(imageBytes) + // Must NOT use Static[^>]*StaticSrc — [^>]* already eats type='StaticSrc.V1', so StaticSrc can never match. + // Match the attribute form: or type="StaticSrc.V1" + val staticQrTextOpen = + """]*>\s*""" + val regexWithComment = Regex( + """(?s)([\s\S]*?QR[\s\S]*?)($staticQrTextOpen)([^<]*)()""", + ) + val regexFallbackNameQr = Regex( + """(?s)(QR[\s\S]*?)($staticQrTextOpen)([^<]*)()""", + ) + val afterComment = oneByteText.replace(regexWithComment, "$1$2$payload$4") + val usedComment = afterComment !== oneByteText + val replaced = if (usedComment) { + afterComment + } else { + oneByteText.replace(regexFallbackNameQr, "$1$2$payload$4") + } + + if (replaced === oneByteText) { + logger.warn( + "OnPack text QR: NO regex match for code={} itemId={} stockInLineId={} templateBytes={} hasNameQr={} hasStaticSrc={} staticPatternSample={}", + forCode, + itemId, + stockInLineId, + imageBytes.size, + oneByteText.contains("QR"), + oneByteText.contains("StaticSrc.V1"), + previewOneLineForLog(oneByteText, 600), + ) + } + val withQrCellWidth = replaceQrV1CellWidth67To50(replaced) + return encodeBack(withQrCellWidth) + } + + /** First `67` inside `` → 50 (export tuning). */ + private fun replaceQrV1CellWidth67To50(xml: String): String { + return Regex( + """(?s)(]*>[\s\S]*?)(67)()""", + ).replace(xml) { m -> + "${m.groupValues[1]}50${m.groupValues[3]}" + } + } + + /** Single-line preview for logs (avoids multiline noise). */ + private fun previewOneLineForLog(s: String, maxLen: Int): String { + val collapsed = s.replace(Regex("\\s+"), " ").trim() + return if (collapsed.length <= maxLen) collapsed else collapsed.take(maxLen) + "…" + } + private fun createMonochromeBitmap(text: String, targetHeight: Int): BitmapResult { // Step 1: Measure text width with temporary image val tempImg = BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY) diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt index 80c49e8..6a97919 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/web/PlasticBagPrinterController.kt @@ -63,6 +63,30 @@ class PlasticBagPrinterController( } } + /** OnPack2023 檸檬機: `onpack2030_2` templates with embedded QR (Static text only; no separate .bmp). */ + @PostMapping("/download-onpack-qr-text") + fun downloadOnPackQrText( + @RequestBody request: OnPackQrDownloadRequest, + response: HttpServletResponse, + ) { + try { + val zipBytes = plasticBagPrinterService.generateOnPackQrTextZip(request.jobOrders) + response.contentType = "application/zip" + response.setHeader( + HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"onpack2023_lemon_qr.zip\"" + ) + response.setContentLength(zipBytes.size) + response.outputStream.write(zipBytes) + response.outputStream.flush() + } catch (e: IllegalArgumentException) { + response.status = HttpServletResponse.SC_BAD_REQUEST + response.contentType = "text/plain;charset=UTF-8" + response.writer.write(e.message ?: "Invalid request") + response.writer.flush() + } + } + /** * Test API to generate and download the printer job files as a ZIP. * ONPACK2030 diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/ItemUomRespository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/ItemUomRespository.kt index 59cb8c3..fa738be 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/ItemUomRespository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ItemUomRespository.kt @@ -13,6 +13,9 @@ interface ItemUomRespository : AbstractRepository { fun findByM18IdAndDeletedIsFalse(m18Id: Serializable): ItemUom? + /** Includes soft-deleted rows — used by [com.ffii.fpsms.modules.master.service.ItemUomService.saveItemUom] to reactivate. */ + fun findFirstByM18IdOrderByIdDesc(m18Id: Long): ItemUom? + fun deleteAllByIdIn(id: List) fun findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(itemId: Serializable): ItemUom? diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt index a60f098..a24e540 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt @@ -205,7 +205,9 @@ open class ItemUomService( // See if need to update the response open fun saveItemUom(request: ItemUomRequest): ItemUom { - val itemUom = request.m18Id?.let { findByM18Id(it) } ?: request.id?.let { findById(it) } ?: ItemUom() + val itemUom = request.m18Id?.let { itemUomRespository.findFirstByM18IdOrderByIdDesc(it) } + ?: request.id?.let { itemUomRespository.findById(it).orElse(null) } + ?: ItemUom() //if (request.m18LastModifyDate == itemUom.m18LastModifyDate) { // logger.info("Skipping update - m18LastModifyDate unchanged") diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrder.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrder.kt index d82ab80..05e9254 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrder.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrder.kt @@ -59,4 +59,8 @@ open class PurchaseOrder : BaseEntity() { @Column(name = "m18BeId") open var m18BeId: Long? = null + + /** M18 PO header [createUid] from sync; used for GRN rules (see [com.ffii.fpsms.m18.M18GrnRules]). */ + @Column(name = "m18CreatedUId") + open var m18CreatedUId: Long? = null } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt index 74f1244..6eb7000 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt @@ -352,6 +352,7 @@ open fun getPoSummariesByIds(ids: List): List { this.type = type this.m18DataLog = m18DataLog m18BeId = request.m18BeId + m18CreatedUId = request.m18CreatedUId } val savedPurchaseOrder = purchaseOrderRepository.saveAndFlush(purchaseOrder).let { diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/model/SavePurchaseOrderRequest.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/model/SavePurchaseOrderRequest.kt index b628e21..162267c 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/model/SavePurchaseOrderRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/model/SavePurchaseOrderRequest.kt @@ -19,5 +19,6 @@ data class SavePurchaseOrderRequest ( val status: String?, val type: String?, val m18DataLogId: Long?, - val m18BeId: Long? + val m18BeId: Long?, + val m18CreatedUId: Long? = null, ) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt index dd6cf93..30e945b 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt @@ -6,6 +6,7 @@ import net.sf.jasperreports.engine.data.JRMapCollectionDataSource import java.io.ByteArrayOutputStream import java.io.InputStream import com.ffii.core.support.JdbcDao +import com.ffii.fpsms.m18.M18GrnRules import com.ffii.fpsms.modules.master.entity.ShopRepository import com.ffii.fpsms.modules.master.enums.ShopType import com.ffii.fpsms.modules.master.service.ItemUomService @@ -962,10 +963,12 @@ return result /** * GRN preview for M18: show both stock qty (acceptedQty) and purchase qty (converted) for a specific receipt date. * This is a DRY-RUN preview only (does not call M18). + * Excludes POs whose [com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder.m18CreatedUId] is in [com.ffii.fpsms.m18.M18GrnRules.SKIP_GRN_FOR_M18_CREATED_UIDS] (same as live GRN posting). */ fun searchGrnPreviewM18(receiptDate: String): List> { val formatted = receiptDate.replace("/", "-") val args = mutableMapOf("receiptDate" to formatted) + val skipGrnM18CreatedUidInList = M18GrnRules.SKIP_GRN_FOR_M18_CREATED_UIDS.joinToString(", ") val sql = """ SELECT sil.id AS stockInLineId, @@ -996,6 +999,7 @@ return result AND DATE(sil.receiptDate) = DATE(:receiptDate) AND sil.purchaseOrderId IS NOT NULL AND sil.status = 'completed' + AND (po.m18CreatedUId IS NULL OR po.m18CreatedUId NOT IN ($skipGrnM18CreatedUidInList)) ORDER BY sil.purchaseOrderId, sil.purchaseOrderLineId, sil.id """.trimIndent() val rows = jdbcDao.queryForList(sql, args) diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt index 8e2a8cb..2fa9f9d 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt @@ -63,6 +63,7 @@ import com.ffii.fpsms.m18.model.GoodsReceiptNoteAnt import com.ffii.fpsms.m18.model.GoodsReceiptNoteAntValue import com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLog import com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLogRepository +import com.ffii.fpsms.m18.M18GrnRules import com.ffii.fpsms.m18.service.M18GoodsReceiptNoteService import com.ffii.fpsms.m18.service.M18PurchaseOrderService import com.ffii.fpsms.m18.utils.CommonUtils @@ -688,6 +689,11 @@ open class StockInLineService( logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] DEBUG: Skipping M18 GRN - missing M18 ids for PO id=${savedPo.id} code=${savedPo.code}. m18BeId=${savedPo.m18BeId}, supplier.m18Id=${savedPo.supplier?.m18Id}, currency.m18Id=${savedPo.currency?.m18Id}") return } + val m18Created = savedPo.m18CreatedUId + if (m18Created != null && m18Created in M18GrnRules.SKIP_GRN_FOR_M18_CREATED_UIDS) { + logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] Skipping M18 GRN - m18CreatedUId=$m18Created in skip list for PO id=${savedPo.id} code=${savedPo.code}") + return + } //temp comment this TODO will only check purchase order + dnNo duplicated //if (m18GoodsReceiptNoteLogRepository.existsByPurchaseOrderIdAndStatusTrue(savedPo.id!!)) { // logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] Skipping M18 GRN - already created for PO id=${savedPo.id} code=${savedPo.code} (avoids core_201 duplicate)") diff --git a/src/main/resources/FGStockInLabel/FGStockInLabel.jrxml b/src/main/resources/FGStockInLabel/FGStockInLabel.jrxml index 3c46650..64ba06c 100644 --- a/src/main/resources/FGStockInLabel/FGStockInLabel.jrxml +++ b/src/main/resources/FGStockInLabel/FGStockInLabel.jrxml @@ -1,20 +1,11 @@ - - - - - - - - - - + - + @@ -27,10 +18,10 @@ - + - + @@ -40,7 +31,7 @@ - + @@ -79,7 +70,7 @@ - + @@ -106,7 +97,7 @@ - + @@ -117,7 +108,7 @@ ]]> - + @@ -147,15 +138,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/src/main/resources/db/changelog/changes/20260324_02_fp/01_update_purchase_order_m18_created_uid.sql b/src/main/resources/db/changelog/changes/20260324_02_fp/01_update_purchase_order_m18_created_uid.sql new file mode 100644 index 0000000..c8cbf89 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260324_02_fp/01_update_purchase_order_m18_created_uid.sql @@ -0,0 +1,7 @@ +-- liquibase formatted sql +-- changeset fp:20260324_purchase_order_m18_created_uid + +-- precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'purchase_order' AND COLUMN_NAME = 'm18CreatedUId' + +ALTER TABLE `purchase_order` + ADD COLUMN `m18CreatedUId` BIGINT NULL COMMENT 'M18 PO create user id (mainpo.createUid)' AFTER `m18BeId`; diff --git a/src/main/resources/db/changelog/changes/20260325_onpack_qr_template_type.sql b/src/main/resources/db/changelog/changes/20260325_onpack_qr_template_type.sql new file mode 100644 index 0000000..58a9045 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260325_onpack_qr_template_type.sql @@ -0,0 +1,5 @@ +--liquibase formatted sql +--changeset fpsms:onpack_qr_template_type + +ALTER TABLE `onpack_qr` + ADD COLUMN `template_type` varchar(20) NOT NULL DEFAULT 'bmp' COMMENT 'bmp: LOGO_4 bmp + onpack2030; text: Static QR text + onpack2030_2' AFTER `filename`; diff --git a/src/main/resources/db/changelog/changes/20260326_onpack_qr_text_rows.sql b/src/main/resources/db/changelog/changes/20260326_onpack_qr_text_rows.sql new file mode 100644 index 0000000..ab15416 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260326_onpack_qr_text_rows.sql @@ -0,0 +1,12 @@ +--liquibase formatted sql +--changeset fpsms:onpack_qr_text_rows + +INSERT INTO `onpack_qr` (`code`, `filename`, `template_type`) VALUES + ('PP2338', '', 'text'), + ('PP1175', '', 'text'), + ('PP2236', '', 'text'), + ('PP2237', '', 'text'), + ('PP2238', '', 'text'), + ('PP2239', '', 'text'), + ('PP2336', '', 'text') +ON DUPLICATE KEY UPDATE `template_type` = VALUES(`template_type`); diff --git a/src/main/resources/onpack2030_2/29BF30F0E9FA4F800147936FC6B1DCFE.bmp b/src/main/resources/onpack2030_2/29BF30F0E9FA4F800147936FC6B1DCFE.bmp new file mode 100644 index 0000000000000000000000000000000000000000..32c0599aea84bed61edc71c547e964d184bd926c GIT binary patch literal 10942 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 Z=7WpI6M*}TUw0vR{Q2*#5e&QE{tH8MG`s)+ literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/72CB65B12DF2E8E4A2698BB3DAE8155A.bmp b/src/main/resources/onpack2030_2/72CB65B12DF2E8E4A2698BB3DAE8155A.bmp new file mode 100644 index 0000000000000000000000000000000000000000..5e8813c772d2c8b582681b158a79c2875d32187d GIT binary patch literal 10270 zcmdU#zm6Qa5ym+hL*u~ED0E<`Q->Ei+BTf~T@*SH)XXltNARPVfF1h@95mp-p<7Vk z2?o>tzOSm-WbaCrZpXM&NjuZOu41uRB&+}I{^5^rew!o@_~hYx{=7)(fPW!>dzBJ@ za9Th3TAz^X?%bC#oK4nH_Ud!g333r1g0I4q&O8CVsS&O5&DALl(l6j?U2{rDG0gF; z6JoPlSF{`HPi8cjod*KsW^8frL>Xy+;-Ta+lJOCO!>7i7+gwQBW)k)YcnS~HBVagk z0b?Yi4SJIQloH1X5#KO?EDSzJN~FK0;bLmjU(>i}cUSd`0=P8HXr99e;MBJNJZn6S zy+;34d|FFqe;sx*9nMttyy;79a2?y@KKppi0XSv{&4%Y*bYl`e=psbDAb!=JcGkByi1kA2$ z5x82{rj(w24Lw(DwuioIjW=tt1?o1ipBi(;n;Q$o_pLKBCK;9AsAhHkv#Vo0JeT+R z{MNbVW7y^IxjgOsAyHg!E}46zVQq?yZ`x8D<2uyYUv_L4oy~sH4&8Xxw%ORC_t^=*0WT!$I@@}T-Hs9@`{Mf>vw|+JDi@QA2)C)%i$od3`}-MO zKul`=Dq@RE;}wr+9r?$JN!ZSZPJDfS%ujrNqJ55nX?kE=Z~Y2IAJR*02ILr^3E*#Ut{CPo#yZB{mtBcTmLLE zEwg6PAmZbmVp$9Z)_TU%mB6XCUr_vNJ%`zhk~&8{6|-!P)xN-Y`@B$4 zlKJ2j@!XX&2SaiHS@8d3jJHbUH%{Dx3ts4q%paV>ALinrWeg8|JGh=J;SD(b+U&`% z8s=s6md0!Po#{CZJ@@(>){XRks{Y^{?Vb6&`=0M0f(uSig5&ZTY*)jSEi<4~?F`4$fGFx<5O%jQCDuL2`hi4#jC!^{pwW6O-lg;G==qKgRWM+m zLv8wq4`38xB6}7R7v__7BLsXM0`Lv)P z%b4MDD{YjdMZ6bet;_)A15) z^M`QVe$O`jo4s@O$1;unb9!g}aoZ`LYs+}Q)G?BZC^%lT$HkyyOo;O>y&I9Q1M7z6 z=W=F#H=d8kKd&#Y&xdY)ZkV5TJ`oCds`I&MzTw*9)pJF&Ubt5Hjo` z2{t3+fx!NT&LNdW@xgfi9e{7zqkR?d?CGUb=&@a0Ym9g-W1}-lgO+&koQ953S~|w& z?Rd?HL`Q{22ot*Q=V5v9z$DxA%cu;3v!) zH!bxU%-?}=-GFBxHVD4P$}r^`79)7ME-<1!yrUt%Coyi;i13Ra`e#2B)+KT)_hAG) znOFJa06YkC5!=+|#{d4dw@8i8U@B8~1a_;7LOTd%!kL?>G{hs&p&E++5&=Rb# zTAz3VTufu1;Q#EnMcQ`)zD?^=HwhTeW8xX%(LRshTbi8lJpHj{yvCm2cKV^bZWzB; zL8;r?<8?Roe#^(y+Os@<2R&ud_)?<($}|3+pyeeUI>z%($9TZ+7!`vY<6%MSc~<{b zU)b}*W}od(OsYQf_T)vQKcQkb%*XMBTt+^*f5{TozoAwTju(7Q2X(6PgE7L5wLU(> zTu-kXJ+FP=@eyF*RmAG++Z|)RtA0f`z(|VOum75nFL({c?qBGC|7OUsOF75Zb@1jO*BP_<;VsJuaMz$MXY!SbvDG@&~dD`)lPX_P=M*7xlNpjBwKf zo%{$^W3~UM+SAvYm#6Y@(D*e!VqW0&{>JqkvsJ4nV~DUnAig&zqOSkTQJkfQ-Wk(s z@jI{RU*q`%@9p8w?AMRAzft44K1Bamq{z?mDp>KY`6U_SwfK1>GdJU1XY%QL=xsgV zJ=Toik?+5n|H_{K7om6l`;HNxwRb%x@`ltj^U1w6qc{2I7eec}q@HoZ7JBX%mu)`m z#E)Wj&uSI^$p!Z+4Wr9*8&BrUH+nsew@e=vK&Kk2~P Lj`?Z&&+vZ%{?QFk literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/731813F5BB5E82176C254DDAF620529A.bmp b/src/main/resources/onpack2030_2/731813F5BB5E82176C254DDAF620529A.bmp new file mode 100644 index 0000000000000000000000000000000000000000..007fdbc016dc45ecc16fe6ede03faf0e06690a0b GIT binary patch literal 11102 zcmeI2&#v9J5yp9{qROJUC*5@sSXmb6#!D}ZOQ8F{LXk(v(hDPE-lIU?p)XJ_3ivhH zRhCuIMO9Qi>i5kbl9F#++p!JTD3f!JewraU91e$+e1G@*Km0aGxySc!@c9OxKm9C( z3;y%+m%9-7!EO2AXMdn?Lo{k5Y`|ncd?qU&Me_+uK51 zWB~7yVb0jM_iCn#di^)&anD_UfcL`BX3P)#3XT2B+f__T?~UPB`L0 zq#^tQzdiHzPI=~)lu6iUGcXJx_Xcgf;3f+y@CR}!2nh*v=hD`r$dk0z^l#xR_g#Nq z;*o`#z93YpK5mpF({a5#9~l-N|}~{+mwz{iz&J?ae3NmGQ%pET1W1QR;s0 zc#~Ds{Jr(-9ZIV#qb6Tv5jNIMuW^wT-!pq^to^<@N3FaV&zn1AZ_;N#6fi)0Oq&rcI*UB|Bqnp9-`qED&H z>*i8fMDq3K{%Uo3^&|C@dqL%5;Z*gRV*7kAVok!$e$B#UAF#()^_cZv*c!`R{Q*fa z|I^hGtUqi|0+;97N%bXA*^RUACAZh+5smisHDn}!rN49k5(&Atm)2W+h~72&e4zQyc6{@A`>5FRk9dXt_^FI0KC1COV2_NIUms}xEgQG~unS5S_5ST~ zqS}+O#2GU#2TB?@mQB?2C)FtLSW{}NuUpDo+bd|S=4YrCr10YVi!d#YRX?L`Ilx%| zl59K#>A_sSVr@YC%K5~KRW3^v_gvrI^J82em%aUEMn5RdN=H^3Adz_V1EjH| z+3N=Rd90Yh#!amHaHAJ(4>aLQ3;N3)vj0U->Q%l))8&uYcj6iR0wT*l+k|V$ClIhb zf5q|cCiU5?JuFBwyG5be)H zx(kRo=WI%_D?yCIxt4~NAnju z3PTQG7$@PyS;S#joi+YozUiM?=ey|}7uV;qOjmgHsrJX$s^Ac|o}0ZYhm#L-Je4N!d^U3+^=X=Baa_=*HU;t)|Rn>mv zizts!&WxQyyanWgE9w z0x7tsTwpx@fsK0t;|&Xo?>@6E05C2%Q@D)^-~ybXDyvZSA>jQea_mn;^g0XkKrH23 zd_#Yqg!cXK#)k4kd0*a+6AhLk^?`naT!?NY59?YYD?l!j;|kzpa`Rt{*GcrLK8u$6 zFI!irCUS8buFy?n`_^&wmrMiXP!PGVb~s*mq)f_d|9iSuz^{z|)$HSC5Ktcju%hf_ za(5SQi~P*ZKPrEjuX`HpfAq)ay?4nx%=PW<`jDesbovKY4$3hq3lf>*rs%U5naNQS1TXq^-r|dEaKap^Ty6 z?^m*}|LYIj>hrYg{H^0M-@+X5NwtYNOl3&Q+a=8isRe&}C-Uur zab(oSDO2a0f0T!(Z=A}+Us#^ly>9`hRAtTv*4$u>hiw$34dXnKXPnYhU-`c3mubj+ z+u-e$EBWUEr#@+aA%BS`zBV4gk731Q<#hl*4MX}|c_ZJ4u{dAA*EB9x|3x*4K%Xa2fL#Di7@clE&a1!4dCRdwz=7oz64lPXdN$>y7-FFI+n26O!`| zIo33W9AV!UgrA15oZ*TPo?Xg;m%$hlhl24Hl`9XKVZiY!kLhBp;f^``>Q^Ga@c@Jr z6N<$%AD8g93GIseR?LU025JUg~ literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/8CF3057D15F4C2931D361668CCCB66A9.bmp b/src/main/resources/onpack2030_2/8CF3057D15F4C2931D361668CCCB66A9.bmp new file mode 100644 index 0000000000000000000000000000000000000000..85da5bb1a7a93fc96ea704bfc2adf340911191ba GIT binary patch literal 11022 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 ac#My~7v_Tt<89@~zqhv8@ccDJ`+oqkFZSsG literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/93628E5851758D32BCA25B00602F6D2E.bmp b/src/main/resources/onpack2030_2/93628E5851758D32BCA25B00602F6D2E.bmp new file mode 100644 index 0000000000000000000000000000000000000000..0b4767de4b6d20db9f325af8ce3a1dcc8ba1486b GIT binary patch literal 10942 zcmeI2v69@z5r$`ZE^fwS=!j6LQlwDP!R6fV!q9<>K&DWS;M7l{LX{Nj8K%mCLP5pA z8*Bmo|G&EjGq@%1#9R8LDi3*!{m?x<-93#NEO!6;x9`77YIk`35`XXT_pcWz9q=#O zkFQeVA5QBJhxI}`@6LT1!}wr^)L%aIIzgVqhv2g?rCXkW+{EzScyVz`o#ZokSl2P7 z8!^;z)CsZatt2Ns^xn`4o21?tQL&qgc`3UZ* zj0Wb=Wf3^r&?c0eV|6VTZT5$}NR2mbu{r8G&>t#w`8!q`^6!t%$e3EM{Kjfl=6`f? z)WcJGpU=<0sFC`5V#(C?XaR9t&R<;7U9bm83zWx-!I~t;^36IT!}>ZNaXk&jId$B- za^!1D!wI@4mL>Ja!#cwjHU{S#@e&NzGvpiJ-e=e={Sg_*(yl@K;2Ac?@JGy$2Osa~ zAwXCD9AOKMWlWy%hRjP+3}+^A?~DTtFddTf2!);p>eua4c=ok`cEX>qNk;xkBi9%7 zV|d@6@kV3+{rDl{k@C7dlE+x!OdnX+qsr5wkX+&||8WBr^W*B80P1I2V;ekQGLxaGG#`p2YsQ)qi8H#`OR%hu?#1rwMw$!5jRE#(S z^OpZC@m7C!fl}=A!xO?s%$Mo0FK;F9-(POUHc8H_RL$>Cb=mMlws?~(z=xjpCYxBU zs!%#B+CiI|#T%=O^+P_;K`r8o>DN`ke3^pzMYjC(eCP3^{~&a>_k5N9hS%cp|Mi{g z>kkuv=J|f!`mQGX^UZ%5L|w{hn5SNd+%4&`;ze^&VvTdw=_tn3S(5<-8T$}sV)@mv^RvGL{E=0Dpu z72|Uks{P@b)m8rS8a-AFEcc5%P3Df|_G}B^*p|Sd$Y)$^Wxa%2#>M0NCy313g1+ca zuw8$C?XC=G= zr$3uM`BlYS7njtZ(>EsPG_>5x7u1d9e=qi69Q~bn_wtU<#DXWBU<;1zGdOOTP96`6 zMJ@O{{{ZXEr++6Zl;>Y>2aG?1A<^@IgENXy_h&aX!@naL`N4q5Urupu?|2QBhr)Uu5MiRx|gA)T|M=Q{?} zf&u+8#3m2_a8~^_T?5{#NuLXeH|CS;Ml|rb3&0WFw2M4yWnxX6J~-WKdAI^s@gN{R zU)iP~A7Y;O z#~ae`@3ZeYW)FLXH}mg{K&=P1x!9@JKiev@T#nf?mp1N$%7_`Km9;>8y! z4Q~UQ`~u5ZS?~YH_3n|_;Zi;SQfWZGRNDdL&5;i-5X?vH55}l}-ooJS%y~*f!#Q;g zpVDo^_`NiJU4HO|+KhCUe9yc5;=a9+PrLcq&)?Y2?>g`1|BTO~)37cbB4ItO|3tw3dR8|w z3!aa-2fQV71;8V7oHHK5{7#m219=8wZNQ-~3{~{PE97uqrY9~SU|2^-9tSaQ)`-{! z@yGEv4%>VDk?VFHhQotdl|2UFPLLhhUoWK#^ z$1@FR=+}#5x-ie;4nj8N<-Eq#iGh2{x7mE`_w%ymFN(K1L59QDU>ckQG3n%S#|2Hk z#ebP2eds28{Qh{vwxJ4LgZ&skpeRS#^*79{kLYgr~2)|OB$SU zU;MsiJa^1*8+kXqt{BfPvD9t-*L1|SU-M0>^?AK~4LRGO_)?<%!WqABsd-L^hH=Bv zFh1Znj9>JSBOWeDEoY5ivoRKeB z4aRCP)BeV{Gk+&GbDLs)`TiTM0Meg|90i2_|0s-oY(D&o@x4DbPWj{cfj!h8{44w` z+1rJB4pGtf-&g5C{pFD1Zn~q9PhfRc|9`7LZM}JUobC>aU-Kj82CwHgE^nB6OTga>RcbgB6rv&-@{RZanT&==BdBVL)#TXJliP-!dIrA0k>wubR!`l7d z6UIt{8^WIPb5^a{{SKBFYkohJG%ilX{gNxd=bruHtMug*>n*kSW403Gr23&gCca10 fz~I=x;(pH+%r9PVw%lPbG;y`X8B0{jKf?b7$pxla literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/A49477813D8529AAE0442F3BCD81327A.bmp b/src/main/resources/onpack2030_2/A49477813D8529AAE0442F3BCD81327A.bmp new file mode 100644 index 0000000000000000000000000000000000000000..5c2028bbb1b46c5eaeb230af3a37cd0829aab1b8 GIT binary patch literal 10142 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/PP1175.image b/src/main/resources/onpack2030_2/PP1175.image new file mode 100644 index 0000000000000000000000000000000000000000..0a8c4973c71276e7e5364760f3a793720de61b20 GIT binary patch literal 4720 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 KFaLKbwtoPWIUz{^ literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/PP2237.image b/src/main/resources/onpack2030_2/PP2237.image new file mode 100644 index 0000000000000000000000000000000000000000..0f9699fbfdf05f5e071fe0020859d9a89034992a GIT binary patch literal 4722 zcmeI0YflqF6o$`F;fwK$fUdMavTh<4fyhlk#TY|MTclbjDO)1`c=dT_y6jGO7re!o zXqs+k&wb8yreD9rwrM?k&-cu_He;z>**R~I?5=ItskI$$&R*FAATzcCq+=g}j%;XS z$MuBOfbYn0wV)V5vBtBFG;!^+lGKrfw6E>N`snOIl~`;oR+m6J{B9uo7^yP>^rgK3 zBkQ)&unKMj{iT!r*7kTmum!shY|a|2daRduc396kxozOHydCk@WwkU}&qCQ_y}(Y7 zEMncZhpZOuoy}L+w+;RjD%m|B*jSIr_LS}Q0vlxiIX>vK*Rz7OYTOodBR=wW5Bn2* z)W-W4yvdF-KDHgFLlI7)%{8^~-3%+#k-h;88pj4QRe>L#OgJ^vOkB$WJt@|D!5G$J(p!e9`INofRdW*@rA$&#ed5$t%I;(W$yUuGMp`JGFF;(%* z?V*~+du)K^`5u?CyGfQV5yJ-02L4`js}`6DxlfQA0a2l47xY*>8u!;mJdQW}G_e}NPvcxi8 zUBq(?h}so>W(c3U-xk^Qf{b5v-l(IJRbyB8DIA&*?|Dy)ZEtu+Vo{wJ@jA~)-Rx6#W6ziZ7i#Ui15X@VUi_0T@Bx^P}7(rHHQihks>Y@}v-aJXKnmjKccNpgLV?l6Ql+m#ZlAu|x-gd^RBe6T&P~KweJ@;L3zwdx<6`;{Iv{eE1efcIGx{tM;zA?W#>%8K3 zz!^>*ewEYq8WbB?w(ff5P1Kr3$9y!|QqDkQ*K5@oi*Whs3Yuin0jhB!k4~YQQYSTh zAwJ}NgEg8M)f$V~kZ&EG<66EVR&!1_b=|Xc>QwYr(5icn{^js;60&56v(h$Cotbv= z&p(j80qDqv zHg;T37!7zvj;jI12#O8fZB`T4J|nF=1)EdZOdI-?%rHQpV@vu@os@L6Uj%({$LC*xTtdyJRa z>50`Cw{3~hLwjrUCHn1xKZi3@k0`t0>0w^})F2f7hIS-Xe+ zF*a&r{VQhDqlk}e&&iO76KFF@4eYmJ$Lw32RaB9^LS`4fGo;nA$-;yq507jU4f@2z z8I&P>!EX(*z2RY=oN=UxaIGB45rXD0!5kGcN?uYc%**@jfjwj(^YUk zj-czYx;&O(k83CMh_~O5w}Ix7y>`6mE;WmYx*>dd>v@hMTr$gKWxLLDASR!-?J-&L z)a{|1##?NF=Gh)=MD;qBl=dy2Ci506E%RP=mS1H25a^Pv0DHoF9iKE9wV<9Ovd@V- zX)CGW>Y>l&$vV8Ez0K!!WUL@nbkjbczwxiE`51YZL`s_-jh#=F1)VA1Pg9l0z%zU! ziJ5Iu|JNM+F8|lixsIi@#*g@a4XcRf&qRRF1J&tb&WE_jWRWn71j$=Zb;#FD)!yQY zMZBtr=NRC%YwFApK2^URqUkvi-*ncfqLNl)m-h)A>JaaksosV4tBNWjZdEe|K?JT$ z{8omG&?v2x$~cYH2eSGnt29|Rt$=sYO9#Je^)vY*rh2J^owDk&y=QdgtWcy=q)48k z|KO&QOh?#@i**mPyqzUvccRc-!zb2If(n4c+Brz>^xZs7M~HikJ<#Q%D>wup3{ z7eYN#Xug&lPEL%{N$0khI8!y(sl-R6KT&4*i0B=<>`#c&zI}#Ed)TBh=;W|UT#NHI zs;nnoAjzM@R8_p(6KBB${ZHLWX@|>b^BwL0y`^oBPN+R3u3E(CvF5n5o>xUFZByI$ zTuVLrZ{@r9TQT4Lnc(hxSGNDp_hLT(&-Xu(?>gsBpK^aL;}5Ars!6&FCw#*yI*l#s z^bJ=}My#S0)XP=dE;x0>c8?p%8+3i(o@Mua3v?$3t=&KyB~V|NZSlh`P^VM1B zCEo#OI92#2r|k_Ww$NTt$om>?)G^8>=CL8)CNjsBe0i+;oNns6XX(@_@2w+M_a6Pr;n#_Yl0D8!yS#N~ z+Q&kB_;R0_?}Fd6hA8tE%UgMiJ*oqKLU!3%Kg9aj2;YyMWR LdtdhdQf%J<6+j_` literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/PP2239.image b/src/main/resources/onpack2030_2/PP2239.image new file mode 100644 index 0000000000000000000000000000000000000000..ef604d0229c0a8436a8ed41a41d71f28c1c389dc GIT binary patch literal 4722 zcmeI0X-^YD7{{NF!W-ioIa*t#$-0SHuE?dJVvHfBEmAF%l&ujzy!!jkblIKmE_lV5 zXqt9spL?EjreD8Awq<>L$M@WNHfM=l*#&Qp?5=IvnROg)!CqPo$egVJ>DmXNV;kAT zaXn!*A>tB*jsJU5Yjg4DSH`of-r zk##$0SOqtR{-cxq#`bwXw1zDKTd*dpKI<00UDoqXZU^{0Z^yj#SS?T2^HBC#H`wX1 z)meA!KC6fJ))q_b+W~(DmF!*&Y^=tldrJ3ui4C&<0v`<6>v>LEIc^)eF&}xmkNq(| z>frrL-egA+AK9+cp$I3?W}4dgZ{3dAw>Yb)B723-9(<>0YvPloDMub2*#;I2$cs}b zW4uz;e+m4@Yn--uWC%p=kz+GV|{iR#PQ1>S@a!Qx#9$ z9?EIF$A(y*?eQUY*T~%(&o!d5!ly|*m&x23-hY71I*U%Za5y!jY?7i3C@9Zg(JR0Z9c*w0dx=fE>! zBaNAF()iaM{I>Ylu~?Q%#*f6mHLZd_lL5XC)TfIzAL1gDN5U!+B=0@-A>T9Ady5i_ zcy$raF(hhN^qCQS>VDf~-E%U2)p@XrN>)u=-6wEpLcHTm{VwcZRa6oAte!CpGVsAP zZe_R#i_%W1iql$iAnSkf$|`cx4tN{AZ1B6*JX0)Ux|b%{2~v;jJ*!LSg(97@OZt@k z``4XhHskY{eeCT9Q9D56CwOD#=^k}lr}kS+uq9TKpW*eEmnE%>l~wn-goxgUYV+~+0LweH>c35-u(C0W*VcgK2xYpSDNI#AnwI#3~Q!{|MhBJBhz(W z2<=Rv`Cf7`Ju%8Ao!esaOx;|k5}%d+M490uvUlXFKOsv8_8BhiVS~<~lfyE3EzX;$ zvYB{}CVvi7SMhp}od*-_KXH=M37664C)_@E%i2DZPdvECcbkGI#ms{Mbx7wh?dz5j`N*Ex6gl>2iPe?T8nPtsjD;Td+(S!~&) zZ@YdnMv5e8maDg&bLxog4mXrH*m}=>8}9iQ=yncTzlOF;puR8Pq(k?yj?*{csk6>Y zohP$O4XUxOpt1D>YX$L6Bg*-ZiYD%3} z@P+u0_chjNVw7vlV?(|TbWSSy@>tC|-PCo@(y3F?TS2StJ^Gi!%ZbU7UCv57{OZiK zhlh5F{*uDWN_8~w3 literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/PP2336.image b/src/main/resources/onpack2030_2/PP2336.image new file mode 100644 index 0000000000000000000000000000000000000000..be627832cee4fa99b6eb75a8910d419709109cfe GIT binary patch literal 4722 zcmeI0X;0fo7{{NF!kbm!EJs6h@oTYYew|u>}7q(;9)^oT8dv7m+%-K4S3;P}D z*hV&STyJ|Jlj@U`KqPSksn(Em+IWxmJ+Y2C{7P?!JoDd1(5!3dKAWub?=0 zZ_7|6th5QF4>p0OVIOU=!oGdhbPa{6xv)<2mfu^8D0@*9aUt<=Sa2~9ql2sE|_bD8j5TE!`zYF_U9aTj3s%H#?3~W#1 zR)veODC?A}IK3z2Sl<8Sl?~)(9q=i7+2B{Ld8SyzbT3V?OQfFJZ@k?(FO=z&UDBuQ zKYHjSvl(B+9AWQ85VbBEzrY(aPYYQ8peytEZ%N$X;5)&18M*~P0T*2N>f z0?U=T?Cg2EGF4--maQROr7 z22Fk+rmo`ko;wew*njCHWfLx^El#*2?3T3&lTd3&UAKs_W7TozGp~+P)@Hu(wU$Nn z`>J;zw{pEVogbgBcUAk3dN0@Wk9vQOde=U8c9;9lD!xk}Qcu!ZIOQ33(OGQOr0=+X zGDeCdXqKzD-LUJ3?I%tsAF%aj_iMW6N1%HJXyXCeset;v{Fn}%$9hiRgs1j8?|BZ{ z!>Pk>u-o2(Vh78%U2Q)`t!Z@0rOB4E2b#EEtIk-2%UAc%#M2H?jSG3S3)Pgms^JUq zA@3>HXkt`rEMh~xZFEj*`HEQ0Ii1vX&eE<^(OXBW&OQ2I>bW< z#PX0YKLx)Zd=7{{NF(l=<|PzpIpD;Y~INe|HrnuQ{SP};aO5Sl7=5aq+~^83%ku_v}$ zc3Gj7K#{NKp68s&-+x86XNkS%y0n2US!y?S#n%)2!*=bH^&Reky|hO_mTVKqnSBI0 zv9V1Z*Hhj`TocFDfnoy1HupZ##C61*q)sfPePtImL}vn3Y>{<%`wZla=Pt6(k-8K> z|FjoiWZeN8y5J_ze|ECp*fHNH*0P7d9$4E>`K=*s6-d*T-F+RY%g_vL9g1Zr9z${J z-kMM)th5DW05*Z9VQ+1v!oCC6^a%>ty%N}1kIB}Qt@Ro`vi}Mn3|Z@CL0UC#AG!%Y zc{{H^>!>1oh0Xzd7ieqap@%b$B0RDk zEEtj(7f{Cd^wFFy#6-3QU4qR6a&L&`qE{Y{;a0qhIhaAS3>0PV-#x6}=92w6G$KPf z&Q`(wdjwsAbVV%1A2&|t33tC9?*n~D_S*4gtJGIS){WsSde3u|;nG>9D_?b912Ofq zXB$+-Gq;9n8t<_Ymgj3!rgn&itX##TYur0X+Tz~jYm@iKm@7A$GrGl?ugv2sZ*CC%D>9|eil#0ms)DnH*e_C5=fHDf zQyu??gWnbZ2A*rpu@kPur_MQb%>!I-ryfO3ih`FR{TXR#**Jh5FwxTR)U8$|Q|GFZ3c=g13c*Iv= z-nFvM+;sNbU74z}#m?M7s`{Q*RM(sTzqOggD6G#6s@;_)`5=gUxf;WoDdT^yA60d3BVsHuH_I zwJf5)R=xYUmFvCb{CIc0tJ;6pd%2!}*Za??ckOc*ce($r;(PQP^(38zQ=VZLUBp&R z`i|=-6QoFjX1RLXCA*H;?lJe?VC#K%wcPV9(ES3mF^6_4puR8PrbFkkzSB45slCoi zo+I{f>hN9m?b}f7V%d(X?c1m|jn4ULvZd^SrmokjGZx|U)eSW9tOHczLLTixHKi_U z_(FWh`xmY1gUfZK74@9{tPV<-}ykA$z3*?%FdQ z;h{rfdBm5Wg5M)Ultqsfy}ZjBH37e%yK1i=VtsCe`Rixe;(1K}=jhbtvH97xc&gXA JFaLijwr>epAtC?( literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/pp1175.job b/src/main/resources/onpack2030_2/pp1175.job new file mode 100644 index 0000000000000000000000000000000000000000..568ca96358ebdfc50b5467d00ed6324b3404462d GIT binary patch literal 112 zcmezW&xXN^A)g_M!H&U(!IL4EA(0`SA(g?6A(J5oNc#c#xj@>EApi&sfzX`6ltB+D S3sY&sppRD{NFHVb0|NlsxD(+3 literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/pp2236.job b/src/main/resources/onpack2030_2/pp2236.job new file mode 100644 index 0000000000000000000000000000000000000000..b4d9637c74b5dfc44e38d656d593567af207e42d GIT binary patch literal 112 zcmezW&xXN^A)g_M!H&U(!IL4EA(0`SA(g?6A(J5oNc#c#xj@>EApi)CfY6x1j6n}5 S3sY&sppRD{NFHVb0|NlseiPsT literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/pp2237.job b/src/main/resources/onpack2030_2/pp2237.job new file mode 100644 index 0000000000000000000000000000000000000000..7f71594c74f9a51ada5de67a65049a0feb4420f0 GIT binary patch literal 112 zcmezW&xXN^A)g_M!H&U(!IL4EA(0`SA(g?6A(J5oNc#c#xj@>EApi)CfY6x1oIwvL S3sY&sppRD{NFHVb0|Nlsx)b35 literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/pp2238.job b/src/main/resources/onpack2030_2/pp2238.job new file mode 100644 index 0000000000000000000000000000000000000000..da678f2ee4022cdf76923b49d9959982e545903c GIT binary patch literal 112 zcmezW&xXN^A)g_M!H&U(!IL4EA(0`SA(g?6A(J5oNc#c#xj@>EApi)CfY6x1fEApi)CfY6x1l0gqB S3sY&sppRD{NFHVb0|NltG85wf literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/pp2336.job b/src/main/resources/onpack2030_2/pp2336.job new file mode 100644 index 0000000000000000000000000000000000000000..3e3ff0800b6672659172fd68c4a292ea48397171 GIT binary patch literal 112 zcmezW&xXN^A)g_M!H&U(!IL4EA(0`SA(g?6A(J5oNc#c#xj@>EApi)C7>t3?j6n}5 S3sY&sppRD{NFHVb0|Nlsz7yd9 literal 0 HcmV?d00001 diff --git a/src/main/resources/onpack2030_2/pp2338.job b/src/main/resources/onpack2030_2/pp2338.job new file mode 100644 index 0000000000000000000000000000000000000000..4bc59b743df196c22c012c99c9d1bd6eae769f06 GIT binary patch literal 112 zcmezW&xXN^A)g_M!H&U(!IL4EA(0`SA(g?6A(J5oNc#c#xj@>EApi)C7>t3?f