Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 

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