Browse Source

fixing the Greate GRN for same PO delivering on different date

master
Fai Luk 2 days ago
parent
commit
cb08be40b6
4 changed files with 63 additions and 20 deletions
  1. +4
    -1
      src/main/java/com/ffii/fpsms/m18/entity/M18GoodsReceiptNoteLogRepository.kt
  2. +20
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt
  3. +13
    -13
      src/main/java/com/ffii/fpsms/modules/stock/service/SearchCompletedDnService.kt
  4. +26
    -6
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt

+ 4
- 1
src/main/java/com/ffii/fpsms/m18/entity/M18GoodsReceiptNoteLogRepository.kt View File

@@ -7,9 +7,12 @@ import java.time.LocalDateTime


interface M18GoodsReceiptNoteLogRepository : AbstractRepository<M18GoodsReceiptNoteLog, Long> { interface M18GoodsReceiptNoteLogRepository : AbstractRepository<M18GoodsReceiptNoteLog, Long> {


/** Returns true if a successful GRN was already created for this PO (avoids core_201 duplicate). */
/** Returns true if a successful GRN was already created for this PO (legacy / broad check). */
fun existsByPurchaseOrderIdAndStatusTrue(purchaseOrderId: Long): Boolean fun existsByPurchaseOrderIdAndStatusTrue(purchaseOrderId: Long): Boolean


/** True if this stock-in line was already included in a successful M18 GRN (one delivery = one GRN batch). */
fun existsByStockInLineIdAndStatusTrue(stockInLineId: Long): Boolean

/** /**
* GRN log rows that need M18 AN code backfill: have record id, no grn_code yet, * GRN log rows that need M18 AN code backfill: have record id, no grn_code yet,
* created in [start, end] inclusive (e.g. start = 4 days ago 00:00, end = now). * created in [start, end] inclusive (e.g. start = 4 days ago 00:00, end = now).


+ 20
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt View File

@@ -80,6 +80,26 @@ fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine?
""") """)
fun findCompletedByPurchaseOrderIdAndDeletedFalseWithItemNames(@Param("purchaseOrderId") purchaseOrderId: Long): List<StockInLine> fun findCompletedByPurchaseOrderIdAndDeletedFalseWithItemNames(@Param("purchaseOrderId") purchaseOrderId: Long): List<StockInLine>


/** Completed stock-in lines for this PO whose calendar receipt day matches (one M18 GRN per delivery date / DN batch). */
@Query(
"""
SELECT DISTINCT sil FROM StockInLine sil
LEFT JOIN FETCH sil.item
LEFT JOIN FETCH sil.purchaseOrderLine pol
LEFT JOIN FETCH pol.item
WHERE sil.purchaseOrder.id = :purchaseOrderId
AND sil.deleted = false
AND sil.status = 'completed'
AND sil.receiptDate IS NOT NULL
AND DATE(sil.receiptDate) = :receiptDate
ORDER BY sil.id
"""
)
fun findCompletedByPurchaseOrderIdAndReceiptDateAndDeletedFalseWithItemNames(
@Param("purchaseOrderId") purchaseOrderId: Long,
@Param("receiptDate") receiptDate: LocalDate,
): List<StockInLine>

@Query(""" @Query("""
SELECT sil FROM StockInLine sil SELECT sil FROM StockInLine sil
WHERE sil.receiptDate IS NOT NULL WHERE sil.receiptDate IS NOT NULL


+ 13
- 13
src/main/java/com/ffii/fpsms/modules/stock/service/SearchCompletedDnService.kt View File

@@ -15,6 +15,7 @@ import java.time.LocalDateTime
/** /**
* Search completed stock-in lines (DN) by receipt date and process M18 GRN creation. * Search completed stock-in lines (DN) by receipt date and process M18 GRN creation.
* Query: receiptDate = yesterday, status = completed, purchaseOrderId not null. * Query: receiptDate = yesterday, status = completed, purchaseOrderId not null.
* Same PO may appear on multiple dates; each receipt date batch can produce its own M18 GRN.
*/ */
@Service @Service
open class SearchCompletedDnService( open class SearchCompletedDnService(
@@ -82,7 +83,7 @@ open class SearchCompletedDnService(


/** /**
* Post completed DNs and process each related purchase order for M18 GRN creation. * Post completed DNs and process each related purchase order for M18 GRN creation.
* Triggered by scheduler. One GRN per Purchase Order, with the PO lines it received (acceptedQty as ant qty).
* Triggered by scheduler. One GRN per PO **per receipt date** for lines completed on that date (same PO may receive multiple GRNs on different days).
* @param receiptDate Default: yesterday * @param receiptDate Default: yesterday
* @param skipFirst For testing/manual trigger: skip the first N POs. 1 = skip 1st, process from 2nd. 0 = process from 1st. * @param skipFirst For testing/manual trigger: skip the first N POs. 1 = skip 1st, process from 2nd. 0 = process from 1st.
* @param limitToFirst For testing/manual trigger: process only the next N POs after skip. 1 = one PO. null = all remaining POs. * @param limitToFirst For testing/manual trigger: process only the next N POs after skip. 1 = one PO. null = all remaining POs.
@@ -101,7 +102,7 @@ open class SearchCompletedDnService(
toProcess.forEach { (poId, silList) -> toProcess.forEach { (poId, silList) ->
silList.firstOrNull()?.let { first -> silList.firstOrNull()?.let { first ->
try { try {
stockInLineService.processPurchaseOrderForGrn(first)
stockInLineService.processPurchaseOrderForGrn(first, grnReceiptDate = receiptDate)
} catch (e: Exception) { } catch (e: Exception) {
logger.error("[postCompletedDnAndProcessGrn] Failed for PO id=$poId: ${e.message}", e) logger.error("[postCompletedDnAndProcessGrn] Failed for PO id=$poId: ${e.message}", e)
} }
@@ -111,13 +112,9 @@ open class SearchCompletedDnService(
} }


/** /**
* Retry GRN creation for completed stock-in lines where the PO does not have a SUCCESS log in
* `m18_goods_receipt_note_log`.
*
* Grouping behavior matches normal scheduler:
* - iterate by `receiptDate` day window
* - for each day, group by `PO (purchaseOrderId)`
* - process one GRN per PO using the first line from that PO group
* Retry GRN creation for completed stock-in lines where **some** line in that PO batch (same receipt date)
* has no SUCCESS row in `m18_goods_receipt_note_log` (matched by `stock_in_line_id`).
* Same PO can receive multiple GRNs on different delivery/receipt dates.
*/ */
@Transactional @Transactional
open fun postCompletedDnAndProcessGrnWithMissingRetry( open fun postCompletedDnAndProcessGrnWithMissingRetry(
@@ -172,9 +169,12 @@ open class SearchCompletedDnService(
val byPo = lines.groupBy { it.purchaseOrder?.id ?: 0L }.filterKeys { it != 0L } val byPo = lines.groupBy { it.purchaseOrder?.id ?: 0L }.filterKeys { it != 0L }
val entries = byPo.entries.toList() val entries = byPo.entries.toList()


// Only retry POs that do NOT have any successful GRN log.
val missingEntries = entries.filter { (poId, _) ->
!m18GoodsReceiptNoteLogRepository.existsByPurchaseOrderIdAndStatusTrue(poId)
// Per delivery date: retry if any completed line for this PO on this date lacks a successful GRN log.
val missingEntries = entries.filter { (_, silList) ->
silList.any { sil ->
val id = sil.id ?: return@any true
!m18GoodsReceiptNoteLogRepository.existsByStockInLineIdAndStatusTrue(id)
}
} }


val toProcess = missingEntries val toProcess = missingEntries
@@ -190,7 +190,7 @@ open class SearchCompletedDnService(
toProcess.forEach { (poId, silList) -> toProcess.forEach { (poId, silList) ->
silList.firstOrNull()?.let { first -> silList.firstOrNull()?.let { first ->
try { try {
stockInLineService.processPurchaseOrderForGrn(first)
stockInLineService.processPurchaseOrderForGrn(first, grnReceiptDate = receiptDate)
} catch (e: Exception) { } catch (e: Exception) {
logger.error("[postCompletedDnAndProcessMissingGrnForReceiptDate] Failed for PO id=$poId: ${e.message}", e) logger.error("[postCompletedDnAndProcessMissingGrnForReceiptDate] Failed for PO id=$poId: ${e.message}", e)
} }


+ 26
- 6
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt View File

@@ -660,10 +660,10 @@ open class StockInLineService(


/** /**
* Processes a purchase order for M18 GRN creation. Updates PO/line status and creates GRN if applicable. * Processes a purchase order for M18 GRN creation. Updates PO/line status and creates GRN if applicable.
* Called by SearchCompletedDnService (scheduler postCompletedDnAndProcessGrn) for batch processing of yesterday's completed DNs.
* @param grnReceiptDate When set (scheduler paths), only completed lines on that calendar receipt day are included — separate deliveries get separate GRNs.
*/ */
open fun processPurchaseOrderForGrn(stockInLine: StockInLine) {
tryUpdatePurchaseOrderAndCreateGrnIfCompleted(stockInLine)
open fun processPurchaseOrderForGrn(stockInLine: StockInLine, grnReceiptDate: LocalDate? = null) {
tryUpdatePurchaseOrderAndCreateGrnIfCompleted(stockInLine, grnReceiptDate = grnReceiptDate)
} }


/** /**
@@ -671,7 +671,10 @@ open class StockInLineService(
* creates M18 Goods Receipt Note. Called after saving stock-in line for both * creates M18 Goods Receipt Note. Called after saving stock-in line for both
* RECEIVED and PENDING/ESCALATED status flows. * RECEIVED and PENDING/ESCALATED status flows.
*/ */
private fun tryUpdatePurchaseOrderAndCreateGrnIfCompleted(savedStockInLine: StockInLine) {
private fun tryUpdatePurchaseOrderAndCreateGrnIfCompleted(
savedStockInLine: StockInLine,
grnReceiptDate: LocalDate? = null,
) {
if (savedStockInLine.purchaseOrderLine == null) return if (savedStockInLine.purchaseOrderLine == null) return
val pol = savedStockInLine.purchaseOrderLine ?: return val pol = savedStockInLine.purchaseOrderLine ?: return
updatePurchaseOrderLineStatus(pol) updatePurchaseOrderLineStatus(pol)
@@ -682,12 +685,29 @@ open class StockInLineService(
// Align POL.m18Lot with M18 before GRN (sourceLot must match M18 PO line lot or AN save may fail). // Align POL.m18Lot with M18 before GRN (sourceLot must match M18 PO line lot or AN save may fail).
syncPurchaseOrderLineM18LotFromM18(savedPo) 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!!)
// Completed lines for GRN: either this receipt date only (multi-delivery POs) or all completed on PO (legacy).
val linesForGrn = if (grnReceiptDate != null) {
stockInLineRepository.findCompletedByPurchaseOrderIdAndReceiptDateAndDeletedFalseWithItemNames(
savedPo.id!!,
grnReceiptDate,
)
} else {
stockInLineRepository.findCompletedByPurchaseOrderIdAndDeletedFalseWithItemNames(savedPo.id!!)
}
if (linesForGrn.isEmpty()) { if (linesForGrn.isEmpty()) {
logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] DEBUG: Skipping M18 GRN - no stock-in lines for PO id=${savedPo.id} code=${savedPo.code}") logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] DEBUG: Skipping M18 GRN - no stock-in lines for PO id=${savedPo.id} code=${savedPo.code}")
return return
} }
if (grnReceiptDate != null && linesForGrn.all { sil ->
val id = sil.id ?: return@all false
m18GoodsReceiptNoteLogRepository.existsByStockInLineIdAndStatusTrue(id)
}) {
logger.info(
"[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] Skipping M18 GRN — all ${linesForGrn.size} line(s) for PO id=${savedPo.id} " +
"code=${savedPo.code} on receipt date $grnReceiptDate already have successful GRN logs"
)
return
}
if (savedPo.m18BeId == null || savedPo.supplier?.m18Id == null || savedPo.currency?.m18Id == null) { if (savedPo.m18BeId == null || savedPo.supplier?.m18Id == null || savedPo.currency?.m18Id == null) {
logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] DEBUG: Skipping M18 GRN - missing M18 ids for PO id=${savedPo.id} code=${savedPo.code}. m18BeId=${savedPo.m18BeId}, supplier.m18Id=${savedPo.supplier?.m18Id}, currency.m18Id=${savedPo.currency?.m18Id}") logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] DEBUG: Skipping M18 GRN - missing M18 ids for PO id=${savedPo.id} code=${savedPo.code}. m18BeId=${savedPo.m18BeId}, supplier.m18Id=${savedPo.supplier?.m18Id}, currency.m18Id=${savedPo.currency?.m18Id}")
return return


Loading…
Cancel
Save