From dd348f36ae6b2f27f305cdd9dca989dadf16e4ba Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Wed, 13 May 2026 21:47:40 +0800 Subject: [PATCH] adding for bom sync --- .gitignore | 3 +- .../fpsms/m18/entity/M18BomShopSyncLog.kt | 9 + .../m18/entity/M18BomShopSyncLogRepository.kt | 9 +- .../m18/model/M18BomForShopSaveRequest.kt | 12 +- .../fpsms/m18/service/M18BomForShopService.kt | 239 +++++++++++++++--- .../modules/master/service/BomService.kt | 3 + .../entity/PurchaseOrderLineRepository.kt | 32 +++ .../20260118_fai/01_insert_scheduler.sql | 2 +- .../01_m18_bom_shop_sync_log_columns.sql | 7 + 9 files changed, 278 insertions(+), 38 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20260515_fpsms/01_m18_bom_shop_sync_log_columns.sql diff --git a/.gitignore b/.gitignore index 28da35d..f0ac852 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ out/ .vscode/ package-lock.json python/Bag3.spec -python/dist/Bag3.exe +python/dist + diff --git a/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLog.kt b/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLog.kt index 5d85df5..64578c4 100644 --- a/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLog.kt +++ b/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLog.kt @@ -17,6 +17,15 @@ open class M18BomShopSyncLog : BaseEntity() { @Column(name = "bom_id", nullable = false) open var bomId: Long? = null + @Column(name = "finished_item_code", length = 100) + open var finishedItemCode: String? = null + + @Column(name = "m18_header_code", length = 200) + open var m18HeaderCode: String? = null + + @Column(name = "request_fingerprint", length = 64) + open var requestFingerprint: String? = null + @Column(name = "m18_record_id") open var m18RecordId: Long? = null diff --git a/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLogRepository.kt b/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLogRepository.kt index bf6c04c..ee1b83a 100644 --- a/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLogRepository.kt +++ b/src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLogRepository.kt @@ -2,4 +2,11 @@ package com.ffii.fpsms.m18.entity import com.ffii.core.support.AbstractRepository -interface M18BomShopSyncLogRepository : AbstractRepository +interface M18BomShopSyncLogRepository : AbstractRepository { + fun findFirstByBomIdOrderByIdDesc(bomId: Long): M18BomShopSyncLog? + + fun findTop100ByBomIdOrderByIdDesc(bomId: Long): List + + /** Successful M18 udfBomForShop saves only — used for `BOM{item}Vnnn` version allocation. */ + fun findTop100ByBomIdAndSyncedIsTrueOrderByIdDesc(bomId: Long): List +} diff --git a/src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt b/src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt index 7b83edc..6dfe3bc 100644 --- a/src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt +++ b/src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt @@ -53,6 +53,8 @@ data class M18MainUdfBomForShopValue( @JsonProperty("udfYieldratePP") val udfYieldratePP: Number? = null, val udftypeoffood: String? = null, + @JsonProperty("udfconfirmed") + val udfconfirmed: Boolean? = null, val staffId: Int? = null, val flowTypeId: Int? = null, val virDeptId: Int? = null, @@ -66,7 +68,7 @@ data class M18UdfProductWrapper( /** * Line payload for `udfproduct.values[]`. **`udfBaseUnit`** is the FPSMS UOM **code** for the line. - * **`udfpurchaseUnit`** / **`udfPackingUnit`** / **`udfPackingQty`** / **`udfproremark`** are not sent. + * **`udfPackingUnit`** / **`udfPackingQty`** / **`udfproremark`** are not sent. */ @JsonInclude(JsonInclude.Include.NON_NULL) data class M18UdfProductSaveValue( @@ -77,7 +79,15 @@ data class M18UdfProductSaveValue( val udfIngredients: String? = null, /** Line UOM: [com.ffii.fpsms.modules.master.entity.UomConversion.code] (same unit as [udfqty]). */ val udfBaseUnit: String? = null, + /** PO supplier [com.ffii.fpsms.modules.master.entity.Shop.m18Id] (via `purchase_order.supplierId`) for latest PO line matching material [com.ffii.fpsms.modules.master.entity.Items.code]. */ val udfSupplier: Long? = null, + /** + * M18 UOM id for price/purchase unit on the **M18-linked** PO line (`m18DataLog` present): + * [com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLine.uomM18] (M18 `unitId`) then + * [com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLine.uom] → [com.ffii.fpsms.modules.master.entity.UomConversion.m18Id]. + */ + @JsonProperty("udfpurchaseUnit") + val udfpurchaseUnit: Long? = null, /** Line sequence, e.g. " 1" */ val itemNo: String? = null, val udfoptions: String? = null, diff --git a/src/main/java/com/ffii/fpsms/m18/service/M18BomForShopService.kt b/src/main/java/com/ffii/fpsms/m18/service/M18BomForShopService.kt index 4ab7dbf..f74dd70 100644 --- a/src/main/java/com/ffii/fpsms/m18/service/M18BomForShopService.kt +++ b/src/main/java/com/ffii/fpsms/m18/service/M18BomForShopService.kt @@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.ffii.fpsms.api.service.ApiCallerService import com.ffii.fpsms.m18.M18Config +import com.ffii.fpsms.m18.entity.M18BomShopSyncLog +import com.ffii.fpsms.m18.entity.M18BomShopSyncLogRepository import com.ffii.fpsms.m18.model.GoodsReceiptNoteResponse import com.ffii.fpsms.m18.model.M18BomForShopSaveRequest import com.ffii.fpsms.m18.model.M18MainUdfBomForShopValue @@ -14,14 +16,17 @@ import com.ffii.fpsms.m18.model.M18UdfProductWrapper import com.ffii.fpsms.modules.master.entity.Bom import com.ffii.fpsms.modules.master.entity.BomMaterial import com.ffii.fpsms.modules.master.service.ItemUomService +import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLineRepository import org.slf4j.Logger import org.slf4j.LoggerFactory +import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Service import org.springframework.util.LinkedMultiValueMap import reactor.core.publisher.Mono import java.math.BigDecimal import java.math.RoundingMode import java.nio.charset.StandardCharsets +import java.security.MessageDigest import java.time.ZoneId /** @@ -33,6 +38,8 @@ open class M18BomForShopService( private val m18Config: M18Config, private val apiCallerService: ApiCallerService, private val itemUomService: ItemUomService, + private val purchaseOrderLineRepository: PurchaseOrderLineRepository, + private val m18BomShopSyncLogRepository: M18BomShopSyncLogRepository, ) { private val logger: Logger = LoggerFactory.getLogger(M18BomForShopService::class.java) @@ -51,6 +58,9 @@ open class M18BomForShopService( companion object { private const val HARVEST_CALC_SCALE = 10 private val m18Tz: ZoneId = ZoneId.of("Asia/Hong_Kong") + + private fun formatBomShopHeaderCode(itemCode: String, version: Int): String = + "BOM${itemCode}V${version.toString().padStart(3, '0')}" } @Suppress("DEPRECATION") @@ -58,55 +68,104 @@ open class M18BomForShopService( disable(JsonGenerator.Feature.ESCAPE_NON_ASCII) } + /** + * Stable hash of payload **excluding** M18 header `id`, `code`, and `rev` (so version bumps do not affect equality). + * Used with [M18BomShopSyncLog] to decide V000 vs V001+. + */ + open fun contentFingerprint(request: M18BomForShopSaveRequest): String { + val json = objectMapper.writeValueAsString(normalizedForFingerprint(request)) + return sha256Hex(json) + } + + private fun normalizedForFingerprint(request: M18BomForShopSaveRequest): M18BomForShopSaveRequest { + val v = request.udfbomforshop.values.firstOrNull() + ?: return request + val headerNorm = v.copy(id = null, code = null, rev = null) + val linesSorted = request.udfproduct.values.sortedWith( + compareBy({ it.itemNo }, { it.udfProduct }, { it.udfIngredients }), + ) + return M18BomForShopSaveRequest( + udfbomforshop = M18MainUdfBomForShopWrapper(values = listOf(headerNorm)), + udfproduct = M18UdfProductWrapper(values = linesSorted), + ) + } + + private fun sha256Hex(text: String): String { + val md = MessageDigest.getInstance("SHA-256") + val bytes = md.digest(text.toByteArray(StandardCharsets.UTF_8)) + return bytes.joinToString("") { "%02x".format(it) } + } + /** * Builds M18 save body from a persisted BOM (materials loaded). - * [headerM18IdOverride] optional M18 header record id (e.g. from `/bom/by-item-code` `bomM18Id`) when DB column is stale. - * Otherwise uses [Bom.m18Id] when set for **update**; omitted for **create**. - * Returns null if required M18 ids are missing (caller should log and skip). + * [headerM18IdOverride] optional M18 header record id when forcing update; skips version/fingerprint logic for **id** only, + * reuses latest logged [M18BomShopSyncLog.m18HeaderCode] when possible. + * Otherwise uses [Bom.m18Id] when the normalized payload matches the latest log; on content change, bumps `BOM{item}Vnnn`. */ open fun buildSaveRequest(bom: Bom, headerM18IdOverride: Long? = null): M18BomForShopSaveRequest? { - val code = bom.code ?: return null - val flowTypeId = resolveFlowTypeId(code) + val bomId = bom.id ?: return null + val routingCode = bom.code ?: return null + val itemCode = bom.item?.code?.trim().orEmpty().ifEmpty { + logger.warn("[M18 BOM] bom.item.code missing; cannot build M18 BOM shop payload. bomId=$bomId") + return null + } + + val flowTypeId = resolveFlowTypeId(routingCode) val udfUnit = bom.uom?.m18Id?.takeIf { it > 0 } ?: return null val outputQty = bom.outputQty ?: BigDecimal.ZERO val (udfHarvest, udfHarvestUnit) = resolveUdfHarvestFields(bom, outputQty) val udfEffectiveDate = bom.created?.atZone(m18Tz)?.toInstant()?.toEpochMilli() - val effectiveHeaderM18Id = - headerM18IdOverride?.takeIf { it > 0 } ?: bom.m18Id?.takeIf { it > 0 } + val lines = bom.bomMaterials + .filter { it.deleted != true } + .sortedBy { it.id ?: 0L } + .mapIndexedNotNull { idx, mat -> toProductLine(mat, idx + 1) } + + if (lines.isEmpty()) { + logger.warn("[M18 BOM] BOM id=$bomId code=$routingCode has no materials; skipping M18 save") + return null + } + + val (headerCode, rev, headerM18IdForRequest) = resolveHeaderCodeAndM18Id( + bomId = bomId, + itemCode = itemCode, + lines = lines, + udfUnit = udfUnit, + udfHarvest = udfHarvest, + udfHarvestUnit = udfHarvestUnit, + udfEffectiveDate = udfEffectiveDate, + bomYield = bom.yield, + bomName = bom.name, + bomDescription = bom.description, + flowTypeId = flowTypeId, + headerM18IdOverride = headerM18IdOverride, + bomM18Id = bom.m18Id?.takeIf { it > 0 }, + ) + val header = M18MainUdfBomForShopValue( - id = effectiveHeaderM18Id?.toString(), - code = code, + id = headerM18IdForRequest?.toString(), + code = headerCode, beId = bomShopMainBeId, desc = bom.name ?: bom.description, descEn = bom.name ?: bom.description, - udfBomCode = deriveUdfBomCode(code), - rev = deriveRev(code), + udfBomCode = itemCode, + rev = rev, udfUnit = udfUnit, udfHarvest = udfHarvest, udfHarvestUnit = udfHarvestUnit, udfEffectiveDate = udfEffectiveDate, udfYieldratePP = bom.yield, udftypeoffood = "半成品", + udfconfirmed = true, staffId = 232, flowTypeId = flowTypeId, virDeptId = 117, status = "Y", ) - val lines = bom.bomMaterials - .filter { it.deleted != true } - .sortedBy { it.id ?: 0L } - .mapIndexedNotNull { idx, mat -> toProductLine(mat, idx + 1) } - - if (lines.isEmpty()) { - logger.warn("[M18 BOM] BOM id=${bom.id} code=$code has no materials; skipping M18 save") - return null - } - logger.info( - "[M18 BOM] buildSaveRequest fpsmsBomId=${bom.id} code=$code mainM18Id=$effectiveHeaderM18Id " + - "(m18HeaderIdOverride=$headerM18IdOverride, bom.m18Id=${bom.m18Id})", + "[M18 BOM] buildSaveRequest fpsmsBomId=$bomId routingCode=$routingCode itemCode=$itemCode headerCode=$headerCode " + + "mainM18Id=$headerM18IdForRequest (override=$headerM18IdOverride, bom.m18Id=${bom.m18Id})", ) return M18BomForShopSaveRequest( @@ -115,6 +174,110 @@ open class M18BomForShopService( ) } + @Suppress("LongParameterList") + private fun resolveHeaderCodeAndM18Id( + bomId: Long, + itemCode: String, + lines: List, + udfUnit: Long, + udfHarvest: String, + udfHarvestUnit: String?, + udfEffectiveDate: Long?, + bomYield: BigDecimal?, + bomName: String?, + bomDescription: String?, + flowTypeId: Int, + headerM18IdOverride: Long?, + bomM18Id: Long?, + ): Triple { + val draftHeader = M18MainUdfBomForShopValue( + id = null, + code = null, + beId = bomShopMainBeId, + desc = bomName ?: bomDescription, + descEn = bomName ?: bomDescription, + udfBomCode = itemCode, + rev = null, + udfUnit = udfUnit, + udfHarvest = udfHarvest, + udfHarvestUnit = udfHarvestUnit, + udfEffectiveDate = udfEffectiveDate, + udfYieldratePP = bomYield, + udftypeoffood = "半成品", + udfconfirmed = true, + staffId = 232, + flowTypeId = flowTypeId, + virDeptId = 117, + status = "Y", + ) + val draftRequest = M18BomForShopSaveRequest( + udfbomforshop = M18MainUdfBomForShopWrapper(values = listOf(draftHeader)), + udfproduct = M18UdfProductWrapper(values = lines), + ) + val fp = contentFingerprint(draftRequest) + + val forcedId = headerM18IdOverride?.takeIf { it > 0 } + if (forcedId != null) { + val latest = m18BomShopSyncLogRepository.findFirstByBomIdOrderByIdDesc(bomId) + val codeForUpdate = + latest?.m18HeaderCode?.takeIf { it.isNotBlank() } + ?: formatBomShopHeaderCode(itemCode, 0) + val forcedRev = parseTrailingVersion(codeForUpdate) ?: "000" + return Triple(codeForUpdate, forcedRev, forcedId) + } + + val latestLog = m18BomShopSyncLogRepository.findFirstByBomIdOrderByIdDesc(bomId) + val prevFp = resolveLogFingerprint(latestLog) + val prevCodeTrimmed = latestLog?.m18HeaderCode?.trim().orEmpty() + val samePayload = latestLog != null && prevFp != null && prevFp == fp && prevCodeTrimmed.isNotEmpty() + + if (samePayload) { + val revReuse = parseTrailingVersion(prevCodeTrimmed) ?: "000" + return Triple(prevCodeTrimmed, revReuse, bomM18Id) + } + + val maxV = maxVersionFromLogs(bomId, itemCode) + val nextV = maxV + 1 + val newCode = formatBomShopHeaderCode(itemCode, nextV) + val rev = nextV.toString().padStart(3, '0') + return Triple(newCode, rev, null) + } + + private fun resolveLogFingerprint(log: M18BomShopSyncLog?): String? { + if (log == null) return null + log.requestFingerprint?.takeIf { it.isNotBlank() }?.let { return it } + val json = log.requestJson ?: return null + return runCatching { + val prev = objectMapper.readValue(json, M18BomForShopSaveRequest::class.java) + contentFingerprint(prev) + }.getOrNull() + } + + private fun maxVersionFromLogs(bomId: Long, itemCode: String): Int { + val versionPat = Regex("^BOM${Regex.escape(itemCode)}V(\\d+)$") + // Only successful syncs advance the numeric tail; failed attempts log a code but must not consume Vnnn. + return m18BomShopSyncLogRepository.findTop100ByBomIdAndSyncedIsTrueOrderByIdDesc(bomId) + .mapNotNull { row -> + val c = row.m18HeaderCode?.trim().orEmpty().ifEmpty { + extractHeaderCodeFromJson(row.requestJson).orEmpty() + } + versionPat.find(c)?.groupValues?.get(1)?.toIntOrNull() + } + .maxOrNull() ?: -1 + } + + private fun extractHeaderCodeFromJson(json: String?): String? { + if (json.isNullOrBlank()) return null + return runCatching { + val node = objectMapper.readTree(json) + val text = node.path("udfbomforshop").path("values").path(0).path("code").asText() + text.trim().takeIf { it.isNotEmpty() } + }.getOrNull() + } + + private fun parseTrailingVersion(headerCode: String): String? = + Regex("V(\\d+)$").find(headerCode.trim())?.groupValues?.get(1)?.padStart(3, '0') + /** * From the **finished-good** [Bom.item] stock unit [com.ffii.fpsms.modules.master.entity.UomConversion.code] * (pattern `LETTER_PREFIX` + `DIGITS` + `UNIT_SUFFIX`, e.g. PACK2LB): harvest qty = outputQty × digits, unit = suffix. @@ -160,6 +323,23 @@ open class M18BomForShopService( logger.warn("[M18 BOM] material UOM code missing bomMaterialId=${mat.id}") return null } + val itemId = mat.item?.id + val latestPoLine = itemId?.let { id -> + purchaseOrderLineRepository.findLatestLinesForBomM18ByItemId(id, PageRequest.of(0, 1)).firstOrNull() + } + val itemCode = mat.item?.code?.trim()?.takeIf { it.isNotEmpty() } + val supplierM18Id = itemCode?.let { code -> + purchaseOrderLineRepository.findLatestPoSupplierM18IdByItemCodeNative(code) + .firstOrNull() + ?.takeIf { it > 0L } + } + /** + * M18 line price unit id ([M18PurchaseOrderPot.unitId]): prefer [PurchaseOrderLine.uomM18] from M18 PO sync, + * else [PurchaseOrderLine.uom] when uomM18 is missing. + */ + val purchaseUnitM18Id = + latestPoLine?.uomM18?.m18Id?.takeIf { it > 0L } + ?: latestPoLine?.uom?.m18Id?.takeIf { it > 0L } val udfqty = (mat.qty ?: BigDecimal.ZERO).setScale(8, RoundingMode.HALF_UP).toDouble() return M18UdfProductSaveValue( id = mat.m18Id?.takeIf { it > 0 }, @@ -167,7 +347,8 @@ open class M18BomForShopService( udfProduct = proId, udfIngredients = mat.itemName ?: mat.item?.name, udfBaseUnit = udfBaseUnit, - udfSupplier = 0L, + udfSupplier = supplierM18Id, + udfpurchaseUnit = purchaseUnitM18Id, itemNo = String.format("%6d", lineNo), udfoptions = "", udfoption = 0.0, @@ -175,16 +356,6 @@ open class M18BomForShopService( ) } - private fun deriveUdfBomCode(fullCode: String): String { - val v = Regex("^(.*)V(\\d+)$").find(fullCode) - return if (v != null) v.groupValues[1] else fullCode - } - - private fun deriveRev(fullCode: String): String? { - val v = Regex("^.*V(\\d+)$").find(fullCode) ?: return null - return v.groupValues[1] - } - private fun resolveFlowTypeId(code: String): Int = when { code.startsWith("TOA") -> 1 code.startsWith("BOMPP") || code.startsWith("PP") -> 3 diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt index efa8f93..b10e26f 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt @@ -495,6 +495,9 @@ open class BomService( m18BomShopSyncLogRepository.save( M18BomShopSyncLog().apply { this.bomId = bomId + finishedItemCode = req.udfbomforshop.values.firstOrNull()?.udfBomCode + m18HeaderCode = req.udfbomforshop.values.firstOrNull()?.code + requestFingerprint = m18BomForShopService.contentFingerprint(req) m18RecordId = recordId.takeIf { it > 0 } m18ApiStatus = apiStatus synced = result.synced diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderLineRepository.kt index abde40e..a5a1896 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderLineRepository.kt @@ -3,6 +3,7 @@ package com.ffii.fpsms.modules.purchaseOrder.entity import com.ffii.core.support.AbstractRepository import com.ffii.fpsms.modules.purchaseOrder.entity.projections.PurchaseOrderLineInfo import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderLineStatus +import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param import org.springframework.stereotype.Repository @@ -10,6 +11,37 @@ import java.io.Serializable @Repository interface PurchaseOrderLineRepository : AbstractRepository { + @Query( + "SELECT pol FROM PurchaseOrderLine pol " + + "LEFT JOIN FETCH pol.purchaseOrder po " + + "LEFT JOIN FETCH po.supplier " + + "JOIN FETCH pol.uom " + + "LEFT JOIN FETCH pol.uomM18 " + + "WHERE pol.deleted = false AND pol.item.id = :itemId AND pol.m18DataLog IS NOT NULL " + + "ORDER BY pol.created DESC", + ) + fun findLatestLinesForBomM18ByItemId( + @Param("itemId") itemId: Long, + pageable: Pageable, + ): List + + /** + * Latest PO (by header `purchase_order.created`) for a material item code: supplier `shop.m18Id` from `purchase_order.supplierId`. + * Mirrors manual SQL: pol → items (code), po, shop on supplier, uom_conversion; order by po.created desc limit 1. + */ + @Query( + value = + "SELECT sh.m18Id FROM purchase_order_line pol " + + "LEFT JOIN items it ON pol.itemId = it.id " + + "LEFT JOIN purchase_order po ON pol.purchaseOrderId = po.id " + + "LEFT JOIN shop sh ON po.supplierId = sh.id " + + "LEFT JOIN uom_conversion um ON pol.uomIdM18 = um.id " + + "WHERE pol.deleted = false AND it.deleted = false AND it.code = :itemCode " + + "ORDER BY po.created DESC LIMIT 1", + nativeQuery = true, + ) + fun findLatestPoSupplierM18IdByItemCodeNative(@Param("itemCode") itemCode: String): List + fun findByM18DataLogIdAndDeletedIsFalse(m18datalogId: Serializable): PurchaseOrderLine? fun findAllPurchaseOrderLineInfoByPurchaseOrderIdAndDeletedIsFalse(purchaseOrderId: Long): List fun findAllByPurchaseOrderIdAndDeletedIsFalse(purchaseOrderId: Long): List diff --git a/src/main/resources/db/changelog/changes/20260118_fai/01_insert_scheduler.sql b/src/main/resources/db/changelog/changes/20260118_fai/01_insert_scheduler.sql index ad4287b..0b3cee9 100644 --- a/src/main/resources/db/changelog/changes/20260118_fai/01_insert_scheduler.sql +++ b/src/main/resources/db/changelog/changes/20260118_fai/01_insert_scheduler.sql @@ -9,7 +9,7 @@ WHERE NOT EXISTS ( ); INSERT INTO `fpsmsdb`.`settings` (`name`, `value`, `category`, `type`) -SELECT 'SCHEDULE.m18.do2', '0 0 13 * * *', 'SCHEDULE', 'string' +SELECT 'SCHEDULE.m18.do2', '0 0 11 * * *', 'SCHEDULE', 'string' FROM DUAL WHERE NOT EXISTS ( SELECT 1 FROM `fpsmsdb`.`settings` WHERE `name` = 'SCHEDULE.m18.do2' diff --git a/src/main/resources/db/changelog/changes/20260515_fpsms/01_m18_bom_shop_sync_log_columns.sql b/src/main/resources/db/changelog/changes/20260515_fpsms/01_m18_bom_shop_sync_log_columns.sql new file mode 100644 index 0000000..0d1d86f --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260515_fpsms/01_m18_bom_shop_sync_log_columns.sql @@ -0,0 +1,7 @@ +--liquibase formatted sql +--changeset fpsms:20260515_m18_bom_shop_sync_log_columns + +ALTER TABLE `m18_bom_shop_sync_log` + ADD COLUMN `finished_item_code` VARCHAR(100) NULL COMMENT 'BOM finished-good item code' AFTER `bom_id`, + ADD COLUMN `m18_header_code` VARCHAR(200) NULL COMMENT 'M18 header code BOM+item+Vnnn' AFTER `finished_item_code`, + ADD COLUMN `request_fingerprint` VARCHAR(64) NULL COMMENT 'SHA-256 of normalized payload for change detection' AFTER `m18_header_code`;