| @@ -35,6 +35,8 @@ public abstract class SettingNames { | |||||
| /** Backfill M18 AN document code (grn_code) via GET /root/api/read/an (default 1:20 AM daily) */ | /** Backfill M18 AN document code (grn_code) via GET /root/api/read/an (default 1:20 AM daily) */ | ||||
| public static final String SCHEDULE_GRN_CODE_SYNC = "SCHEDULE.grn.grnCode.m18"; | public static final String SCHEDULE_GRN_CODE_SYNC = "SCHEDULE.grn.grnCode.m18"; | ||||
| /** Mark expired inventory lot lines as unavailable (default 00:05 daily) */ | |||||
| public static final String SCHEDULE_INVENTORY_LOT_EXPIRY = "SCHEDULE.inventoryLot.expiry"; | |||||
| public static final String SCHEDULE_PROD_ROUGH = "SCHEDULE.prod.rough"; | public static final String SCHEDULE_PROD_ROUGH = "SCHEDULE.prod.rough"; | ||||
| @@ -12,6 +12,7 @@ import com.ffii.fpsms.m18.model.SyncResult | |||||
| import com.ffii.fpsms.modules.common.SettingNames | import com.ffii.fpsms.modules.common.SettingNames | ||||
| import com.ffii.fpsms.modules.master.service.ProductionScheduleService | import com.ffii.fpsms.modules.master.service.ProductionScheduleService | ||||
| import com.ffii.fpsms.modules.stock.service.SearchCompletedDnService | import com.ffii.fpsms.modules.stock.service.SearchCompletedDnService | ||||
| import com.ffii.fpsms.modules.stock.service.InventoryLotLineService | |||||
| import com.ffii.fpsms.modules.settings.service.SettingsService | import com.ffii.fpsms.modules.settings.service.SettingsService | ||||
| import jakarta.annotation.PostConstruct | import jakarta.annotation.PostConstruct | ||||
| import org.springframework.beans.factory.annotation.Value | import org.springframework.beans.factory.annotation.Value | ||||
| @@ -37,6 +38,7 @@ open class SchedulerService( | |||||
| * missing `grn_code`. Example: 4 = from 4 days ago 00:00 to now. | * missing `grn_code`. Example: 4 = from 4 days ago 00:00 to now. | ||||
| */ | */ | ||||
| @Value("\${scheduler.grnCodeSync.syncOffsetDays:0}") val grnCodeSyncSyncOffsetDays: Int, | @Value("\${scheduler.grnCodeSync.syncOffsetDays:0}") val grnCodeSyncSyncOffsetDays: Int, | ||||
| @Value("\${scheduler.inventoryLotExpiry.enabled:true}") val inventoryLotExpiryEnabled: Boolean, | |||||
| val settingsService: SettingsService, | val settingsService: SettingsService, | ||||
| val taskScheduler: TaskScheduler, | val taskScheduler: TaskScheduler, | ||||
| val productionScheduleService: ProductionScheduleService, | val productionScheduleService: ProductionScheduleService, | ||||
| @@ -46,6 +48,7 @@ open class SchedulerService( | |||||
| val schedulerSyncLogRepository: SchedulerSyncLogRepository, | val schedulerSyncLogRepository: SchedulerSyncLogRepository, | ||||
| val searchCompletedDnService: SearchCompletedDnService, | val searchCompletedDnService: SearchCompletedDnService, | ||||
| val m18GrnCodeSyncService: M18GrnCodeSyncService, | val m18GrnCodeSyncService: M18GrnCodeSyncService, | ||||
| val inventoryLotLineService: InventoryLotLineService, | |||||
| ) { | ) { | ||||
| var logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) | var logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java) | ||||
| val dataStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd") | val dataStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd") | ||||
| @@ -64,6 +67,7 @@ open class SchedulerService( | |||||
| var scheduledPostCompletedDnGrn: ScheduledFuture<*>? = null | var scheduledPostCompletedDnGrn: ScheduledFuture<*>? = null | ||||
| var scheduledGrnCodeSync: ScheduledFuture<*>? = null | var scheduledGrnCodeSync: ScheduledFuture<*>? = null | ||||
| var scheduledInventoryLotExpiry: ScheduledFuture<*>? = null | |||||
| //@Volatile | //@Volatile | ||||
| //var scheduledRoughProd: ScheduledFuture<*>? = null | //var scheduledRoughProd: ScheduledFuture<*>? = null | ||||
| @@ -110,6 +114,7 @@ open class SchedulerService( | |||||
| scheduleM18MasterData(); | scheduleM18MasterData(); | ||||
| schedulePostCompletedDnGrn(); | schedulePostCompletedDnGrn(); | ||||
| scheduleGrnCodeSync(); | scheduleGrnCodeSync(); | ||||
| scheduleInventoryLotExpiry(); | |||||
| //scheduleRoughProd(); | //scheduleRoughProd(); | ||||
| //scheduleDetailedProd(); | //scheduleDetailedProd(); | ||||
| } | } | ||||
| @@ -193,6 +198,31 @@ open class SchedulerService( | |||||
| ) | ) | ||||
| } | } | ||||
| /** Mark expired inventory lot lines as unavailable daily. Set scheduler.inventoryLotExpiry.enabled=false to disable. */ | |||||
| fun scheduleInventoryLotExpiry() { | |||||
| if (!inventoryLotExpiryEnabled) { | |||||
| scheduledInventoryLotExpiry?.cancel(false) | |||||
| scheduledInventoryLotExpiry = null | |||||
| logger.info("Inventory lot expiry scheduler disabled (scheduler.inventoryLotExpiry.enabled=false)") | |||||
| return | |||||
| } | |||||
| scheduledInventoryLotExpiry = commonSchedule( | |||||
| scheduledInventoryLotExpiry, | |||||
| SettingNames.SCHEDULE_INVENTORY_LOT_EXPIRY, | |||||
| "0 5 0 * * *", | |||||
| { markExpiredInventoryLotLinesAsUnavailable() } | |||||
| ) | |||||
| } | |||||
| open fun markExpiredInventoryLotLinesAsUnavailable() { | |||||
| try { | |||||
| val updatedRows = inventoryLotLineService.markExpiredLotLinesAsUnavailable() | |||||
| logger.info("Scheduler - Inventory lot expiry update done, rows updated=$updatedRows") | |||||
| } catch (e: Exception) { | |||||
| logger.error("Scheduler - Inventory lot expiry update failed: ${e.message}", e) | |||||
| } | |||||
| } | |||||
| // Function for schedule | // Function for schedule | ||||
| // --------------------------- FP-MTMS --------------------------- // | // --------------------------- FP-MTMS --------------------------- // | ||||
| @@ -61,6 +61,12 @@ class SchedulerController( | |||||
| return "M18 GRN code sync (read/an) triggered" | return "M18 GRN code sync (read/an) triggered" | ||||
| } | } | ||||
| @GetMapping("/trigger/inventory-lot-expiry") | |||||
| fun triggerInventoryLotExpiry(): String { | |||||
| schedulerService.markExpiredInventoryLotLinesAsUnavailable() | |||||
| return "Inventory lot expiry status update triggered" | |||||
| } | |||||
| @GetMapping("/trigger/post-completed-dn-grn") | @GetMapping("/trigger/post-completed-dn-grn") | ||||
| 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, | ||||
| @@ -96,6 +96,8 @@ class DoPickOrderRecord { | |||||
| var handlerName: String? = null | var handlerName: String? = null | ||||
| @Column(name = "release_type", length = 100) | @Column(name = "release_type", length = 100) | ||||
| var releaseType: String? = null | var releaseType: String? = null | ||||
| @Column(name = "cartonQty") | |||||
| var cartonQty: Int? = null | |||||
| // Default constructor for Hibernate | // Default constructor for Hibernate | ||||
| constructor() | constructor() | ||||
| @@ -1308,6 +1308,7 @@ open class DeliveryOrderService( | |||||
| val driver = A4PrintDriverRegistry.getDriver(printer.brand) | val driver = A4PrintDriverRegistry.getDriver(printer.brand) | ||||
| driver.print(tempPdfFile, ip, port, printQty) | driver.print(tempPdfFile, ip, port, printQty) | ||||
| } | } | ||||
| updateRecordCartonQty(request.doPickOrderId, request.numOfCarton) | |||||
| } finally { | } finally { | ||||
| //tempPdfFile.delete() | //tempPdfFile.delete() | ||||
| } | } | ||||
| @@ -1406,6 +1407,7 @@ open class DeliveryOrderService( | |||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| updateRecordCartonQty(request.doPickOrderId, request.numOfCarton) | |||||
| println("Test PDF saved to: ${tempPdfFile.absolutePath}") | println("Test PDF saved to: ${tempPdfFile.absolutePath}") | ||||
| @@ -1415,6 +1417,13 @@ open class DeliveryOrderService( | |||||
| } | } | ||||
| private fun updateRecordCartonQty(doPickOrderRecordId: Long, cartonQty: Int) { | |||||
| if (cartonQty <= 0) return | |||||
| val record = doPickOrderRecordRepository.findById(doPickOrderRecordId).orElse(null) ?: return | |||||
| record.cartonQty = cartonQty | |||||
| doPickOrderRecordRepository.save(record) | |||||
| } | |||||
| @Transactional(rollbackFor = [Exception::class]) | @Transactional(rollbackFor = [Exception::class]) | ||||
| open fun releaseDeliveryOrderWithoutTicket(request: ReleaseDoRequest): ReleaseDoResult { | open fun releaseDeliveryOrderWithoutTicket(request: ReleaseDoRequest): ReleaseDoResult { | ||||
| println(" DEBUG: Starting releaseDeliveryOrderWithoutTicket for DO ID: ${request.id}") | println(" DEBUG: Starting releaseDeliveryOrderWithoutTicket for DO ID: ${request.id}") | ||||
| @@ -5,8 +5,10 @@ import com.ffii.fpsms.modules.stock.entity.projection.CurrentInventoryItemInfo | |||||
| import com.ffii.fpsms.modules.stock.entity.projection.InventoryLotLineInfo | import com.ffii.fpsms.modules.stock.entity.projection.InventoryLotLineInfo | ||||
| import org.springframework.data.domain.Page | import org.springframework.data.domain.Page | ||||
| import org.springframework.data.domain.Pageable | import org.springframework.data.domain.Pageable | ||||
| import org.springframework.data.jpa.repository.Modifying | |||||
| 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 org.springframework.transaction.annotation.Transactional | |||||
| import java.io.Serializable | import java.io.Serializable | ||||
| import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | ||||
| import org.springframework.data.repository.query.Param | import org.springframework.data.repository.query.Param | ||||
| @@ -145,4 +147,21 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long | |||||
| fun findAllByStoreIdsAndHasStockInLine( | fun findAllByStoreIdsAndHasStockInLine( | ||||
| @Param("storeIds") storeIds: List<String> | @Param("storeIds") storeIds: List<String> | ||||
| ): List<InventoryLotLine> | ): List<InventoryLotLine> | ||||
| @Modifying | |||||
| @Transactional | |||||
| @Query( | |||||
| value = """ | |||||
| UPDATE inventory_lot_line ill | |||||
| INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId | |||||
| SET ill.status = 'unavailable' | |||||
| WHERE il.deleted = 0 | |||||
| AND ill.deleted = 0 | |||||
| AND il.expiryDate IS NOT NULL | |||||
| AND il.expiryDate < CURRENT_DATE | |||||
| AND (ill.status IS NULL OR LOWER(ill.status) <> 'unavailable') | |||||
| """, | |||||
| nativeQuery = true | |||||
| ) | |||||
| fun markExpiredLotLinesAsUnavailable(): Int | |||||
| } | } | ||||
| @@ -60,6 +60,11 @@ open class InventoryLotLineService( | |||||
| @Lazy | @Lazy | ||||
| private val jobOrderService: JobOrderService | private val jobOrderService: JobOrderService | ||||
| ) { | ) { | ||||
| @Transactional | |||||
| open fun markExpiredLotLinesAsUnavailable(): Int { | |||||
| return inventoryLotLineRepository.markExpiredLotLinesAsUnavailable() | |||||
| } | |||||
| open fun findById(id: Long): Optional<InventoryLotLine> { | open fun findById(id: Long): Optional<InventoryLotLine> { | ||||
| return inventoryLotLineRepository.findById(id) | return inventoryLotLineRepository.findById(id) | ||||
| } | } | ||||
| @@ -1162,7 +1162,7 @@ open class StockInLineService( | |||||
| this.type = stockInLine.type | this.type = stockInLine.type | ||||
| this.itemId = item.id | this.itemId = item.id | ||||
| this.itemCode = item.code | this.itemCode = item.code | ||||
| this.uomId = inventory.uom?.id ?: itemUomService.findStockUnitByItemId(item.id!!)?.uom?.id | |||||
| this.uomId = itemUomService.findStockUnitByItemId(item.id!!)?.uom?.id ?: inventory.uom?.id | |||||
| this.date = LocalDate.now() | this.date = LocalDate.now() | ||||
| } | } | ||||
| @@ -656,31 +656,22 @@ private fun getStockOutIdFromPickOrderLine(pickOrderLineId: Long): Long { | |||||
| val statusLower = request.status.trim().lowercase() | val statusLower = request.status.trim().lowercase() | ||||
| val isPickEnd = statusLower == "completed" || statusLower == "partially_completed" | val isPickEnd = statusLower == "completed" || statusLower == "partially_completed" | ||||
| val deltaQty = request.qty | val deltaQty = request.qty | ||||
| val skipLedgerWrite = request.skipLedgerWrite == true | |||||
| // non jo/do: 手工拣货增量扣减时,補寫出库 ledger(幂等 + 跳过 jo/do,避免 double write) | |||||
| if (isPickEnd && deltaQty != null && deltaQty > 0 && savedStockOutLine.id != null) { | |||||
| // 手工拣货增量扣减时补写出库 ledger(DO/JO 也需要写) | |||||
| // 批量提交流程会显式传 skipLedgerWrite=true,避免重复写入。 | |||||
| if (!skipLedgerWrite && isPickEnd && deltaQty != null && deltaQty > 0 && savedStockOutLine.id != null) { | |||||
| val itemId = savedStockOutLine.item?.id | val itemId = savedStockOutLine.item?.id | ||||
| val po = savedStockOutLine.pickOrderLine?.pickOrder | |||||
| if (itemId != null && po != null) { | |||||
| val isJoOrDo = (po.jobOrder != null || po.deliveryOrder != null) | |||||
| if (!isJoOrDo) { | |||||
| val exists = jdbcDao.queryForInt( | |||||
| "SELECT COUNT(*) FROM stock_ledger WHERE deleted = false AND stockOutLineId = :id", | |||||
| mapOf("id" to savedStockOutLine.id) | |||||
| ) > 0 | |||||
| if (!exists) { | |||||
| val invBefore = inventoryRepository.findByItemId(itemId).orElse(null) | |||||
| val onHandBefore = invBefore?.onHandQty?.toDouble() ?: 0.0 | |||||
| createStockLedgerForPickDelta( | |||||
| stockOutLineId = savedStockOutLine.id!!, | |||||
| deltaQty = BigDecimal(deltaQty.toString()), | |||||
| onHandQtyBeforeUpdate = onHandBefore | |||||
| ) | |||||
| } | |||||
| } | |||||
| if (itemId != null) { | |||||
| val invBefore = inventoryRepository.findByItemId(itemId).orElse(null) | |||||
| val onHandBefore = invBefore?.onHandQty?.toDouble() ?: 0.0 | |||||
| createStockLedgerForPickDelta( | |||||
| stockOutLineId = savedStockOutLine.id!!, | |||||
| deltaQty = BigDecimal(deltaQty.toString()), | |||||
| onHandQtyBeforeUpdate = onHandBefore | |||||
| ) | |||||
| } | } | ||||
| } | } | ||||
| println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}") | println("Updated StockOutLine: ${savedStockOutLine.id} with status: ${savedStockOutLine.status}") | ||||
| @@ -1295,7 +1286,8 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | |||||
| UpdateStockOutLineStatusRequest( | UpdateStockOutLineStatusRequest( | ||||
| id = line.stockOutLineId, | id = line.stockOutLineId, | ||||
| status = newStatus, // 例如前端传来的 "completed" | status = newStatus, // 例如前端传来的 "completed" | ||||
| qty = 0.0 // 不改变现有 qty | |||||
| qty = 0.0, // 不改变现有 qty | |||||
| skipLedgerWrite = true | |||||
| ) | ) | ||||
| ) | ) | ||||
| @@ -1308,7 +1300,8 @@ open fun newBatchSubmit(request: QrPickBatchSubmitRequest): MessageResponse { | |||||
| UpdateStockOutLineStatusRequest( | UpdateStockOutLineStatusRequest( | ||||
| id = line.stockOutLineId, | id = line.stockOutLineId, | ||||
| status = newStatus, | status = newStatus, | ||||
| qty = submitQty.toDouble() | |||||
| qty = submitQty.toDouble(), | |||||
| skipLedgerWrite = true | |||||
| ) | ) | ||||
| ) | ) | ||||
| @@ -1509,8 +1502,8 @@ affectedConsoCodes.forEach { consoCode -> | |||||
| this.type = stockOutLine.type | this.type = stockOutLine.type | ||||
| this.itemId = item.id | this.itemId = item.id | ||||
| this.itemCode = item.code | this.itemCode = item.code | ||||
| this.uomId = inventory.uom?.id | |||||
| ?: itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id | |||||
| this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id | |||||
| ?: inventory.uom?.id | |||||
| this.date = LocalDate.now() | this.date = LocalDate.now() | ||||
| } | } | ||||
| @@ -1607,8 +1600,8 @@ affectedConsoCodes.forEach { consoCode -> | |||||
| this.type = "NOR" | this.type = "NOR" | ||||
| this.itemId = item.id | this.itemId = item.id | ||||
| this.itemCode = item.code | this.itemCode = item.code | ||||
| this.uomId = inventory.uom?.id | |||||
| ?: itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id | |||||
| this.uomId = itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(item.id!!)?.uom?.id | |||||
| ?: inventory.uom?.id | |||||
| this.date = LocalDate.now() | this.date = LocalDate.now() | ||||
| } | } | ||||
| @@ -40,6 +40,7 @@ import java.sql.ResultSet | |||||
| 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 com.ffii.fpsms.modules.stock.entity.StockLedger | import com.ffii.fpsms.modules.stock.entity.StockLedger | ||||
| import com.ffii.fpsms.modules.master.entity.UomConversionRepository | |||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | ||||
| import java.math.RoundingMode | import java.math.RoundingMode | ||||
| @@ -59,7 +60,8 @@ class StockTakeRecordService( | |||||
| val stockInLineRepository: StockInLineRepository, | val stockInLineRepository: StockInLineRepository, | ||||
| val inventoryLotRepository: InventoryLotRepository, | val inventoryLotRepository: InventoryLotRepository, | ||||
| val stockLedgerRepository: StockLedgerRepository, | val stockLedgerRepository: StockLedgerRepository, | ||||
| val inventoryRepository: InventoryRepository | |||||
| val inventoryRepository: InventoryRepository, | |||||
| val uomConversionRepository: UomConversionRepository | |||||
| ) { | ) { | ||||
| private val logger: Logger = LoggerFactory.getLogger(StockTakeRecordService::class.java) | private val logger: Logger = LoggerFactory.getLogger(StockTakeRecordService::class.java) | ||||
| @@ -1706,6 +1708,10 @@ fun searchStockTransactions(request: SearchStockTransactionRequest): RecordsRes< | |||||
| itemId = ledger.itemId ?: 0L, | itemId = ledger.itemId ?: 0L, | ||||
| itemCode = ledger.itemCode, | itemCode = ledger.itemCode, | ||||
| itemName = ledger.inventory?.item?.name, | itemName = ledger.inventory?.item?.name, | ||||
| uomId = ledger.uomId, | |||||
| uomDesc = ledger.uomId?.let { uomId -> | |||||
| uomConversionRepository.findByIdAndDeletedFalse(uomId)?.udfudesc | |||||
| }, | |||||
| balanceQty = ledger.balance?.let { balance: Double -> BigDecimal(balance.toString()) }, | balanceQty = ledger.balance?.let { balance: Double -> BigDecimal(balance.toString()) }, | ||||
| qty = if (ledger.inQty != null && ledger.inQty!! > 0) { | qty = if (ledger.inQty != null && ledger.inQty!! > 0) { | ||||
| BigDecimal(ledger.inQty.toString()) | BigDecimal(ledger.inQty.toString()) | ||||
| @@ -59,7 +59,8 @@ data class UpdateStockOutLineStatusRequest( | |||||
| val id: Long, | val id: Long, | ||||
| val status: String, | val status: String, | ||||
| val qty: Double? = null, | val qty: Double? = null, | ||||
| val remarks: String? = null | |||||
| val remarks: String? = null, | |||||
| val skipLedgerWrite: Boolean? = false | |||||
| ) | ) | ||||
| data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest( | data class UpdateStockOutLineStatusByQRCodeAndLotNoRequest( | ||||
| val pickOrderLineId: Long, | val pickOrderLineId: Long, | ||||
| @@ -127,6 +127,8 @@ data class StockTransactionResponse( | |||||
| val itemId: Long, | val itemId: Long, | ||||
| val itemCode: String?, | val itemCode: String?, | ||||
| val itemName: String?, | val itemName: String?, | ||||
| val uomId: Long? = null, | |||||
| val uomDesc: String? = null, | |||||
| val balanceQty: BigDecimal? = null, | val balanceQty: BigDecimal? = null, | ||||
| val qty: BigDecimal, | val qty: BigDecimal, | ||||
| val type: String?, | val type: String?, | ||||
| @@ -27,6 +27,8 @@ scheduler: | |||||
| grnCodeSync: | grnCodeSync: | ||||
| enabled: true | enabled: true | ||||
| syncOffsetDays: 10 # from (today − 10) 00:00 to now, rows missing grn_code | syncOffsetDays: 10 # from (today − 10) 00:00 to now, rows missing grn_code | ||||
| inventoryLotExpiry: | |||||
| enabled: true | |||||
| m18: | m18: | ||||
| config: | config: | ||||
| @@ -19,6 +19,8 @@ scheduler: | |||||
| enabled: false # set true in prod; backfills grn_code from M18 GET /root/api/read/an | enabled: false # set true in prod; backfills grn_code from M18 GET /root/api/read/an | ||||
| # Lookback: created from start of (today − N days) through now, missing grn_code. E.g. 4 = last 4 days + today. | # Lookback: created from start of (today − N days) through now, missing grn_code. E.g. 4 = last 4 days + today. | ||||
| syncOffsetDays: 0 | syncOffsetDays: 0 | ||||
| inventoryLotExpiry: | |||||
| enabled: true | |||||
| spring: | spring: | ||||
| servlet: | servlet: | ||||