Procházet zdrojové kódy

creating the GRN daily enabled

reset-do-picking-order
[email protected] před 1 týdnem
rodič
revize
291d039dc6
9 změnil soubory, kde provedl 143 přidání a 25 odebrání
  1. +87
    -0
      src/main/java/com/ffii/fpsms/api/service/ApiCallerService.kt
  2. +5
    -1
      src/main/java/com/ffii/fpsms/m18/model/GoodsReceiptNoteRequest.kt
  3. +14
    -6
      src/main/java/com/ffii/fpsms/m18/service/M18GoodsReceiptNoteService.kt
  4. +8
    -4
      src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt
  5. +1
    -1
      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. +6
    -5
      src/main/java/com/ffii/fpsms/modules/stock/service/SearchCompletedDnService.kt
  8. +7
    -4
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
  9. +4
    -4
      src/main/resources/application.yml

+ 87
- 0
src/main/java/com/ffii/fpsms/api/service/ApiCallerService.kt Zobrazit soubor

@@ -21,12 +21,17 @@ import org.springframework.context.annotation.Lazy
import org.springframework.http.HttpMethod import org.springframework.http.HttpMethod
import org.springframework.http.HttpRequest import org.springframework.http.HttpRequest
import org.springframework.http.HttpStatusCode import org.springframework.http.HttpStatusCode
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.core.io.buffer.DefaultDataBufferFactory
import org.springframework.web.reactive.function.BodyInserters import org.springframework.web.reactive.function.BodyInserters
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import org.springframework.web.reactive.function.client.ClientRequest import org.springframework.web.reactive.function.client.ClientRequest
import org.springframework.web.reactive.function.client.ExchangeFilterFunction import org.springframework.web.reactive.function.client.ExchangeFilterFunction
import org.springframework.web.reactive.function.client.WebClientResponseException import org.springframework.web.reactive.function.client.WebClientResponseException
import org.springframework.web.service.invoker.HttpRequestValues import org.springframework.web.service.invoker.HttpRequestValues
import reactor.util.retry.Retry import reactor.util.retry.Retry
import java.net.URI
import java.time.Duration import java.time.Duration
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference


@@ -328,6 +333,88 @@ open class ApiCallerService(
} }


// ------------------------------------ PUT ------------------------------------ // // ------------------------------------ PUT ------------------------------------ //
/**
* Performs a PUT HTTP request with a pre-serialized JSON string body.
* Sends the body as explicit UTF-8 bytes. Use Content-Type application/json only (no charset)
* so M18 persists bDesc/bDesc_en like when sent from Postman.
*/
inline fun <reified T : Any> putWithJsonString(
urlPath: String,
queryParams: MultiValueMap<String, String>,
bodyJson: String,
customHeaders: Map<String, String>? = null
): Mono<T> {
val contentType = MediaType.APPLICATION_JSON
val utf8Bytes = bodyJson.toByteArray(StandardCharsets.UTF_8)
val bodyBuffer: DataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(utf8Bytes)
return webClient.put()
.uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(queryParams).build() }
.contentType(contentType)
.headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } }
.body(BodyInserters.fromDataBuffers(Mono.just(bodyBuffer)))
.retrieve()
.bodyToMono(T::class.java)
.doOnError { error -> logger.error("PUT error: ${error.message}") }
.onErrorResume(WebClientResponseException::class.java) { error ->
logger.error("WebClientResponseException: ${error.statusCode} - ${error.statusText}, Body: ${error.responseBodyAsString}")
if (error.statusCode == HttpStatusCode.valueOf(400) || error.statusCode == HttpStatusCode.valueOf(401)) {
updateToken().flatMap { newToken ->
val retryBuffer = DefaultDataBufferFactory.sharedInstance.wrap(utf8Bytes)
webClient.put()
.uri { uriBuilder -> uriBuilder.path(urlPath).queryParams(queryParams).build() }
.header(HttpHeaders.AUTHORIZATION, "Bearer $newToken")
.contentType(contentType)
.headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } }
.body(BodyInserters.fromDataBuffers(Mono.just(retryBuffer)))
.retrieve()
.bodyToMono(T::class.java)
}
} else {
Mono.error(error)
}
}
}

/**
* Performs a PUT with the JSON payload in the "param" query parameter (URL-encoded) and empty body.
* Builds the URI manually so JSON braces are not treated as URI template variables.
*/
inline fun <reified T : Any> putWithParamJson(
urlPath: String,
menuCode: String,
bodyJson: String,
customHeaders: Map<String, String>? = null
): Mono<T> {
val encodedParam = URLEncoder.encode(bodyJson, StandardCharsets.UTF_8).replace("+", "%20")
val base = m18Config.BASE_URL.removeSuffix("/")
val path = urlPath.removePrefix("/")
val fullUri = "$base/$path?menuCode=$menuCode&param=$encodedParam"
val uri = URI.create(fullUri)
return webClient.put()
.uri(uri)
.headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } }
.bodyValue("")
.retrieve()
.bodyToMono(T::class.java)
.doOnError { error -> logger.error("PUT (param) error: ${error.message}") }
.onErrorResume(WebClientResponseException::class.java) { error ->
logger.error("WebClientResponseException: ${error.statusCode} - ${error.statusText}, Body: ${error.responseBodyAsString}")
if (error.statusCode == HttpStatusCode.valueOf(400) || error.statusCode == HttpStatusCode.valueOf(401)) {
updateToken().flatMap { newToken ->
webClient.put()
.uri(uri)
.header(HttpHeaders.AUTHORIZATION, "Bearer $newToken")
.headers { headers -> customHeaders?.forEach { (k, v) -> headers.set(k, v) } }
.bodyValue("")
.retrieve()
.bodyToMono(T::class.java)
}
} else {
Mono.error(error)
}
}
}

/** /**
* Performs a PUT HTTP request to the specified URL path with query parameters and JSON body. * Performs a PUT HTTP request to the specified URL path with query parameters and JSON body.
* *


+ 5
- 1
src/main/java/com/ffii/fpsms/m18/model/GoodsReceiptNoteRequest.kt Zobrazit soubor

@@ -1,6 +1,7 @@
package com.ffii.fpsms.m18.model package com.ffii.fpsms.m18.model


import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty


/** /**
* Request body for M18 Goods Receipt Note (AN) save API. * Request body for M18 Goods Receipt Note (AN) save API.
@@ -55,5 +56,8 @@ data class GoodsReceiptNoteAntValue(
val amt: Number, val amt: Number,
val beId: Int? = null, val beId: Int? = null,
val flowTypeId: Int? = null, val flowTypeId: Int? = null,
val bDesc: String? = null, // itemName
@JsonProperty("bDesc")
val itemDesc: String? = null, // itemName (Chinese); Kotlin property renamed so Jackson outputs "bDesc" not "bdesc"
@JsonProperty("bDesc_en")
val itemDescEn: String? = null, // itemName (English); M18 expects exact key "bDesc_en"
) )

+ 14
- 6
src/main/java/com/ffii/fpsms/m18/service/M18GoodsReceiptNoteService.kt Zobrazit soubor

@@ -1,10 +1,12 @@
package com.ffii.fpsms.m18.service package com.ffii.fpsms.m18.service


import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.ffii.fpsms.api.service.ApiCallerService import com.ffii.fpsms.api.service.ApiCallerService
import com.ffii.fpsms.m18.M18Config import com.ffii.fpsms.m18.M18Config
import com.ffii.fpsms.m18.model.GoodsReceiptNoteRequest import com.ffii.fpsms.m18.model.GoodsReceiptNoteRequest
import com.ffii.fpsms.m18.model.GoodsReceiptNoteResponse import com.ffii.fpsms.m18.model.GoodsReceiptNoteResponse
import com.google.gson.Gson
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -27,6 +29,12 @@ open class M18GoodsReceiptNoteService(
private val M18_SAVE_GOODS_RECEIPT_NOTE_API = "/root/api/save/an" private val M18_SAVE_GOODS_RECEIPT_NOTE_API = "/root/api/save/an"
private val MENU_CODE_AN = "an" private val MENU_CODE_AN = "an"


// Serialize with UTF-8 characters (no \\uXXXX) so M18 persists bDesc/bDesc_en like Postman
@Suppress("DEPRECATION")
private val grnRequestMapper: ObjectMapper = jacksonObjectMapper().apply {
disable(JsonGenerator.Feature.ESCAPE_NON_ASCII)
}

/** /**
* Creates a goods receipt note in M18. * Creates a goods receipt note in M18.
* *
@@ -58,14 +66,14 @@ open class M18GoodsReceiptNoteService(
} }
val queryString = queryParams.entries.flatMap { (k, v) -> v.map { value -> "$k=$value" } }.joinToString("&") val queryString = queryParams.entries.flatMap { (k, v) -> v.map { value -> "$k=$value" } }.joinToString("&")
val fullUrl = "${m18Config.BASE_URL}$M18_SAVE_GOODS_RECEIPT_NOTE_API?$queryString" val fullUrl = "${m18Config.BASE_URL}$M18_SAVE_GOODS_RECEIPT_NOTE_API?$queryString"
val requestJson = Gson().toJson(request)
logger.info("[M18 GRN API] call=PUT url=$fullUrl queryParams=$queryParams body=$requestJson")
return apiCallerService.put<GoodsReceiptNoteResponse>(
val requestJson = grnRequestMapper.writeValueAsString(request)
logger.info("[M18 GRN API] call=PUT url=$fullUrl body=$requestJson")
return apiCallerService.putWithJsonString<GoodsReceiptNoteResponse>(
urlPath = M18_SAVE_GOODS_RECEIPT_NOTE_API, urlPath = M18_SAVE_GOODS_RECEIPT_NOTE_API,
queryParams = queryParams, queryParams = queryParams,
body = request,
bodyJson = requestJson,
).doOnSuccess { response -> ).doOnSuccess { response ->
val responseJson = Gson().toJson(response)
val responseJson = grnRequestMapper.writeValueAsString(response)
logger.info("[M18 GRN API] response (all): $responseJson") logger.info("[M18 GRN API] response (all): $responseJson")
if (response.status) { if (response.status) {
logger.info("Goods receipt note created in M18. recordId=${response.recordId}") logger.info("Goods receipt note created in M18. recordId=${response.recordId}")


+ 8
- 4
src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt Zobrazit soubor

@@ -141,7 +141,7 @@ open class SchedulerService(
else try { LocalDate.parse(s) } catch (e: Exception) { 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. */
/** Post completed DN and create M18 GRN at 00:01 daily; processes all POs with receipt date = yesterday. Set scheduler.postCompletedDnGrn.enabled=false to disable. */
fun schedulePostCompletedDnGrn() { fun schedulePostCompletedDnGrn() {
if (!postCompletedDnGrnEnabled) { if (!postCompletedDnGrnEnabled) {
scheduledPostCompletedDnGrn?.cancel(false) scheduledPostCompletedDnGrn?.cancel(false)
@@ -149,8 +149,12 @@ open class SchedulerService(
logger.info("PostCompletedDn GRN scheduler disabled (scheduler.postCompletedDnGrn.enabled=false)") logger.info("PostCompletedDn GRN scheduler disabled (scheduler.postCompletedDnGrn.enabled=false)")
return 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) })
commonSchedule(
scheduledPostCompletedDnGrn,
SettingNames.SCHEDULE_POST_COMPLETED_DN_GRN,
"0 1 0 * * *",
{ getPostCompletedDnAndProcessGrn(receiptDate = getPostCompletedDnGrnReceiptDate()) }
)
} }


// Function for schedule // Function for schedule
@@ -323,7 +327,7 @@ open class SchedulerService(
open fun getPostCompletedDnAndProcessGrn( open fun getPostCompletedDnAndProcessGrn(
receiptDate: java.time.LocalDate? = null, receiptDate: java.time.LocalDate? = null,
skipFirst: Int = 0, skipFirst: Int = 0,
limitToFirst: Int? = 1,
limitToFirst: Int? = null,
) { ) {
logger.info("Scheduler - Post completed DN and process GRN") logger.info("Scheduler - Post completed DN and process GRN")
val date = receiptDate ?: java.time.LocalDate.now().minusDays(1) val date = receiptDate ?: java.time.LocalDate.now().minusDays(1)


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/common/scheduler/web/SchedulerController.kt Zobrazit soubor

@@ -59,7 +59,7 @@ class SchedulerController(
fun triggerPostCompletedDnGrn( fun triggerPostCompletedDnGrn(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) receiptDate: LocalDate? = null, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) receiptDate: LocalDate? = null,
@RequestParam(required = false, defaultValue = "0") skipFirst: Int = 0, @RequestParam(required = false, defaultValue = "0") skipFirst: Int = 0,
@RequestParam(required = false) limitToFirst: Int? = 1,
@RequestParam(required = false) limitToFirst: Int? = null,
): String { ): String {
schedulerService.getPostCompletedDnAndProcessGrn(receiptDate = receiptDate, skipFirst = skipFirst, limitToFirst = limitToFirst) schedulerService.getPostCompletedDnAndProcessGrn(receiptDate = receiptDate, skipFirst = skipFirst, limitToFirst = limitToFirst)
return "Post completed DN and process GRN triggered" return "Post completed DN and process GRN triggered"


+ 11
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt Zobrazit soubor

@@ -57,6 +57,17 @@ fun searchStockInLines(
fun findAllByItemIdInAndDeletedFalse(itemIds: List<Long>): List<StockInLine> fun findAllByItemIdInAndDeletedFalse(itemIds: List<Long>): List<StockInLine>
fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine? fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine?


/** Loads stock-in lines with item and purchaseOrderLine.item so bDesc/bDesc_en (item name) are available for M18 GRN. */
@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
ORDER BY sil.id
""")
fun findAllByPurchaseOrderIdAndDeletedFalseWithItemNames(@Param("purchaseOrderId") purchaseOrderId: Long): 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


+ 6
- 5
src/main/java/com/ffii/fpsms/modules/stock/service/SearchCompletedDnService.kt Zobrazit soubor

@@ -47,7 +47,8 @@ open class SearchCompletedDnService(
sil.demandQty as demandQty, sil.demandQty as demandQty,
sil.acceptedQty as acceptedQty, sil.acceptedQty as acceptedQty,
sil.receiptDate as receiptDate, sil.receiptDate as receiptDate,
CASE WHEN sil.dnNo = 'DN00000' THEN '' ELSE sil.dnNo END as dnNo
-- CASE WHEN sil.dnNo = 'DN00000' THEN '' ELSE sil.dnNo END as dnNo
sil.dnNo
FROM stock_in_line sil FROM stock_in_line sil
LEFT JOIN items it ON sil.itemId = it.id LEFT JOIN items it ON sil.itemId = it.id
WHERE sil.receiptDate IS NOT NULL WHERE sil.receiptDate IS NOT NULL
@@ -81,14 +82,14 @@ 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 Purchase Order, with the PO lines it received (acceptedQty as ant qty).
* @param receiptDate Default: yesterday * @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.
* @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.
*/ */
@Transactional @Transactional
open fun postCompletedDnAndProcessGrn( open fun postCompletedDnAndProcessGrn(
receiptDate: LocalDate = LocalDate.now().minusDays(2),
receiptDate: LocalDate = LocalDate.now().minusDays(1),
skipFirst: Int = 0, skipFirst: Int = 0,
limitToFirst: Int? = 1,
limitToFirst: Int? = null,
): Int { ): Int {
val lines = searchCompletedDn(receiptDate) val lines = searchCompletedDn(receiptDate)
val byPo = lines.groupBy { it.purchaseOrder?.id ?: 0L }.filterKeys { it != 0L } val byPo = lines.groupBy { it.purchaseOrder?.id ?: 0L }.filterKeys { it != 0L }


+ 7
- 4
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Zobrazit soubor

@@ -426,7 +426,8 @@ open class StockInLineService(
else -> 1 else -> 1
} }
val firstLine = stockInLines.firstOrNull() val firstLine = stockInLines.firstOrNull()
val doNo = (firstLine?.dnNo?.takeIf { it != "DN00000" } ?: "").trim()
// Use DN number as-is (including \"DN00000\"); only trim whitespace
val doNo = (firstLine?.dnNo ?: "").trim()
val tDate = firstLine?.receiptDate?.toLocalDate()?.format(DateTimeFormatter.ofPattern("MM/dd/yyyy")) val tDate = firstLine?.receiptDate?.toLocalDate()?.format(DateTimeFormatter.ofPattern("MM/dd/yyyy"))
?: LocalDate.now().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 // Group by POL first for udfpartiallyreceived: true = any POL short (accepted < ordered), false = all fully received
@@ -460,11 +461,12 @@ open class StockInLineService(
val pol = sil.purchaseOrderLine!! val pol = sil.purchaseOrderLine!!
val totalQty = silList.sumOf { it.acceptedQty ?: BigDecimal.ZERO } val totalQty = silList.sumOf { it.acceptedQty ?: BigDecimal.ZERO }
val unitIdFromDataLog = (pol.m18DataLog?.dataLog?.get("unitId") as? Number)?.toLong()?.toInt() val unitIdFromDataLog = (pol.m18DataLog?.dataLog?.get("unitId") as? Number)?.toLong()?.toInt()
val itemName = (sil.item?.name ?: pol.item?.name).orEmpty() // always non-null for M18 bDesc/bDesc_en
GoodsReceiptNoteAntValue( GoodsReceiptNoteAntValue(
sourceType = "po", sourceType = "po",
sourceId = sourceId, sourceId = sourceId,
sourceLot = pol.m18Lot ?: "", sourceLot = pol.m18Lot ?: "",
proId = (sil.item?.m18Id ?: 0L).toInt(),
proId = (sil.item?.m18Id ?: pol.item?.m18Id ?: 0L).toInt(),
locId = 155, locId = 155,
unitId = unitIdFromDataLog ?: (pol.uom?.m18Id ?: 0L).toInt(), unitId = unitIdFromDataLog ?: (pol.uom?.m18Id ?: 0L).toInt(),
qty = totalQty.toDouble(), qty = totalQty.toDouble(),
@@ -476,7 +478,8 @@ open class StockInLineService(
), ),
beId = beId, beId = beId,
flowTypeId = flowTypeId, flowTypeId = flowTypeId,
bDesc = sil.item?.name,
itemDesc = itemName,
itemDescEn = itemName,
) )
} }
val ant = GoodsReceiptNoteAnt(values = antValues) val ant = GoodsReceiptNoteAnt(values = antValues)
@@ -536,7 +539,7 @@ open class StockInLineService(
logger.info("[updatePurchaseOrderStatus] savedPo id=${savedPo.id}, status=${savedPo.status}") 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 // TODO: For test only - normally check savedPo.status == PurchaseOrderStatus.COMPLETED and use only COMPLETE lines
try { try {
val allLines = stockInLineRepository.findAllByPurchaseOrderIdAndDeletedFalse(savedPo.id!!).orElse(emptyList())
val allLines = stockInLineRepository.findAllByPurchaseOrderIdAndDeletedFalseWithItemNames(savedPo.id!!)
val linesForGrn = allLines // TODO test: use all lines; normally .filter { it.status == StockInLineStatus.COMPLETE.status } val linesForGrn = allLines // TODO test: use all lines; normally .filter { it.status == StockInLineStatus.COMPLETE.status }
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}")


+ 4
- 4
src/main/resources/application.yml Zobrazit soubor

@@ -9,12 +9,12 @@ server:
error: error:
include-message: always 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.
# PostCompletedDn GRN: runs daily at 00:01, processes all POs with receipt date = yesterday.
# Set enabled: false to disable. Optional receiptDate: "yyyy-MM-dd" overrides for testing only.
scheduler: scheduler:
postCompletedDnGrn: postCompletedDnGrn:
enabled: false
receiptDate: 2026-03-07
enabled: true
# receiptDate: # leave unset for production (uses yesterday)


spring: spring:
servlet: servlet:


Načítá se…
Zrušit
Uložit