Ver código fonte

fixing the item_uom with those problem item of uom g and lb

master
Fai Luk 3 dias atrás
pai
commit
9d9523b835
18 arquivos alterados com 510 adições e 17 exclusões
  1. +45
    -0
      src/main/java/com/ffii/fpsms/m18/entity/M18Cunit.kt
  2. +21
    -0
      src/main/java/com/ffii/fpsms/m18/entity/M18CunitRepository.kt
  3. +17
    -1
      src/main/java/com/ffii/fpsms/m18/model/M18MasterDataResponse.kt
  4. +57
    -0
      src/main/java/com/ffii/fpsms/m18/service/M18CunitService.kt
  5. +146
    -5
      src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt
  6. +3
    -0
      src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt
  7. +9
    -0
      src/main/java/com/ffii/fpsms/modules/common/SettingNames.java
  8. +98
    -1
      src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt
  9. +6
    -0
      src/main/java/com/ffii/fpsms/modules/common/scheduler/web/SchedulerController.kt
  10. +8
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/ItemUom.kt
  11. +24
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/ItemUomRespository.kt
  12. +6
    -0
      src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt
  13. +2
    -0
      src/main/java/com/ffii/fpsms/modules/master/web/models/ItemUomRequest.kt
  14. +20
    -9
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt
  15. +8
    -1
      src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt
  16. +23
    -0
      src/main/resources/db/changelog/changes/20260311_fai/01_m18_cunit.sql
  17. +11
    -0
      src/main/resources/db/changelog/changes/20260322_fai/01_m18_units_sync_setting.sql
  18. +6
    -0
      src/main/resources/db/changelog/changes/20260323_fai/01_update_item_uom_add_item_ratios.sql

+ 45
- 0
src/main/java/com/ffii/fpsms/m18/entity/M18Cunit.kt Ver arquivo

@@ -0,0 +1,45 @@
package com.ffii.fpsms.m18.entity

import com.ffii.core.entity.BaseEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Table
import jakarta.validation.constraints.NotNull
import java.math.BigDecimal

/**
* M18 `cunit` lines from GET /root/api/read/unit?id=… (data.cunit[]).
* [ratioN] / [ratioD] here are **swapped** vs M18 JSON: DB ratioN ← JSON ratioD, DB ratioD ← JSON ratioN.
*/
@Entity
@Table(name = "m18_cunit")
open class M18Cunit : BaseEntity<Long>() {

/** M18 unit id (same as unit.id / list value id). */
@NotNull
@Column(name = "m18Id", nullable = false)
open var m18Id: Long = 0L

/** M18 primary key of the cunit row (JSON `id`). */
@NotNull
@Column(name = "m18CunitLineId", nullable = false)
open var m18CunitLineId: Long = 0L

@Column(name = "ratioN", precision = 20, scale = 10)
open var ratioN: BigDecimal? = null

@Column(name = "ratioD", precision = 20, scale = 10)
open var ratioD: BigDecimal? = null

@Column(name = "hId")
open var hId: Long? = null

@Column(name = "unitId")
open var unitId: Long? = null

@Column(name = "iRev")
open var iRev: Int? = null

@Column(name = "itemNo", length = 32)
open var itemNo: String? = null
}

+ 21
- 0
src/main/java/com/ffii/fpsms/m18/entity/M18CunitRepository.kt Ver arquivo

@@ -0,0 +1,21 @@
package com.ffii.fpsms.m18.entity

import com.ffii.core.support.AbstractRepository
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository

@Repository
interface M18CunitRepository : AbstractRepository<M18Cunit, Long> {

@Modifying
@Query("DELETE FROM M18Cunit c WHERE c.m18Id = :m18Id")
fun deleteByM18Id(@Param("m18Id") m18Id: Long): Int

fun findFirstByM18IdAndUnitIdAndDeletedFalse(m18Id: Long, unitId: Long): M18Cunit?

fun findFirstByM18IdAndDeletedFalseOrderByIdAsc(m18Id: Long): M18Cunit?

fun countByDeletedFalse(): Long
}

+ 17
- 1
src/main/java/com/ffii/fpsms/m18/model/M18MasterDataResponse.kt Ver arquivo

@@ -105,15 +105,20 @@ data class M18UnitListValue (
)

/** Unit Response */
@JsonIgnoreProperties(ignoreUnknown = true)
data class M18UnitResponse (
val data: M18UnitData?,
val messages: List<M18ErrorMessages>?
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class M18UnitData (
val unit: List<M18UnitUnit>
val unit: List<M18UnitUnit>,
/** Conversion-unit lines; ratios are swapped when persisted to [com.ffii.fpsms.m18.entity.M18Cunit]. */
val cunit: List<M18UnitCunit>? = null,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class M18UnitUnit (
val id: Long,
val expiredDate: Long,
@@ -124,6 +129,17 @@ data class M18UnitUnit (
val status: String,
)

@JsonIgnoreProperties(ignoreUnknown = true)
data class M18UnitCunit (
val id: Long,
val hId: Long? = null,
val ratioN: BigDecimal? = null,
val ratioD: BigDecimal? = null,
val unitId: Long? = null,
val iRev: Int? = null,
val itemNo: String? = null,
)

/** Currency List Response */
data class M18CurrencyListResponse (
val values: List<M18CurrencyListValue>?,


+ 57
- 0
src/main/java/com/ffii/fpsms/m18/service/M18CunitService.kt Ver arquivo

@@ -0,0 +1,57 @@
package com.ffii.fpsms.m18.service

import com.ffii.fpsms.m18.entity.M18Cunit
import com.ffii.fpsms.m18.entity.M18CunitRepository
import com.ffii.fpsms.m18.model.M18UnitData
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal

/**
* Persists M18 `cunit` lines from GET /root/api/read/unit.
* DB [M18Cunit.ratioN] ← JSON `ratioD`, DB [M18Cunit.ratioD] ← JSON `ratioN`.
*/
@Service
open class M18CunitService(
private val m18CunitRepository: M18CunitRepository,
) {
data class UnitRatios(
val ratioN: BigDecimal?,
val ratioD: BigDecimal?,
)

@Transactional
open fun replaceForUnit(m18Id: Long, data: M18UnitData) {
m18CunitRepository.deleteByM18Id(m18Id)
data.cunit.orEmpty().forEach { c ->
val row = M18Cunit().apply {
this.m18Id = m18Id
m18CunitLineId = c.id
ratioN = c.ratioD
ratioD = c.ratioN
hId = c.hId
unitId = c.unitId
iRev = c.iRev
itemNo = c.itemNo?.trim()?.ifBlank { null }
}
m18CunitRepository.save(row)
}
}

open fun countActiveRows(): Long {
return m18CunitRepository.countByDeletedFalse()
}

/**
* Resolve unit conversion ratio by M18 unit id from m18_cunit.
* Prefer exact row m18Id+unitId; fallback to first row under m18Id.
*/
open fun resolveRatiosByM18UnitId(unitM18Id: Long): UnitRatios? {
val exact = m18CunitRepository.findFirstByM18IdAndUnitIdAndDeletedFalse(unitM18Id, unitM18Id)
if (exact != null) {
return UnitRatios(ratioN = exact.ratioN, ratioD = exact.ratioD)
}
val fallback = m18CunitRepository.findFirstByM18IdAndDeletedFalseOrderByIdAsc(unitM18Id)
return fallback?.let { UnitRatios(ratioN = it.ratioN, ratioD = it.ratioD) }
}
}

+ 146
- 5
src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt Ver arquivo

@@ -33,11 +33,15 @@ open class M18MasterDataService(
val itemUomService: ItemUomService,
val bomService: BomService,
val bomMaterialService: BomMaterialService,
val m18CunitService: M18CunitService,
) {

val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java)
val commonUtils = CommonUtils()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
private val one = BigDecimal.ONE
private val ratio2268 = BigDecimal("2268")
private val ratio5 = BigDecimal("5")

// M18 Conditions
// val lastModifyDate = LocalDate.now().minusDays(1)
@@ -61,6 +65,49 @@ open class M18MasterDataService(
val M18_LOAD_BOM_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.BOM.value}"
val M18_LOAD_BUSINESS_UNIT_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.BUSINESS_UNIT.value}" // for shop po?

private fun isBothOne(ratioN: BigDecimal?, ratioD: BigDecimal?): Boolean {
return ratioN?.compareTo(one) == 0 && ratioD?.compareTo(one) == 0
}

private fun isPair2268And5(ratioN: BigDecimal?, ratioD: BigDecimal?): Boolean {
return ratioN?.compareTo(ratio2268) == 0 && ratioD?.compareTo(ratio5) == 0
}

/**
* Problematic ratio cases that should be corrected by m18_cunit:
* 1) uomId=4 and ratioN/ratioD are not both 1
* 2) uomId=785 and ratioN/ratioD do not contain 453
*/
private fun shouldSyncRatioFromCunit(unitId: Long, ratioN: BigDecimal?, ratioD: BigDecimal?): Boolean {
val caseUom4 = unitId == 4L && !isBothOne(ratioN, ratioD)
val caseUom785 = unitId == 785L && !isPair2268And5(ratioN, ratioD)
return caseUom4 || caseUom785
}

/**
* Same rule as [shouldSyncRatioFromCunit], but based on local uom_conversion.id.
*/
private fun shouldSyncRatioFromCunitByLocalUomId(localUomId: Long?, ratioN: BigDecimal?, ratioD: BigDecimal?): Boolean {
if (localUomId == null) return false
val caseUom4 = localUomId == 4L && !isBothOne(ratioN, ratioD)
val caseUom785 = localUomId == 785L && !isPair2268And5(ratioN, ratioD)
return caseUom4 || caseUom785
}

/**
* Safety net: when m18_cunit has no rows, force a full unit sync first so cunit ratios are available.
*/
open fun ensureCunitSeededForAllIfEmpty() {
val cunitRows = m18CunitService.countActiveRows()
if (cunitRows > 0L) return
logger.warn("m18_cunit has 0 rows; triggering full unit sync first to seed cunit data for all units.")
val result = saveUnits(M18CommonRequest())
logger.warn(
"m18_cunit seed sync finished: processed=${result.totalProcessed}, " +
"success=${result.totalSuccess}, fail=${result.totalFail}"
)
}

// --------------------------------------------- Common Function --------------------------------------------- ///
private inline fun <reified T : Any> getList(
stSearch: String?,
@@ -143,6 +190,7 @@ open class M18MasterDataService(

open fun saveProduct(id: Long): MessageResponse? {
try {
ensureCunitSeededForAllIfEmpty()
val itemDetail = getProduct(id)
val pro = itemDetail?.data?.pro?.get(0)
val price = itemDetail?.data?.price
@@ -197,7 +245,23 @@ open class M18MasterDataService(

// Update the item uom
logger.info("Updating item uom...")
val hasBaseUom4 = price?.any { p ->
p.basicUnit && (uomConversionService.findByM18Id(p.unitId)?.id == 4L)
} == true
val forceCunitForAllRows = hasBaseUom4 && (price?.any { p ->
val localBaseId = uomConversionService.findByM18Id(p.unitId)?.id
shouldSyncRatioFromCunitByLocalUomId(localBaseId, p.ratioN, p.ratioD)
} == true)
price?.forEach {
val localUomId = uomConversionService.findByM18Id(it.unitId)?.id
val keepOriginalItemRatio = forceCunitForAllRows
val useUnitRatios = forceCunitForAllRows
val unitRatios = if (useUnitRatios) m18CunitService.resolveRatiosByM18UnitId(it.unitId) else null
logger.info(
"ItemUom ratio apply (single): itemM18Id=$id m18UomId=${it.unitId} localUomId=$localUomId " +
"hasBaseUom4=$hasBaseUom4 forceCunitForAllRows=$forceCunitForAllRows useCunit=$useUnitRatios oldRatioN=${it.ratioN} oldRatioD=${it.ratioD} " +
"cunitRatioN=${unitRatios?.ratioN} cunitRatioD=${unitRatios?.ratioD}"
)
val itemUomRequest = ItemUomRequest(
m18UomId = it.unitId,
itemId = savedItem.id,
@@ -210,8 +274,10 @@ open class M18MasterDataService(
currencyId = null,
m18Id = it.id,
m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate),
ratioD = it.ratioD,
ratioN = it.ratioN,
ratioD = if (useUnitRatios) (unitRatios?.ratioD ?: it.ratioD) else it.ratioD,
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
)

@@ -236,6 +302,7 @@ open class M18MasterDataService(

open fun saveProducts(request: M18CommonRequest): SyncResult {
logger.info("--------------------------------------------Start - Saving M18 Products / Materials--------------------------------------------")
ensureCunitSeededForAllIfEmpty()

val items = getProducts(request)
val exampleProducts = listOf<Long>(10946L, 3825L)
@@ -310,10 +377,26 @@ open class M18MasterDataService(

// Update / create UOMs from M18
logger.info("Updating item uom...")
val hasBaseUom4 = price?.any { p ->
p.basicUnit && (uomConversionService.findByM18Id(p.unitId)?.id == 4L)
} == true
val forceCunitForAllRows = hasBaseUom4 && (price?.any { p ->
val localBaseId = uomConversionService.findByM18Id(p.unitId)?.id
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
val unitRatios = if (useUnitRatios) m18CunitService.resolveRatiosByM18UnitId(it.unitId) else null
logger.info(
"ItemUom ratio apply (bulk): itemM18Id=${item.id} m18UomId=${it.unitId} localUomId=$localUomId " +
"hasBaseUom4=$hasBaseUom4 forceCunitForAllRows=$forceCunitForAllRows useCunit=$useUnitRatios oldRatioN=${it.ratioN} oldRatioD=${it.ratioD} " +
"cunitRatioN=${unitRatios?.ratioN} cunitRatioD=${unitRatios?.ratioD}"
)

val itemUomRequest = ItemUomRequest(
m18UomId = it.unitId,
@@ -327,8 +410,10 @@ open class M18MasterDataService(
currencyId = null,
m18Id = it.id,
m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate),
ratioD = it.ratioD,
ratioN = it.ratioN,
ratioD = if (useUnitRatios) (unitRatios?.ratioD ?: it.ratioD) else it.ratioD,
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)
)

@@ -368,6 +453,49 @@ open class M18MasterDataService(
)
}

/**
* Targeted re-sync for locally identified problematic item_uom ratios:
* - uomId = 4 and ratioN != 1
* - uomId = 785 and ratioN != 453
*
* Uses local item_uom + item.m18Id to fetch only affected products from M18 and re-save their item_uom.
*/
open fun resyncProblemItemUomsFromM18(): SyncResult {
logger.info("--------------------------------------------Start - Re-sync Problem Item UOMs--------------------------------------------")
val m18Ids = itemUomService.findDistinctProblemItemM18IdsForUomResync().distinct().sorted()
val successList = mutableListOf<Long>()
val failList = mutableListOf<Long>()
logger.warn("Problem item_uom selector found ${m18Ids.size} item(s) for re-sync.")
if (m18Ids.isNotEmpty()) {
logger.warn("Problem item m18Id sample (up to 20): ${m18Ids.take(20)}")
}

m18Ids.forEach { m18ItemId ->
try {
logger.warn("Re-sync item_uom: calling M18 product API for id=$m18ItemId")
val result = saveProduct(m18ItemId)
if (result != null) {
successList.add(m18ItemId)
logger.warn("Re-sync item_uom: M18 API sync success for id=$m18ItemId")
} else {
failList.add(m18ItemId)
logger.warn("Re-sync item_uom: M18 API sync returned null for id=$m18ItemId")
}
} catch (e: Exception) {
failList.add(m18ItemId)
logger.error("Re-sync problem item uom failed for m18ItemId=$m18ItemId: ${e.message}", e)
}
}

logger.info("Problem item_uom re-sync done. total=${m18Ids.size}, success=${successList.size}, fail=${failList.size}")
logger.info("--------------------------------------------End - Re-sync Problem Item UOMs--------------------------------------------")
return SyncResult(
totalProcessed = m18Ids.size,
totalSuccess = successList.size,
totalFail = failList.size
)
}

// --------------------------------------------- Vendor --------------------------------------------- ///
open fun getVendors(request: M18CommonRequest): M18VendorListResponse? {
return getList<M18VendorListResponse>(
@@ -477,7 +605,7 @@ open class M18MasterDataService(
)
}

open fun saveUnits(request: M18CommonRequest) {
open fun saveUnits(request: M18CommonRequest): SyncResult {
logger.info("--------------------------------------------Start - Saving M18 Units--------------------------------------------")
val units = getUnits(request)

@@ -497,6 +625,11 @@ open class M18MasterDataService(

if (unitDetail != null && unitDetail.data?.unit != null) {
val unit = unitDetail.data.unit[0]
try {
m18CunitService.replaceForUnit(unit.id, unitDetail.data!!)
} catch (e: Exception) {
logger.error("M18 cunit save failed for unit ${unit.id}: ${e.message}", e)
}
val tempObject = UomConversionService.BomObject().apply {
code = unit.code
udfudesc = unit.udfudesc
@@ -549,6 +682,14 @@ open class M18MasterDataService(
logger.error("Total Save Fail (${failSaveList.size}): $failSaveList")
}
logger.info("--------------------------------------------End - Saving M18 Units--------------------------------------------")
val processed = values?.size ?: 0
val failed = failTransformList.size + failSaveList.size
return SyncResult(
totalProcessed = processed,
totalSuccess = successSaveList.size,
totalFail = failed,
query = "stSearch=${StSearchType.UNIT.value}",
)
}

// --------------------------------------------- Currency --------------------------------------------- ///


+ 3
- 0
src/main/java/com/ffii/fpsms/m18/web/M18TestController.kt Ver arquivo

@@ -83,6 +83,7 @@ class M18TestController (
// Master Data
m18MasterDataService.saveUnits(mdRequest)
m18MasterDataService.saveProducts(mdRequest)
m18MasterDataService.resyncProblemItemUomsFromM18()
m18MasterDataService.saveVendors(mdRequest)
m18MasterDataService.saveBusinessUnits(mdRequest)
m18MasterDataService.saveCurrencies(mdRequest)
@@ -96,8 +97,10 @@ class M18TestController (
@PostMapping("/master-data")
fun m18MasterData(@Valid @RequestBody request: M18CommonRequest) {
// Master Data
m18MasterDataService.ensureCunitSeededForAllIfEmpty()
m18MasterDataService.saveUnits(request)
m18MasterDataService.saveProducts(request)
m18MasterDataService.resyncProblemItemUomsFromM18()
m18MasterDataService.saveVendors(request)
m18MasterDataService.saveBusinessUnits(request)
m18MasterDataService.saveCurrencies(request)


+ 9
- 0
src/main/java/com/ffii/fpsms/modules/common/SettingNames.java Ver arquivo

@@ -30,6 +30,15 @@ public abstract class SettingNames {

public static final String SCHEDULE_M18_MASTER = "SCHEDULE.m18.master";

/** M18 unit master sync via GET /search/search?stSearch=unit (cron, e.g. "0 40 12 * * *" for 12:40 daily) */
public static final String SCHEDULE_M18_UNITS = "SCHEDULE.m18.units";

/**
* After first successful full unit sync (no lastModifyDate filter), set to "true".
* While "false", unit sync fetches all units; once "true", sync uses rolling lastModifyDate window only.
*/
public static final String M18_UNITS_SYNC_INITIAL_FULL_SYNC_DONE = "M18.units.sync.initialFullSyncDone";

/** Post completed DN and process M18 GRN (cron, e.g. "0 40 23 * * *" for 23:40 daily) */
public static final String SCHEDULE_POST_COMPLETED_DN_GRN = "SCHEDULE.postCompletedDn.grn";



+ 98
- 1
src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt Ver arquivo

@@ -12,6 +12,7 @@ import com.ffii.fpsms.m18.model.SyncResult
import com.ffii.fpsms.modules.common.SettingNames
import com.ffii.fpsms.modules.master.service.ProductionScheduleService
import com.ffii.fpsms.modules.stock.service.SearchCompletedDnService
import com.ffii.fpsms.modules.settings.entity.Settings
import com.ffii.fpsms.modules.settings.service.SettingsService
import jakarta.annotation.PostConstruct
import org.springframework.beans.factory.annotation.Value
@@ -37,6 +38,12 @@ open class SchedulerService(
* missing `grn_code`. Example: 4 = from 4 days ago 00:00 to now.
*/
@Value("\${scheduler.grnCodeSync.syncOffsetDays:0}") val grnCodeSyncSyncOffsetDays: Int,
@Value("\${scheduler.m18Units.enabled:true}") val m18UnitsSchedulerEnabled: Boolean,
/**
* After [SettingNames.M18_UNITS_SYNC_INITIAL_FULL_SYNC_DONE] is true, unit search uses
* lastModifyDate from **start of (today − N days)** through **end of today** (inclusive).
*/
@Value("\${scheduler.m18Units.incrementalLookbackDays:3}") val m18UnitsIncrementalLookbackDays: Int,
val settingsService: SettingsService,
val taskScheduler: TaskScheduler,
val productionScheduleService: ProductionScheduleService,
@@ -61,6 +68,8 @@ open class SchedulerService(
@Volatile
var scheduledM18Master: ScheduledFuture<*>? = null

var scheduledM18Units: ScheduledFuture<*>? = null

var scheduledPostCompletedDnGrn: ScheduledFuture<*>? = null

var scheduledGrnCodeSync: ScheduledFuture<*>? = null
@@ -146,6 +155,26 @@ open class SchedulerService(
commonSchedule(scheduledM18Master, SettingNames.SCHEDULE_M18_MASTER, ::getM18MasterData)
}

/**
* Daily sync of M18 units via GET /search/search?stSearch=unit (list + full line per id).
* Cron from settings [SettingNames.SCHEDULE_M18_UNITS] (default 12:40 local server time).
* Set scheduler.m18Units.enabled=false to disable.
*/
fun scheduleM18Units() {
if (!m18UnitsSchedulerEnabled) {
scheduledM18Units?.cancel(false)
scheduledM18Units = null
logger.info("M18 units scheduler disabled (scheduler.m18Units.enabled=false)")
return
}
commonSchedule(
scheduledM18Units,
SettingNames.SCHEDULE_M18_UNITS,
"0 40 12 * * *",
::getM18Units,
)
}

private fun getPostCompletedDnGrnReceiptDate(): LocalDate {
val s = postCompletedDnGrnReceiptDate.trim()
return if (s.isEmpty()) LocalDate.now().minusDays(1)
@@ -372,6 +401,7 @@ open class SchedulerService(

open fun getM18MasterData() {
logger.info("Daily Scheduler - Master Data")
m18MasterDataService.ensureCunitSeededForAllIfEmpty()
var currentTime = LocalDateTime.now()
val today = currentTime.toLocalDate().atStartOfDay()
val yesterday = today.minusDays(1L)
@@ -379,8 +409,17 @@ open class SchedulerService(
modifiedDateTo = today.format(dataStringFormat),
modifiedDateFrom = yesterday.format(dataStringFormat)
)
m18MasterDataService.saveUnits(request)

// Keep units as part of master-data sync and run it first with the same modified-date window.
val resultUnits = m18MasterDataService.saveUnits(request)
saveSyncLog(
type = "Units",
status = "SUCCESS",
result = resultUnits,
start = currentTime
)

currentTime = LocalDateTime.now()
val resultProducts = m18MasterDataService.saveProducts(request)
saveSyncLog(
type = "Products",
@@ -388,6 +427,14 @@ open class SchedulerService(
result = resultProducts,
start = currentTime
)
currentTime = LocalDateTime.now()
val resultProblemItemUomResync = m18MasterDataService.resyncProblemItemUomsFromM18()
saveSyncLog(
type = "ProductsUomResync",
status = "SUCCESS",
result = resultProblemItemUomResync,
start = currentTime
)
// m18MasterDataService.saveBoms(request)
currentTime = LocalDateTime.now()
val resultVendors = m18MasterDataService.saveVendors(request)
@@ -418,4 +465,54 @@ open class SchedulerService(

}

/**
* M18 units: first run (until setting [SettingNames.M18_UNITS_SYNC_INITIAL_FULL_SYNC_DONE] is true)
* calls search with **no** lastModifyDate filter (full list). After a successful full run, that flag is set
* and subsequent runs use lastModifyDate from **(today − [m18UnitsIncrementalLookbackDays])** through **today**.
*/
open fun getM18Units() {
logger.info("Scheduler - M18 units (search/search?stSearch=unit)")
val currentTime = LocalDateTime.now()
val today = currentTime.toLocalDate().atStartOfDay()
val todayStr = today.format(dataStringFormat)

val initialFullSyncDone = settingsService.findByName(SettingNames.M18_UNITS_SYNC_INITIAL_FULL_SYNC_DONE)
.map { Settings.VALUE_BOOLEAN_TRUE == it.value }
.orElse(false)

val request = if (initialFullSyncDone) {
val fromStr = today.minusDays(m18UnitsIncrementalLookbackDays.toLong()).format(dataStringFormat)
logger.info(
"M18 units sync mode=INCREMENTAL lastModifyDate from=$fromStr to=$todayStr " +
"(lookbackDays=$m18UnitsIncrementalLookbackDays)",
)
M18CommonRequest(
modifiedDateFrom = fromStr,
modifiedDateTo = todayStr,
)
} else {
logger.info("M18 units sync mode=FULL (no lastModifyDate filter; first successful run will switch to incremental)")
M18CommonRequest(
modifiedDateFrom = null,
modifiedDateTo = null,
)
}

val result = m18MasterDataService.saveUnits(request)
saveSyncLog(
type = "Units",
status = "SUCCESS",
result = result,
start = currentTime,
)

if (!initialFullSyncDone && result.totalFail == 0) {
settingsService.update(SettingNames.M18_UNITS_SYNC_INITIAL_FULL_SYNC_DONE, Settings.VALUE_BOOLEAN_TRUE)
logger.info(
"M18 units: initial full sync finished with no failures; " +
"setting ${SettingNames.M18_UNITS_SYNC_INITIAL_FULL_SYNC_DONE}=true — next runs use incremental window.",
)
}
}
}

+ 6
- 0
src/main/java/com/ffii/fpsms/modules/common/scheduler/web/SchedulerController.kt Ver arquivo

@@ -55,6 +55,12 @@ class SchedulerController(
return "M18 Master Data Sync Triggered Successfully"
}

@GetMapping("/trigger/m18-units")
fun triggerM18Units(): String {
schedulerService.getM18Units()
return "M18 units sync (/search/search?stSearch=unit) triggered"
}

@GetMapping("/trigger/grn-code-sync")
fun triggerGrnCodeSync(): String {
schedulerService.syncGrnCodesFromM18()


+ 8
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/ItemUom.kt Ver arquivo

@@ -63,4 +63,12 @@ open class ItemUom : BaseEntity<Long>() {
@NotNull
@Column(name = "ratioD", nullable = false, precision = 14, scale = 2)
open var ratioD: BigDecimal? = null

/** Original item-level ratio from M18 product price line. */
@Column(name = "itemRatioN", precision = 14, scale = 2)
open var itemRatioN: BigDecimal? = null

/** Original item-level ratio from M18 product price line. */
@Column(name = "itemRatioD", precision = 14, scale = 2)
open var itemRatioD: BigDecimal? = null
}

+ 24
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/ItemUomRespository.kt Ver arquivo

@@ -29,4 +29,28 @@ interface ItemUomRespository : AbstractRepository<ItemUom, Long> {
fun findByItemIdAndUomIdAndStockUnitAndDeletedIsFalse(itemId: Long, uomId: Long, stockUnit: Boolean): ItemUom?

fun findFirstByItemIdAndStockUnitIsTrueAndDeletedIsFalseOrderByIdAsc(itemId: Long): ItemUom?

@Query(
"""
SELECT DISTINCT iu.item.m18Id
FROM ItemUom iu
WHERE iu.deleted = false
AND iu.item.deleted = false
AND iu.item.m18Id IS NOT NULL
AND EXISTS (
SELECT 1
FROM ItemUom iub
WHERE iub.item.id = iu.item.id
AND iub.deleted = false
AND iub.baseUnit = true
AND iub.uom.id = 4
)
AND (
(iu.uom.id = 4 AND NOT (iu.ratioN = 1 AND iu.ratioD = 1))
OR
(iu.uom.id = 785 AND NOT (iu.ratioN = 2268 AND iu.ratioD = 5))
)
"""
)
fun findDistinctProblemItemM18IdsForUomResync(): List<Long>
}

+ 6
- 0
src/main/java/com/ffii/fpsms/modules/master/service/ItemUomService.kt Ver arquivo

@@ -53,6 +53,10 @@ open class ItemUomService(
return itemUomRespository.findByItemIdAndUomIdAndStockUnitAndDeletedIsFalse(itemId, uomId, stockUnit)
}

open fun findDistinctProblemItemM18IdsForUomResync(): List<Long> {
return itemUomRespository.findDistinctProblemItemM18IdsForUomResync()
}

open fun findPurchaseUnitByM18ItemId(m18ItemId: Long): ItemUom? {
return itemUomRespository.findByItemM18IdAndPurchaseUnitIsTrueAndDeletedIsFalse(m18ItemId)
}
@@ -227,6 +231,8 @@ open class ItemUomService(
m18LastModifyDate = request.m18LastModifyDate
ratioD = request.ratioD
ratioN = request.ratioN
itemRatioD = request.itemRatioD
itemRatioN = request.itemRatioN
deleted = request.deleted // ← EXPLICIT assignment (same as before)
}



+ 2
- 0
src/main/java/com/ffii/fpsms/modules/master/web/models/ItemUomRequest.kt Ver arquivo

@@ -26,6 +26,8 @@ data class ItemUomRequest(
val m18LastModifyDate: LocalDateTime?,
val ratioD: BigDecimal?,
val ratioN: BigDecimal?,
val itemRatioD: BigDecimal?,
val itemRatioN: BigDecimal?,
val deleted: Boolean?,
)
data class BomFormatIssue(


+ 20
- 9
src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt Ver arquivo

@@ -1,4 +1,4 @@
package com.ffii.fpsms.modules.report.service
package com.ffii.fpsms.modules.report.service

import org.springframework.stereotype.Service
import net.sf.jasperreports.engine.*
@@ -747,7 +747,7 @@ return result
fun searchGrnReport(
receiptDateStart: String?,
receiptDateEnd: String?,
itemCode: String?,
itemCode: String?,
supplier: String?,
poCode: String?,
grnCode: String?,
@@ -763,10 +763,10 @@ return result
args["receiptDateEnd"] = formatted
"AND DATE(sil.receiptDate) <= DATE(:receiptDateEnd)"
} else ""
val itemSql = buildMultiValueCodeOrNameLikeClause(itemCode, "it.code", "it.name", "grnItem", args)
val supplierSql = buildMultiValueCodeOrNameLikeClause(supplier, "sp.code", "sp.name", "grnSupp", args)
val poCodeSql = buildMultiValueLikeClause(poCode, "po.code", "grnPo", args)
val grnExistsSql = buildMultiValueGrnExistsClause(grnCode, "grnG", args)
val itemSql = buildMultiValueCodeOrNameLikeClause(itemCode, "it.code", "it.name", "listedItem", args)
val supplierSql = buildMultiValueCodeOrNameLikeClause(supplier, "sp.code", "sp.name", "listedSupp", args)
val poCodeSql = buildMultiValueLikeClause(poCode, "po.code", "listedPo", args)
val grnExistsSql = buildMultiValueGrnExistsClause(grnCode, "listedGrn", args)

val sql = """
SELECT
@@ -866,7 +866,10 @@ return result
fun searchGrnListedPoAmounts(
receiptDateStart: String?,
receiptDateEnd: String?,
itemCode: String?
itemCode: String?,
supplier: String?,
poCode: String?,
grnCode: String?,
): Map<String, Any> {
val args = mutableMapOf<String, Any>()
val receiptDateStartSql = if (!receiptDateStart.isNullOrBlank()) {
@@ -879,7 +882,10 @@ return result
args["receiptDateEnd"] = formatted
"AND DATE(sil.receiptDate) <= DATE(:receiptDateEnd)"
} else ""
val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args)
val itemSql = buildMultiValueCodeOrNameLikeClause(itemCode, "it.code", "it.name", "listedItem", args)
val supplierSql = buildMultiValueCodeOrNameLikeClause(supplier, "sp.code", "sp.name", "listedSupp", args)
val poCodeSql = buildMultiValueLikeClause(poCode, "po.code", "listedPo", args)
val grnExistsSql = buildMultiValueGrnExistsClause(grnCode, "listedGrn", args)

val lineSubquery = """
SELECT
@@ -889,6 +895,8 @@ return result
ROUND(COALESCE(pol.up, 0) * COALESCE(sil.acceptedQty, 0), 2) AS line_amt
FROM stock_in_line sil
INNER JOIN purchase_order_line pol ON sil.purchaseOrderLineId = pol.id
INNER JOIN purchase_order po ON sil.purchaseOrderId = po.id
LEFT JOIN shop sp ON po.supplierId = sp.id
LEFT JOIN items it ON sil.itemId = it.id
WHERE sil.deleted = false
AND sil.receiptDate IS NOT NULL
@@ -896,7 +904,10 @@ return result
AND pol.status = 'completed'
$receiptDateStartSql
$receiptDateEndSql
$itemCodeSql
$itemSql
$supplierSql
$poCodeSql
$grnExistsSql
""".trimIndent()

val currencySql = """


+ 8
- 1
src/main/java/com/ffii/fpsms/modules/report/web/ReportController.kt Ver arquivo

@@ -255,7 +255,14 @@ class ReportController(
}
val listedPoAmounts =
if (isAdmin) {
reportService.searchGrnListedPoAmounts(receiptDateStart, receiptDateEnd, itemCode)
reportService.searchGrnListedPoAmounts(
receiptDateStart,
receiptDateEnd,
itemCode,
supplier,
poCode,
grnCode,
)
} else {
mapOf(
"currencyTotals" to emptyList<Map<String, Any?>>(),


+ 23
- 0
src/main/resources/db/changelog/changes/20260311_fai/01_m18_cunit.sql Ver arquivo

@@ -0,0 +1,23 @@
--liquibase formatted sql
--changeset fai:20260311_m18_cunit

CREATE TABLE `m18_cunit` (
`id` bigint NOT NULL AUTO_INCREMENT,
`version` int NOT NULL DEFAULT '0',
`created` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`createdBy` varchar(30) DEFAULT NULL,
`modified` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
`modifiedBy` varchar(30) DEFAULT NULL,
`deleted` tinyint(1) NOT NULL DEFAULT '0',
`m18Id` bigint NOT NULL COMMENT 'M18 unit id (parent unit from read/unit)',
`m18CunitLineId` bigint NOT NULL COMMENT 'M18 cunit row id',
`ratioN` decimal(20, 10) DEFAULT NULL COMMENT 'Stored from M18 ratioD (swapped)',
`ratioD` decimal(20, 10) DEFAULT NULL COMMENT 'Stored from M18 ratioN (swapped)',
`hId` bigint DEFAULT NULL,
`unitId` bigint DEFAULT NULL,
`iRev` int DEFAULT NULL,
`itemNo` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_m18_cunit_line` (`m18CunitLineId`),
KEY `idx_m18_cunit_m18Id` (`m18Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

+ 11
- 0
src/main/resources/db/changelog/changes/20260322_fai/01_m18_units_sync_setting.sql Ver arquivo

@@ -0,0 +1,11 @@
--liquibase formatted sql
--changeset fai:20260322_m18_units_sync_setting

INSERT INTO `settings` (`name`, `value`, `category`, `type`)
SELECT v.name, v.value, v.category, v.type
FROM (
SELECT 'M18.units.sync.initialFullSyncDone' AS name, 'false' AS value, 'M18' AS category, 'boolean' AS type
) v
WHERE NOT EXISTS (
SELECT 1 FROM `settings` s WHERE s.name = 'M18.units.sync.initialFullSyncDone'
);

+ 6
- 0
src/main/resources/db/changelog/changes/20260323_fai/01_update_item_uom_add_item_ratios.sql Ver arquivo

@@ -0,0 +1,6 @@
--liquibase formatted sql
--changeset fai:20260323_item_uom_item_ratios

ALTER TABLE `item_uom`
ADD COLUMN `itemRatioN` DECIMAL(14,2) NULL AFTER `ratioD`,
ADD COLUMN `itemRatioD` DECIMAL(14,2) NULL AFTER `itemRatioN`;

Carregando…
Cancelar
Salvar