Przeglądaj źródła

adding GRN query for daily stock in and refining the m18 log and an/ant data

reset-do-picking-order
[email protected] 1 tydzień temu
rodzic
commit
7b60ef3b94
11 zmienionych plików z 271 dodań i 28 usunięć
  1. +6
    -2
      src/main/java/com/ffii/fpsms/m18/entity/M18GoodsReceiptNoteLog.kt
  2. +5
    -2
      src/main/java/com/ffii/fpsms/m18/model/GoodsReceiptNoteRequest.kt
  3. +3
    -0
      src/main/java/com/ffii/fpsms/modules/common/SettingNames.java
  4. +43
    -3
      src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt
  5. +12
    -0
      src/main/java/com/ffii/fpsms/modules/common/scheduler/web/SchedulerController.kt
  6. +11
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt
  7. +109
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/SearchCompletedDnService.kt
  8. +49
    -21
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
  9. +21
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchCompletedDnResult.kt
  10. +7
    -0
      src/main/resources/application.yml
  11. +5
    -0
      src/main/resources/db/changelog/changes/20260308_fai/01_add_request_json.sql

+ 6
- 2
src/main/java/com/ffii/fpsms/m18/entity/M18GoodsReceiptNoteLog.kt Wyświetl plik

@@ -9,8 +9,8 @@ import jakarta.validation.constraints.Size

/**
* Logs the result of creating a Goods Receipt Note (AN) in M18.
* One row per stock-in line included in the GRN, so we can trace
* m18RecordId back to stockInLineId, purchaseOrderLineId, and poCode.
* One row per purchase order line (POL) in the GRN, so we can trace
* m18RecordId back by stockInLineId or purchaseOrderLineId.
*/
@Entity
@Table(name = "m18_goods_receipt_note_log")
@@ -50,4 +50,8 @@ open class M18GoodsReceiptNoteLog : BaseEntity<Long>() {
@Size(max = 1000)
@Column(name = "message", length = 1000)
open var message: String? = null

/** Request JSON sent to M18 GRN API (for debugging/audit). */
@Column(name = "request_json", columnDefinition = "LONGTEXT")
open var requestJson: String? = null
}

+ 5
- 2
src/main/java/com/ffii/fpsms/m18/model/GoodsReceiptNoteRequest.kt Wyświetl plik

@@ -27,12 +27,14 @@ data class GoodsReceiptNoteMainanValue(
val rate: Number,
val status: String? = "Y",
val docDate: String? = null,
val tDate: String? = null, // PO delivery date (estimatedArrivalDate)
val tDate: String? = null, // receiptDate from stock-in line (MM/dd/yyyy)
val locId: Int? = null,
val flowTypeId: Int,
val staffId: Int, // required by M18 (core_101905)
val staffId: Int = 329,
val cnDeptId: Int? = null,
val virDeptId: Int? = null,
val udfMTMSDNNO2: String? = null, // doNo (dnNo), same for same PO
val udfpartiallyreceived: Boolean? = null, // true if any line acc>=demand, else false
)

@JsonInclude(JsonInclude.Include.NON_NULL)
@@ -53,4 +55,5 @@ data class GoodsReceiptNoteAntValue(
val amt: Number,
val beId: Int? = null,
val flowTypeId: Int? = null,
val bDesc: String? = null, // itemName
)

+ 3
- 0
src/main/java/com/ffii/fpsms/modules/common/SettingNames.java Wyświetl plik

@@ -30,6 +30,9 @@ public abstract class SettingNames {

public static final String SCHEDULE_M18_MASTER = "SCHEDULE.m18.master";

/** Post completed DN and process M18 GRN (cron, e.g. "0 40 23 * * *" for 23:40 daily) */
public static final String SCHEDULE_POST_COMPLETED_DN_GRN = "SCHEDULE.postCompletedDn.grn";

public static final String SCHEDULE_PROD_ROUGH = "SCHEDULE.prod.rough";

public static final String SCHEDULE_PROD_DETAILED = "SCHEDULE.prod.detailed";


+ 43
- 3
src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt Wyświetl plik

@@ -10,13 +10,16 @@ import com.ffii.fpsms.m18.entity.SchedulerSyncLogRepository
import com.ffii.fpsms.m18.model.SyncResult
import com.ffii.fpsms.modules.common.SettingNames
import com.ffii.fpsms.modules.master.service.ProductionScheduleService
import com.ffii.fpsms.modules.stock.service.SearchCompletedDnService
import com.ffii.fpsms.modules.settings.service.SettingsService
import jakarta.annotation.PostConstruct
import org.springframework.beans.factory.annotation.Value
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.scheduling.TaskScheduler
import org.springframework.scheduling.support.CronTrigger
import org.springframework.stereotype.Service
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.HashMap
@@ -25,6 +28,8 @@ import kotlin.jvm.optionals.getOrNull

@Service
open class SchedulerService(
@Value("\${scheduler.postCompletedDnGrn.enabled:true}") val postCompletedDnGrnEnabled: Boolean,
@Value("\${scheduler.postCompletedDnGrn.receiptDate:}") val postCompletedDnGrnReceiptDate: String,
val settingsService: SettingsService,
val taskScheduler: TaskScheduler,
val productionScheduleService: ProductionScheduleService,
@@ -32,6 +37,7 @@ open class SchedulerService(
val m18DeliveryOrderService: M18DeliveryOrderService,
val m18MasterDataService: M18MasterDataService,
val schedulerSyncLogRepository: SchedulerSyncLogRepository,
val searchCompletedDnService: SearchCompletedDnService,
) {
var logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java)
val dataStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd")
@@ -47,6 +53,8 @@ open class SchedulerService(
@Volatile
var scheduledM18Master: ScheduledFuture<*>? = null

var scheduledPostCompletedDnGrn: ScheduledFuture<*>? = null

//@Volatile
//var scheduledRoughProd: ScheduledFuture<*>? = null

@@ -64,15 +72,18 @@ open class SchedulerService(
}

fun commonSchedule(scheduled: ScheduledFuture<*>?, settingName: String, scheduleFunc: () -> Unit): ScheduledFuture<*>? {
return commonSchedule(scheduled, settingName, defaultCronExpression, scheduleFunc)
}

fun commonSchedule(scheduled: ScheduledFuture<*>?, settingName: String, defaultCron: String, scheduleFunc: () -> Unit): ScheduledFuture<*>? {
scheduled?.cancel(false)

var cron = settingsService.findByName(settingName).getOrNull()?.value ?: defaultCronExpression
var cron = settingsService.findByName(settingName).getOrNull()?.value ?: defaultCron

if (!isValidCronExpression(cron)) {
cron = defaultCronExpression
cron = defaultCron
}

// Now Kotlin is happy because both the method and the function allow nulls
return taskScheduler.schedule(
{ scheduleFunc() },
CronTrigger(cron)
@@ -87,6 +98,7 @@ open class SchedulerService(
scheduleM18Do1();
scheduleM18Do2();
scheduleM18MasterData();
schedulePostCompletedDnGrn();
//scheduleRoughProd();
//scheduleDetailedProd();
}
@@ -123,6 +135,24 @@ open class SchedulerService(
commonSchedule(scheduledM18Master, SettingNames.SCHEDULE_M18_MASTER, ::getM18MasterData)
}

private fun getPostCompletedDnGrnReceiptDate(): LocalDate {
val s = postCompletedDnGrnReceiptDate.trim()
return if (s.isEmpty()) LocalDate.now().minusDays(1)
else try { LocalDate.parse(s) } catch (e: Exception) { LocalDate.now().minusDays(1) }
}

/** Post completed DN and create M18 GRN. Default 23:45 daily. Set scheduler.postCompletedDnGrn.enabled=false to disable. */
fun schedulePostCompletedDnGrn() {
if (!postCompletedDnGrnEnabled) {
scheduledPostCompletedDnGrn?.cancel(false)
scheduledPostCompletedDnGrn = null
logger.info("PostCompletedDn GRN scheduler disabled (scheduler.postCompletedDnGrn.enabled=false)")
return
}
// TODO temp: skipFirst=1, limitToFirst=2 to test 2nd and 3rd POs. Revert to ::getPostCompletedDnAndProcessGrn for production.
commonSchedule(scheduledPostCompletedDnGrn, SettingNames.SCHEDULE_POST_COMPLETED_DN_GRN, "0 3 1 * * *", { getPostCompletedDnAndProcessGrn(receiptDate = getPostCompletedDnGrnReceiptDate(), skipFirst = 1, limitToFirst = 2) })
}

// Function for schedule
// --------------------------- FP-MTMS --------------------------- //
@@ -290,6 +320,16 @@ open class SchedulerService(
)
}

open fun getPostCompletedDnAndProcessGrn(
receiptDate: java.time.LocalDate? = null,
skipFirst: Int = 0,
limitToFirst: Int? = 1,
) {
logger.info("Scheduler - Post completed DN and process GRN")
val date = receiptDate ?: java.time.LocalDate.now().minusDays(1)
searchCompletedDnService.postCompletedDnAndProcessGrn(receiptDate = date, skipFirst = skipFirst, limitToFirst = limitToFirst)
}

open fun getM18MasterData() {
logger.info("Daily Scheduler - Master Data")
var currentTime = LocalDateTime.now()


+ 12
- 0
src/main/java/com/ffii/fpsms/modules/common/scheduler/web/SchedulerController.kt Wyświetl plik

@@ -4,10 +4,12 @@ import com.ffii.fpsms.modules.common.SettingNames
import com.ffii.fpsms.modules.common.scheduler.service.SchedulerService
import com.ffii.fpsms.modules.settings.service.SettingsService
import jakarta.validation.Valid
import org.springframework.format.annotation.DateTimeFormat
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.time.LocalDate


@RestController
@@ -53,6 +55,16 @@ class SchedulerController(
return "M18 Master Data Sync Triggered Successfully"
}

@GetMapping("/trigger/post-completed-dn-grn")
fun triggerPostCompletedDnGrn(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) receiptDate: LocalDate? = null,
@RequestParam(required = false, defaultValue = "0") skipFirst: Int = 0,
@RequestParam(required = false) limitToFirst: Int? = 1,
): String {
schedulerService.getPostCompletedDnAndProcessGrn(receiptDate = receiptDate, skipFirst = skipFirst, limitToFirst = limitToFirst)
return "Post completed DN and process GRN triggered"
}

@GetMapping("/refresh-cron")
fun refreshCron(): String {
schedulerService.init()


+ 11
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt Wyświetl plik

@@ -56,4 +56,15 @@ fun searchStockInLines(
@Query("SELECT sil FROM StockInLine sil WHERE sil.item.id IN :itemIds AND sil.deleted = false")
fun findAllByItemIdInAndDeletedFalse(itemIds: List<Long>): List<StockInLine>
fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine?

@Query("""
SELECT sil FROM StockInLine sil
WHERE sil.receiptDate IS NOT NULL
AND DATE(sil.receiptDate) = :receiptDate
AND sil.status = 'completed'
AND sil.purchaseOrder IS NOT NULL
AND sil.deleted = false
ORDER BY sil.purchaseOrder.id
""")
fun findCompletedDnByReceiptDate(@Param("receiptDate") receiptDate: LocalDate): List<StockInLine>
}

+ 109
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/SearchCompletedDnService.kt Wyświetl plik

@@ -0,0 +1,109 @@
package com.ffii.fpsms.modules.stock.service

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.stock.entity.StockInLine
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.modules.stock.web.model.SearchCompletedDnResult
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.time.LocalDate
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.
*/
@Service
open class SearchCompletedDnService(
private val jdbcDao: JdbcDao,
private val stockInLineRepository: StockInLineRepository,
private val stockInLineService: StockInLineService,
) {
private val logger = LoggerFactory.getLogger(SearchCompletedDnService::class.java)

/**
* Search completed DNs by receipt date (default: yesterday).
* Returns stock-in lines with receiptDate = date, status = completed, purchaseOrder not null.
*/
open fun searchCompletedDn(receiptDate: LocalDate = LocalDate.now().minusDays(1)): List<StockInLine> {
return stockInLineRepository.findCompletedDnByReceiptDate(receiptDate)
}

/**
* Search completed DNs by receipt date with exact query format.
* dnNo: empty string when original is 'DN00000'.
*/
open fun searchCompletedDnAsResult(receiptDate: LocalDate = LocalDate.now().minusDays(1)): List<SearchCompletedDnResult> {
val sql = """
SELECT
sil.purchaseOrderId as purchaseOrderId,
sil.purchaseOrderLineId as purchaseOrderLineId,
sil.itemId as itemId,
sil.itemNo as itemNo,
it.name as itemName,
sil.stockInId as stockInId,
sil.demandQty as demandQty,
sil.acceptedQty as acceptedQty,
sil.receiptDate as receiptDate,
CASE WHEN sil.dnNo = 'DN00000' THEN '' ELSE sil.dnNo END as dnNo
FROM stock_in_line sil
LEFT JOIN items it ON sil.itemId = it.id
WHERE sil.receiptDate IS NOT NULL
AND DATE(sil.receiptDate) = :receiptDate
AND sil.status = 'completed'
AND sil.purchaseOrderId IS NOT NULL
AND sil.deleted = false
ORDER BY sil.purchaseOrderId
""".trimIndent()
val rows = jdbcDao.queryForList(sql, mapOf("receiptDate" to receiptDate.toString()))
return rows.map { row ->
val getLong = { k: String -> ((row[k] ?: row[k.lowercase()]) as? Number)?.toLong() ?: 0L }
val getBd = { k: String -> (row[k] ?: row[k.lowercase()])?.let { when (it) { is BigDecimal -> it; is Number -> BigDecimal(it.toString()); else -> BigDecimal(it.toString()) } } }
val getTs = { k: String -> (row[k] ?: row[k.lowercase()])?.let { when (it) { is java.sql.Timestamp -> it.toLocalDateTime(); is java.sql.Date -> it.toLocalDate().atStartOfDay(); else -> null } } }
SearchCompletedDnResult(
purchaseOrderId = getLong("purchaseOrderId"),
purchaseOrderLineId = getLong("purchaseOrderLineId"),
itemId = getLong("itemId"),
itemNo = (row["itemNo"] ?: row["itemno"])?.toString(),
itemName = (row["itemName"] ?: row["itemname"])?.toString(),
stockInId = getLong("stockInId"),
demandQty = getBd("demandQty"),
acceptedQty = getBd("acceptedQty"),
receiptDate = getTs("receiptDate"),
dnNo = (row["dnNo"] ?: row["dnno"])?.toString()?.takeIf { it.isNotEmpty() },
)
}
}

/**
* 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).
* @param receiptDate Default: yesterday
* @param skipFirst For testing: skip the first N POs. 1 = skip 1st, process from 2nd. 0 = process from 1st.
* @param limitToFirst For testing: process only the next N POs after skip. 1 = one PO. null = all remaining POs.
*/
@Transactional
open fun postCompletedDnAndProcessGrn(
receiptDate: LocalDate = LocalDate.now().minusDays(2),
skipFirst: Int = 0,
limitToFirst: Int? = 1,
): Int {
val lines = searchCompletedDn(receiptDate)
val byPo = lines.groupBy { it.purchaseOrder?.id ?: 0L }.filterKeys { it != 0L }
val entries = byPo.entries.toList()
val toProcess = entries.drop(skipFirst).let { if (limitToFirst != null) it.take(limitToFirst) else it }
logger.info("[postCompletedDnAndProcessGrn] receiptDate=$receiptDate, found ${lines.size} lines, ${byPo.size} POs, processing ${toProcess.size} (skipFirst=$skipFirst, limitToFirst=$limitToFirst)")
toProcess.forEach { (poId, silList) ->
silList.firstOrNull()?.let { first ->
try {
stockInLineService.processPurchaseOrderForGrn(first)
} catch (e: Exception) {
logger.error("[postCompletedDnAndProcessGrn] Failed for PO id=$poId: ${e.message}", e)
}
}
}
return toProcess.size
}
}

+ 49
- 21
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Wyświetl plik

@@ -425,27 +425,40 @@ open class StockInLineService(
poCode.startsWith("PP") -> 3
else -> 1
}
val firstLine = stockInLines.firstOrNull()
val doNo = firstLine?.dnNo?.takeIf { it != "DN00000" } ?: ""
val tDate = firstLine?.receiptDate?.toLocalDate()?.format(DateTimeFormatter.ofPattern("MM/dd/yyyy"))
?: LocalDate.now().format(DateTimeFormatter.ofPattern("MM/dd/yyyy"))
// Group by POL first for udfpartiallyreceived: true = any POL short (accepted < ordered), false = all fully received
val byPol = stockInLines.groupBy { it.purchaseOrderLine!!.id!! }
val udfpartiallyreceived = byPol.any { (_, silList) ->
val pol = silList.first().purchaseOrderLine!!
val totalAccepted = silList.sumOf { it.acceptedQty ?: BigDecimal.ZERO }
totalAccepted < (pol.qty ?: BigDecimal.ZERO)
}
val mainan = GoodsReceiptNoteMainan(
values = listOf(
GoodsReceiptNoteMainanValue(
id = null, // omit; "0" and "" didn't fix core_201
id = null,
beId = beId,
code = null, // omit; empty string may cause 400
code = null,
venId = (po.supplier?.m18Id ?: 0L).toInt(),
curId = (po.currency?.m18Id ?: 0L).toInt(),
rate = 1,
flowTypeId = flowTypeId,
staffId = 194, // Steve
staffId = 329,
virDeptId = po.shop?.m18Id?.toInt(),
tDate = (po.estimatedArrivalDate?.toLocalDate() ?: LocalDate.now()).format(DateTimeFormatter.ofPattern("MM/dd/yyyy")),
status = null, // commented out - omit
// docDate = ..., locId = ..., cnDeptId = ...
tDate = tDate,
udfMTMSDNNO2 = doNo.ifEmpty { null },
udfpartiallyreceived = udfpartiallyreceived,
)
)
)
val sourceId = po.m18DataLog?.m18Id ?: 0L
val antValues = stockInLines.map { sil ->
val antValues = byPol.map { (_, silList) ->
val sil = silList.first()
val pol = sil.purchaseOrderLine!!
val totalQty = silList.sumOf { it.acceptedQty ?: BigDecimal.ZERO }
val unitIdFromDataLog = (pol.m18DataLog?.dataLog?.get("unitId") as? Number)?.toLong()?.toInt()
GoodsReceiptNoteAntValue(
sourceType = "po",
@@ -454,15 +467,16 @@ open class StockInLineService(
proId = (sil.item?.m18Id ?: 0L).toInt(),
locId = 155,
unitId = unitIdFromDataLog ?: (pol.uom?.m18Id ?: 0L).toInt(),
qty = sil.acceptedQty?.toDouble() ?: 0.0,
qty = totalQty.toDouble(),
up = pol.up?.toDouble() ?: 0.0,
amt = CommonUtils.getAmt(
up = pol.up ?: BigDecimal.ZERO,
discount = pol.m18Discount ?: BigDecimal.ZERO,
qty = sil.acceptedQty ?: BigDecimal.ZERO
qty = totalQty
),
beId = beId, // same as header
flowTypeId = flowTypeId // same as header: TOA->1, PF->2, PP->3
beId = beId,
flowTypeId = flowTypeId,
bDesc = sil.item?.name,
)
}
val ant = GoodsReceiptNoteAnt(values = antValues)
@@ -501,6 +515,14 @@ open class StockInLineService(
polRepository.saveAndFlush(pol)
}

/**
* 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.
*/
open fun processPurchaseOrderForGrn(stockInLine: StockInLine) {
tryUpdatePurchaseOrderAndCreateGrnIfCompleted(stockInLine)
}

/**
* Updates purchase order status from its lines and, when PO becomes COMPLETED,
* creates M18 Goods Receipt Note. Called after saving stock-in line for both
@@ -524,10 +546,11 @@ open class StockInLineService(
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
}
if (m18GoodsReceiptNoteLogRepository.existsByPurchaseOrderIdAndStatusTrue(savedPo.id!!)) {
logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] Skipping M18 GRN - already created for PO id=${savedPo.id} code=${savedPo.code} (avoids core_201 duplicate)")
return
}
//temp comment this TODO will only check purchase order + dnNo duplicated
//if (m18GoodsReceiptNoteLogRepository.existsByPurchaseOrderIdAndStatusTrue(savedPo.id!!)) {
// logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] Skipping M18 GRN - already created for PO id=${savedPo.id} code=${savedPo.code} (avoids core_201 duplicate)")
// return
//}
val grnRequest = buildGoodsReceiptNoteRequest(savedPo, linesForGrn)
val grnRequestJson = Gson().toJson(grnRequest)
logger.info("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] M18 GRN API request (for discussion with M18) PO id=${savedPo.id} code=${savedPo.code}: $grnRequestJson")
@@ -536,34 +559,39 @@ open class StockInLineService(
logger.warn("[tryUpdatePurchaseOrderAndCreateGrnIfCompleted] DEBUG: M18 API returned null for PO id=${savedPo.id} code=${savedPo.code}. API call may have failed or timed out.")
return
}
val byPol = linesForGrn.groupBy { it.purchaseOrderLine!!.id!! }
if (grnResponse.status == true) {
logger.info("M18 Goods Receipt Note created for PO ${savedPo.code}, goodsReceiptNote id (recordId)=${grnResponse.recordId}")
linesForGrn.forEach { sil ->
val linePol = sil.purchaseOrderLine!!
byPol.forEach { (_, silList) ->
val sil = silList.first()
val pol = sil.purchaseOrderLine!!
val logEntry = M18GoodsReceiptNoteLog().apply {
m18RecordId = grnResponse.recordId
stockInLineId = sil.id
purchaseOrderLineId = linePol.id
purchaseOrderLineId = pol.id
purchaseOrderId = savedPo.id
poCode = savedPo.code
status = true
message = null
requestJson = grnRequestJson
}
m18GoodsReceiptNoteLogRepository.save(logEntry)
}
} else {
logger.warn("M18 Goods Receipt Note save returned status=false for PO ${savedPo.code}, recordId=${grnResponse?.recordId}, messages=${grnResponse?.messages}. Request sent (for M18 discussion): $grnRequestJson")
val msg = grnResponse?.messages?.joinToString { it.msgDetail ?: it.msgCode ?: "" } ?: ""
linesForGrn.forEach { sil ->
val linePol = sil.purchaseOrderLine!!
byPol.forEach { (_, silList) ->
val sil = silList.first()
val pol = sil.purchaseOrderLine!!
val logEntry = M18GoodsReceiptNoteLog().apply {
m18RecordId = grnResponse?.recordId ?: 0L
stockInLineId = sil.id
purchaseOrderLineId = linePol.id
purchaseOrderLineId = pol.id
purchaseOrderId = savedPo.id
poCode = savedPo.code
status = false
message = msg.take(1000)
requestJson = grnRequestJson
}
m18GoodsReceiptNoteLogRepository.save(logEntry)
}


+ 21
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/SearchCompletedDnResult.kt Wyświetl plik

@@ -0,0 +1,21 @@
package com.ffii.fpsms.modules.stock.web.model

import java.math.BigDecimal
import java.time.LocalDateTime

/**
* Result of searchCompletedDn query.
* dnNo: empty string when original is 'DN00000'.
*/
data class SearchCompletedDnResult(
val purchaseOrderId: Long,
val purchaseOrderLineId: Long,
val itemId: Long,
val itemNo: String?,
val itemName: String?,
val stockInId: Long,
val demandQty: BigDecimal?,
val acceptedQty: BigDecimal?,
val receiptDate: LocalDateTime?,
val dnNo: String?,
)

+ 7
- 0
src/main/resources/application.yml Wyświetl plik

@@ -9,6 +9,13 @@ server:
error:
include-message: always

# Set to false to temporarily disable the PostCompletedDn GRN scheduler only
# receiptDate: override for testing (e.g. 2026-03-07). When unset, uses yesterday.
scheduler:
postCompletedDnGrn:
enabled: false
receiptDate: 2026-03-07

spring:
servlet:
multipart:


+ 5
- 0
src/main/resources/db/changelog/changes/20260308_fai/01_add_request_json.sql Wyświetl plik

@@ -0,0 +1,5 @@
--liquibase formatted sql
--changeset fai:add_request_json_to_m18_goods_receipt_note_log

ALTER TABLE `m18_goods_receipt_note_log`
ADD COLUMN `request_json` LONGTEXT NULL COMMENT 'Request JSON sent to M18 GRN API (for debugging/audit)' AFTER `message`;

Ładowanie…
Anuluj
Zapisz