| @@ -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<Long> = setOf(2569L, 2676L) | |||||
| } | |||||
| @@ -19,6 +19,8 @@ data class M18PurchaseOrderData ( | |||||
| data class M18PurchaseOrderMainPo ( | data class M18PurchaseOrderMainPo ( | ||||
| val id: Long, | val id: Long, | ||||
| val code: String, | 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 */ | /** Supplier Id */ | ||||
| val venId: Long, | val venId: Long, | ||||
| /** ETA */ | /** ETA */ | ||||
| @@ -151,9 +151,54 @@ open class M18DeliveryOrderService( | |||||
| return deliveryOrder | 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) | 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<M18PurchaseOrderListResponse, M18PurchaseOrderListRequest>( | |||||
| 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<Long>() | val successList = mutableListOf<Long>() | ||||
| val successDetailList = mutableListOf<Long>() | val successDetailList = mutableListOf<Long>() | ||||
| @@ -253,6 +253,9 @@ open class M18MasterDataService( | |||||
| shouldSyncRatioFromCunitByLocalUomId(localBaseId, p.ratioN, p.ratioD) | shouldSyncRatioFromCunitByLocalUomId(localBaseId, p.ratioN, p.ratioD) | ||||
| } == true) | } == true) | ||||
| price?.forEach { | price?.forEach { | ||||
| val endMillis = it.endDate | |||||
| val endInstant = Instant.ofEpochMilli(endMillis) | |||||
| val now = Instant.now() | |||||
| val localUomId = uomConversionService.findByM18Id(it.unitId)?.id | val localUomId = uomConversionService.findByM18Id(it.unitId)?.id | ||||
| val keepOriginalItemRatio = forceCunitForAllRows | val keepOriginalItemRatio = forceCunitForAllRows | ||||
| val useUnitRatios = forceCunitForAllRows | val useUnitRatios = forceCunitForAllRows | ||||
| @@ -278,7 +281,7 @@ open class M18MasterDataService( | |||||
| ratioN = if (useUnitRatios) (unitRatios?.ratioN ?: it.ratioN) else it.ratioN, | ratioN = if (useUnitRatios) (unitRatios?.ratioN ?: it.ratioN) else it.ratioN, | ||||
| itemRatioD = if (keepOriginalItemRatio) it.ratioD else null, | itemRatioD = if (keepOriginalItemRatio) it.ratioD else null, | ||||
| itemRatioN = if (keepOriginalItemRatio) it.ratioN 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}") | // logger.info("saved item id: ${savedItem.id}") | ||||
| @@ -414,7 +417,7 @@ open class M18MasterDataService( | |||||
| ratioN = if (useUnitRatios) (unitRatios?.ratioN ?: it.ratioN) else it.ratioN, | ratioN = if (useUnitRatios) (unitRatios?.ratioN ?: it.ratioN) else it.ratioN, | ||||
| itemRatioD = if (keepOriginalItemRatio) it.ratioD else null, | itemRatioD = if (keepOriginalItemRatio) it.ratioD else null, | ||||
| itemRatioN = if (keepOriginalItemRatio) it.ratioN else null, | itemRatioN = if (keepOriginalItemRatio) it.ratioN else null, | ||||
| deleted = endInstant.isBefore(now) | |||||
| deleted = it.expired || endInstant.isBefore(now) | |||||
| ) | ) | ||||
| itemUomService.saveItemUom(itemUomRequest) | itemUomService.saveItemUom(itemUomRequest) | ||||
| @@ -356,7 +356,8 @@ open class M18PurchaseOrderService( | |||||
| status = PurchaseOrderStatus.PENDING.value, | status = PurchaseOrderStatus.PENDING.value, | ||||
| type = resolvePoTypeByBeId(mainpo.beId).value, | type = resolvePoTypeByBeId(mainpo.beId).value, | ||||
| m18DataLogId = saveM18PurchaseOrderLog.id, | m18DataLogId = saveM18PurchaseOrderLog.id, | ||||
| m18BeId = mainpo.beId | |||||
| m18BeId = mainpo.beId, | |||||
| m18CreatedUId = mainpo.createUid, | |||||
| ) | ) | ||||
| val savePurchaseOrderResponse = | val savePurchaseOrderResponse = | ||||
| @@ -69,6 +69,11 @@ class M18TestController ( | |||||
| fun testSyncPoByCode(@RequestParam code: String): SyncResult { | fun testSyncPoByCode(@RequestParam code: String): SyncResult { | ||||
| return m18PurchaseOrderService.savePurchaseOrderByCode(code) | return m18PurchaseOrderService.savePurchaseOrderByCode(code) | ||||
| } | } | ||||
| @GetMapping("/test/do-by-code") | |||||
| fun testSyncDoByCode(@RequestParam code: String): SyncResult { | |||||
| return m18DeliveryOrderService.saveDeliveryOrderByCode(code) | |||||
| } | |||||
| // --------------------------------------------- Scheduler --------------------------------------------- /// | // --------------------------------------------- Scheduler --------------------------------------------- /// | ||||
| // @GetMapping("/schedule/po") | // @GetMapping("/schedule/po") | ||||
| // fun schedulePo(@RequestParam @Valid newCron: String) { | // fun schedulePo(@RequestParam @Valid newCron: String) { | ||||
| @@ -31,6 +31,7 @@ import java.io.InputStreamReader | |||||
| import java.net.ConnectException | import java.net.ConnectException | ||||
| import java.net.SocketTimeoutException | import java.net.SocketTimeoutException | ||||
| import org.springframework.core.io.ClassPathResource | import org.springframework.core.io.ClassPathResource | ||||
| import org.slf4j.LoggerFactory | |||||
| // Data class to store bitmap bytes + width (for XML) | // Data class to store bitmap bytes + width (for XML) | ||||
| data class BitmapResult(val bytes: ByteArray, val width: Int) | data class BitmapResult(val bytes: ByteArray, val width: Int) | ||||
| @@ -41,6 +42,7 @@ open class PlasticBagPrinterService( | |||||
| private val jdbcDao: JdbcDao, | private val jdbcDao: JdbcDao, | ||||
| private val stockInLineRepository: StockInLineRepository, | private val stockInLineRepository: StockInLineRepository, | ||||
| ) { | ) { | ||||
| private val logger = LoggerFactory.getLogger(javaClass) | |||||
| fun generatePrintJobBundle( | fun generatePrintJobBundle( | ||||
| itemCode: String, | itemCode: String, | ||||
| @@ -186,7 +188,7 @@ open class PlasticBagPrinterService( | |||||
| val packagingJobOrders = normalizedJobOrders.filter { it.jobOrderId in allowedJobOrderIds } | val packagingJobOrders = normalizedJobOrders.filter { it.jobOrderId in allowedJobOrderIds } | ||||
| require(packagingJobOrders.isNotEmpty()) { "No 包裝 process job orders found for export" } | require(packagingJobOrders.isNotEmpty()) { "No 包裝 process job orders found for export" } | ||||
| val exportItems = packagingJobOrders | |||||
| val exportItemsRaw = packagingJobOrders | |||||
| .groupBy { it.itemCode.trim().lowercase() } | .groupBy { it.itemCode.trim().lowercase() } | ||||
| .mapNotNull { (codeLower, orders) -> | .mapNotNull { (codeLower, orders) -> | ||||
| val order = orders.firstOrNull() ?: return@mapNotNull null | val order = orders.firstOrNull() ?: return@mapNotNull null | ||||
| @@ -197,7 +199,13 @@ open class PlasticBagPrinterService( | |||||
| Triple(codeLower, itemId, stockInLineId) | 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() | val baos = ByteArrayOutputStream() | ||||
| ZipOutputStream(baos).use { zos -> | ZipOutputStream(baos).use { zos -> | ||||
| @@ -228,6 +236,124 @@ open class PlasticBagPrinterService( | |||||
| return baos.toByteArray() | return baos.toByteArray() | ||||
| } | } | ||||
| /** | |||||
| * OnPack2023 檸檬機: templates under classpath `onpack2030_2/{code}.image` with embedded QR (Static text). | |||||
| * Only replaces `<Text>...</Text>` under `<Static type="StaticSrc.V1">` with JSON payload. No separate .bmp in zip. | |||||
| */ | |||||
| fun generateOnPackQrTextZip(jobOrders: List<OnPackQrJobOrderRequest>): 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<String>() | |||||
| 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<String>, templateType: String): Set<String> { | |||||
| 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? { | private fun loadOnPackImageTemplateOrNull(codeLower: String): ByteArray? { | ||||
| val resourcePath = "onpack2030/${codeLower}.image" | val resourcePath = "onpack2030/${codeLower}.image" | ||||
| val resource = ClassPathResource(resourcePath) | val resource = ClassPathResource(resourcePath) | ||||
| @@ -235,6 +361,31 @@ open class PlasticBagPrinterService( | |||||
| return resource.inputStream.use { it.readBytes() } | 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 `<FileName>xxx.bmp</FileName>` inside each `<Logo>...</Logo>` block (decoded template XML). */ | |||||
| private fun extractLogoBmpFileNamesFromOnPackImageXml(xml: String): Set<String> { | |||||
| val out = linkedSetOf<String>() | |||||
| Regex("""(?s)<Logo[^>]*>[\s\S]*?</Logo>""").findAll(xml).forEach { block -> | |||||
| Regex("""<FileName>([^<]+\.bmp)</FileName>""", RegexOption.IGNORE_CASE).findAll(block.value).forEach { m -> | |||||
| out.add(m.groupValues[1].trim()) | |||||
| } | |||||
| } | |||||
| return out | |||||
| } | |||||
| private fun withOnPackLogo4Bmp(imageBytes: ByteArray, qrBmpFileName: String): ByteArray { | private fun withOnPackLogo4Bmp(imageBytes: ByteArray, qrBmpFileName: String): ByteArray { | ||||
| // Use ISO-8859-1 one-byte mapping so all original bytes are preserved, | // Use ISO-8859-1 one-byte mapping so all original bytes are preserved, | ||||
| // while replacing only ASCII XML fragment for LOGO_4 filename. | // while replacing only ASCII XML fragment for LOGO_4 filename. | ||||
| @@ -246,6 +397,95 @@ open class PlasticBagPrinterService( | |||||
| return replaced.toByteArray(StandardCharsets.ISO_8859_1) | 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<String, (String) -> 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: <Static type='StaticSrc.V1'> or type="StaticSrc.V1" | |||||
| val staticQrTextOpen = | |||||
| """<Static\s+type=(?:'|\u0022)StaticSrc\.V1(?:'|\u0022)[^>]*>\s*<Text>""" | |||||
| val regexWithComment = Regex( | |||||
| """(?s)(<!--\s*2\.\s*QR\s+Code\s*-\s*Change\s+this\s+part\s+daily\s+for\s+dynamic\s+JSON\s*-->[\s\S]*?<Name>QR</Name>[\s\S]*?)($staticQrTextOpen)([^<]*)(</Text>)""", | |||||
| ) | |||||
| val regexFallbackNameQr = Regex( | |||||
| """(?s)(<Name>QR</Name>[\s\S]*?)($staticQrTextOpen)([^<]*)(</Text>)""", | |||||
| ) | |||||
| 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("<Name>QR</Name>"), | |||||
| oneByteText.contains("StaticSrc.V1"), | |||||
| previewOneLineForLog(oneByteText, 600), | |||||
| ) | |||||
| } | |||||
| val withQrCellWidth = replaceQrV1CellWidth67To50(replaced) | |||||
| return encodeBack(withQrCellWidth) | |||||
| } | |||||
| /** First `<CellWidth>67</CellWidth>` inside `<QR type='QR.V1'>` → 50 (export tuning). */ | |||||
| private fun replaceQrV1CellWidth67To50(xml: String): String { | |||||
| return Regex( | |||||
| """(?s)(<QR\s+type=['"]QR\.V1['"][^>]*>[\s\S]*?<CellWidth>)(67)(</CellWidth>)""", | |||||
| ).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 { | private fun createMonochromeBitmap(text: String, targetHeight: Int): BitmapResult { | ||||
| // Step 1: Measure text width with temporary image | // Step 1: Measure text width with temporary image | ||||
| val tempImg = BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY) | val tempImg = BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY) | ||||
| @@ -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. | * Test API to generate and download the printer job files as a ZIP. | ||||
| * ONPACK2030 | * ONPACK2030 | ||||
| @@ -13,6 +13,9 @@ interface ItemUomRespository : AbstractRepository<ItemUom, Long> { | |||||
| fun findByM18IdAndDeletedIsFalse(m18Id: Serializable): ItemUom? | 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<Serializable>) | fun deleteAllByIdIn(id: List<Serializable>) | ||||
| fun findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(itemId: Serializable): ItemUom? | fun findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(itemId: Serializable): ItemUom? | ||||
| @@ -205,7 +205,9 @@ open class ItemUomService( | |||||
| // See if need to update the response | // See if need to update the response | ||||
| open fun saveItemUom(request: ItemUomRequest): ItemUom { | 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) { | //if (request.m18LastModifyDate == itemUom.m18LastModifyDate) { | ||||
| // logger.info("Skipping update - m18LastModifyDate unchanged") | // logger.info("Skipping update - m18LastModifyDate unchanged") | ||||
| @@ -59,4 +59,8 @@ open class PurchaseOrder : BaseEntity<Long>() { | |||||
| @Column(name = "m18BeId") | @Column(name = "m18BeId") | ||||
| open var m18BeId: Long? = null | 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 | |||||
| } | } | ||||
| @@ -352,6 +352,7 @@ open fun getPoSummariesByIds(ids: List<Long>): List<PurchaseOrderSummary> { | |||||
| this.type = type | this.type = type | ||||
| this.m18DataLog = m18DataLog | this.m18DataLog = m18DataLog | ||||
| m18BeId = request.m18BeId | m18BeId = request.m18BeId | ||||
| m18CreatedUId = request.m18CreatedUId | |||||
| } | } | ||||
| val savedPurchaseOrder = purchaseOrderRepository.saveAndFlush(purchaseOrder).let { | val savedPurchaseOrder = purchaseOrderRepository.saveAndFlush(purchaseOrder).let { | ||||
| @@ -19,5 +19,6 @@ data class SavePurchaseOrderRequest ( | |||||
| val status: String?, | val status: String?, | ||||
| val type: String?, | val type: String?, | ||||
| val m18DataLogId: Long?, | val m18DataLogId: Long?, | ||||
| val m18BeId: Long? | |||||
| val m18BeId: Long?, | |||||
| val m18CreatedUId: Long? = null, | |||||
| ) | ) | ||||
| @@ -6,6 +6,7 @@ import net.sf.jasperreports.engine.data.JRMapCollectionDataSource | |||||
| import java.io.ByteArrayOutputStream | import java.io.ByteArrayOutputStream | ||||
| import java.io.InputStream | import java.io.InputStream | ||||
| import com.ffii.core.support.JdbcDao | 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.entity.ShopRepository | ||||
| import com.ffii.fpsms.modules.master.enums.ShopType | import com.ffii.fpsms.modules.master.enums.ShopType | ||||
| import com.ffii.fpsms.modules.master.service.ItemUomService | 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. | * 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). | * 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<Map<String, Any?>> { | fun searchGrnPreviewM18(receiptDate: String): List<Map<String, Any?>> { | ||||
| val formatted = receiptDate.replace("/", "-") | val formatted = receiptDate.replace("/", "-") | ||||
| val args = mutableMapOf<String, Any>("receiptDate" to formatted) | val args = mutableMapOf<String, Any>("receiptDate" to formatted) | ||||
| val skipGrnM18CreatedUidInList = M18GrnRules.SKIP_GRN_FOR_M18_CREATED_UIDS.joinToString(", ") | |||||
| val sql = """ | val sql = """ | ||||
| SELECT | SELECT | ||||
| sil.id AS stockInLineId, | sil.id AS stockInLineId, | ||||
| @@ -996,6 +999,7 @@ return result | |||||
| AND DATE(sil.receiptDate) = DATE(:receiptDate) | AND DATE(sil.receiptDate) = DATE(:receiptDate) | ||||
| AND sil.purchaseOrderId IS NOT NULL | AND sil.purchaseOrderId IS NOT NULL | ||||
| AND sil.status = 'completed' | AND sil.status = 'completed' | ||||
| AND (po.m18CreatedUId IS NULL OR po.m18CreatedUId NOT IN ($skipGrnM18CreatedUidInList)) | |||||
| ORDER BY sil.purchaseOrderId, sil.purchaseOrderLineId, sil.id | ORDER BY sil.purchaseOrderId, sil.purchaseOrderLineId, sil.id | ||||
| """.trimIndent() | """.trimIndent() | ||||
| val rows = jdbcDao.queryForList(sql, args) | val rows = jdbcDao.queryForList(sql, args) | ||||
| @@ -63,6 +63,7 @@ import com.ffii.fpsms.m18.model.GoodsReceiptNoteAnt | |||||
| import com.ffii.fpsms.m18.model.GoodsReceiptNoteAntValue | import com.ffii.fpsms.m18.model.GoodsReceiptNoteAntValue | ||||
| import com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLog | import com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLog | ||||
| import com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLogRepository | 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.M18GoodsReceiptNoteService | ||||
| import com.ffii.fpsms.m18.service.M18PurchaseOrderService | import com.ffii.fpsms.m18.service.M18PurchaseOrderService | ||||
| import com.ffii.fpsms.m18.utils.CommonUtils | 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}") | 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 | 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 | //temp comment this TODO will only check purchase order + dnNo duplicated | ||||
| //if (m18GoodsReceiptNoteLogRepository.existsByPurchaseOrderIdAndStatusTrue(savedPo.id!!)) { | //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)") | // logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] Skipping M18 GRN - already created for PO id=${savedPo.id} code=${savedPo.code} (avoids core_201 duplicate)") | ||||
| @@ -1,20 +1,11 @@ | |||||
| <?xml version="1.0" encoding="UTF-8"?> | <?xml version="1.0" encoding="UTF-8"?> | ||||
| <!-- Created with Jaspersoft Studio version 6.21.3.final using JasperReports Library version 6.21.3-4a3078d20785ebe464f18037d738d12fc98c13cf --> | <!-- 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="Blank_A4" pageWidth="425" pageHeight="283" columnWidth="385" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="baa9f270-b398-4f1c-b01e-ba216b7997e9"> | |||||
| <property name="com.jaspersoft.studio.unit." value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.pageWidth" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.topMargin" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.bottomMargin" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.leftMargin" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.rightMargin" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.columnWidth" value="pixel"/> | |||||
| <property name="com.jaspersoft.studio.unit.columnSpacing" value="pixel"/> | |||||
| <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="Blank_A4" pageWidth="410" pageHeight="243" columnWidth="380" leftMargin="20" rightMargin="10" topMargin="5" bottomMargin="5" uuid="baa9f270-b398-4f1c-b01e-ba216b7997e9"> | |||||
| <parameter name="jobOrderCode" class="java.lang.String"/> | <parameter name="jobOrderCode" class="java.lang.String"/> | ||||
| <parameter name="finishedGoodItemCode" class="java.lang.String"/> | <parameter name="finishedGoodItemCode" class="java.lang.String"/> | ||||
| <parameter name="finishedGoodItemName" class="java.lang.String"/> | <parameter name="finishedGoodItemName" class="java.lang.String"/> | ||||
| <parameter name="finishedGoodLotNo" class="java.lang.String"/> | <parameter name="finishedGoodLotNo" class="java.lang.String"/> | ||||
| <parameter name="FGStockInQty" class="java.lang.String"/> | |||||
| <parameter name="FGStockInQty" class="java.lang.String"/> | |||||
| <parameter name="productionDate" class="java.lang.String"/> | <parameter name="productionDate" class="java.lang.String"/> | ||||
| <parameter name="expiryDate" class="java.lang.String"/> | <parameter name="expiryDate" class="java.lang.String"/> | ||||
| <parameter name="uom" class="java.lang.String"/> | <parameter name="uom" class="java.lang.String"/> | ||||
| @@ -27,10 +18,10 @@ | |||||
| <band splitType="Stretch"/> | <band splitType="Stretch"/> | ||||
| </background> | </background> | ||||
| <detail> | <detail> | ||||
| <band height="243" splitType="Stretch"> | |||||
| <band height="233" splitType="Stretch"> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | <property name="com.jaspersoft.studio.unit.height" value="px"/> | ||||
| <textField> | <textField> | ||||
| <reportElement x="80" y="180" width="180" height="30" uuid="8fac39f8-4936-43a5-8e1f-1afbc8ccca9c"> | |||||
| <reportElement x="80" y="172" width="180" height="30" uuid="8fac39f8-4936-43a5-8e1f-1afbc8ccca9c"> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | <property name="com.jaspersoft.studio.unit.height" value="px"/> | ||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | <property name="com.jaspersoft.studio.unit.width" value="px"/> | ||||
| </reportElement> | </reportElement> | ||||
| @@ -40,7 +31,7 @@ | |||||
| <textFieldExpression><![CDATA[$P{productionDate}]]></textFieldExpression> | <textFieldExpression><![CDATA[$P{productionDate}]]></textFieldExpression> | ||||
| </textField> | </textField> | ||||
| <staticText> | <staticText> | ||||
| <reportElement x="0" y="180" width="80" height="30" uuid="e03fcb92-259c-4427-a68e-60fe5924d763"> | |||||
| <reportElement x="0" y="172" width="80" height="30" uuid="e03fcb92-259c-4427-a68e-60fe5924d763"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | <property name="com.jaspersoft.studio.unit.width" value="px"/> | ||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | <property name="com.jaspersoft.studio.unit.height" value="px"/> | ||||
| </reportElement> | </reportElement> | ||||
| @@ -79,7 +70,7 @@ | |||||
| <textElement verticalAlignment="Middle"> | <textElement verticalAlignment="Middle"> | ||||
| <font fontName="微軟正黑體" size="16" isBold="false"/> | <font fontName="微軟正黑體" size="16" isBold="false"/> | ||||
| </textElement> | </textElement> | ||||
| <textFieldExpression><![CDATA[$P{FGStockInQty} + $P{shortName} + " (" + $P{uom} +")"]]></textFieldExpression> | |||||
| <textFieldExpression><![CDATA[$P{FGStockInQty} + $P{shortName} + " (" + $P{uom} +")"]]></textFieldExpression> | |||||
| </textField> | </textField> | ||||
| <line> | <line> | ||||
| <reportElement x="0" y="160" width="380" height="1" uuid="3e37c027-d6e9-4a88-b64d-58ba1dd3b22e"> | <reportElement x="0" y="160" width="380" height="1" uuid="3e37c027-d6e9-4a88-b64d-58ba1dd3b22e"> | ||||
| @@ -106,7 +97,7 @@ | |||||
| <textFieldExpression><![CDATA[$P{finishedGoodLotNo}]]></textFieldExpression> | <textFieldExpression><![CDATA[$P{finishedGoodLotNo}]]></textFieldExpression> | ||||
| </textField> | </textField> | ||||
| <staticText> | <staticText> | ||||
| <reportElement x="0" y="210" width="80" height="30" uuid="93d48729-edea-4acf-beca-00c6221c10bb"> | |||||
| <reportElement x="0" y="202" width="80" height="30" uuid="93d48729-edea-4acf-beca-00c6221c10bb"> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | <property name="com.jaspersoft.studio.unit.width" value="px"/> | ||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | <property name="com.jaspersoft.studio.unit.height" value="px"/> | ||||
| </reportElement> | </reportElement> | ||||
| @@ -117,7 +108,7 @@ | |||||
| ]]></text> | ]]></text> | ||||
| </staticText> | </staticText> | ||||
| <textField> | <textField> | ||||
| <reportElement x="80" y="210" width="180" height="30" uuid="802555cd-a78b-49c4-b018-61c55f28070c"> | |||||
| <reportElement x="80" y="202" width="180" height="30" uuid="802555cd-a78b-49c4-b018-61c55f28070c"> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | <property name="com.jaspersoft.studio.unit.height" value="px"/> | ||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | <property name="com.jaspersoft.studio.unit.width" value="px"/> | ||||
| </reportElement> | </reportElement> | ||||
| @@ -147,15 +138,15 @@ | |||||
| <text><![CDATA[數量: | <text><![CDATA[數量: | ||||
| ]]></text> | ]]></text> | ||||
| </staticText> | </staticText> | ||||
| <image> | |||||
| <reportElement x="265" y="38" width="115" height="115" uuid="f61ca1c1-333b-42e1-9a00-85f5ef1e3492"> | |||||
| <property name="com.jaspersoft.studio.unit.x" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| <imageExpression><![CDATA[$P{qrCode}]]></imageExpression> | |||||
| </image> | |||||
| <image> | |||||
| <reportElement x="265" y="38" width="115" height="115" uuid="f61ca1c1-333b-42e1-9a00-85f5ef1e3492"> | |||||
| <property name="com.jaspersoft.studio.unit.x" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.y" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.width" value="px"/> | |||||
| <property name="com.jaspersoft.studio.unit.height" value="px"/> | |||||
| </reportElement> | |||||
| <imageExpression><![CDATA[$P{qrCode}]]></imageExpression> | |||||
| </image> | |||||
| </band> | </band> | ||||
| </detail> | </detail> | ||||
| </jasperReport> | </jasperReport> | ||||
| @@ -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`; | |||||
| @@ -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`; | |||||
| @@ -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`); | |||||