| @@ -63,6 +63,7 @@ import com.ffii.fpsms.m18.model.GoodsReceiptNoteAntValue | |||
| import com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLog | |||
| import com.ffii.fpsms.m18.entity.M18GoodsReceiptNoteLogRepository | |||
| import com.ffii.fpsms.m18.service.M18GoodsReceiptNoteService | |||
| import com.ffii.fpsms.m18.service.M18PurchaseOrderService | |||
| import com.ffii.fpsms.m18.utils.CommonUtils | |||
| import com.google.gson.Gson | |||
| import org.slf4j.LoggerFactory | |||
| @@ -97,6 +98,7 @@ open class StockInLineService( | |||
| private val inventoryRepository: InventoryRepository, | |||
| private val m18GoodsReceiptNoteService: M18GoodsReceiptNoteService, | |||
| private val m18GoodsReceiptNoteLogRepository: M18GoodsReceiptNoteLogRepository, | |||
| private val m18PurchaseOrderService: M18PurchaseOrderService, | |||
| ) : AbstractBaseEntityService<StockInLine, Long, StockInLineRepository>(jdbcDao, stockInLineRepository) { | |||
| private val logger = LoggerFactory.getLogger(StockInLineService::class.java) | |||
| @@ -502,6 +504,46 @@ open class StockInLineService( | |||
| return poRepository.saveAndFlush(po) | |||
| } | |||
| /** | |||
| * Calls M18 GET `/root/api/read/po?id={m18PoId}` (same as [M18PurchaseOrderService.getPurchaseOrder]) using | |||
| * [PurchaseOrder.m18DataLog].m18Id, compares each local POL's [PurchaseOrderLine.m18Lot] to M18 `pot[].lot` | |||
| * (matched by POL [com.ffii.fpsms.m18.entity.M18DataLog.m18Id] == M18 line id), and persists updates when different. | |||
| * Run before building the GRN so `sourceLot` matches M18. | |||
| */ | |||
| private fun syncPurchaseOrderLineM18LotFromM18(po: PurchaseOrder) { | |||
| val m18PoId = po.m18DataLog?.m18Id ?: run { | |||
| logger.debug("[GRN m18Lot sync] PO id=${po.id} code=${po.code} has no m18DataLog.m18Id, skip") | |||
| return | |||
| } | |||
| val response = try { | |||
| m18PurchaseOrderService.getPurchaseOrder(m18PoId) | |||
| } catch (e: Exception) { | |||
| logger.warn("[GRN m18Lot sync] read/po failed m18PoId=$m18PoId PO=${po.code}: ${e.message}") | |||
| null | |||
| } ?: return | |||
| val pot = response.data?.pot ?: return | |||
| if (pot.isEmpty()) return | |||
| val lotByM18LineId = pot.associate { it.id to (it.lot ?: "").trim() } | |||
| val pols = polRepository.findAllByPurchaseOrderIdAndDeletedIsFalseAndM18DataLogIsNotNull(po.id!!) | |||
| var changed = 0 | |||
| for (pol in pols) { | |||
| val m18LineId = pol.m18DataLog?.m18Id ?: continue | |||
| if (!lotByM18LineId.containsKey(m18LineId)) continue | |||
| val remoteLot = lotByM18LineId[m18LineId]!! | |||
| val localLot = (pol.m18Lot ?: "").trim() | |||
| if (localLot == remoteLot) continue | |||
| logger.info("[GRN m18Lot sync] PO ${po.code} POL id=${pol.id} m18LineId=$m18LineId: local m18Lot='$localLot' -> M18 lot='$remoteLot'") | |||
| pol.m18Lot = if (remoteLot.isEmpty()) null else remoteLot | |||
| polRepository.saveAndFlush(pol) | |||
| changed++ | |||
| } | |||
| if (changed > 0) { | |||
| logger.info("[GRN m18Lot sync] Updated m18Lot on $changed POL row(s) for PO id=${po.id} code=${po.code}") | |||
| } | |||
| } | |||
| /** | |||
| * Builds M18 Goods Receipt Note (AN) request from completed PO and its completed stock-in lines. | |||
| */ | |||
| @@ -631,6 +673,9 @@ open class StockInLineService( | |||
| logger.info("[updatePurchaseOrderStatus] savedPo id=${savedPo.id}, status=${savedPo.status}") | |||
| // TODO: For test only - normally check savedPo.status == PurchaseOrderStatus.COMPLETED and use only COMPLETE lines | |||
| try { | |||
| // Align POL.m18Lot with M18 before GRN (sourceLot must match M18 PO line lot or AN save may fail). | |||
| syncPurchaseOrderLineM18LotFromM18(savedPo) | |||
| // Defensive: load only completed stock-in lines for the PO, so GRN payload can't include pending/escalated. | |||
| val linesForGrn = stockInLineRepository.findCompletedByPurchaseOrderIdAndDeletedFalseWithItemNames(savedPo.id!!) | |||
| if (linesForGrn.isEmpty()) { | |||