ソースを参照

adding for bom sync

production
コミット
dd348f36ae
9個のファイルの変更278行の追加38行の削除
  1. +2
    -1
      .gitignore
  2. +9
    -0
      src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLog.kt
  3. +8
    -1
      src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLogRepository.kt
  4. +11
    -1
      src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt
  5. +205
    -34
      src/main/java/com/ffii/fpsms/m18/service/M18BomForShopService.kt
  6. +3
    -0
      src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt
  7. +32
    -0
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/entity/PurchaseOrderLineRepository.kt
  8. +1
    -1
      src/main/resources/db/changelog/changes/20260118_fai/01_insert_scheduler.sql
  9. +7
    -0
      src/main/resources/db/changelog/changes/20260515_fpsms/01_m18_bom_shop_sync_log_columns.sql

+ 2
- 1
.gitignore ファイルの表示

@@ -37,4 +37,5 @@ out/
.vscode/ .vscode/
package-lock.json package-lock.json
python/Bag3.spec python/Bag3.spec
python/dist/Bag3.exe
python/dist


+ 9
- 0
src/main/java/com/ffii/fpsms/m18/entity/M18BomShopSyncLog.kt ファイルの表示

@@ -17,6 +17,15 @@ open class M18BomShopSyncLog : BaseEntity<Long>() {
@Column(name = "bom_id", nullable = false) @Column(name = "bom_id", nullable = false)
open var bomId: Long? = null 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") @Column(name = "m18_record_id")
open var m18RecordId: Long? = null open var m18RecordId: Long? = null




+ 8
- 1
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 import com.ffii.core.support.AbstractRepository


interface M18BomShopSyncLogRepository : AbstractRepository<M18BomShopSyncLog, Long>
interface M18BomShopSyncLogRepository : AbstractRepository<M18BomShopSyncLog, Long> {
fun findFirstByBomIdOrderByIdDesc(bomId: Long): M18BomShopSyncLog?

fun findTop100ByBomIdOrderByIdDesc(bomId: Long): List<M18BomShopSyncLog>

/** Successful M18 udfBomForShop saves only — used for `BOM{item}Vnnn` version allocation. */
fun findTop100ByBomIdAndSyncedIsTrueOrderByIdDesc(bomId: Long): List<M18BomShopSyncLog>
}

+ 11
- 1
src/main/java/com/ffii/fpsms/m18/model/M18BomForShopSaveRequest.kt ファイルの表示

@@ -53,6 +53,8 @@ data class M18MainUdfBomForShopValue(
@JsonProperty("udfYieldratePP") @JsonProperty("udfYieldratePP")
val udfYieldratePP: Number? = null, val udfYieldratePP: Number? = null,
val udftypeoffood: String? = null, val udftypeoffood: String? = null,
@JsonProperty("udfconfirmed")
val udfconfirmed: Boolean? = null,
val staffId: Int? = null, val staffId: Int? = null,
val flowTypeId: Int? = null, val flowTypeId: Int? = null,
val virDeptId: 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. * 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) @JsonInclude(JsonInclude.Include.NON_NULL)
data class M18UdfProductSaveValue( data class M18UdfProductSaveValue(
@@ -77,7 +79,15 @@ data class M18UdfProductSaveValue(
val udfIngredients: String? = null, val udfIngredients: String? = null,
/** Line UOM: [com.ffii.fpsms.modules.master.entity.UomConversion.code] (same unit as [udfqty]). */ /** Line UOM: [com.ffii.fpsms.modules.master.entity.UomConversion.code] (same unit as [udfqty]). */
val udfBaseUnit: String? = null, 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, 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" */ /** Line sequence, e.g. " 1" */
val itemNo: String? = null, val itemNo: String? = null,
val udfoptions: String? = null, val udfoptions: String? = null,


+ 205
- 34
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.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.ffii.fpsms.api.service.ApiCallerService import com.ffii.fpsms.api.service.ApiCallerService
import com.ffii.fpsms.m18.M18Config 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.GoodsReceiptNoteResponse
import com.ffii.fpsms.m18.model.M18BomForShopSaveRequest import com.ffii.fpsms.m18.model.M18BomForShopSaveRequest
import com.ffii.fpsms.m18.model.M18MainUdfBomForShopValue 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.Bom
import com.ffii.fpsms.modules.master.entity.BomMaterial import com.ffii.fpsms.modules.master.entity.BomMaterial
import com.ffii.fpsms.modules.master.service.ItemUomService import com.ffii.fpsms.modules.master.service.ItemUomService
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLineRepository
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.util.LinkedMultiValueMap import org.springframework.util.LinkedMultiValueMap
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.time.ZoneId import java.time.ZoneId


/** /**
@@ -33,6 +38,8 @@ open class M18BomForShopService(
private val m18Config: M18Config, private val m18Config: M18Config,
private val apiCallerService: ApiCallerService, private val apiCallerService: ApiCallerService,
private val itemUomService: ItemUomService, private val itemUomService: ItemUomService,
private val purchaseOrderLineRepository: PurchaseOrderLineRepository,
private val m18BomShopSyncLogRepository: M18BomShopSyncLogRepository,
) { ) {
private val logger: Logger = LoggerFactory.getLogger(M18BomForShopService::class.java) private val logger: Logger = LoggerFactory.getLogger(M18BomForShopService::class.java)


@@ -51,6 +58,9 @@ open class M18BomForShopService(
companion object { companion object {
private const val HARVEST_CALC_SCALE = 10 private const val HARVEST_CALC_SCALE = 10
private val m18Tz: ZoneId = ZoneId.of("Asia/Hong_Kong") 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") @Suppress("DEPRECATION")
@@ -58,55 +68,104 @@ open class M18BomForShopService(
disable(JsonGenerator.Feature.ESCAPE_NON_ASCII) 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). * 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? { 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 udfUnit = bom.uom?.m18Id?.takeIf { it > 0 } ?: return null
val outputQty = bom.outputQty ?: BigDecimal.ZERO val outputQty = bom.outputQty ?: BigDecimal.ZERO
val (udfHarvest, udfHarvestUnit) = resolveUdfHarvestFields(bom, outputQty) val (udfHarvest, udfHarvestUnit) = resolveUdfHarvestFields(bom, outputQty)
val udfEffectiveDate = bom.created?.atZone(m18Tz)?.toInstant()?.toEpochMilli() 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( val header = M18MainUdfBomForShopValue(
id = effectiveHeaderM18Id?.toString(),
code = code,
id = headerM18IdForRequest?.toString(),
code = headerCode,
beId = bomShopMainBeId, beId = bomShopMainBeId,
desc = bom.name ?: bom.description, desc = bom.name ?: bom.description,
descEn = bom.name ?: bom.description, descEn = bom.name ?: bom.description,
udfBomCode = deriveUdfBomCode(code),
rev = deriveRev(code),
udfBomCode = itemCode,
rev = rev,
udfUnit = udfUnit, udfUnit = udfUnit,
udfHarvest = udfHarvest, udfHarvest = udfHarvest,
udfHarvestUnit = udfHarvestUnit, udfHarvestUnit = udfHarvestUnit,
udfEffectiveDate = udfEffectiveDate, udfEffectiveDate = udfEffectiveDate,
udfYieldratePP = bom.yield, udfYieldratePP = bom.yield,
udftypeoffood = "半成品", udftypeoffood = "半成品",
udfconfirmed = true,
staffId = 232, staffId = 232,
flowTypeId = flowTypeId, flowTypeId = flowTypeId,
virDeptId = 117, virDeptId = 117,
status = "Y", 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( 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( return M18BomForShopSaveRequest(
@@ -115,6 +174,110 @@ open class M18BomForShopService(
) )
} }


@Suppress("LongParameterList")
private fun resolveHeaderCodeAndM18Id(
bomId: Long,
itemCode: String,
lines: List<M18UdfProductSaveValue>,
udfUnit: Long,
udfHarvest: String,
udfHarvestUnit: String?,
udfEffectiveDate: Long?,
bomYield: BigDecimal?,
bomName: String?,
bomDescription: String?,
flowTypeId: Int,
headerM18IdOverride: Long?,
bomM18Id: Long?,
): Triple<String, String?, Long?> {
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] * 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. * (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}") logger.warn("[M18 BOM] material UOM code missing bomMaterialId=${mat.id}")
return null 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() val udfqty = (mat.qty ?: BigDecimal.ZERO).setScale(8, RoundingMode.HALF_UP).toDouble()
return M18UdfProductSaveValue( return M18UdfProductSaveValue(
id = mat.m18Id?.takeIf { it > 0 }, id = mat.m18Id?.takeIf { it > 0 },
@@ -167,7 +347,8 @@ open class M18BomForShopService(
udfProduct = proId, udfProduct = proId,
udfIngredients = mat.itemName ?: mat.item?.name, udfIngredients = mat.itemName ?: mat.item?.name,
udfBaseUnit = udfBaseUnit, udfBaseUnit = udfBaseUnit,
udfSupplier = 0L,
udfSupplier = supplierM18Id,
udfpurchaseUnit = purchaseUnitM18Id,
itemNo = String.format("%6d", lineNo), itemNo = String.format("%6d", lineNo),
udfoptions = "", udfoptions = "",
udfoption = 0.0, 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 { private fun resolveFlowTypeId(code: String): Int = when {
code.startsWith("TOA") -> 1 code.startsWith("TOA") -> 1
code.startsWith("BOMPP") || code.startsWith("PP") -> 3 code.startsWith("BOMPP") || code.startsWith("PP") -> 3


+ 3
- 0
src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt ファイルの表示

@@ -495,6 +495,9 @@ open class BomService(
m18BomShopSyncLogRepository.save( m18BomShopSyncLogRepository.save(
M18BomShopSyncLog().apply { M18BomShopSyncLog().apply {
this.bomId = bomId this.bomId = bomId
finishedItemCode = req.udfbomforshop.values.firstOrNull()?.udfBomCode
m18HeaderCode = req.udfbomforshop.values.firstOrNull()?.code
requestFingerprint = m18BomForShopService.contentFingerprint(req)
m18RecordId = recordId.takeIf { it > 0 } m18RecordId = recordId.takeIf { it > 0 }
m18ApiStatus = apiStatus m18ApiStatus = apiStatus
synced = result.synced synced = result.synced


+ 32
- 0
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.core.support.AbstractRepository
import com.ffii.fpsms.modules.purchaseOrder.entity.projections.PurchaseOrderLineInfo import com.ffii.fpsms.modules.purchaseOrder.entity.projections.PurchaseOrderLineInfo
import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderLineStatus 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.jpa.repository.Query
import org.springframework.data.repository.query.Param import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@@ -10,6 +11,37 @@ import java.io.Serializable


@Repository @Repository
interface PurchaseOrderLineRepository : AbstractRepository<PurchaseOrderLine, Long> { interface PurchaseOrderLineRepository : AbstractRepository<PurchaseOrderLine, Long> {
@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<PurchaseOrderLine>

/**
* 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<Long>

fun findByM18DataLogIdAndDeletedIsFalse(m18datalogId: Serializable): PurchaseOrderLine? fun findByM18DataLogIdAndDeletedIsFalse(m18datalogId: Serializable): PurchaseOrderLine?
fun findAllPurchaseOrderLineInfoByPurchaseOrderIdAndDeletedIsFalse(purchaseOrderId: Long): List<PurchaseOrderLineInfo> fun findAllPurchaseOrderLineInfoByPurchaseOrderIdAndDeletedIsFalse(purchaseOrderId: Long): List<PurchaseOrderLineInfo>
fun findAllByPurchaseOrderIdAndDeletedIsFalse(purchaseOrderId: Long): List<PurchaseOrderLine> fun findAllByPurchaseOrderIdAndDeletedIsFalse(purchaseOrderId: Long): List<PurchaseOrderLine>


+ 1
- 1
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`) 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 FROM DUAL
WHERE NOT EXISTS ( WHERE NOT EXISTS (
SELECT 1 FROM `fpsmsdb`.`settings` WHERE `name` = 'SCHEDULE.m18.do2' SELECT 1 FROM `fpsmsdb`.`settings` WHERE `name` = 'SCHEDULE.m18.do2'

+ 7
- 0
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`;

読み込み中…
キャンセル
保存