| @@ -23,6 +23,8 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic | |||||
| import com.ffii.fpsms.config.security.jwt.JwtRequestFilter; | import com.ffii.fpsms.config.security.jwt.JwtRequestFilter; | ||||
| import org.springframework.http.HttpMethod; | |||||
| import jakarta.servlet.http.HttpServletResponse; | import jakarta.servlet.http.HttpServletResponse; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||
| @@ -82,6 +84,13 @@ public class SecurityConfig { | |||||
| authRequest -> authRequest | authRequest -> authRequest | ||||
| .requestMatchers(URL_WHITELIST).permitAll() | .requestMatchers(URL_WHITELIST).permitAll() | ||||
| .requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll() | .requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll() | ||||
| /* PO stock-in nav alerts: TESTING / ADMIN / STOCK (no @PreAuthorize on Kotlin controllers). */ | |||||
| .requestMatchers(HttpMethod.GET, "/stockInLine/alerts/purchase-incomplete-count") | |||||
| .hasAnyAuthority("TESTING", "ADMIN", "STOCK") | |||||
| .requestMatchers(HttpMethod.GET, "/stockInLine/alerts/purchase-incomplete") | |||||
| .hasAnyAuthority("TESTING", "ADMIN", "STOCK") | |||||
| .requestMatchers(HttpMethod.GET, "/product-process/Demo/Process/alerts/fg-qc-putaway") | |||||
| .hasAuthority("TESTING") | |||||
| .anyRequest().authenticated()) | .anyRequest().authenticated()) | ||||
| .httpBasic(httpBasic -> httpBasic.authenticationEntryPoint( | .httpBasic(httpBasic -> httpBasic.authenticationEntryPoint( | ||||
| (request, response, authException) -> sendUnauthorizedJson(response, "Unauthorized", "UNAUTHORIZED"))) | (request, response, authException) -> sendUnauthorizedJson(response, "Unauthorized", "UNAUTHORIZED"))) | ||||
| @@ -1760,6 +1760,89 @@ open class ProductProcessService( | |||||
| ) | ) | ||||
| } | } | ||||
| /** | |||||
| * FG QC / 上架 reminders: same eligibility as 完成QC工單 (qcReady): all lines Completed/Pass, stock-in exists and not completed/rejected; | |||||
| * only job orders that have a product process dated **today or yesterday** (server local date). | |||||
| */ | |||||
| open fun findJobOrderFgQcAndPutAwayAlertsForTodayYesterday(): JobOrderFgAlertsResponse { | |||||
| val today = LocalDate.now() | |||||
| val yesterday = today.minusDays(1) | |||||
| val processesToday = productProcessRepository.findAllByDeletedIsFalseAndDate(today) | |||||
| val processesYesterday = productProcessRepository.findAllByDeletedIsFalseAndDate(yesterday) | |||||
| val candidateJobOrderIds = (processesToday + processesYesterday) | |||||
| .asSequence() | |||||
| .filter { it.jobOrder?.isHidden != true } | |||||
| .mapNotNull { it.jobOrder?.id } | |||||
| .distinct() | |||||
| .toList() | |||||
| if (candidateJobOrderIds.isEmpty()) { | |||||
| return JobOrderFgAlertsResponse(qc = emptyList(), putAway = emptyList()) | |||||
| } | |||||
| val allProcesses = productProcessRepository.findByJobOrder_IdInAndDeletedIsFalse(candidateJobOrderIds) | |||||
| val allProcessIds = allProcesses.mapNotNull { it.id } | |||||
| val lines = if (allProcessIds.isNotEmpty()) { | |||||
| productProcessLineRepository.findByProductProcess_IdInWithOperatorAndEquipment(allProcessIds) | |||||
| } else { | |||||
| emptyList() | |||||
| } | |||||
| val linesByProcessId = lines.groupBy { it.productProcess.id ?: 0L } | |||||
| val processesByJobOrderId = allProcesses | |||||
| .filter { it.jobOrder?.id != null } | |||||
| .groupBy { it.jobOrder!!.id!! } | |||||
| val jobOrders = jobOrderRepository.findAllById(candidateJobOrderIds) | |||||
| val jobOrderById = jobOrders.associateBy { it.id } | |||||
| val qcRows = mutableListOf<JobOrderFgAlertRowResponse>() | |||||
| val putAwayRows = mutableListOf<JobOrderFgAlertRowResponse>() | |||||
| for (jobOrderId in candidateJobOrderIds) { | |||||
| val jobOrder = jobOrderById[jobOrderId] ?: continue | |||||
| if (jobOrder.status == JobOrderStatus.PLANNING) continue | |||||
| val stockInLine = stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(jobOrderId) ?: continue | |||||
| if (stockInLine.purchaseOrderLine != null) continue | |||||
| val statusNorm = (stockInLine.status ?: "").trim().lowercase() | |||||
| if (statusNorm == "completed" || statusNorm == "rejected") continue | |||||
| val processes = processesByJobOrderId[jobOrderId].orEmpty() | |||||
| if (processes.isEmpty()) continue | |||||
| val jobOrderLines = processes.flatMap { p -> linesByProcessId[p.id ?: 0L].orEmpty() } | |||||
| val allLinesDone = jobOrderLines.isNotEmpty() && | |||||
| jobOrderLines.all { it.status == "Completed" || it.status == "Pass" } | |||||
| if (!allLinesDone) continue | |||||
| val maxDate = processes.mapNotNull { it.date }.maxOrNull() | |||||
| val row = JobOrderFgAlertRowResponse( | |||||
| stockInLineId = stockInLine.id!!, | |||||
| jobOrderId = jobOrderId, | |||||
| jobOrderCode = jobOrder.code, | |||||
| itemNo = stockInLine.itemNo, | |||||
| itemName = stockInLine.item?.name, | |||||
| status = stockInLine.status, | |||||
| processDate = maxDate, | |||||
| lotNo = stockInLine.lotNo, | |||||
| ) | |||||
| when (statusNorm) { | |||||
| "received", "partially_completed" -> putAwayRows.add(row) | |||||
| else -> qcRows.add(row) | |||||
| } | |||||
| } | |||||
| val sortByDate = compareByDescending<JobOrderFgAlertRowResponse> { it.processDate ?: LocalDate.MIN } | |||||
| return JobOrderFgAlertsResponse( | |||||
| qc = qcRows.sortedWith(sortByDate), | |||||
| putAway = putAwayRows.sortedWith(sortByDate), | |||||
| ) | |||||
| } | |||||
| open fun updateProductProcessLineStartTime(productProcessLineId: Long): MessageResponse { | open fun updateProductProcessLineStartTime(productProcessLineId: Long): MessageResponse { | ||||
| val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) | val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null) | ||||
| productProcessLine.startTime = LocalDateTime.now() | productProcessLine.startTime = LocalDateTime.now() | ||||
| @@ -230,6 +230,12 @@ class ProductProcessController( | |||||
| size = size | size = size | ||||
| ) | ) | ||||
| } | } | ||||
| /** Nav: QC vs 上架 reminders (今日/昨日產程、等同完成QC工單列表資格). */ | |||||
| @GetMapping("/Demo/Process/alerts/fg-qc-putaway") | |||||
| fun getFgQcPutAwayAlerts(): JobOrderFgAlertsResponse = | |||||
| productProcessService.findJobOrderFgQcAndPutAwayAlertsForTodayYesterday() | |||||
| @PostMapping("/Demo/ProcessLine/start/{lineId}") | @PostMapping("/Demo/ProcessLine/start/{lineId}") | ||||
| fun startProductProcessLine(@PathVariable lineId: Long): MessageResponse { | fun startProductProcessLine(@PathVariable lineId: Long): MessageResponse { | ||||
| return productProcessService.StartProductProcessLine(lineId) | return productProcessService.StartProductProcessLine(lineId) | ||||
| @@ -192,6 +192,27 @@ data class JobOrderProductProcessPageResponse( | |||||
| val page: Int, | val page: Int, | ||||
| val size: Int | val size: Int | ||||
| ) | ) | ||||
| /** | |||||
| * Nav alerts aligned with 完成QC工單 list: all product process lines Completed/Pass, stock-in not completed/rejected; | |||||
| * job order has at least one process dated today or yesterday. [qc] = before received; [putAway] = received / partially_completed. | |||||
| */ | |||||
| data class JobOrderFgAlertRowResponse( | |||||
| val stockInLineId: Long, | |||||
| val jobOrderId: Long, | |||||
| val jobOrderCode: String?, | |||||
| val itemNo: String?, | |||||
| val itemName: String?, | |||||
| val status: String?, | |||||
| val processDate: LocalDate?, | |||||
| val lotNo: String?, | |||||
| ) | |||||
| data class JobOrderFgAlertsResponse( | |||||
| val qc: List<JobOrderFgAlertRowResponse>, | |||||
| val putAway: List<JobOrderFgAlertRowResponse>, | |||||
| ) | |||||
| data class ProductProcessInfoResponse( | data class ProductProcessInfoResponse( | ||||
| val id: Long, | val id: Long, | ||||
| val operatorId: Long?, | val operatorId: Long?, | ||||
| @@ -57,7 +57,8 @@ class PurchaseOrderController( | |||||
| return RecordsRes(paginatedList, fullList.size) | return RecordsRes(paginatedList, fullList.size) | ||||
| } | } | ||||
| @GetMapping("/po/summary") | |||||
| /** Class mapping is `/po`; path must be `/summary` → full path `/api/po/summary` (not `/po/po/summary`). */ | |||||
| @GetMapping("/summary") | |||||
| fun getPoSummaries( | fun getPoSummaries( | ||||
| @RequestParam ids: List<Long> | @RequestParam ids: List<Long> | ||||
| ): List<PurchaseOrderSummary> { | ): List<PurchaseOrderSummary> { | ||||
| @@ -3,10 +3,13 @@ package com.ffii.fpsms.modules.stock.entity | |||||
| import com.ffii.core.support.AbstractRepository | import com.ffii.core.support.AbstractRepository | ||||
| import com.ffii.fpsms.modules.stock.entity.projection.QrCodeInfo | import com.ffii.fpsms.modules.stock.entity.projection.QrCodeInfo | ||||
| import com.ffii.fpsms.modules.stock.entity.projection.StockInLineInfo | import com.ffii.fpsms.modules.stock.entity.projection.StockInLineInfo | ||||
| import com.ffii.fpsms.modules.stock.web.model.PurchaseStockInAlertRow | |||||
| import org.springframework.data.domain.Pageable | |||||
| import org.springframework.data.jpa.repository.Query | import org.springframework.data.jpa.repository.Query | ||||
| import org.springframework.stereotype.Repository | import org.springframework.stereotype.Repository | ||||
| import java.util.Optional | import java.util.Optional | ||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import java.time.LocalDateTime | |||||
| import org.springframework.data.repository.query.Param | import org.springframework.data.repository.query.Param | ||||
| @@ -118,4 +121,37 @@ fun findFirstByJobOrder_IdAndDeletedFalse(jobOrderId: Long): StockInLine? | |||||
| and sil.lotNo like concat(:prefix, '%') | and sil.lotNo like concat(:prefix, '%') | ||||
| """) | """) | ||||
| fun findLatestLotNoByPrefix(@Param("prefix") prefix: String): String? | fun findLatestLotNoByPrefix(@Param("prefix") prefix: String): String? | ||||
| /** | |||||
| * PO stock-in lines in pending / receiving, created on or after [since] (recent backlog). | |||||
| * ([StockInLineService] filters out lines whose POL already has put-away >= order qty in stock units.) | |||||
| */ | |||||
| @Query( | |||||
| """ | |||||
| SELECT new com.ffii.fpsms.modules.stock.web.model.PurchaseStockInAlertRow( | |||||
| sil.id, | |||||
| po.id, | |||||
| pol.id, | |||||
| po.code, | |||||
| sil.itemNo, | |||||
| item.name, | |||||
| sil.status, | |||||
| sil.created, | |||||
| sil.receiptDate, | |||||
| sil.lotNo | |||||
| ) | |||||
| FROM StockInLine sil | |||||
| JOIN sil.purchaseOrderLine pol | |||||
| JOIN sil.purchaseOrder po | |||||
| JOIN sil.item item | |||||
| WHERE sil.deleted = false | |||||
| AND LOWER(COALESCE(sil.status, '')) IN ('pending', 'receiving') | |||||
| AND sil.created >= :since | |||||
| ORDER BY sil.created DESC | |||||
| """ | |||||
| ) | |||||
| fun listPurchaseStockInAlertsSince( | |||||
| @Param("since") since: LocalDateTime, | |||||
| pageable: Pageable, | |||||
| ): List<PurchaseStockInAlertRow> | |||||
| } | } | ||||
| @@ -54,6 +54,7 @@ import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintQrCodeForDoRequest | |||||
| import com.ffii.fpsms.modules.stock.service.InventoryLotLineService | import com.ffii.fpsms.modules.stock.service.InventoryLotLineService | ||||
| import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository | import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryRepository | import com.ffii.fpsms.modules.stock.entity.InventoryRepository | ||||
| import org.springframework.data.domain.PageRequest | |||||
| import org.springframework.http.HttpStatus | import org.springframework.http.HttpStatus | ||||
| import org.springframework.web.server.ResponseStatusException | import org.springframework.web.server.ResponseStatusException | ||||
| import kotlin.text.toDouble | import kotlin.text.toDouble | ||||
| @@ -105,6 +106,8 @@ open class StockInLineService( | |||||
| @Lazy private val m18PurchaseOrderService: M18PurchaseOrderService, | @Lazy private val m18PurchaseOrderService: M18PurchaseOrderService, | ||||
| /** When false (default), M18 GRN create API is not called — set true in production only. */ | /** When false (default), M18 GRN create API is not called — set true in production only. */ | ||||
| @Value("\${scheduler.m18Grn.createEnabled:false}") private val m18GrnCreateEnabled: Boolean, | @Value("\${scheduler.m18Grn.createEnabled:false}") private val m18GrnCreateEnabled: Boolean, | ||||
| /** Recent window for PO stock-in alerts (pending / receiving) in nav / list UI. */ | |||||
| @Value("\${fpsms.purchase-stock-in-alert.lookback-days:7}") private val purchaseStockInAlertLookbackDays: Int, | |||||
| ) : AbstractBaseEntityService<StockInLine, Long, StockInLineRepository>(jdbcDao, stockInLineRepository) { | ) : AbstractBaseEntityService<StockInLine, Long, StockInLineRepository>(jdbcDao, stockInLineRepository) { | ||||
| private val logger = LoggerFactory.getLogger(StockInLineService::class.java) | private val logger = LoggerFactory.getLogger(StockInLineService::class.java) | ||||
| @@ -118,6 +121,89 @@ open class StockInLineService( | |||||
| open fun getReceivedStockInLineInfo(stockInLineId: Long): StockInLineInfo { | open fun getReceivedStockInLineInfo(stockInLineId: Long): StockInLineInfo { | ||||
| return stockInLineRepository.findStockInLineInfoByIdAndStatusAndDeletedFalse(id = stockInLineId, status = StockInLineStatus.RECEIVED.status).orElseThrow() | return stockInLineRepository.findStockInLineInfoByIdAndStatusAndDeletedFalse(id = stockInLineId, status = StockInLineStatus.RECEIVED.status).orElseThrow() | ||||
| } | } | ||||
| open fun purchaseStockInAlertSinceDays(overrideDays: Int?): LocalDateTime { | |||||
| val d = (overrideDays ?: purchaseStockInAlertLookbackDays).coerceIn(1, 90) | |||||
| return LocalDateTime.now().minusDays(d.toLong()) | |||||
| } | |||||
| /** | |||||
| * When total put-away (inventory lot line inQty, stock unit) for the POL is already at or above | |||||
| * order demand in stock units, do not count / list the line for PO stock-in reminders. | |||||
| */ | |||||
| @Transactional(readOnly = true) | |||||
| open fun countPurchaseStockInAlertsSince(since: LocalDateTime): Long { | |||||
| val suppressCache = mutableMapOf<Long, Boolean>() | |||||
| var total = 0L | |||||
| var page = 0 | |||||
| val pageSize = 100 | |||||
| while (true) { | |||||
| val batch = stockInLineRepository.listPurchaseStockInAlertsSince(since, PageRequest.of(page, pageSize)) | |||||
| if (batch.isEmpty()) break | |||||
| total += batch.count { !polPutAwaySuppressesPoStockInAlert(it.purchaseOrderLineId, suppressCache) } | |||||
| page++ | |||||
| if (batch.size < pageSize) break | |||||
| } | |||||
| return total | |||||
| } | |||||
| @Transactional(readOnly = true) | |||||
| open fun listPurchaseStockInAlerts(since: LocalDateTime, limit: Int): List<PurchaseStockInAlertRow> { | |||||
| val cap = limit.coerceIn(1, 200) | |||||
| val out = mutableListOf<PurchaseStockInAlertRow>() | |||||
| val suppressCache = mutableMapOf<Long, Boolean>() | |||||
| var page = 0 | |||||
| val pageSize = 50 | |||||
| while (out.size < cap) { | |||||
| val batch = stockInLineRepository.listPurchaseStockInAlertsSince(since, PageRequest.of(page, pageSize)) | |||||
| if (batch.isEmpty()) break | |||||
| for (row in batch) { | |||||
| if (!polPutAwaySuppressesPoStockInAlert(row.purchaseOrderLineId, suppressCache)) { | |||||
| out.add(row) | |||||
| } | |||||
| if (out.size >= cap) break | |||||
| } | |||||
| page++ | |||||
| if (batch.size < pageSize) break | |||||
| } | |||||
| return out | |||||
| } | |||||
| private fun polPutAwaySuppressesPoStockInAlert(polId: Long, cache: MutableMap<Long, Boolean>): Boolean = | |||||
| cache.getOrPut(polId) { | |||||
| val pol = polRepository.findById(polId).orElse(null) ?: return@getOrPut false | |||||
| val orderStock = polOrderStockQtyForAlert(pol) | |||||
| if (orderStock <= BigDecimal.ZERO) return@getOrPut false | |||||
| val putAway = totalPutAwayStockQtyForPol(polId) | |||||
| putAway.compareTo(orderStock) >= 0 | |||||
| } | |||||
| private fun polOrderStockQtyForAlert(pol: PurchaseOrderLine): BigDecimal { | |||||
| val itemId = pol.item?.id ?: return BigDecimal.ZERO | |||||
| val qtyM18 = pol.qtyM18 | |||||
| val uomM18Id = pol.uomM18?.id | |||||
| val stockQtyFromM18 = | |||||
| if (qtyM18 != null && uomM18Id != null) { | |||||
| itemUomService.convertQtyToStockQtyPrecise(itemId, uomM18Id, qtyM18) | |||||
| } else { | |||||
| BigDecimal.ZERO | |||||
| } | |||||
| return if (stockQtyFromM18 > BigDecimal.ZERO) { | |||||
| stockQtyFromM18 | |||||
| } else { | |||||
| itemUomService.convertPurchaseQtyToStockQtyPrecise(itemId, pol.qty ?: BigDecimal.ZERO) | |||||
| } | |||||
| } | |||||
| private fun totalPutAwayStockQtyForPol(polId: Long): BigDecimal { | |||||
| return stockInLineRepository.findStockInLineInfoByPurchaseOrderLineIdAndDeletedFalse(polId) | |||||
| .asSequence() | |||||
| .flatMap { it.putAwayLines?.asSequence() ?: emptySequence() } | |||||
| .fold(BigDecimal.ZERO) { acc, line -> | |||||
| acc + (line.stockQty ?: BigDecimal.ZERO).coerceAtLeast(BigDecimal.ZERO) | |||||
| } | |||||
| } | |||||
| /* | /* | ||||
| open fun assignLotNo(): String { | open fun assignLotNo(): String { | ||||
| val prefix = "LT" | val prefix = "LT" | ||||
| @@ -6,6 +6,7 @@ import com.ffii.fpsms.modules.stock.entity.StockInLine | |||||
| import com.ffii.fpsms.modules.stock.entity.projection.StockInLineInfo | import com.ffii.fpsms.modules.stock.entity.projection.StockInLineInfo | ||||
| import com.ffii.fpsms.modules.stock.service.StockInLineService | import com.ffii.fpsms.modules.stock.service.StockInLineService | ||||
| import com.ffii.fpsms.modules.stock.web.model.ExportQrCodeRequest | import com.ffii.fpsms.modules.stock.web.model.ExportQrCodeRequest | ||||
| import com.ffii.fpsms.modules.stock.web.model.PurchaseStockInAlertRow | |||||
| import com.ffii.fpsms.modules.stock.web.model.PrintQrCodeForSilRequest | import com.ffii.fpsms.modules.stock.web.model.PrintQrCodeForSilRequest | ||||
| import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest | import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest | ||||
| import jakarta.servlet.http.HttpServletResponse | import jakarta.servlet.http.HttpServletResponse | ||||
| @@ -25,6 +26,25 @@ import java.text.ParseException | |||||
| class StockInLineController( | class StockInLineController( | ||||
| private val stockInLineService: StockInLineService | private val stockInLineService: StockInLineService | ||||
| ) { | ) { | ||||
| /** Count of recent PO stock-in lines in pending / receiving (same filter as [listPurchaseStockInAlerts]). */ | |||||
| @GetMapping("/alerts/purchase-incomplete-count") | |||||
| fun getPurchaseStockInIncompleteCount( | |||||
| @RequestParam(name = "days", required = false) days: Int?, | |||||
| ): Map<String, Long> { | |||||
| val since = stockInLineService.purchaseStockInAlertSinceDays(days) | |||||
| return mapOf("count" to stockInLineService.countPurchaseStockInAlertsSince(since)) | |||||
| } | |||||
| /** Actionable list: recent PO stock-in lines in pending / receiving (deep-link to PO edit). */ | |||||
| @GetMapping("/alerts/purchase-incomplete") | |||||
| fun listPurchaseStockInAlerts( | |||||
| @RequestParam(name = "days", required = false) days: Int?, | |||||
| @RequestParam(name = "limit", defaultValue = "50") limit: Int, | |||||
| ): List<PurchaseStockInAlertRow> { | |||||
| val since = stockInLineService.purchaseStockInAlertSinceDays(days) | |||||
| return stockInLineService.listPurchaseStockInAlerts(since, limit) | |||||
| } | |||||
| @GetMapping("/{stockInLineId}") | @GetMapping("/{stockInLineId}") | ||||
| fun get(@PathVariable stockInLineId: Long): StockInLineInfo { | fun get(@PathVariable stockInLineId: Long): StockInLineInfo { | ||||
| return stockInLineService.getStockInLineInfo(stockInLineId) | return stockInLineService.getStockInLineInfo(stockInLineId) | ||||
| @@ -0,0 +1,19 @@ | |||||
| package com.ffii.fpsms.modules.stock.web.model | |||||
| import java.time.LocalDateTime | |||||
| /** | |||||
| * PO-linked stock-in line needing user action (pending / receiving), for alert list UI. | |||||
| */ | |||||
| data class PurchaseStockInAlertRow( | |||||
| val stockInLineId: Long, | |||||
| val purchaseOrderId: Long, | |||||
| val purchaseOrderLineId: Long, | |||||
| val poCode: String?, | |||||
| val itemNo: String?, | |||||
| val itemName: String?, | |||||
| val status: String?, | |||||
| val lineCreated: LocalDateTime?, | |||||
| val receiptDate: LocalDateTime?, | |||||
| val lotNo: String?, | |||||
| ) | |||||
| @@ -25,6 +25,11 @@ scheduler: | |||||
| inventoryLotExpiry: | inventoryLotExpiry: | ||||
| enabled: true | enabled: true | ||||
| # Nav: PO stock_in_line pending/receiving within last N days (see ProductProcessService for 工單 QC/上架:今日+昨日). | |||||
| fpsms: | |||||
| purchase-stock-in-alert: | |||||
| lookback-days: 7 | |||||
| spring: | spring: | ||||
| servlet: | servlet: | ||||
| multipart: | multipart: | ||||