| @@ -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 | |||||
| } | |||||
| @@ -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 | |||||
| } | |||||
| @@ -105,15 +105,20 @@ data class M18UnitListValue ( | |||||
| ) | ) | ||||
| /** Unit Response */ | /** Unit Response */ | ||||
| @JsonIgnoreProperties(ignoreUnknown = true) | |||||
| data class M18UnitResponse ( | data class M18UnitResponse ( | ||||
| val data: M18UnitData?, | val data: M18UnitData?, | ||||
| val messages: List<M18ErrorMessages>? | val messages: List<M18ErrorMessages>? | ||||
| ) | ) | ||||
| @JsonIgnoreProperties(ignoreUnknown = true) | |||||
| data class M18UnitData ( | 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 ( | data class M18UnitUnit ( | ||||
| val id: Long, | val id: Long, | ||||
| val expiredDate: Long, | val expiredDate: Long, | ||||
| @@ -124,6 +129,17 @@ data class M18UnitUnit ( | |||||
| val status: String, | 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 */ | /** Currency List Response */ | ||||
| data class M18CurrencyListResponse ( | data class M18CurrencyListResponse ( | ||||
| val values: List<M18CurrencyListValue>?, | val values: List<M18CurrencyListValue>?, | ||||
| @@ -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) } | |||||
| } | |||||
| } | |||||
| @@ -33,11 +33,15 @@ open class M18MasterDataService( | |||||
| val itemUomService: ItemUomService, | val itemUomService: ItemUomService, | ||||
| val bomService: BomService, | val bomService: BomService, | ||||
| val bomMaterialService: BomMaterialService, | val bomMaterialService: BomMaterialService, | ||||
| val m18CunitService: M18CunitService, | |||||
| ) { | ) { | ||||
| val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) | val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) | ||||
| val commonUtils = CommonUtils() | val commonUtils = CommonUtils() | ||||
| val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") | 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 | // M18 Conditions | ||||
| // val lastModifyDate = LocalDate.now().minusDays(1) | // 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_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? | 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 --------------------------------------------- /// | // --------------------------------------------- Common Function --------------------------------------------- /// | ||||
| private inline fun <reified T : Any> getList( | private inline fun <reified T : Any> getList( | ||||
| stSearch: String?, | stSearch: String?, | ||||
| @@ -143,6 +190,7 @@ open class M18MasterDataService( | |||||
| open fun saveProduct(id: Long): MessageResponse? { | open fun saveProduct(id: Long): MessageResponse? { | ||||
| try { | try { | ||||
| ensureCunitSeededForAllIfEmpty() | |||||
| val itemDetail = getProduct(id) | val itemDetail = getProduct(id) | ||||
| val pro = itemDetail?.data?.pro?.get(0) | val pro = itemDetail?.data?.pro?.get(0) | ||||
| val price = itemDetail?.data?.price | val price = itemDetail?.data?.price | ||||
| @@ -197,7 +245,23 @@ open class M18MasterDataService( | |||||
| // Update the item uom | // Update the item uom | ||||
| logger.info("Updating 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 { | 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( | val itemUomRequest = ItemUomRequest( | ||||
| m18UomId = it.unitId, | m18UomId = it.unitId, | ||||
| itemId = savedItem.id, | itemId = savedItem.id, | ||||
| @@ -210,8 +274,10 @@ open class M18MasterDataService( | |||||
| currencyId = null, | currencyId = null, | ||||
| m18Id = it.id, | m18Id = it.id, | ||||
| m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate), | 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 | deleted = it.expired | ||||
| ) | ) | ||||
| @@ -236,6 +302,7 @@ open class M18MasterDataService( | |||||
| open fun saveProducts(request: M18CommonRequest): SyncResult { | open fun saveProducts(request: M18CommonRequest): SyncResult { | ||||
| logger.info("--------------------------------------------Start - Saving M18 Products / Materials--------------------------------------------") | logger.info("--------------------------------------------Start - Saving M18 Products / Materials--------------------------------------------") | ||||
| ensureCunitSeededForAllIfEmpty() | |||||
| val items = getProducts(request) | val items = getProducts(request) | ||||
| val exampleProducts = listOf<Long>(10946L, 3825L) | val exampleProducts = listOf<Long>(10946L, 3825L) | ||||
| @@ -310,10 +377,26 @@ open class M18MasterDataService( | |||||
| // Update / create UOMs from M18 | // Update / create UOMs from M18 | ||||
| logger.info("Updating 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 { | price?.forEach { | ||||
| val endMillis = it.endDate | val endMillis = it.endDate | ||||
| val endInstant = Instant.ofEpochMilli(endMillis) | val endInstant = Instant.ofEpochMilli(endMillis) | ||||
| val now = Instant.now() | 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( | val itemUomRequest = ItemUomRequest( | ||||
| m18UomId = it.unitId, | m18UomId = it.unitId, | ||||
| @@ -327,8 +410,10 @@ open class M18MasterDataService( | |||||
| currencyId = null, | currencyId = null, | ||||
| m18Id = it.id, | m18Id = it.id, | ||||
| m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate), | 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) | 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 --------------------------------------------- /// | // --------------------------------------------- Vendor --------------------------------------------- /// | ||||
| open fun getVendors(request: M18CommonRequest): M18VendorListResponse? { | open fun getVendors(request: M18CommonRequest): M18VendorListResponse? { | ||||
| return getList<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--------------------------------------------") | logger.info("--------------------------------------------Start - Saving M18 Units--------------------------------------------") | ||||
| val units = getUnits(request) | val units = getUnits(request) | ||||
| @@ -497,6 +625,11 @@ open class M18MasterDataService( | |||||
| if (unitDetail != null && unitDetail.data?.unit != null) { | if (unitDetail != null && unitDetail.data?.unit != null) { | ||||
| val unit = unitDetail.data.unit[0] | 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 { | val tempObject = UomConversionService.BomObject().apply { | ||||
| code = unit.code | code = unit.code | ||||
| udfudesc = unit.udfudesc | udfudesc = unit.udfudesc | ||||
| @@ -549,6 +682,14 @@ open class M18MasterDataService( | |||||
| logger.error("Total Save Fail (${failSaveList.size}): $failSaveList") | logger.error("Total Save Fail (${failSaveList.size}): $failSaveList") | ||||
| } | } | ||||
| logger.info("--------------------------------------------End - Saving M18 Units--------------------------------------------") | 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 --------------------------------------------- /// | // --------------------------------------------- Currency --------------------------------------------- /// | ||||
| @@ -83,6 +83,7 @@ class M18TestController ( | |||||
| // Master Data | // Master Data | ||||
| m18MasterDataService.saveUnits(mdRequest) | m18MasterDataService.saveUnits(mdRequest) | ||||
| m18MasterDataService.saveProducts(mdRequest) | m18MasterDataService.saveProducts(mdRequest) | ||||
| m18MasterDataService.resyncProblemItemUomsFromM18() | |||||
| m18MasterDataService.saveVendors(mdRequest) | m18MasterDataService.saveVendors(mdRequest) | ||||
| m18MasterDataService.saveBusinessUnits(mdRequest) | m18MasterDataService.saveBusinessUnits(mdRequest) | ||||
| m18MasterDataService.saveCurrencies(mdRequest) | m18MasterDataService.saveCurrencies(mdRequest) | ||||
| @@ -96,8 +97,10 @@ class M18TestController ( | |||||
| @PostMapping("/master-data") | @PostMapping("/master-data") | ||||
| fun m18MasterData(@Valid @RequestBody request: M18CommonRequest) { | fun m18MasterData(@Valid @RequestBody request: M18CommonRequest) { | ||||
| // Master Data | // Master Data | ||||
| m18MasterDataService.ensureCunitSeededForAllIfEmpty() | |||||
| m18MasterDataService.saveUnits(request) | m18MasterDataService.saveUnits(request) | ||||
| m18MasterDataService.saveProducts(request) | m18MasterDataService.saveProducts(request) | ||||
| m18MasterDataService.resyncProblemItemUomsFromM18() | |||||
| m18MasterDataService.saveVendors(request) | m18MasterDataService.saveVendors(request) | ||||
| m18MasterDataService.saveBusinessUnits(request) | m18MasterDataService.saveBusinessUnits(request) | ||||
| m18MasterDataService.saveCurrencies(request) | m18MasterDataService.saveCurrencies(request) | ||||
| @@ -30,6 +30,15 @@ public abstract class SettingNames { | |||||
| public static final String SCHEDULE_M18_MASTER = "SCHEDULE.m18.master"; | 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) */ | /** 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"; | public static final String SCHEDULE_POST_COMPLETED_DN_GRN = "SCHEDULE.postCompletedDn.grn"; | ||||
| @@ -12,6 +12,7 @@ import com.ffii.fpsms.m18.model.SyncResult | |||||
| import com.ffii.fpsms.modules.common.SettingNames | import com.ffii.fpsms.modules.common.SettingNames | ||||
| import com.ffii.fpsms.modules.master.service.ProductionScheduleService | import com.ffii.fpsms.modules.master.service.ProductionScheduleService | ||||
| import com.ffii.fpsms.modules.stock.service.SearchCompletedDnService | 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 com.ffii.fpsms.modules.settings.service.SettingsService | ||||
| import jakarta.annotation.PostConstruct | import jakarta.annotation.PostConstruct | ||||
| import org.springframework.beans.factory.annotation.Value | 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. | * missing `grn_code`. Example: 4 = from 4 days ago 00:00 to now. | ||||
| */ | */ | ||||
| @Value("\${scheduler.grnCodeSync.syncOffsetDays:0}") val grnCodeSyncSyncOffsetDays: Int, | @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 settingsService: SettingsService, | ||||
| val taskScheduler: TaskScheduler, | val taskScheduler: TaskScheduler, | ||||
| val productionScheduleService: ProductionScheduleService, | val productionScheduleService: ProductionScheduleService, | ||||
| @@ -61,6 +68,8 @@ open class SchedulerService( | |||||
| @Volatile | @Volatile | ||||
| var scheduledM18Master: ScheduledFuture<*>? = null | var scheduledM18Master: ScheduledFuture<*>? = null | ||||
| var scheduledM18Units: ScheduledFuture<*>? = null | |||||
| var scheduledPostCompletedDnGrn: ScheduledFuture<*>? = null | var scheduledPostCompletedDnGrn: ScheduledFuture<*>? = null | ||||
| var scheduledGrnCodeSync: ScheduledFuture<*>? = null | var scheduledGrnCodeSync: ScheduledFuture<*>? = null | ||||
| @@ -146,6 +155,26 @@ open class SchedulerService( | |||||
| commonSchedule(scheduledM18Master, SettingNames.SCHEDULE_M18_MASTER, ::getM18MasterData) | 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 { | private fun getPostCompletedDnGrnReceiptDate(): LocalDate { | ||||
| val s = postCompletedDnGrnReceiptDate.trim() | val s = postCompletedDnGrnReceiptDate.trim() | ||||
| return if (s.isEmpty()) LocalDate.now().minusDays(1) | return if (s.isEmpty()) LocalDate.now().minusDays(1) | ||||
| @@ -372,6 +401,7 @@ open class SchedulerService( | |||||
| open fun getM18MasterData() { | open fun getM18MasterData() { | ||||
| logger.info("Daily Scheduler - Master Data") | logger.info("Daily Scheduler - Master Data") | ||||
| m18MasterDataService.ensureCunitSeededForAllIfEmpty() | |||||
| var currentTime = LocalDateTime.now() | var currentTime = LocalDateTime.now() | ||||
| val today = currentTime.toLocalDate().atStartOfDay() | val today = currentTime.toLocalDate().atStartOfDay() | ||||
| val yesterday = today.minusDays(1L) | val yesterday = today.minusDays(1L) | ||||
| @@ -379,8 +409,17 @@ open class SchedulerService( | |||||
| modifiedDateTo = today.format(dataStringFormat), | modifiedDateTo = today.format(dataStringFormat), | ||||
| modifiedDateFrom = yesterday.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) | val resultProducts = m18MasterDataService.saveProducts(request) | ||||
| saveSyncLog( | saveSyncLog( | ||||
| type = "Products", | type = "Products", | ||||
| @@ -388,6 +427,14 @@ open class SchedulerService( | |||||
| result = resultProducts, | result = resultProducts, | ||||
| start = currentTime | start = currentTime | ||||
| ) | ) | ||||
| currentTime = LocalDateTime.now() | |||||
| val resultProblemItemUomResync = m18MasterDataService.resyncProblemItemUomsFromM18() | |||||
| saveSyncLog( | |||||
| type = "ProductsUomResync", | |||||
| status = "SUCCESS", | |||||
| result = resultProblemItemUomResync, | |||||
| start = currentTime | |||||
| ) | |||||
| // m18MasterDataService.saveBoms(request) | // m18MasterDataService.saveBoms(request) | ||||
| currentTime = LocalDateTime.now() | currentTime = LocalDateTime.now() | ||||
| val resultVendors = m18MasterDataService.saveVendors(request) | 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.", | |||||
| ) | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -55,6 +55,12 @@ class SchedulerController( | |||||
| return "M18 Master Data Sync Triggered Successfully" | 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") | @GetMapping("/trigger/grn-code-sync") | ||||
| fun triggerGrnCodeSync(): String { | fun triggerGrnCodeSync(): String { | ||||
| schedulerService.syncGrnCodesFromM18() | schedulerService.syncGrnCodesFromM18() | ||||
| @@ -63,4 +63,12 @@ open class ItemUom : BaseEntity<Long>() { | |||||
| @NotNull | @NotNull | ||||
| @Column(name = "ratioD", nullable = false, precision = 14, scale = 2) | @Column(name = "ratioD", nullable = false, precision = 14, scale = 2) | ||||
| open var ratioD: BigDecimal? = null | 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 | |||||
| } | } | ||||
| @@ -29,4 +29,28 @@ interface ItemUomRespository : AbstractRepository<ItemUom, Long> { | |||||
| fun findByItemIdAndUomIdAndStockUnitAndDeletedIsFalse(itemId: Long, uomId: Long, stockUnit: Boolean): ItemUom? | fun findByItemIdAndUomIdAndStockUnitAndDeletedIsFalse(itemId: Long, uomId: Long, stockUnit: Boolean): ItemUom? | ||||
| fun findFirstByItemIdAndStockUnitIsTrueAndDeletedIsFalseOrderByIdAsc(itemId: Long): 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> | |||||
| } | } | ||||
| @@ -53,6 +53,10 @@ open class ItemUomService( | |||||
| return itemUomRespository.findByItemIdAndUomIdAndStockUnitAndDeletedIsFalse(itemId, uomId, stockUnit) | return itemUomRespository.findByItemIdAndUomIdAndStockUnitAndDeletedIsFalse(itemId, uomId, stockUnit) | ||||
| } | } | ||||
| open fun findDistinctProblemItemM18IdsForUomResync(): List<Long> { | |||||
| return itemUomRespository.findDistinctProblemItemM18IdsForUomResync() | |||||
| } | |||||
| open fun findPurchaseUnitByM18ItemId(m18ItemId: Long): ItemUom? { | open fun findPurchaseUnitByM18ItemId(m18ItemId: Long): ItemUom? { | ||||
| return itemUomRespository.findByItemM18IdAndPurchaseUnitIsTrueAndDeletedIsFalse(m18ItemId) | return itemUomRespository.findByItemM18IdAndPurchaseUnitIsTrueAndDeletedIsFalse(m18ItemId) | ||||
| } | } | ||||
| @@ -227,6 +231,8 @@ open class ItemUomService( | |||||
| m18LastModifyDate = request.m18LastModifyDate | m18LastModifyDate = request.m18LastModifyDate | ||||
| ratioD = request.ratioD | ratioD = request.ratioD | ||||
| ratioN = request.ratioN | ratioN = request.ratioN | ||||
| itemRatioD = request.itemRatioD | |||||
| itemRatioN = request.itemRatioN | |||||
| deleted = request.deleted // ← EXPLICIT assignment (same as before) | deleted = request.deleted // ← EXPLICIT assignment (same as before) | ||||
| } | } | ||||
| @@ -26,6 +26,8 @@ data class ItemUomRequest( | |||||
| val m18LastModifyDate: LocalDateTime?, | val m18LastModifyDate: LocalDateTime?, | ||||
| val ratioD: BigDecimal?, | val ratioD: BigDecimal?, | ||||
| val ratioN: BigDecimal?, | val ratioN: BigDecimal?, | ||||
| val itemRatioD: BigDecimal?, | |||||
| val itemRatioN: BigDecimal?, | |||||
| val deleted: Boolean?, | val deleted: Boolean?, | ||||
| ) | ) | ||||
| data class BomFormatIssue( | data class BomFormatIssue( | ||||
| @@ -1,4 +1,4 @@ | |||||
| package com.ffii.fpsms.modules.report.service | |||||
| package com.ffii.fpsms.modules.report.service | |||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import net.sf.jasperreports.engine.* | import net.sf.jasperreports.engine.* | ||||
| @@ -747,7 +747,7 @@ return result | |||||
| fun searchGrnReport( | fun searchGrnReport( | ||||
| receiptDateStart: String?, | receiptDateStart: String?, | ||||
| receiptDateEnd: String?, | receiptDateEnd: String?, | ||||
| itemCode: String?, | |||||
| itemCode: String?, | |||||
| supplier: String?, | supplier: String?, | ||||
| poCode: String?, | poCode: String?, | ||||
| grnCode: String?, | grnCode: String?, | ||||
| @@ -763,10 +763,10 @@ return result | |||||
| args["receiptDateEnd"] = formatted | args["receiptDateEnd"] = formatted | ||||
| "AND DATE(sil.receiptDate) <= DATE(:receiptDateEnd)" | "AND DATE(sil.receiptDate) <= DATE(:receiptDateEnd)" | ||||
| } else "" | } 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 = """ | val sql = """ | ||||
| SELECT | SELECT | ||||
| @@ -866,7 +866,10 @@ return result | |||||
| fun searchGrnListedPoAmounts( | fun searchGrnListedPoAmounts( | ||||
| receiptDateStart: String?, | receiptDateStart: String?, | ||||
| receiptDateEnd: String?, | receiptDateEnd: String?, | ||||
| itemCode: String? | |||||
| itemCode: String?, | |||||
| supplier: String?, | |||||
| poCode: String?, | |||||
| grnCode: String?, | |||||
| ): Map<String, Any> { | ): Map<String, Any> { | ||||
| val args = mutableMapOf<String, Any>() | val args = mutableMapOf<String, Any>() | ||||
| val receiptDateStartSql = if (!receiptDateStart.isNullOrBlank()) { | val receiptDateStartSql = if (!receiptDateStart.isNullOrBlank()) { | ||||
| @@ -879,7 +882,10 @@ return result | |||||
| args["receiptDateEnd"] = formatted | args["receiptDateEnd"] = formatted | ||||
| "AND DATE(sil.receiptDate) <= DATE(:receiptDateEnd)" | "AND DATE(sil.receiptDate) <= DATE(:receiptDateEnd)" | ||||
| } else "" | } 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 = """ | val lineSubquery = """ | ||||
| SELECT | SELECT | ||||
| @@ -889,6 +895,8 @@ return result | |||||
| ROUND(COALESCE(pol.up, 0) * COALESCE(sil.acceptedQty, 0), 2) AS line_amt | ROUND(COALESCE(pol.up, 0) * COALESCE(sil.acceptedQty, 0), 2) AS line_amt | ||||
| FROM stock_in_line sil | FROM stock_in_line sil | ||||
| INNER JOIN purchase_order_line pol ON sil.purchaseOrderLineId = pol.id | 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 | LEFT JOIN items it ON sil.itemId = it.id | ||||
| WHERE sil.deleted = false | WHERE sil.deleted = false | ||||
| AND sil.receiptDate IS NOT NULL | AND sil.receiptDate IS NOT NULL | ||||
| @@ -896,7 +904,10 @@ return result | |||||
| AND pol.status = 'completed' | AND pol.status = 'completed' | ||||
| $receiptDateStartSql | $receiptDateStartSql | ||||
| $receiptDateEndSql | $receiptDateEndSql | ||||
| $itemCodeSql | |||||
| $itemSql | |||||
| $supplierSql | |||||
| $poCodeSql | |||||
| $grnExistsSql | |||||
| """.trimIndent() | """.trimIndent() | ||||
| val currencySql = """ | val currencySql = """ | ||||
| @@ -255,7 +255,14 @@ class ReportController( | |||||
| } | } | ||||
| val listedPoAmounts = | val listedPoAmounts = | ||||
| if (isAdmin) { | if (isAdmin) { | ||||
| reportService.searchGrnListedPoAmounts(receiptDateStart, receiptDateEnd, itemCode) | |||||
| reportService.searchGrnListedPoAmounts( | |||||
| receiptDateStart, | |||||
| receiptDateEnd, | |||||
| itemCode, | |||||
| supplier, | |||||
| poCode, | |||||
| grnCode, | |||||
| ) | |||||
| } else { | } else { | ||||
| mapOf( | mapOf( | ||||
| "currencyTotals" to emptyList<Map<String, Any?>>(), | "currencyTotals" to emptyList<Map<String, Any?>>(), | ||||
| @@ -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; | |||||
| @@ -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' | |||||
| ); | |||||
| @@ -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`; | |||||