Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 
 

1047 righe
48 KiB

  1. package com.ffii.fpsms.m18.service
  2. import com.ffii.core.utils.JwtTokenUtil
  3. import com.ffii.fpsms.api.service.ApiCallerService
  4. import com.ffii.fpsms.m18.M18Config
  5. import com.ffii.fpsms.m18.model.*
  6. import com.ffii.fpsms.m18.utils.CommonUtils
  7. import com.ffii.fpsms.m18.web.models.M18CommonRequest
  8. import com.ffii.fpsms.modules.master.entity.UomConversion
  9. import com.ffii.fpsms.modules.master.enums.ShopType
  10. import com.ffii.fpsms.modules.master.service.*
  11. import com.ffii.fpsms.modules.master.web.models.*
  12. import org.slf4j.Logger
  13. import org.slf4j.LoggerFactory
  14. import org.springframework.stereotype.Service
  15. import java.math.BigDecimal
  16. import java.time.LocalDateTime
  17. import java.time.format.DateTimeFormatter
  18. import com.ffii.fpsms.m18.model.SyncResult
  19. import com.ffii.fpsms.modules.master.entity.Items
  20. import java.time.Instant
  21. import java.time.LocalDate
  22. import java.time.ZoneId
  23. @Service
  24. open class M18MasterDataService(
  25. val m18Config: M18Config,
  26. val apiCallerService: ApiCallerService,
  27. val itemsService: ItemsService,
  28. val shopService: ShopService,
  29. val uomConversionService: UomConversionService,
  30. val currencyService: CurrencyService,
  31. val itemUomService: ItemUomService,
  32. val bomService: BomService,
  33. val bomMaterialService: BomMaterialService,
  34. val m18CunitService: M18CunitService,
  35. ) {
  36. val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java)
  37. val commonUtils = CommonUtils()
  38. val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
  39. private val one = BigDecimal.ONE
  40. private val ratio2268 = BigDecimal("2268")
  41. private val ratio5 = BigDecimal("5")
  42. // M18 Conditions
  43. // val lastModifyDate = LocalDate.now().minusDays(1)
  44. // val lastModifyDateConds = "lastModifyDate=largerThan=$lastModifyDate"
  45. val seriesIdList =
  46. listOf(m18Config.SERIESID_SC, m18Config.SERIESID_SE, m18Config.SERIESID_SF, m18Config.SERIESID_SR)
  47. val seriesIdConds =
  48. "(" + commonUtils.listToString(seriesIdList.filterNotNull(), "seriesId=unequal=", "=or=") + ")"
  49. val beIdList = listOf(m18Config.BEID_PF, m18Config.BEID_PP, m18Config.BEID_TOA)
  50. val beIdConds = "(" + commonUtils.listToString(beIdList.filterNotNull(), "beId=equal=", "=or=") + ")"
  51. // val beIdConds = commonUtils.BEID_CONDS
  52. // M18 API
  53. val M18_COMMON_FETCH_LIST_API = "/search/search"
  54. val M18_COMMON_LOAD_LINE_API = "/root/api/read"
  55. val M18_LOAD_PRODUCT_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.PRODUCT.value}"
  56. val M18_LOAD_VENDOR_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.VENDOR.value}"
  57. val M18_LOAD_UNIT_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.UNIT.value}"
  58. val M18_LOAD_CURRENCY_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.CURRENCY.value}"
  59. val M18_LOAD_BOM_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.BOM.value}"
  60. val M18_LOAD_BUSINESS_UNIT_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.BUSINESS_UNIT.value}" // for shop po?
  61. private fun isBothOne(ratioN: BigDecimal?, ratioD: BigDecimal?): Boolean {
  62. return ratioN?.compareTo(one) == 0 && ratioD?.compareTo(one) == 0
  63. }
  64. private fun isPair2268And5(ratioN: BigDecimal?, ratioD: BigDecimal?): Boolean {
  65. return ratioN?.compareTo(ratio2268) == 0 && ratioD?.compareTo(ratio5) == 0
  66. }
  67. /**
  68. * Problematic ratio cases that should be corrected by m18_cunit:
  69. * 1) uomId=4 and ratioN/ratioD are not both 1
  70. * 2) uomId=785 and ratioN/ratioD do not contain 453
  71. */
  72. private fun shouldSyncRatioFromCunit(unitId: Long, ratioN: BigDecimal?, ratioD: BigDecimal?): Boolean {
  73. val caseUom4 = unitId == 4L && !isBothOne(ratioN, ratioD)
  74. val caseUom785 = unitId == 785L && !isPair2268And5(ratioN, ratioD)
  75. return caseUom4 || caseUom785
  76. }
  77. /**
  78. * Same rule as [shouldSyncRatioFromCunit], but based on local uom_conversion.id.
  79. */
  80. private fun shouldSyncRatioFromCunitByLocalUomId(localUomId: Long?, ratioN: BigDecimal?, ratioD: BigDecimal?): Boolean {
  81. if (localUomId == null) return false
  82. val caseUom4 = localUomId == 4L && !isBothOne(ratioN, ratioD)
  83. val caseUom785 = localUomId == 785L && !isPair2268And5(ratioN, ratioD)
  84. return caseUom4 || caseUom785
  85. }
  86. /**
  87. * Safety net: when m18_cunit has no rows, force a full unit sync first so cunit ratios are available.
  88. */
  89. open fun ensureCunitSeededForAllIfEmpty() {
  90. val cunitRows = m18CunitService.countActiveRows()
  91. if (cunitRows > 0L) return
  92. logger.warn("m18_cunit has 0 rows; triggering full unit sync first to seed cunit data for all units.")
  93. val result = saveUnits(M18CommonRequest())
  94. logger.warn(
  95. "m18_cunit seed sync finished: processed=${result.totalProcessed}, " +
  96. "success=${result.totalSuccess}, fail=${result.totalFail}"
  97. )
  98. }
  99. // --------------------------------------------- Common Function --------------------------------------------- ///
  100. private inline fun <reified T : Any> getList(
  101. stSearch: String?,
  102. params: String? = null,
  103. conds: String? = null,
  104. request: M18CommonRequest,
  105. ): T? {
  106. val lastModifyDateFromConds = request.modifiedDateFrom?.let { "lastModifyDate=largerOrEqual=${it}" }
  107. val lastModifyDateToConds = request.modifiedDateTo?.let{ "lastModifyDate=lessOrEqual=${it}" }
  108. val haveFromAndTo = lastModifyDateFromConds != null && lastModifyDateToConds != null
  109. val finalConds = if (lastModifyDateFromConds == null && lastModifyDateToConds == null) {
  110. conds
  111. } else {
  112. conds + "=and=(${lastModifyDateFromConds ?: ""}${if(haveFromAndTo) "=and=" else ""}${lastModifyDateToConds ?: ""})"
  113. }
  114. val request = M18CommonListRequest(
  115. stSearch = stSearch,
  116. params = params,
  117. conds = finalConds
  118. )
  119. val response = apiCallerService.get<T, M18CommonListRequest>(
  120. M18_COMMON_FETCH_LIST_API,
  121. request
  122. ).block()
  123. return response
  124. }
  125. private inline fun <reified T : Any> getLine(
  126. id: Long,
  127. params: String?,
  128. api: String,
  129. ): T? {
  130. val request = M18CommonLineRequest(
  131. id = id,
  132. params = params,
  133. )
  134. val response = apiCallerService.get<T, M18CommonLineRequest>(
  135. api,
  136. request
  137. ).block()
  138. return response
  139. }
  140. // --------------------------------------------- Product --------------------------------------------- ///
  141. open fun getProducts(request: M18CommonRequest): M18ProductListResponse? {
  142. // seems no beId
  143. return getList<M18ProductListResponse>(
  144. stSearch = StSearchType.PRODUCT.value,
  145. params = null,
  146. conds = seriesIdConds,
  147. request = request
  148. )
  149. // val itemsParams = M18CommonListRequest(
  150. // stSearch = StSearchType.PRODUCT.value,
  151. // params = null,
  152. // conds = seriesIdConds
  153. // )
  154. //
  155. // val items = apiCallerService.get<M18ProductListResponse, M18CommonListRequest>(
  156. // M18_COMMON_FETCH_LIST_API,
  157. // itemsParams
  158. // ).block()
  159. //
  160. // return items
  161. }
  162. open fun getProduct(id: Long): M18ProductResponse? {
  163. logger.info("M18 Product ID: $id")
  164. return getLine<M18ProductResponse>(
  165. id = id,
  166. params = null,
  167. api = M18_LOAD_PRODUCT_API
  168. )
  169. }
  170. /** Resolve local items.id for an M18 product id; sync from M18 when missing. */
  171. open fun resolveLocalItemId(m18ItemId: Long): Long? {
  172. itemsService.findByM18Id(m18ItemId)?.id?.let { return it }
  173. saveProduct(m18ItemId)?.id?.let { return it }
  174. return itemsService.findByM18Id(m18ItemId)?.id
  175. }
  176. private fun mapM18ProductType(udfProducttype: String?): String {
  177. return when (udfProducttype) {
  178. M18ItemType.CONSUMABLES.type -> ItemType.CONSUMABLES.type
  179. M18ItemType.NONCONSUMABLES.type -> ItemType.NONCONSUMABLES.type
  180. M18ItemType.FG.type -> ItemType.FG.type
  181. M18ItemType.SFG.type -> ItemType.SFG.type
  182. M18ItemType.ITEM.type -> ItemType.ITEM.type
  183. else -> ItemType.MATERIAL.type
  184. }
  185. }
  186. open fun saveProduct(id: Long): MessageResponse? {
  187. try {
  188. ensureCunitSeededForAllIfEmpty()
  189. val itemDetail = getProduct(id)
  190. val pro = itemDetail?.data?.pro?.get(0)
  191. val price = itemDetail?.data?.price
  192. if (itemDetail != null && pro != null) {
  193. val mappedType = mapM18ProductType(pro.udfProducttype)
  194. val existingItem = itemsService.findByM18Id(id)
  195. val saveItemRequest = NewItemRequest(
  196. code = pro.code,
  197. name = pro.desc,
  198. // type = if (pro.seriesId == m18Config.SERIESID_PF) ProductType.MATERIAL
  199. // else ItemType.PRODUCT,
  200. type = mappedType,
  201. id = existingItem?.id,
  202. description = pro.desc,
  203. remarks = null,
  204. shelfLife = null,
  205. countryOfOrigin = null,
  206. maxQty = null,
  207. m18Id = id,
  208. m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate),
  209. qcCategoryId = null,
  210. store_id = null,
  211. warehouse = null,
  212. area = null,
  213. slot = null,
  214. LocationCode = null,
  215. isEgg = null,
  216. isFee = null,
  217. isBag = null,
  218. qcType = null,
  219. )
  220. val savedItem = itemsService.saveItem(saveItemRequest)
  221. val localItemId = savedItem.id
  222. if (localItemId == null) {
  223. logger.error("saveItem returned null id for M18 item $id (code=${pro.code}): ${savedItem.message}")
  224. return null
  225. }
  226. if (savedItem.errorPosition == "code") {
  227. logger.error("saveItem duplicate code for M18 item $id (code=${pro.code}): ${savedItem.message}")
  228. return null
  229. }
  230. if (ItemM18IdRemapSupport.isM18IdLinkOnly(existingItem == null, savedItem.message)) {
  231. logger.warn(
  232. "Linked M18 product id=$id code=${pro.code} to existing local item id=$localItemId; skip UOM rebuild"
  233. )
  234. return savedItem.copy(id = localItemId)
  235. }
  236. logger.info("Processing item uom...")
  237. // Find the item uom that ready to delete (not in m18)
  238. val existingItemUoms = itemUomService.findAllByItemsId(localItemId)
  239. val m18ItemUomIds = price?.map { it.id } ?: listOf()
  240. // Delete the item uom
  241. logger.info("Deleting item uom...")
  242. // logger.info("Item Uom: ${existingItemUoms?.map { it.m18Id }}")
  243. // logger.info("M18: ${m18ItemUomIds}")
  244. existingItemUoms?.filter { it.m18Id !in m18ItemUomIds }?.mapNotNull { it.id }
  245. ?.let { itemUomService.deleteItemUoms(it) }
  246. // Update the item uom
  247. logger.info("Updating item uom...")
  248. val hasBaseUom4 = price?.any { p ->
  249. p.basicUnit && (uomConversionService.findByM18Id(p.unitId)?.id == 4L)
  250. } == true
  251. val forceCunitForAllRows = hasBaseUom4 && (price?.any { p ->
  252. val localBaseId = uomConversionService.findByM18Id(p.unitId)?.id
  253. shouldSyncRatioFromCunitByLocalUomId(localBaseId, p.ratioN, p.ratioD)
  254. } == true)
  255. price?.forEach {
  256. val endMillis = it.endDate
  257. val endInstant = Instant.ofEpochMilli(endMillis)
  258. val now = Instant.now()
  259. val localUomId = uomConversionService.findByM18Id(it.unitId)?.id
  260. val keepOriginalItemRatio = forceCunitForAllRows
  261. val useUnitRatios = forceCunitForAllRows
  262. val unitRatios = if (useUnitRatios) m18CunitService.resolveRatiosByM18UnitId(it.unitId) else null
  263. logger.info(
  264. "ItemUom ratio apply (single): itemM18Id=$id m18UomId=${it.unitId} localUomId=$localUomId " +
  265. "hasBaseUom4=$hasBaseUom4 forceCunitForAllRows=$forceCunitForAllRows useCunit=$useUnitRatios oldRatioN=${it.ratioN} oldRatioD=${it.ratioD} " +
  266. "cunitRatioN=${unitRatios?.ratioN} cunitRatioD=${unitRatios?.ratioD}"
  267. )
  268. val itemUomRequest = ItemUomRequest(
  269. m18UomId = it.unitId,
  270. itemId = localItemId,
  271. baseUnit = it.basicUnit,
  272. stockUnit = it.stkUnit,
  273. pickingUnit = it.pickUnit,
  274. salesUnit = it.saleUnit,
  275. purchaseUnit = it.purUnit,
  276. price = null,
  277. currencyId = null,
  278. m18Id = it.id,
  279. m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate),
  280. ratioD = if (useUnitRatios) (unitRatios?.ratioD ?: it.ratioD) else it.ratioD,
  281. ratioN = if (useUnitRatios) (unitRatios?.ratioN ?: it.ratioN) else it.ratioN,
  282. itemRatioD = if (keepOriginalItemRatio) it.ratioD else null,
  283. itemRatioN = if (keepOriginalItemRatio) it.ratioN else null,
  284. deleted = it.expired || endInstant.isBefore(now)
  285. )
  286. itemUomService.saveItemUom(itemUomRequest)
  287. }
  288. logger.info("Success (M18 Item): ${id} | ${pro.code} | ${pro.desc}")
  289. return savedItem.copy(id = localItemId)
  290. } else {
  291. logger.error("Fail Message: ${itemDetail?.messages?.get(0)?.msgDetail}")
  292. logger.error("Fail: Item ID - ${id} Not Found")
  293. return null
  294. }
  295. } catch (e: Exception) {
  296. logger.error("Exception")
  297. logger.error("Fail Message: ${e.message}")
  298. logger.error("Fail: Item ID - ${id}")
  299. return null
  300. }
  301. }
  302. /** Sync one product/material from M18 by item code (search list, then load line — same idea as PO/DO by code). */
  303. open fun saveProductByCode(code: String): SyncResult {
  304. val trimmed = code.trim()
  305. if (trimmed.isEmpty()) {
  306. return SyncResult(totalProcessed = 1, totalSuccess = 0, totalFail = 1, query = "empty code")
  307. }
  308. ensureCunitSeededForAllIfEmpty()
  309. val fromLocal = itemsService.findByCode(trimmed)?.m18Id
  310. val m18Id = fromLocal ?: run {
  311. val conds = "(code=equal=$trimmed)"
  312. val listResponse = try {
  313. getList<M18ProductListResponse>(
  314. stSearch = StSearchType.PRODUCT.value,
  315. params = null,
  316. conds = conds,
  317. request = M18CommonRequest(),
  318. )
  319. } catch (e: Exception) {
  320. logger.error("(saveProductByCode) M18 search failed: ${e.message}", e)
  321. null
  322. }
  323. listResponse?.values?.firstOrNull()?.id
  324. }
  325. if (m18Id == null) {
  326. return SyncResult(
  327. totalProcessed = 1,
  328. totalSuccess = 0,
  329. totalFail = 1,
  330. query = "code=equal=$trimmed",
  331. )
  332. }
  333. val result = saveProduct(m18Id)
  334. return if (result != null) {
  335. SyncResult(totalProcessed = 1, totalSuccess = 1, totalFail = 0, query = "code=equal=$trimmed")
  336. } else {
  337. SyncResult(totalProcessed = 1, totalSuccess = 0, totalFail = 1, query = "code=equal=$trimmed")
  338. }
  339. }
  340. open fun saveProducts(request: M18CommonRequest): SyncResult {
  341. logger.info("--------------------------------------------Start - Saving M18 Products / Materials--------------------------------------------")
  342. ensureCunitSeededForAllIfEmpty()
  343. val items = getProducts(request)
  344. val exampleProducts = listOf<Long>(10946L, 3825L)
  345. // ── New: cache for findByM18Id ─────────────────────────────────────────
  346. // M18 item.id → internal Item entity (or just the id if you only need id)
  347. val itemCache = mutableMapOf<Long, Items?>()
  348. val successList = mutableListOf<Long>()
  349. val failList = mutableListOf<Long>()
  350. val values = items?.values?.sortedBy { it.id }
  351. if (values != null) {
  352. values.forEach { item ->
  353. // if (item.id in exampleProducts) { // keep your debug filter if needed
  354. try {
  355. val itemDetail = getProduct(item.id)
  356. val pro = itemDetail?.data?.pro?.get(0)
  357. val price = itemDetail?.data?.price
  358. if (itemDetail != null && pro != null) {
  359. val mappedType = mapM18ProductType(pro.udfProducttype)
  360. // ── Use cache instead of direct call ────────────────────────
  361. val existingItem = itemCache.getOrPut(item.id) {
  362. itemsService.findByM18Id(item.id)
  363. }
  364. val saveItemRequest = NewItemRequest(
  365. code = pro.code,
  366. name = pro.desc,
  367. type = mappedType,
  368. id = existingItem?.id,
  369. description = pro.desc,
  370. remarks = null,
  371. shelfLife = null,
  372. countryOfOrigin = null,
  373. maxQty = null,
  374. m18Id = item.id,
  375. m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate),
  376. qcCategoryId = null,
  377. store_id = null,
  378. warehouse = null,
  379. area = null,
  380. slot = null,
  381. LocationCode = null,
  382. isEgg = null,
  383. isFee = null,
  384. isBag = null,
  385. qcType = null
  386. )
  387. val savedItem = itemsService.saveItem(saveItemRequest)
  388. val localItemId = savedItem.id
  389. if (localItemId == null) {
  390. failList.add(item.id)
  391. logger.error("saveItem returned null id for M18 item ${item.id} (code=${pro.code}): ${savedItem.message}")
  392. return@forEach
  393. }
  394. if (savedItem.errorPosition == "code") {
  395. failList.add(item.id)
  396. logger.error("saveItem duplicate code for M18 item ${item.id} (code=${pro.code}): ${savedItem.message}")
  397. return@forEach
  398. }
  399. if (ItemM18IdRemapSupport.isM18IdLinkOnly(existingItem == null, savedItem.message)) {
  400. logger.warn(
  401. "Linked M18 product id=${item.id} code=${pro.code} to existing local item id=$localItemId; skip UOM rebuild"
  402. )
  403. successList.add(item.id)
  404. return@forEach
  405. }
  406. logger.info("Processing item uom...")
  407. val existingItemUoms = itemUomService.findAllByItemsId(localItemId)
  408. val m18ItemUomIds = price?.map { it.id } ?: listOf()
  409. // Delete old UOMs not present in M18
  410. logger.info("Deleting item uom...")
  411. existingItemUoms?.filter { it.m18Id !in m18ItemUomIds }?.mapNotNull { it.id }
  412. ?.let { itemUomService.deleteItemUoms(it) }
  413. // Update / create UOMs from M18
  414. logger.info("Updating item uom...")
  415. val hasBaseUom4 = price?.any { p ->
  416. p.basicUnit && (uomConversionService.findByM18Id(p.unitId)?.id == 4L)
  417. } == true
  418. val forceCunitForAllRows = hasBaseUom4 && (price?.any { p ->
  419. val localBaseId = uomConversionService.findByM18Id(p.unitId)?.id
  420. shouldSyncRatioFromCunitByLocalUomId(localBaseId, p.ratioN, p.ratioD)
  421. } == true)
  422. price?.forEach {
  423. val endMillis = it.endDate
  424. val endInstant = Instant.ofEpochMilli(endMillis)
  425. val now = Instant.now()
  426. val localUomId = uomConversionService.findByM18Id(it.unitId)?.id
  427. val keepOriginalItemRatio = forceCunitForAllRows
  428. val useUnitRatios = forceCunitForAllRows
  429. val unitRatios = if (useUnitRatios) m18CunitService.resolveRatiosByM18UnitId(it.unitId) else null
  430. logger.info(
  431. "ItemUom ratio apply (bulk): itemM18Id=${item.id} m18UomId=${it.unitId} localUomId=$localUomId " +
  432. "hasBaseUom4=$hasBaseUom4 forceCunitForAllRows=$forceCunitForAllRows useCunit=$useUnitRatios oldRatioN=${it.ratioN} oldRatioD=${it.ratioD} " +
  433. "cunitRatioN=${unitRatios?.ratioN} cunitRatioD=${unitRatios?.ratioD}"
  434. )
  435. val itemUomRequest = ItemUomRequest(
  436. m18UomId = it.unitId,
  437. itemId = localItemId,
  438. baseUnit = it.basicUnit,
  439. stockUnit = it.stkUnit,
  440. pickingUnit = it.pickUnit,
  441. salesUnit = it.saleUnit,
  442. purchaseUnit = it.purUnit,
  443. price = null,
  444. currencyId = null,
  445. m18Id = it.id,
  446. m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate),
  447. ratioD = if (useUnitRatios) (unitRatios?.ratioD ?: it.ratioD) else it.ratioD,
  448. ratioN = if (useUnitRatios) (unitRatios?.ratioN ?: it.ratioN) else it.ratioN,
  449. itemRatioD = if (keepOriginalItemRatio) it.ratioD else null,
  450. itemRatioN = if (keepOriginalItemRatio) it.ratioN else null,
  451. deleted = it.expired || endInstant.isBefore(now)
  452. )
  453. itemUomService.saveItemUom(itemUomRequest)
  454. }
  455. successList.add(item.id)
  456. logger.info("Success Count ${successList.size}: ${item.id} | ${pro.code} | ${pro.desc}")
  457. } else {
  458. failList.add(item.id)
  459. logger.error("Fail Message: ${itemDetail?.messages?.get(0)?.msgDetail}")
  460. logger.error("Fail Count ${failList.size}: Item ID - ${item.id} Not Found")
  461. }
  462. } catch (e: Exception) {
  463. failList.add(item.id)
  464. logger.error("Exception")
  465. logger.error("Fail Message: ${e.message}")
  466. logger.error("Fail Count ${failList.size}: Item ID - ${item.id}")
  467. }
  468. // } // end of exampleProducts filter
  469. }
  470. } else {
  471. logger.error("Items List is null. May occur errors.")
  472. }
  473. logger.info("Total Success (${successList.size})")
  474. if (failList.size > 0) {
  475. logger.error("Total Fail (${failList.size}): $failList")
  476. }
  477. logger.info("--------------------------------------------End - Saving M18 Products / Materials--------------------------------------------")
  478. return SyncResult(
  479. totalProcessed = successList.size + failList.size,
  480. totalSuccess = successList.size,
  481. totalFail = failList.size
  482. )
  483. }
  484. /**
  485. * Targeted re-sync for locally identified problematic item_uom ratios:
  486. * - uomId = 4 and ratioN != 1
  487. * - uomId = 785 and ratioN != 453
  488. *
  489. * Uses local item_uom + item.m18Id to fetch only affected products from M18 and re-save their item_uom.
  490. */
  491. open fun resyncProblemItemUomsFromM18(): SyncResult {
  492. logger.info("--------------------------------------------Start - Re-sync Problem Item UOMs--------------------------------------------")
  493. val m18Ids = itemUomService.findDistinctProblemItemM18IdsForUomResync().distinct().sorted()
  494. val successList = mutableListOf<Long>()
  495. val failList = mutableListOf<Long>()
  496. logger.warn("Problem item_uom selector found ${m18Ids.size} item(s) for re-sync.")
  497. if (m18Ids.isNotEmpty()) {
  498. logger.warn("Problem item m18Id sample (up to 20): ${m18Ids.take(20)}")
  499. }
  500. m18Ids.forEach { m18ItemId ->
  501. try {
  502. logger.warn("Re-sync item_uom: calling M18 product API for id=$m18ItemId")
  503. val result = saveProduct(m18ItemId)
  504. if (result != null) {
  505. successList.add(m18ItemId)
  506. logger.warn("Re-sync item_uom: M18 API sync success for id=$m18ItemId")
  507. } else {
  508. failList.add(m18ItemId)
  509. logger.warn("Re-sync item_uom: M18 API sync returned null for id=$m18ItemId")
  510. }
  511. } catch (e: Exception) {
  512. failList.add(m18ItemId)
  513. logger.error("Re-sync problem item uom failed for m18ItemId=$m18ItemId: ${e.message}", e)
  514. }
  515. }
  516. logger.info("Problem item_uom re-sync done. total=${m18Ids.size}, success=${successList.size}, fail=${failList.size}")
  517. logger.info("--------------------------------------------End - Re-sync Problem Item UOMs--------------------------------------------")
  518. return SyncResult(
  519. totalProcessed = m18Ids.size,
  520. totalSuccess = successList.size,
  521. totalFail = failList.size
  522. )
  523. }
  524. // --------------------------------------------- Vendor --------------------------------------------- ///
  525. open fun getVendors(request: M18CommonRequest): M18VendorListResponse? {
  526. return getList<M18VendorListResponse>(
  527. stSearch = StSearchType.VENDOR.value,
  528. params = null,
  529. conds = beIdConds,
  530. request = request
  531. )
  532. }
  533. open fun getVendor(id: Long): M18VendorResponse? {
  534. logger.info("M18 Vendor ID: $id")
  535. return getLine<M18VendorResponse>(
  536. id = id,
  537. params = null,
  538. api = M18_LOAD_VENDOR_API
  539. )
  540. }
  541. open fun saveVendors(request: M18CommonRequest) : SyncResult{
  542. logger.info("--------------------------------------------Start - Saving M18 Vendors--------------------------------------------")
  543. val vendors = getVendors(request)
  544. val exampleVendors = listOf<Long>(191L)
  545. val successList = mutableListOf<Long>()
  546. val failList = mutableListOf<Long>()
  547. val values = vendors?.values?.sortedBy { it.id }
  548. if (values != null) {
  549. values.forEach { vendor ->
  550. // if (vendor.id in exampleVendors) {
  551. try {
  552. val vendorDetail = getVendor(vendor.id)
  553. if (vendorDetail != null && vendorDetail.data?.ven != null) {
  554. val ven = vendorDetail.data.ven[0]
  555. val saveShopRequest = SaveShopRequest(
  556. id = null,
  557. code = ven.code,
  558. name = ven.descZhTW.ifEmpty { ven.descZhCN.ifEmpty { ven.desc } },
  559. brNo = null,
  560. contactNo = ven.tel,
  561. contactEmail = ven.email,
  562. contactName = null,
  563. addr1 = ven.ad1,
  564. addr2 = ven.ad2,
  565. addr3 = ven.ad3,
  566. addr4 = ven.ad4,
  567. district = null,
  568. type = ShopType.SUPPLIER.value,
  569. m18Id = vendor.id,
  570. m18LastModifyDate = commonUtils.timestampToLocalDateTime(ven.lastModifyDate)
  571. )
  572. shopService.saveShop(saveShopRequest)
  573. successList.add(vendor.id)
  574. logger.info("Success Count ${successList.size}: ${vendor.id} | ${ven.code} | ${ven.descZhTW.ifEmpty { ven.descZhCN.ifEmpty { ven.desc } }}")
  575. } else {
  576. failList.add(vendor.id)
  577. logger.error("Fail Message: ${vendorDetail?.messages?.get(0)?.msgDetail}")
  578. logger.error("Fail Count ${failList.size}: Vendor ID - ${vendor.id} Not Found")
  579. }
  580. } catch (e: Exception) {
  581. failList.add(vendor.id)
  582. logger.error("Exception")
  583. logger.error("Fail Message: ${e.message}")
  584. logger.error("Fail Count ${failList.size}: Vendor ID - ${vendor.id}")
  585. }
  586. // }
  587. }
  588. } else {
  589. logger.error("Vendor List is null. May occur errors.")
  590. }
  591. logger.info("Total Success (${successList.size})")
  592. if (failList.size > 0) {
  593. logger.error("Total Fail (${failList.size}): $failList")
  594. }
  595. logger.info("--------------------------------------------End - Saving M18 Vendors--------------------------------------------")
  596. return SyncResult(
  597. totalProcessed = successList.size + failList.size,
  598. totalSuccess = successList.size,
  599. totalFail = failList.size
  600. )
  601. }
  602. // --------------------------------------------- Unit (UoM) --------------------------------------------- ///
  603. open fun getUnits(request: M18CommonRequest): M18UnitListResponse? {
  604. // seems no beId
  605. return getList<M18UnitListResponse>(
  606. stSearch = StSearchType.UNIT.value,
  607. params = null,
  608. conds = null,
  609. request = request
  610. )
  611. }
  612. open fun getUnit(id: Long): M18UnitResponse? {
  613. logger.info("M18 Unit ID: $id")
  614. return getLine<M18UnitResponse>(
  615. id = id,
  616. params = null,
  617. api = M18_LOAD_UNIT_API
  618. )
  619. }
  620. open fun saveUnits(request: M18CommonRequest): SyncResult {
  621. logger.info("--------------------------------------------Start - Saving M18 Units--------------------------------------------")
  622. val units = getUnits(request)
  623. val successTransformList = mutableListOf<Long>()
  624. val successSaveList = mutableListOf<Long>()
  625. val failTransformList = mutableListOf<Long>()
  626. val failSaveList = mutableListOf<Long>()
  627. val values = units?.values?.sortedBy { it.id }
  628. if (values != null) {
  629. val finalUnitList = arrayListOf<UomConversion>()
  630. // transform unit
  631. values.forEach { value ->
  632. try {
  633. val unitDetail = getUnit(value.id)
  634. if (unitDetail != null && unitDetail.data?.unit != null) {
  635. val unit = unitDetail.data.unit[0]
  636. try {
  637. m18CunitService.replaceForUnit(unit.id, unitDetail.data!!)
  638. } catch (e: Exception) {
  639. logger.error("M18 cunit save failed for unit ${unit.id}: ${e.message}", e)
  640. }
  641. val tempObject = UomConversionService.BomObject().apply {
  642. code = unit.code
  643. udfudesc = unit.udfudesc
  644. udfShortDesc = unit.udfShortDesc
  645. lastModifyDate = commonUtils.timestampToLocalDateTime(unit.lastModifyDate).format(formatter)
  646. id = unit.id
  647. }
  648. finalUnitList += uomConversionService.transformItem(tempObject)
  649. successTransformList += unit.id
  650. logger.info("Transform Success (M18): ${unit.id}")
  651. } else {
  652. failTransformList.add(value.id)
  653. logger.error("Fail Message: ${unitDetail?.messages?.get(0)?.msgDetail}")
  654. logger.error("Fail Count ${failTransformList.size}: Unit ID - ${value.id} Not Found")
  655. }
  656. } catch (e: Exception) {
  657. failTransformList.add(value.id)
  658. logger.error("Transform Exception")
  659. logger.error("Transform Fail Message: ${e.message}")
  660. logger.error("Transform Fail Count ${failTransformList.size}: Unit ID - ${value.id}")
  661. }
  662. }
  663. uomConversionService.calculateSizeInGram(finalUnitList)
  664. finalUnitList.forEach {
  665. try {
  666. uomConversionService.saveUomConversion(it)
  667. successSaveList += it.m18Id
  668. logger.info("Save Success (M18): ${it.m18Id}")
  669. } catch (e: Exception) {
  670. failSaveList.add(it.m18Id)
  671. logger.error("Save Exception")
  672. logger.error("Save Fail Message: ${e.message}")
  673. logger.error("Save Fail Count ${failTransformList.size}: Unit ID - ${it.m18Id}")
  674. }
  675. }
  676. } else {
  677. logger.error("Unit List is null. May occur errors.")
  678. }
  679. logger.info("Total Transform Success (${successTransformList.size})")
  680. logger.info("Total Save Success (${successSaveList.size})")
  681. if (failTransformList.size > 0) {
  682. logger.error("Total Transform Fail (${failTransformList.size}): $failTransformList")
  683. }
  684. if (failSaveList.size > 0) {
  685. logger.error("Total Save Fail (${failSaveList.size}): $failSaveList")
  686. }
  687. logger.info("--------------------------------------------End - Saving M18 Units--------------------------------------------")
  688. val processed = values?.size ?: 0
  689. val failed = failTransformList.size + failSaveList.size
  690. return SyncResult(
  691. totalProcessed = processed,
  692. totalSuccess = successSaveList.size,
  693. totalFail = failed,
  694. query = "stSearch=${StSearchType.UNIT.value}",
  695. )
  696. }
  697. // --------------------------------------------- Currency --------------------------------------------- ///
  698. open fun getCurrencies(request: M18CommonRequest): M18CurrencyListResponse? {
  699. return getList<M18CurrencyListResponse>(
  700. stSearch = StSearchType.CURRENCY.value,
  701. params = null,
  702. conds = null,
  703. request = request
  704. )
  705. }
  706. open fun getCurrency(id: Long): M18CurrencyResponse? {
  707. logger.info("M18 Currency ID: $id")
  708. return getLine<M18CurrencyResponse>(
  709. id = id,
  710. params = null,
  711. api = M18_LOAD_CURRENCY_API
  712. )
  713. }
  714. open fun saveCurrencies(request: M18CommonRequest) : SyncResult{
  715. logger.info("--------------------------------------------Start - Saving M18 Currencies--------------------------------------------")
  716. val currencies = getCurrencies(request)
  717. val successList = mutableListOf<Long>()
  718. val failList = mutableListOf<Long>()
  719. val values = currencies?.values?.sortedBy { it.id }
  720. if (values != null) {
  721. // save currency
  722. values.forEach { currency ->
  723. try {
  724. val currencyRequest = SaveCurrencyRequest(
  725. id = null,
  726. code = currency.code,
  727. name = currency.sym,
  728. description = currency.curDesc,
  729. m18Id = currency.id,
  730. m18LastModifyDate = LocalDateTime.parse(currency.lastModifyDate, formatter)
  731. )
  732. currencyService.saveCurrency(currencyRequest)
  733. successList += currency.id
  734. logger.info("Save Success (M18): ${currency.id}")
  735. } catch (e: Exception) {
  736. failList += currency.id
  737. logger.error("Exception")
  738. logger.error("Fail Message: ${e.message}")
  739. logger.error("Fail Count ${failList.size}: Unit ID - ${currency.id}")
  740. }
  741. }
  742. } else {
  743. logger.error("Currency List is null. May occur errors.")
  744. }
  745. logger.info("Total Save Success (${successList.size})")
  746. if (failList.size > 0) {
  747. logger.error("Total Fail (${failList.size}): $failList")
  748. }
  749. logger.info("--------------------------------------------End - Saving Currencies--------------------------------------------")
  750. return SyncResult(
  751. totalProcessed = successList.size + failList.size,
  752. totalSuccess = successList.size,
  753. totalFail = failList.size
  754. )
  755. }
  756. // --------------------------------------------- Bom --------------------------------------------- ///
  757. open fun getBoms(request: M18CommonRequest): M18BomListResponse? {
  758. return getList<M18BomListResponse>(
  759. stSearch = StSearchType.BOM.value,
  760. params = null,
  761. conds = beIdConds,
  762. request = request
  763. )
  764. }
  765. open fun getBom(id: Long): M18BomResponse? {
  766. logger.info("M18 Bom ID: $id")
  767. return getLine<M18BomResponse>(
  768. id = id,
  769. params = null,
  770. api = M18_LOAD_BOM_API
  771. )
  772. }
  773. open fun saveBoms(request: M18CommonRequest) {
  774. logger.info("--------------------------------------------Start - Saving M18 Boms--------------------------------------------")
  775. val boms = getBoms(request)
  776. val successList = mutableListOf<Long>()
  777. val successDetailList = mutableListOf<Long>()
  778. val failList = mutableListOf<Long>()
  779. val failDetailList = mutableListOf<Pair<Long, MutableList<Long>>>()
  780. var failDetailCount = 0
  781. val values = boms?.values?.sortedBy { it.id }
  782. if (values != null) {
  783. values.forEach { bom ->
  784. try {
  785. val bomDetail = getBom(bom.id)
  786. val bomUdfBomForShop = bomDetail?.data?.udfbomforshop?.get(0)
  787. val bomUdfProduct = bomDetail?.data?.udfproduct
  788. logger.info(bomUdfBomForShop.toString())
  789. logger.info(bomUdfProduct.toString())
  790. if (bomUdfBomForShop != null && bomUdfProduct != null) {
  791. // Save Bom
  792. val saveBomRequest = SaveBomRequest(
  793. // itemId = itemsService.findByNameAndM18UomId(bomUdfBomForShop.desc, bomUdfBomForShop.udfUnit)?.id,
  794. code = bomUdfBomForShop.code,
  795. name = bomUdfBomForShop.desc,
  796. description = bomUdfBomForShop.desc,
  797. outputQty = if (bomUdfBomForShop.udfHarvest.trim().toBigDecimalOrNull() != null) bomUdfBomForShop.udfHarvest.trim().toBigDecimal() else BigDecimal(0),
  798. outputQtyUom = bomUdfBomForShop.udfHarvestUnit,
  799. yield = bomUdfBomForShop.udfYieldratePP,
  800. m18UomId = bomUdfBomForShop.udfUnit,
  801. m18Id = bomUdfBomForShop.id,
  802. m18LastModifyDate = commonUtils.timestampToLocalDateTime(bomUdfBomForShop.lastModifyDate)
  803. )
  804. val bomId = bomService.saveBom(saveBomRequest).id
  805. successList += bom.id
  806. // Save Bom Material
  807. logger.info("Start saving bom material...")
  808. val tempFailList = mutableListOf<Long>()
  809. bomUdfProduct.forEach { bomMaterial ->
  810. try {
  811. val saveBomMaterialRequest = SaveBomMaterialRequest(
  812. m18ItemId = bomMaterial.udfProduct,
  813. itemName = bomMaterial.udfIngredients,
  814. qty = bomMaterial.udfqty,
  815. m18UomId = bomMaterial.udfpurchaseUnit,
  816. uomName = bomMaterial.udfBaseUnit,
  817. bomId = bomId,
  818. m18Id = bomMaterial.id,
  819. m18LastModifyDate = commonUtils.timestampToLocalDateTime(bomUdfBomForShop.lastModifyDate)
  820. )
  821. bomMaterialService.saveBomMaterial(saveBomMaterialRequest)
  822. successDetailList += bomMaterial.id
  823. } catch (e: Exception) {
  824. tempFailList += bomMaterial.id
  825. logger.error("(Bom Material) Exception")
  826. logger.error("(Bom Material) Fail Message: ${e.message}")
  827. logger.error("(Bom Material) Fail Count ${++failDetailCount}: Bom Material ID - ${bomMaterial.id} | Bom ID - ${bom.id}")
  828. }
  829. }
  830. failDetailList += Pair(bom.id, tempFailList)
  831. logger.info("Save Success (M18): ${bom.id}")
  832. } else {
  833. failList.add(bom.id)
  834. logger.error("(Bom) Fail Message: ${bomDetail?.messages?.get(0)?.msgDetail}")
  835. logger.error("(Bom) Fail Count ${failList.size}: Bom ID - ${bom.id} Not Found")
  836. }
  837. } catch (e: Exception) {
  838. failList += bom.id
  839. logger.error("(Bom) Exception")
  840. logger.error("(Bom) Fail Message: ${e.message}")
  841. logger.error("(Bom) Fail Count ${failList.size}: Bom ID - ${bom.id}")
  842. }
  843. }
  844. } else {
  845. logger.error("Currency List is null. May occur errors.")
  846. }
  847. logger.info("Total Bom Save Success (${successList.size})")
  848. logger.info("Total Bom Save Detail Success (${successDetailList.size})")
  849. if (failList.size > 0) {
  850. logger.error("Total Bom Fail (${failList.size}): $failList")
  851. }
  852. if (failDetailCount > 0) {
  853. logger.error("Total Bom Detail Fail (${failDetailCount}): $failDetailList")
  854. }
  855. logger.info("--------------------------------------------End - Saving Boms--------------------------------------------")
  856. }
  857. // --------------------------------------------- Business Unit (Shop) --------------------------------------------- ///
  858. open fun getBusinessUnits(request: M18CommonRequest): M18BusinessUnitListResponse? {
  859. // seems no beId
  860. return getList<M18BusinessUnitListResponse>(
  861. stSearch = StSearchType.BUSINESS_UNIT.value,
  862. params = null,
  863. // conds = beIdConds
  864. request = request
  865. )
  866. }
  867. open fun getBusinessUnit(id: Long): M18BusinessUnitResponse? {
  868. logger.info("M18 Business Unit ID: $id")
  869. return getLine<M18BusinessUnitResponse>(
  870. id = id,
  871. params = null,
  872. api = M18_LOAD_BUSINESS_UNIT_API
  873. )
  874. }
  875. open fun saveBusinessUnits(request: M18CommonRequest) : SyncResult {
  876. logger.info("--------------------------------------------Start - Saving M18 Business Units (Shops)--------------------------------------------")
  877. val businessUnits = getBusinessUnits(request)
  878. val successList = mutableListOf<Long>()
  879. val failList = mutableListOf<Long>()
  880. val values = businessUnits?.values?.sortedBy { it.id }
  881. val busMessages = if(businessUnits?.messages?.isNotEmpty() == true) businessUnits.messages[0] else null
  882. if (values != null) {
  883. values.forEach { businessUnit ->
  884. // if (vendor.id in exampleVendors) {
  885. try {
  886. val businessUnitDetail = getBusinessUnit(businessUnit.id)
  887. val virdept = businessUnitDetail?.data?.virdept?.get(0)
  888. val buMessages = if(businessUnitDetail?.messages?.isNotEmpty() == true) businessUnitDetail?.messages[0] else null
  889. if (virdept != null) {
  890. val saveShopRequest = SaveShopRequest(
  891. id = null,
  892. code = virdept.code,
  893. name = virdept.descZhTW.ifEmpty { virdept.descZhCN.ifEmpty { virdept.desc } },
  894. brNo = null,
  895. contactNo = virdept.tel,
  896. contactEmail = virdept.email,
  897. contactName = null,
  898. addr1 = virdept.addr.ifEmpty { virdept.addr_en },
  899. addr2 = virdept.addr2.ifEmpty { virdept.addr2_en },
  900. addr3 = virdept.addr3.ifEmpty { virdept.addr3_en },
  901. addr4 = null,
  902. district = null,
  903. type = ShopType.SHOP.value,
  904. m18Id = businessUnit.id,
  905. m18LastModifyDate = commonUtils.timestampToLocalDateTime(virdept.lastModifyDate)
  906. )
  907. shopService.saveShop(saveShopRequest)
  908. successList.add(businessUnit.id)
  909. logger.info("Success Count ${successList.size}: ${businessUnit.id} | ${virdept.code} | ${virdept.descZhTW.ifEmpty { virdept.descZhCN.ifEmpty { virdept.desc } }}")
  910. } else {
  911. failList.add(businessUnit.id)
  912. logger.error("(Business Unit) Fail Message: ${buMessages?.msgDetail}")
  913. logger.error("(Business Unit) Fail Count ${failList.size}: Business Unit ID - ${businessUnit.id} Not Found")
  914. }
  915. } catch (e: Exception) {
  916. failList.add(businessUnit.id)
  917. logger.error("(Business Unit) Exception")
  918. logger.error("(Business Unit) Fail Message: ${e.message}")
  919. logger.error("(Business Unit) Fail Count ${failList.size}: Business Unit ID - ${businessUnit.id}")
  920. }
  921. // }
  922. }
  923. } else {
  924. logger.error("(Business Unit) Business Unit List is null. May occur errors.")
  925. logger.error("(Business Unit)) Fail Message: ${busMessages?.msgDetail}")
  926. logger.error("(Business Unit) Business Unit List is null. May occur errors.")
  927. }
  928. logger.info("Total Success (${successList.size})")
  929. if (failList.size > 0) {
  930. logger.error("Total Fail (${failList.size}): $failList")
  931. }
  932. logger.info("--------------------------------------------End - Saving M18 Business Units--------------------------------------------")
  933. return SyncResult(
  934. totalProcessed = successList.size + failList.size,
  935. totalSuccess = successList.size,
  936. totalFail = failList.size
  937. )
  938. }
  939. }