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 0000000..32c0599 Binary files /dev/null and b/src/main/resources/onpack2030_2/29BF30F0E9FA4F800147936FC6B1DCFE.bmp differ diff --git a/src/main/resources/onpack2030_2/33D5006F86DAFDE8BA743C4C55DEAEDA.bmp b/src/main/resources/onpack2030_2/33D5006F86DAFDE8BA743C4C55DEAEDA.bmp new file mode 100644 index 0000000..45fed3c Binary files /dev/null and b/src/main/resources/onpack2030_2/33D5006F86DAFDE8BA743C4C55DEAEDA.bmp differ diff --git a/src/main/resources/onpack2030_2/72CB65B12DF2E8E4A2698BB3DAE8155A.bmp b/src/main/resources/onpack2030_2/72CB65B12DF2E8E4A2698BB3DAE8155A.bmp new file mode 100644 index 0000000..5e8813c Binary files /dev/null and b/src/main/resources/onpack2030_2/72CB65B12DF2E8E4A2698BB3DAE8155A.bmp differ diff --git a/src/main/resources/onpack2030_2/731813F5BB5E82176C254DDAF620529A.bmp b/src/main/resources/onpack2030_2/731813F5BB5E82176C254DDAF620529A.bmp new file mode 100644 index 0000000..007fdbc Binary files /dev/null and b/src/main/resources/onpack2030_2/731813F5BB5E82176C254DDAF620529A.bmp differ diff --git a/src/main/resources/onpack2030_2/8CF3057D15F4C2931D361668CCCB66A9.bmp b/src/main/resources/onpack2030_2/8CF3057D15F4C2931D361668CCCB66A9.bmp new file mode 100644 index 0000000..85da5bb Binary files /dev/null and b/src/main/resources/onpack2030_2/8CF3057D15F4C2931D361668CCCB66A9.bmp differ diff --git a/src/main/resources/onpack2030_2/93628E5851758D32BCA25B00602F6D2E.bmp b/src/main/resources/onpack2030_2/93628E5851758D32BCA25B00602F6D2E.bmp new file mode 100644 index 0000000..0b4767d Binary files /dev/null and b/src/main/resources/onpack2030_2/93628E5851758D32BCA25B00602F6D2E.bmp differ diff --git a/src/main/resources/onpack2030_2/A49477813D8529AAE0442F3BCD81327A.bmp b/src/main/resources/onpack2030_2/A49477813D8529AAE0442F3BCD81327A.bmp new file mode 100644 index 0000000..5c2028b Binary files /dev/null and b/src/main/resources/onpack2030_2/A49477813D8529AAE0442F3BCD81327A.bmp differ diff --git a/src/main/resources/onpack2030_2/PP1175.image b/src/main/resources/onpack2030_2/PP1175.image new file mode 100644 index 0000000..0a8c497 Binary files /dev/null and b/src/main/resources/onpack2030_2/PP1175.image differ diff --git a/src/main/resources/onpack2030_2/PP2236.image b/src/main/resources/onpack2030_2/PP2236.image new file mode 100644 index 0000000..3686434 Binary files /dev/null and b/src/main/resources/onpack2030_2/PP2236.image differ diff --git a/src/main/resources/onpack2030_2/PP2237.image b/src/main/resources/onpack2030_2/PP2237.image new file mode 100644 index 0000000..0f9699f Binary files /dev/null and b/src/main/resources/onpack2030_2/PP2237.image differ diff --git a/src/main/resources/onpack2030_2/PP2238.image b/src/main/resources/onpack2030_2/PP2238.image new file mode 100644 index 0000000..06f7459 Binary files /dev/null and b/src/main/resources/onpack2030_2/PP2238.image differ diff --git a/src/main/resources/onpack2030_2/PP2239.image b/src/main/resources/onpack2030_2/PP2239.image new file mode 100644 index 0000000..ef604d0 Binary files /dev/null and b/src/main/resources/onpack2030_2/PP2239.image differ diff --git a/src/main/resources/onpack2030_2/PP2336.image b/src/main/resources/onpack2030_2/PP2336.image new file mode 100644 index 0000000..be62783 Binary files /dev/null and b/src/main/resources/onpack2030_2/PP2336.image differ diff --git a/src/main/resources/onpack2030_2/PP2338.image b/src/main/resources/onpack2030_2/PP2338.image new file mode 100644 index 0000000..a62945f Binary files /dev/null and b/src/main/resources/onpack2030_2/PP2338.image differ diff --git a/src/main/resources/onpack2030_2/pp1175.job b/src/main/resources/onpack2030_2/pp1175.job new file mode 100644 index 0000000..568ca96 Binary files /dev/null and b/src/main/resources/onpack2030_2/pp1175.job differ diff --git a/src/main/resources/onpack2030_2/pp2236.job b/src/main/resources/onpack2030_2/pp2236.job new file mode 100644 index 0000000..b4d9637 Binary files /dev/null and b/src/main/resources/onpack2030_2/pp2236.job differ diff --git a/src/main/resources/onpack2030_2/pp2237.job b/src/main/resources/onpack2030_2/pp2237.job new file mode 100644 index 0000000..7f71594 Binary files /dev/null and b/src/main/resources/onpack2030_2/pp2237.job differ diff --git a/src/main/resources/onpack2030_2/pp2238.job b/src/main/resources/onpack2030_2/pp2238.job new file mode 100644 index 0000000..da678f2 Binary files /dev/null and b/src/main/resources/onpack2030_2/pp2238.job differ diff --git a/src/main/resources/onpack2030_2/pp2239.job b/src/main/resources/onpack2030_2/pp2239.job new file mode 100644 index 0000000..81a7533 Binary files /dev/null and b/src/main/resources/onpack2030_2/pp2239.job differ diff --git a/src/main/resources/onpack2030_2/pp2336.job b/src/main/resources/onpack2030_2/pp2336.job new file mode 100644 index 0000000..3e3ff08 Binary files /dev/null and b/src/main/resources/onpack2030_2/pp2336.job differ diff --git a/src/main/resources/onpack2030_2/pp2338.job b/src/main/resources/onpack2030_2/pp2338.job new file mode 100644 index 0000000..4bc59b7 Binary files /dev/null and b/src/main/resources/onpack2030_2/pp2338.job differ