You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

768 rivejä
40 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.enums.M18DataLogStatus
  6. import com.ffii.fpsms.m18.model.*
  7. import com.ffii.fpsms.m18.utils.CommonUtils
  8. import com.ffii.fpsms.m18.web.models.M18CommonRequest
  9. import com.ffii.fpsms.modules.deliveryOrder.service.DeliveryOrderLineService
  10. import com.ffii.fpsms.modules.deliveryOrder.service.DeliveryOrderService
  11. import com.ffii.fpsms.modules.master.service.ItemUomService
  12. import com.ffii.fpsms.modules.master.service.ItemsService
  13. import com.ffii.fpsms.modules.master.service.ShopService
  14. import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderLineStatus
  15. import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderStatus
  16. import com.ffii.fpsms.modules.purchaseOrder.enums.PurchaseOrderType
  17. import com.ffii.fpsms.modules.common.SettingNames
  18. import com.ffii.fpsms.modules.purchaseOrder.service.PurchaseOrderLineService
  19. import com.ffii.fpsms.modules.purchaseOrder.service.PurchaseOrderService
  20. import com.ffii.fpsms.modules.purchaseOrder.web.model.SavePurchaseOrderLineRequest
  21. import com.ffii.fpsms.modules.purchaseOrder.web.model.SavePurchaseOrderRequest
  22. import com.ffii.fpsms.modules.settings.service.SettingsService
  23. import org.slf4j.Logger
  24. import org.slf4j.LoggerFactory
  25. import org.springframework.stereotype.Service
  26. import java.time.LocalDateTime
  27. import kotlin.reflect.full.memberProperties
  28. import java.time.format.DateTimeFormatter
  29. import com.ffii.fpsms.m18.model.SyncResult
  30. @Service
  31. open class M18PurchaseOrderService(
  32. val m18Config: M18Config,
  33. val apiCallerService: ApiCallerService,
  34. val m18DataLogService: M18DataLogService,
  35. val purchaseOrderService: PurchaseOrderService,
  36. val purchaseOrderLineService: PurchaseOrderLineService,
  37. val deliveryOrderService: DeliveryOrderService,
  38. val deliveryOrderLineService: DeliveryOrderLineService,
  39. val itemsService: ItemsService,
  40. val shopService: ShopService,
  41. val itemUomService: ItemUomService,
  42. val m18MasterDataService: M18MasterDataService,
  43. val settingsService: SettingsService,
  44. ) {
  45. companion object {
  46. fun isStockInLookupPoCode(code: String?): Boolean {
  47. val trimmed = code?.trim().orEmpty()
  48. return trimmed.length > 14 && (trimmed.startsWith("PP") || trimmed.startsWith("PF"))
  49. }
  50. }
  51. val commonUtils = CommonUtils()
  52. val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java)
  53. val lastModifyDateStart = "2025-05-14 14:00:00"
  54. val lastModifyDateEnd = "2025-05-14 14:30:00"
  55. // val lastModifyDateConds =
  56. // "lastModifyDate=largerOrEqual=${lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${lastModifyDateEnd}"
  57. // val lastModifyDate = LocalDateTime.now().minusMinutes(30)
  58. // val commonConds =
  59. // "(beId=equal=${m18Config.BEID_PF}=or=beId=equal=${m18Config.BEID_PP}=or=beId=equal=${m18Config.BEID_TOA})=and=lastModifyDate=largerOrEqual=${lastModifyDate}"
  60. // M18 API
  61. val M18_LOAD_PURCHASE_ORDER_API = "/root/api/read/po"
  62. val M18_LOAD_PURCHASE_ORDER_BY_CODE_API = "/root/api/read/po"
  63. val M18_FETCH_PURCHASE_ORDER_LIST_API = "/search/search"
  64. // Include material po, oem po
  65. open fun getPurchaseOrdersWithType(request: M18CommonRequest): M18PurchaseOrderListResponseWithType? {
  66. val purchaseOrders = M18PurchaseOrderListResponseWithType(mutableListOf())
  67. val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
  68. val lastDateConds = if (request.modifiedDateFrom != null && request.modifiedDateTo != null) {
  69. val dateFrom = LocalDateTime.parse(request.modifiedDateFrom, formatter).toString()
  70. val dateTo = LocalDateTime.parse(request.modifiedDateTo, formatter).toString()
  71. "lastModifyDate=largerOrEqual=$dateFrom=and=lastModifyDate=lessOrEqual=$dateTo"
  72. } else {
  73. ""
  74. }
  75. val dDateConds = if (request.dDateFrom != null && request.dDateTo != null) {
  76. val dateFrom = LocalDateTime.parse(request.dDateFrom, formatter).toLocalDate().toString()
  77. val dateTo = LocalDateTime.parse(request.dDateTo, formatter).toLocalDate().toString()
  78. "dDate=largerOrEqual=$dateFrom=and=dDate=lessOrEqual=$dateTo"
  79. } else {
  80. ""
  81. }
  82. val dDateEqualConds = if (request.dDateEqual != null) {
  83. val dDateEqual = LocalDateTime.parse(request.dDateEqual, formatter).toLocalDate().toString()
  84. "dDate=equal=$dDateEqual"
  85. } else {
  86. ""
  87. }
  88. // Material PO — same BE/supplier scope as [buildMaterialPoScopeConds]
  89. var materialPoConds = buildMaterialPoScopeConds()
  90. if (request.modifiedDateFrom != null && request.modifiedDateTo != null) {
  91. materialPoConds += "=and=(${lastDateConds})"
  92. }
  93. if (request.dDateFrom != null && request.dDateTo != null) {
  94. materialPoConds += "=and=(${dDateConds})"
  95. }
  96. if (request.dDateEqual != null) {
  97. materialPoConds += "=and=(${dDateEqualConds})"
  98. }
  99. println("materialPoConds: ${materialPoConds}")
  100. val materialPoParams = M18PurchaseOrderListRequest(
  101. params = null,
  102. conds = materialPoConds
  103. )
  104. try {
  105. purchaseOrders.valuesWithType += Pair(
  106. PurchaseOrderType.MATERIAL,
  107. apiCallerService.get<M18PurchaseOrderListResponse, M18PurchaseOrderListRequest>(
  108. M18_FETCH_PURCHASE_ORDER_LIST_API,
  109. materialPoParams
  110. ).block()
  111. )
  112. } catch (e: Exception) {
  113. logger.error("(Getting Material Po list) Error on Function - ${e.stackTrace}")
  114. logger.error(e.message)
  115. }
  116. purchaseOrders.query = materialPoConds
  117. // Shop PO
  118. /* val shopPoBuyers = commonUtils.listToString(listOf(m18Config.BEID_TOA), "beId=equal=", "=or=")
  119. val shopPoSupplier = commonUtils.listToString(
  120. shopService.findM18VendorIdsByCodeRegexp(m18Config.SHOP_PO_SUPPLIER),
  121. "venId=equal=",
  122. "=or="
  123. )
  124. val shopPoConds = "(${shopPoBuyers})=and=(${shopPoSupplier})=and=(${lastModifyDateConds})"
  125. println("shopPoConds: ${shopPoConds}")
  126. val shopPoParams = M18PurchaseOrderListRequest(
  127. params = null,
  128. conds = shopPoConds
  129. )
  130. try {
  131. purchaseOrders.valuesWithType += Pair(
  132. PurchaseOrderType.SHOP, apiCallerService.get<M18PurchaseOrderListResponse, M18PurchaseOrderListRequest>(
  133. M18_FETCH_PURCHASE_ORDER_LIST_API,
  134. shopPoParams
  135. ).block()
  136. )
  137. } catch (e: Exception) {
  138. logger.error("(Getting Shop Po list) Error on Function - ${e.stackTrace}")
  139. logger.error(e.message)
  140. } */
  141. // OEM PO
  142. /* val oemPoBuyers = commonUtils.listToString(listOf(m18Config.BEID_PF, m18Config.BEID_PP), "beId=equal=", "=or=")
  143. val oemPoSupplier = commonUtils.listToString(
  144. shopService.findM18VendorIdsByCodeRegexp(m18Config.OEM_PO_SUPPLIER),
  145. "venId=equal=",
  146. "=or="
  147. )
  148. val oemPoConds = "(${oemPoBuyers})=and=(${oemPoSupplier})=and=(${lastModifyDateConds})"
  149. println("oemPoConds: ${oemPoConds}")
  150. val oemPoParams = M18PurchaseOrderListRequest(
  151. params = null,
  152. conds = oemPoConds
  153. )
  154. try {
  155. purchaseOrders.valuesWithType += Pair(
  156. PurchaseOrderType.OEM, apiCallerService.get<M18PurchaseOrderListResponse, M18PurchaseOrderListRequest>(
  157. M18_FETCH_PURCHASE_ORDER_LIST_API,
  158. oemPoParams
  159. ).block()
  160. )
  161. } catch (e: Exception) {
  162. logger.error("(Getting OEM Po list) Error on Function - ${e.stackTrace}")
  163. logger.error(e.message)
  164. } */
  165. return purchaseOrders
  166. }
  167. open fun getPurchaseOrder(id: Long): M18PurchaseOrderResponse? {
  168. val purchaseOrderParams = M18PurchaseOrderRequest(
  169. id = id
  170. )
  171. var purchaseOrder: M18PurchaseOrderResponse? = null
  172. try {
  173. purchaseOrder = apiCallerService.get<M18PurchaseOrderResponse, M18PurchaseOrderRequest>(
  174. M18_LOAD_PURCHASE_ORDER_API,
  175. purchaseOrderParams
  176. ).block()
  177. } catch (e: Exception) {
  178. logger.error("(Getting Po Detail) Error on Function - ${e.stackTrace}")
  179. logger.error(e.message)
  180. }
  181. return purchaseOrder
  182. }
  183. open fun getPurchaseOrderByCode(code: String): M18PurchaseOrderResponse? {
  184. var purchaseOrder: M18PurchaseOrderResponse? = null
  185. try {
  186. purchaseOrder = apiCallerService.get<M18PurchaseOrderResponse>(
  187. M18_LOAD_PURCHASE_ORDER_BY_CODE_API,
  188. mapOf("code" to code)
  189. ).block()
  190. } catch (e: Exception) {
  191. logger.error("(Getting Po Detail By Code) Error on Function - ${e.stackTrace}")
  192. logger.error(e.message)
  193. }
  194. return purchaseOrder
  195. }
  196. private fun resolvePoTypeByBeId(beId: Long?): PurchaseOrderType {
  197. return if (beId?.toString() == m18Config.BEID_TOA) PurchaseOrderType.SHOP else PurchaseOrderType.MATERIAL
  198. }
  199. /**
  200. * Material PO BE/supplier scope used by scheduler and by-code sync:
  201. * beId in (PP, PF) and venId not in [M18Config.MATERIAL_PO_SUPPLIER_NOT] (e.g. P06/P07 shop suppliers).
  202. */
  203. private fun buildMaterialPoScopeConds(): String {
  204. val materialPoBuyers =
  205. commonUtils.listToString(listOf(m18Config.BEID_PP, m18Config.BEID_PF), "beId=equal=", "=or=")
  206. val materialPoSupplierNot = commonUtils.listToString(
  207. shopService.findM18VendorIdsByCodeRegexp(m18Config.MATERIAL_PO_SUPPLIER_NOT),
  208. "venId=unequal=",
  209. "=or="
  210. )
  211. return "(${materialPoBuyers})=and=(${materialPoSupplierNot})"
  212. }
  213. open fun savePurchaseOrderByCode(code: String): SyncResult {
  214. // Same approach as scheduler list sync, restricted to one document code:
  215. // 1) /search/search with material-PO BE/supplier scope + code
  216. // 2) sync by PO id (reuses create/update logic)
  217. // Rejects shop POs (TOA / P06|P07) that belong to DO sync instead.
  218. val trimmed = code.trim()
  219. val conds = "(code=equal=$trimmed)=and=${buildMaterialPoScopeConds()}"
  220. return saveFromPoListSearch(conds, searchPurchaseOrdersByConds(conds))
  221. }
  222. /**
  223. * Stock-in `/po` lookup.
  224. * - Missing local PO: always sync from M18 (same as [savePurchaseOrderByCode]).
  225. * - Local pending + setting [SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED] true:
  226. * sync from M18 (overwrite pending). Receiving / completed are not refreshed.
  227. * - Local PO + setting not true: do not call M18.
  228. */
  229. open fun savePurchaseOrderByCodeIfNewer(code: String): SyncResult =
  230. refreshForStockInLookup(code).syncResult
  231. open fun refreshForStockInLookup(code: String): StockInPoRefreshOutcome {
  232. val trimmed = code.trim()
  233. val enabled = isStockInRefreshIfNewerEnabled()
  234. val localStatus = purchaseOrderService.findActiveStatusByCode(trimmed)
  235. logger.info(
  236. "Purchase Order: stock-in ifNewer code={} settingEnabled={} localStatus={}",
  237. trimmed,
  238. enabled,
  239. localStatus ?: "<none>",
  240. )
  241. if (localStatus != null && !enabled) {
  242. logger.info(
  243. "Purchase Order: Skipping M18 sync — setting ${SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED} " +
  244. "is not true. code=$trimmed status=$localStatus"
  245. )
  246. return StockInPoRefreshOutcome(
  247. action = StockInPoRefreshAction.SKIPPED_DISABLED,
  248. syncResult = SyncResult(
  249. totalProcessed = 0,
  250. totalSuccess = 0,
  251. totalFail = 0,
  252. query = "skipped (ifNewer): setting ${SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED} is not true"
  253. ),
  254. )
  255. }
  256. if (localStatus != null &&
  257. !PurchaseOrderStatus.PENDING.value.equals(localStatus, ignoreCase = true)
  258. ) {
  259. logger.info(
  260. "Purchase Order: Skipping M18 sync — local PO code=$trimmed status=$localStatus (only pending may be refreshed)."
  261. )
  262. return StockInPoRefreshOutcome(
  263. action = StockInPoRefreshAction.SKIPPED_NOT_PENDING,
  264. syncResult = SyncResult(
  265. totalProcessed = 0,
  266. totalSuccess = 0,
  267. totalFail = 0,
  268. query = "skipped (ifNewer): local PO not pending: $trimmed status=$localStatus"
  269. ),
  270. )
  271. }
  272. return StockInPoRefreshOutcome(
  273. action = StockInPoRefreshAction.SYNCED,
  274. syncResult = savePurchaseOrderByCode(trimmed),
  275. )
  276. }
  277. private fun isStockInRefreshIfNewerEnabled(): Boolean =
  278. settingsService.isBooleanTrueFromDb(SettingNames.M18_PO_STOCK_IN_REFRESH_IF_NEWER_ENABLED)
  279. private fun saveFromPoListSearch(
  280. query: String,
  281. poListResponse: M18PurchaseOrderListResponse?,
  282. ): SyncResult {
  283. val poValues = poListResponse?.values
  284. if (poValues.isNullOrEmpty()) {
  285. return SyncResult(
  286. totalProcessed = 1,
  287. totalSuccess = 0,
  288. totalFail = 1,
  289. query = query
  290. )
  291. }
  292. val request = M18PurchaseOrderListResponseWithType(
  293. valuesWithType = mutableListOf(Pair(PurchaseOrderType.MATERIAL, poListResponse)),
  294. query = query
  295. )
  296. return savePurchaseOrdersWithPreparedList(purchaseOrdersWithType = request)
  297. }
  298. private fun searchPurchaseOrdersByConds(conds: String): M18PurchaseOrderListResponse? {
  299. val searchRequest = M18PurchaseOrderListRequest(
  300. stSearch = "po",
  301. params = null,
  302. conds = conds
  303. )
  304. return try {
  305. apiCallerService.get<M18PurchaseOrderListResponse, M18PurchaseOrderListRequest>(
  306. M18_FETCH_PURCHASE_ORDER_LIST_API,
  307. searchRequest
  308. ).block()
  309. } catch (e: Exception) {
  310. logger.error("(Getting Po List By Code) Error on Function - ${e.stackTrace}")
  311. logger.error(e.message)
  312. null
  313. }
  314. }
  315. open fun savePurchaseOrders(request: M18CommonRequest) : SyncResult{
  316. val purchaseOrdersWithType = getPurchaseOrdersWithType(request)
  317. return savePurchaseOrdersWithPreparedList(
  318. purchaseOrdersWithType = purchaseOrdersWithType,
  319. preloadPurchaseOrderDetails = null
  320. )
  321. }
  322. private fun savePurchaseOrdersWithPreparedList(
  323. purchaseOrdersWithType: M18PurchaseOrderListResponseWithType?,
  324. preloadPurchaseOrderDetails: Map<Long, M18PurchaseOrderResponse?>? = null
  325. ): SyncResult {
  326. logger.info("--------------------------------------------Start - Saving M18 Purchase Order--------------------------------------------")
  327. val successList = mutableListOf<Long>()
  328. val successDetailList = mutableListOf<Long>()
  329. val failList = mutableListOf<Long>()
  330. val failDetailList = mutableListOf<Long>()
  331. val skippedNotPendingList = mutableListOf<String>()
  332. val affectedItemIds = mutableSetOf<Long>()
  333. val uomByM18IdCache = mutableMapOf<Long, com.ffii.fpsms.modules.master.entity.ItemUom?>()
  334. val poRefType = "Purchase Order"
  335. val poLineRefType = "Purchase Order Line"
  336. if (purchaseOrdersWithType != null) {
  337. // Loop for Purchase Orders (values)
  338. purchaseOrdersWithType.valuesWithType.forEach { purchaseOrderWithType ->
  339. // if success
  340. val purchaseOrdersValues = purchaseOrderWithType.second?.values
  341. // if fail
  342. val purchaseOrdersMessages = purchaseOrderWithType.second?.messages
  343. if (purchaseOrdersValues != null) {
  344. purchaseOrdersValues.forEach { purchaseOrder ->
  345. val purchaseOrderDetail =
  346. preloadPurchaseOrderDetails?.get(purchaseOrder.id) ?: getPurchaseOrder(purchaseOrder.id)
  347. var purchaseOrderId: Long? = null //FP-MTMS
  348. // Process for Purchase Order (mainpo)
  349. // Assume only one PO in the PO (search by PO ID)
  350. val mainpo = purchaseOrderDetail?.data?.mainpo?.get(0)
  351. val pot = purchaseOrderDetail?.data?.pot
  352. val purchaseOrderLineMessage = purchaseOrderDetail?.messages
  353. logger.info("purchaseOrderDetail: data is null? ${purchaseOrderDetail?.data == null} | mainpo is null? ${purchaseOrderDetail?.data?.mainpo == null} | get(0) is null? ${purchaseOrderDetail?.data?.mainpo?.get(0) == null}")
  354. // purchase_order + m18_data_log table
  355. if (mainpo != null) {
  356. val m18PurchaseOrderId = mainpo.id
  357. // Find the latest m18 data log by m18 id & type
  358. // logger.info("${poRefType}: Finding For Latest M18 Data Log...")
  359. val latestPurchaseOrderLog =
  360. m18DataLogService.findLatestM18DataLogWithSuccess(m18PurchaseOrderId, poRefType)
  361. val existingPurchaseOrderForSync =
  362. latestPurchaseOrderLog?.id?.let { purchaseOrderService.findByM18DataLogId(it) }
  363. if (existingPurchaseOrderForSync != null &&
  364. existingPurchaseOrderForSync.status != PurchaseOrderStatus.PENDING
  365. ) {
  366. val statusValue = existingPurchaseOrderForSync.status?.value ?: "unknown"
  367. logger.info(
  368. "${poRefType}: Skipping M18 sync — local PO id=${existingPurchaseOrderForSync.id} " +
  369. "code=${existingPurchaseOrderForSync.code} status=$statusValue " +
  370. "(only pending may be overwritten). M18 ID: $m18PurchaseOrderId"
  371. )
  372. skippedNotPendingList.add(
  373. "code=${existingPurchaseOrderForSync.code} status=$statusValue"
  374. )
  375. return@forEach
  376. }
  377. // logger.info(latestPurchaseOrderLog.toString())
  378. // Save to m18_data_log table
  379. // logger.info("${poRefType}: Saving for M18 Data Log...")
  380. val mainpoJson =
  381. mainpo::class.memberProperties.associate { prop -> prop.name to prop.getter.call(mainpo) }
  382. .toMutableMap()
  383. val saveM18PurchaseOrderLogRequest = SaveM18DataLogRequest(
  384. id = null,
  385. refType = poRefType,
  386. m18Id = m18PurchaseOrderId,
  387. m18LastModifyDate = commonUtils.timestampToLocalDateTime(mainpo.lastModifyDate),
  388. // dataLog = mainpoJson,
  389. statusEnum = M18DataLogStatus.NOT_PROCESS
  390. )
  391. val saveM18PurchaseOrderLog =
  392. m18DataLogService.saveM18DataLog(saveM18PurchaseOrderLogRequest)
  393. // logger.info("${poRefType}: Saved M18 Data Log. ID: ${saveM18PurchaseOrderLog.id}")
  394. try {
  395. // Find the purchase_order if exist (re-use lookup from pending guard above)
  396. // logger.info("${poRefType}: Finding exising purchase order...")
  397. val existingPurchaseOrder = existingPurchaseOrderForSync
  398. // logger.info("${poRefType}: Exising purchase order ID: ${existingPurchaseOrder?.id}")
  399. // Save to purchase_order table
  400. // logger.info("${poRefType}: Saving purchase order...")
  401. val savePurchaseOrderRequest = SavePurchaseOrderRequest(
  402. id = existingPurchaseOrder?.id,
  403. code = mainpo.code,
  404. m18SupplierId = mainpo.venId,
  405. m18ShopId = mainpo.virDeptId,
  406. m18CurrencyId = mainpo.curId,
  407. orderDate = commonUtils.timestampToLocalDateTime(mainpo.tDate),
  408. estimatedArrivalDate = commonUtils.timestampToLocalDateTime(mainpo.dDate),
  409. completeDate = null,
  410. status = PurchaseOrderStatus.PENDING.value,
  411. type = resolvePoTypeByBeId(mainpo.beId).value,
  412. m18DataLogId = saveM18PurchaseOrderLog.id,
  413. m18BeId = mainpo.beId,
  414. m18CreatedUId = mainpo.createUid,
  415. )
  416. val savePurchaseOrderResponse =
  417. purchaseOrderService.savePurchaseOrder(savePurchaseOrderRequest)
  418. purchaseOrderId = savePurchaseOrderResponse.id
  419. // Update m18_data_log with success
  420. val successSaveM18PurchaseOrderLogRequest = SaveM18DataLogRequest(
  421. id = saveM18PurchaseOrderLog.id,
  422. dataLog = mainpoJson,
  423. statusEnum = M18DataLogStatus.SUCCESS
  424. )
  425. m18DataLogService.saveM18DataLog(successSaveM18PurchaseOrderLogRequest)
  426. // log success info
  427. successList.add(m18PurchaseOrderId)
  428. logger.info("${poRefType}: Saved purchase order. ID: ${savePurchaseOrderResponse.id} | M18 ${poRefType} ID: ${m18PurchaseOrderId}")
  429. } catch (e: Exception) {
  430. failList.add(m18PurchaseOrderId)
  431. // logger.error("${poRefType}: Saving Failure!")
  432. logger.error("Error on Function - ${e.stackTrace} | Type: ${poRefType} | M18 ID: ${m18PurchaseOrderId} | Different? ${mainpo.id}")
  433. logger.error(e.message)
  434. val errorSaveM18PurchaseOrderLogRequest = SaveM18DataLogRequest(
  435. id = saveM18PurchaseOrderLog.id,
  436. dataLog = mutableMapOf(Pair("Exception Message", e.message)),
  437. statusEnum = M18DataLogStatus.FAIL
  438. )
  439. m18DataLogService.saveM18DataLog(errorSaveM18PurchaseOrderLogRequest)
  440. // logger.error("${poRefType}: M18 Data Log Updated! Please see the error. ID: ${saveM18PurchaseOrderLogRequest.id}")
  441. }
  442. // purchase_order_line + m18_data_log
  443. if (pot != null) {
  444. // Loop for Purchase Order Lines (pot)
  445. pot.forEach { line ->
  446. // Find the latest m18 data log by m18 id & type
  447. // logger.info("${poLineRefType}: Finding For Latest M18 Data Log...")
  448. val latestPurchaseOrderLineLog =
  449. m18DataLogService.findLatestM18DataLogWithSuccess(line.id, poLineRefType)
  450. // logger.info("${poLineRefType}: Latest M18 Data Log ID: ${latestPurchaseOrderLineLog?.id}")
  451. // Save to m18_data_log table
  452. // logger.info("${poLineRefType}: Saving for M18 Data Log...")
  453. val lineJson =
  454. line::class.memberProperties.associate { prop ->
  455. prop.name to prop.getter.call(
  456. line
  457. )
  458. }
  459. .toMutableMap()
  460. val saveM18PurchaseOrderLineLogRequest = SaveM18DataLogRequest(
  461. id = null,
  462. refType = poLineRefType,
  463. m18Id = line.id,
  464. m18LastModifyDate = commonUtils.timestampToLocalDateTime(mainpo.lastModifyDate),
  465. // dataLog = lineJson,
  466. statusEnum = M18DataLogStatus.NOT_PROCESS
  467. )
  468. val saveM18PurchaseOrderLineLog =
  469. m18DataLogService.saveM18DataLog(saveM18PurchaseOrderLineLogRequest)
  470. // logger.info("${poLineRefType}: Saved M18 Data Log. ID: ${saveM18PurchaseOrderLineLog.id}")
  471. // logger.info("${poLineRefType}: Finding item...")
  472. val itemId: Long? = m18MasterDataService.resolveLocalItemId(line.proId)
  473. logger.info("${poLineRefType}: Item ID: ${itemId} | M18 Item ID: ${line.proId}")
  474. if (itemId == null) {
  475. failDetailList.add(line.id)
  476. logger.error(
  477. "${poLineRefType}: PO ${mainpo.code}: Cannot resolve local item for M18 proId=${line.proId}, skipping line ${line.id}"
  478. )
  479. val errorSaveM18PurchaseOrderLineLogRequest = SaveM18DataLogRequest(
  480. id = saveM18PurchaseOrderLineLog.id,
  481. dataLog = mutableMapOf(
  482. "Exception Message" to "PO ${mainpo.code} Cannot resolve local item for M18 proId=${line.proId}"
  483. ),
  484. statusEnum = M18DataLogStatus.FAIL
  485. )
  486. m18DataLogService.saveM18DataLog(errorSaveM18PurchaseOrderLineLogRequest)
  487. return@forEach
  488. }
  489. try {
  490. // Find the purchase_order_line if exist (stable key: PO + M18 line id)
  491. // logger.info("${poLineRefType}: Finding exising purchase order line...")
  492. val existingPurchaseOrderLine =
  493. purchaseOrderId?.let { pid ->
  494. purchaseOrderLineService.findPurchaseOrderLineByPurchaseOrderAndM18LineId(
  495. pid,
  496. line.id
  497. )
  498. } ?: latestPurchaseOrderLineLog?.id?.let {
  499. purchaseOrderLineService.findPurchaseOrderLineByM18Id(it)
  500. }
  501. // logger.info("${poLineRefType}: Exising purchase order line ID: ${existingPurchaseOrderLine?.id}")
  502. // Save to purchase_order_line table
  503. // logger.info("${poLineRefType}: Saving purchase order line...")
  504. val purchaseItemUom = itemId?.let { itemUomService.findPurchaseUnitByItemId(it) }
  505. val m18ItemUom = uomByM18IdCache.getOrPut(line.unitId) {
  506. itemUomService.findByM18Id(line.unitId)
  507. }
  508. val qtyM18 = line.qty
  509. val sourceUomId = m18ItemUom?.uom?.id
  510. // qtyM18 is the original qty in M18 line uom.
  511. // System PO line.qty must be qty in purchase unit.
  512. val convertedQty = if (itemId != null && sourceUomId != null) {
  513. itemUomService.convertQtyToPurchaseQty(itemId, sourceUomId, qtyM18)
  514. } else {
  515. qtyM18
  516. }
  517. val savePurchaseOrderLineRequest = SavePurchaseOrderLineRequest(
  518. id = existingPurchaseOrderLine?.id,
  519. itemId = itemId,
  520. uomId = purchaseItemUom?.uom?.id,
  521. purchaseOrderId = purchaseOrderId,
  522. qty = convertedQty,
  523. qtyM18 = qtyM18,
  524. up = line.up,
  525. price = line.amt,
  526. // m18CurrencyId = mainpo.curId,
  527. status = existingPurchaseOrderLine?.status?.value
  528. ?: PurchaseOrderLineStatus.PENDING.value,
  529. m18DataLogId = saveM18PurchaseOrderLineLog.id,
  530. m18Discount = line.disc,
  531. m18Lot = line.lot,
  532. uomIdM18 = sourceUomId
  533. )
  534. val savePurchaseOrderLineResponse =
  535. purchaseOrderLineService.savePurchaseOrderLine(savePurchaseOrderLineRequest)
  536. // Update m18_data_log with success
  537. val successSaveM18PurchaseOrderLineLogRequest = SaveM18DataLogRequest(
  538. id = saveM18PurchaseOrderLineLog.id,
  539. dataLog = lineJson,
  540. statusEnum = M18DataLogStatus.SUCCESS
  541. )
  542. m18DataLogService.saveM18DataLog(successSaveM18PurchaseOrderLineLogRequest)
  543. // log success info
  544. successDetailList.add(line.id)
  545. // logger.info("${poLineRefType}: Purchase order ID: ${purchaseOrderId} | M18 ID: ${purchaseOrder.id}")
  546. logger.info("${poLineRefType}: Saved purchase order line. ID: ${savePurchaseOrderLineResponse.id} | M18 Line ID: ${line.id} | Purchase order ID: ${purchaseOrderId} | M18 ID: ${purchaseOrder.id}")
  547. itemId?.let { affectedItemIds.add(it) }
  548. } catch (e: Exception) {
  549. failDetailList.add(line.id)
  550. // logger.error("${poLineRefType}: Saving Failure!")
  551. logger.error("Error on Function - ${e.stackTrace} | Type: ${poLineRefType} | M18 ID: ${line.id}")
  552. logger.error(e.message)
  553. val errorSaveM18PurchaseOrderLineLogRequest = SaveM18DataLogRequest(
  554. id = saveM18PurchaseOrderLineLog.id,
  555. dataLog = mutableMapOf(Pair("Exception Message", e.message)),
  556. statusEnum = M18DataLogStatus.FAIL
  557. )
  558. m18DataLogService.saveM18DataLog(errorSaveM18PurchaseOrderLineLogRequest)
  559. // logger.error("${poLineRefType}: M18 Data Log Updated! Please see the error. ID: ${saveM18PurchaseOrderLineLog.id}")
  560. }
  561. }
  562. // Mark as deleted any local PO lines that no longer exist in M18 (removed there)
  563. if (purchaseOrderId != null) {
  564. val m18LineIds = pot.map { it.id }.toSet()
  565. val (markedDeleted, deletedItemIds) =
  566. purchaseOrderLineService.markDeletedLinesNotInM18(purchaseOrderId, m18LineIds)
  567. affectedItemIds.addAll(deletedItemIds)
  568. if (markedDeleted > 0) {
  569. logger.info("${poLineRefType}: Marked $markedDeleted line(s) as deleted (not in M18). PO ID: $purchaseOrderId | M18 PO ID: ${purchaseOrder.id}")
  570. }
  571. val (dupesRemoved, dupeItemIds) =
  572. purchaseOrderLineService.markDuplicatePurchaseOrderLinesForSameM18LineId(
  573. purchaseOrderId,
  574. m18LineIds
  575. )
  576. affectedItemIds.addAll(dupeItemIds)
  577. if (dupesRemoved > 0) {
  578. logger.info("${poLineRefType}: Marked $dupesRemoved duplicate line(s) as deleted (same M18 line id). PO ID: $purchaseOrderId | M18 PO ID: ${purchaseOrder.id}")
  579. }
  580. }
  581. } else {
  582. // pot
  583. // logger.error("${poLineRefType}: Saving Failure!")
  584. val saveM18PurchaseOrderLineLogRequest = SaveM18DataLogRequest(
  585. id = null,
  586. refType = "${poLineRefType}",
  587. m18Id = m18PurchaseOrderId,
  588. m18LastModifyDate = commonUtils.timestampToLocalDateTime(mainpo.lastModifyDate),
  589. // dataLog = mutableMapOf(Pair("Error Message", "${poLineRefType} is null")),
  590. dataLog = mutableMapOf(
  591. Pair(
  592. "${poLineRefType} Error Message",
  593. "pot is null"
  594. ),
  595. Pair(
  596. "${poLineRefType} Error Code",
  597. purchaseOrderLineMessage?.get(0)?.msgCode ?: "No Msg Code from M18"
  598. ),
  599. Pair(
  600. "${poLineRefType} Error Detail",
  601. purchaseOrderLineMessage?.get(0)?.msgDetail ?: "No Msg Detail from M18"
  602. ),
  603. ),
  604. statusEnum = M18DataLogStatus.FAIL
  605. )
  606. val errorLog = m18DataLogService.saveM18DataLog(saveM18PurchaseOrderLineLogRequest)
  607. logger.error("${poLineRefType}: M18 Data Log Updated! Please see the error. ID: ${errorLog.id}")
  608. }
  609. } else {
  610. // mainpo
  611. failList.add(purchaseOrder.id)
  612. // logger.error("${poRefType}: Saving Failure!")
  613. val saveM18DataLogRequest = SaveM18DataLogRequest(
  614. id = null,
  615. refType = "${poRefType}",
  616. m18Id = purchaseOrder.id,
  617. // m18LastModifyDate = if(mainpo?.lastModifyDate != null) commonUtils.instantToLocalDateTime(mainpo.lastModifyDate) else LocalDateTime.now(),
  618. m18LastModifyDate = LocalDateTime.now(),
  619. dataLog = mutableMapOf(
  620. Pair(
  621. "${poRefType} Error",
  622. "mainpo is null"
  623. ),
  624. Pair(
  625. "${poRefType} Error Code",
  626. purchaseOrdersMessages?.get(0)?.msgCode ?: "No Msg Code from M18"
  627. ),
  628. Pair(
  629. "${poRefType} Error Detail",
  630. purchaseOrdersMessages?.get(0)?.msgDetail ?: "No Msg Detail from M18"
  631. ),
  632. ),
  633. statusEnum = M18DataLogStatus.FAIL
  634. )
  635. val errorLog = m18DataLogService.saveM18DataLog(saveM18DataLogRequest)
  636. logger.error("${poLineRefType}: M18 Data Log Updated! Please see the error. ID: ${errorLog.id}")
  637. }
  638. }
  639. } else {
  640. logger.error("${poRefType} List is null. May occur errors.")
  641. }
  642. }
  643. } else {
  644. logger.error("${poRefType} List is null. May occur errors.")
  645. }
  646. // End of save. Check result
  647. // logger.info("Total Success (${poRefType}) (${successList.size}): $successList")
  648. logger.info("Total Success (${poRefType}) (${successList.size})")
  649. // if (failList.size > 0) {
  650. logger.error("Total Fail (${poRefType}) (${failList.size}): $failList")
  651. // }
  652. // logger.info("Total Success (${poLineRefType}) (${successDetailList.size}): $successDetailList")
  653. logger.info("Total Success (${poLineRefType}) (${successDetailList.size})")
  654. // if (failDetailList.size > 0) {
  655. logger.error("Total Fail (${poLineRefType}) (${failDetailList.size}): $failDetailList")
  656. // }
  657. val (feeMarked, feeItemIds) = purchaseOrderLineService.markDeletedLinesWithFeeItems()
  658. affectedItemIds.addAll(feeItemIds)
  659. if (feeMarked > 0) {
  660. logger.info("Marked $feeMarked PO line(s) as deleted (isFee items).")
  661. }
  662. if (affectedItemIds.isNotEmpty()) {
  663. itemsService.updateAverageUnitPriceForItems(affectedItemIds)
  664. logger.info("Average unit price updated for ${affectedItemIds.size} item(s) after PO sync.")
  665. }
  666. logger.info("--------------------------------------------End - Saving M18 Purchase Order--------------------------------------------")
  667. val baseQuery = purchaseOrdersWithType?.query ?: ""
  668. val skippedSuffix = skippedNotPendingQuerySuffix(skippedNotPendingList)
  669. return SyncResult(
  670. totalProcessed = successList.size + failList.size + skippedNotPendingList.size,
  671. totalSuccess = successList.size,
  672. totalFail = failList.size,
  673. query = baseQuery + skippedSuffix
  674. )
  675. }
  676. }
  677. enum class StockInPoRefreshAction(val headerValue: String) {
  678. SKIPPED_DISABLED("skipped_disabled"),
  679. SKIPPED_NOT_PENDING("skipped_not_pending"),
  680. SYNCED("synced"),
  681. }
  682. data class StockInPoRefreshOutcome(
  683. val action: StockInPoRefreshAction,
  684. val syncResult: SyncResult,
  685. )