瀏覽代碼

fixing the Greate GRN for same PO delivering on different date

master
Fai Luk 2 天之前
父節點
當前提交
cb08be40b6
共有 4 個檔案被更改,包括 63 行新增20 行删除
  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 查看文件

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

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

/** 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,
* 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 查看文件

@@ -80,6 +80,26 @@ fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): 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("""
SELECT sil FROM StockInLine sil
WHERE sil.receiptDate IS NOT NULL


+ 13
- 13
src/main/java/com/ffii/fpsms/modules/stock/service/SearchCompletedDnService.kt 查看文件

@@ -15,6 +15,7 @@ import java.time.LocalDateTime
/**
* Search completed stock-in lines (DN) by receipt date and process M18 GRN creation.
* 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
open class SearchCompletedDnService(
@@ -82,7 +83,7 @@ open class SearchCompletedDnService(

/**
* 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 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.
@@ -101,7 +102,7 @@ open class SearchCompletedDnService(
toProcess.forEach { (poId, silList) ->
silList.firstOrNull()?.let { first ->
try {
stockInLineService.processPurchaseOrderForGrn(first)
stockInLineService.processPurchaseOrderForGrn(first, grnReceiptDate = receiptDate)
} catch (e: Exception) {
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
open fun postCompletedDnAndProcessGrnWithMissingRetry(
@@ -172,9 +169,12 @@ open class SearchCompletedDnService(
val byPo = lines.groupBy { it.purchaseOrder?.id ?: 0L }.filterKeys { it != 0L }
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
@@ -190,7 +190,7 @@ open class SearchCompletedDnService(
toProcess.forEach { (poId, silList) ->
silList.firstOrNull()?.let { first ->
try {
stockInLineService.processPurchaseOrderForGrn(first)
stockInLineService.processPurchaseOrderForGrn(first, grnReceiptDate = receiptDate)
} catch (e: Exception) {
logger.error("[postCompletedDnAndProcessMissingGrnForReceiptDate] Failed for PO id=$poId: ${e.message}", e)
}


+ 26
- 6
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt 查看文件

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

/**
* 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
* RECEIVED and PENDING/ESCALATED status flows.
*/
private fun tryUpdatePurchaseOrderAndCreateGrnIfCompleted(savedStockInLine: StockInLine) {
private fun tryUpdatePurchaseOrderAndCreateGrnIfCompleted(
savedStockInLine: StockInLine,
grnReceiptDate: LocalDate? = null,
) {
if (savedStockInLine.purchaseOrderLine == null) return
val pol = savedStockInLine.purchaseOrderLine ?: return
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).
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()) {
logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] DEBUG: Skipping M18 GRN - no stock-in lines for PO id=${savedPo.id} code=${savedPo.code}")
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) {
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


Loading…
取消
儲存