From df14864c0fc2437c211b9f53f8a4c01038083fb0 Mon Sep 17 00:00:00 2001 From: Harry Groves Date: Tue, 6 Oct 2026 12:35:06 +0800 Subject: [PATCH] Keep demo sample dates current so delivery orders and lots stay visible. Shift DEMO- dates forward each day, add a today row for every DO tab and status, and hide goods-receipt rows that have no purchase order. --- docs/DEMO_BRANCH_SETUP.md | 4 + .../modules/common/demo/DemoDateKeeper.kt | 332 ++++++++++++++++++ .../modules/common/demo/DemoSandboxSeeder.kt | 191 +++++++++- .../service/GoodsReceiptStatusService.kt | 11 +- src/main/resources/application-db-demo.yml | 3 + .../modules/common/demo/DemoDateShiftTest.kt | 33 ++ 6 files changed, 566 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/common/demo/DemoDateKeeper.kt create mode 100644 src/test/kotlin/com/ffii/fpsms/modules/common/demo/DemoDateShiftTest.kt diff --git a/docs/DEMO_BRANCH_SETUP.md b/docs/DEMO_BRANCH_SETUP.md index 5c2f8608..aeb6f639 100644 --- a/docs/DEMO_BRANCH_SETUP.md +++ b/docs/DEMO_BRANCH_SETUP.md @@ -93,6 +93,10 @@ JDK 17. From `FPSMS-backend`, on branch `debrand`: The first boot inserts the fictional sandbox (`DemoSandboxSeeder`) and the extra report rows (`src/main/resources/demo/demo-coverage.sql`). Later boots skip both when user `demo` and setting `DEMO.coverage.v1` already exist. +`DemoDateKeeper` then keeps those sample dates current. It runs at startup, every day at 00:10, and when the truck board is opened. It moves `DEMO-` expiry, delivery, order, pick, job, and stock-take dates forward by the number of days since the last run (stored as setting `DEMO.dates.anchor`). Lots stay unexpired, and screens that default to today still show the sample orders. You do not need to re-seed or set the date filter back to the original seed day. Quarterly stock-ledger samples stay in January, April, July, and September, and roll into the new year. + +On the first run it also fills 「送貨訂單」 so a search for today returns a row on each tab: 2/F and 4/F (pending, receiving, completed), Truck X, 加單, and 補貨. Those rows use `DEMO-` codes, so the same daily shift keeps their dates on today. The marker is setting `DEMO.do.showcase.v1`. + In the log, expect: `Demo database login cannot read live schema fpsmsdb (MySQL error 1142).` diff --git a/src/main/java/com/ffii/fpsms/modules/common/demo/DemoDateKeeper.kt b/src/main/java/com/ffii/fpsms/modules/common/demo/DemoDateKeeper.kt new file mode 100644 index 00000000..0308a446 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/common/demo/DemoDateKeeper.kt @@ -0,0 +1,332 @@ +package com.ffii.fpsms.modules.common.demo + +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.ObjectProvider +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.ApplicationRunner +import org.springframework.context.annotation.Profile +import org.springframework.core.annotation.Order +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import org.springframework.transaction.support.TransactionTemplate +import java.time.LocalDate +import java.time.temporal.ChronoUnit +import javax.sql.DataSource + +/** + * Keeps fictional sandbox dates lined up with today. + * + * Seed data is inserted once, with expiry and delivery dates relative to that day. + * Without a later shift, lots expire and list screens that default to today stop showing the samples. + * This only runs on the db-demo profile, and only against fpsms_demo, and only rows coded DEMO-. + */ +@Component +@Profile("db-demo") +@Order(20) +class DemoDateKeeper( + @Value("\${spring.datasource.jdbc-url}") private val jdbcUrl: String, + private val dataSource: DataSource, + private val jdbcTemplate: JdbcTemplate, + private val transactionTemplate: TransactionTemplate, + private val sandboxSeeder: ObjectProvider, +) : ApplicationRunner { + + private val log = LoggerFactory.getLogger(DemoDateKeeper::class.java) + + override fun run(args: ApplicationArguments) { + refresh() + } + + /** Catch a process that stays up past midnight. Startup and the truck board also call [refresh]. */ + @Scheduled(cron = "\${fpsms.demo.date-refresh-cron:0 10 0 * * *}") + fun scheduledRefresh() { + try { + refresh() + } catch (ex: Exception) { + log.error("Demo date refresh failed", ex) + } + } + + @Synchronized + fun refresh() { + assertDemoDatabase() + transactionTemplate.executeWithoutResult { + val stored = readAnchor() + val anchor = stored ?: inferAnchorFromPurchaseOrder() + val today = LocalDate.now() + if (anchor == null) { + saveAnchor(today) + log.warn("Demo date keeper found no DEMO-PO-001 order date. Marked {} without shifting.", today) + return@executeWithoutResult + } + when (val shift = demoDateShiftDays(anchor, today)) { + is DemoDateShift.TooLarge -> { + log.error( + "Demo date anchor {} is {} days before today, over the {} day limit. Dates were not moved.", + anchor, + shift.days, + DEMO_DATE_SHIFT_MAX_DAYS, + ) + return@executeWithoutResult + } + is DemoDateShift.Forward -> { + val updated = shiftDemoDates(shift.days.toInt()) + // Truck tickets are also pinned to today/tomorrow by the seeder. Re-pin after the + // shift so a board that was already on today is not pushed further ahead. + sandboxSeeder.getIfAvailable()?.realignDashboardTrucks() + log.info( + "Demo dates moved forward {} day(s), {} row update(s). Anchor {} -> {}.", + shift.days, + updated, + anchor, + today, + ) + } + DemoDateShift.None -> Unit + } + val rolled = rollQuarterlyLedgerIntoCurrentYear() + if (rolled > 0) { + log.info("Rolled {} demo stock-ledger sample row(s) into the current year.", rolled) + } + if (stored != today) { + saveAnchor(today) + } + // After the shift, so a multi-day catch-up does not push brand-new "today" rows into the future. + sandboxSeeder.getIfAvailable()?.ensureDeliveryShowcase() + } + } + + private fun assertDemoDatabase() { + val configuredName = databaseName(jdbcUrl) + val connectedName = dataSource.connection.use { it.catalog } + if (configuredName != DEMO_DATABASE || !DEMO_DATABASE.equals(connectedName, ignoreCase = true)) { + throw IllegalStateException( + "Demo date keeper refused to run. Configured database is '$configuredName', connected database is '$connectedName'.", + ) + } + } + + private fun readAnchor(): LocalDate? { + val value = jdbcTemplate.query( + "SELECT value FROM settings WHERE name = ? ORDER BY id DESC LIMIT 1", + { rs, _ -> rs.getString("value") }, + ANCHOR_SETTING, + ).firstOrNull() ?: return null + return runCatching { LocalDate.parse(value) }.getOrNull() + } + + /** DEMO-PO-001 is inserted with orderDate = the seed day, so an existing sandbox can catch up once. */ + private fun inferAnchorFromPurchaseOrder(): LocalDate? { + return jdbcTemplate.query( + "SELECT DATE(orderDate) AS anchorDate FROM purchase_order WHERE code = 'DEMO-PO-001' LIMIT 1", + { rs, _ -> rs.getDate("anchorDate")?.toLocalDate() }, + ).firstOrNull() + } + + private fun saveAnchor(today: LocalDate) { + val value = today.toString() + val updated = jdbcTemplate.update( + "UPDATE settings SET value = ? WHERE name = ?", + value, + ANCHOR_SETTING, + ) + if (updated == 0) { + jdbcTemplate.update( + """ + INSERT INTO settings (name, value, category, type) + SELECT ?, ?, 'DEMO', 'date' + WHERE NOT EXISTS (SELECT 1 FROM settings WHERE name = ?) + """.trimIndent(), + ANCHOR_SETTING, + value, + ANCHOR_SETTING, + ) + } + } + + private fun shiftDemoDates(days: Int): Int { + val statements = listOf( + """ + UPDATE purchase_order + SET orderDate = DATE_ADD(orderDate, INTERVAL $days DAY), + estimatedArrivalDate = DATE_ADD(estimatedArrivalDate, INTERVAL $days DAY), + completeDate = DATE_ADD(completeDate, INTERVAL $days DAY) + WHERE code LIKE 'DEMO-%' + """, + """ + UPDATE delivery_order + SET orderDate = DATE_ADD(orderDate, INTERVAL $days DAY), + estimatedArrivalDate = DATE_ADD(estimatedArrivalDate, INTERVAL $days DAY), + completeDate = DATE_ADD(completeDate, INTERVAL $days DAY) + WHERE code LIKE 'DEMO-%' + """, + """ + UPDATE delivery_order_pick_order + SET requiredDeliveryDate = DATE_ADD(requiredDeliveryDate, INTERVAL $days DAY), + ticketReleaseTime = DATE_ADD(ticketReleaseTime, INTERVAL $days DAY), + ticketCompleteDateTime = DATE_ADD(ticketCompleteDateTime, INTERVAL $days DAY) + WHERE ticketNo LIKE 'DEMO-%' + """, + """ + UPDATE do_pick_order_record + SET RequiredDeliveryDate = DATE_ADD(RequiredDeliveryDate, INTERVAL $days DAY), + ticket_release_time = DATE_ADD(ticket_release_time, INTERVAL $days DAY), + ticketCompleteDateTime = DATE_ADD(ticketCompleteDateTime, INTERVAL $days DAY) + WHERE ticket_no LIKE 'DEMO-%' + OR delivery_order_code LIKE 'DEMO-%' + OR pick_order_code LIKE 'DEMO-%' + """, + """ + UPDATE do_replenishment + SET deliveryDate = DATE_ADD(deliveryDate, INTERVAL $days DAY) + WHERE code LIKE 'DEMO-%' + """, + """ + UPDATE job_order + SET planStart = DATE_ADD(planStart, INTERVAL $days DAY), + actualStart = DATE_ADD(actualStart, INTERVAL $days DAY), + planEnd = DATE_ADD(planEnd, INTERVAL $days DAY), + actualEnd = DATE_ADD(actualEnd, INTERVAL $days DAY) + WHERE code LIKE 'DEMO-%' + """, + """ + UPDATE productprocess + SET startTime = DATE_ADD(startTime, INTERVAL $days DAY), + endTime = DATE_ADD(endTime, INTERVAL $days DAY), + date = DATE_ADD(date, INTERVAL $days DAY) + WHERE productprocesscode LIKE 'DEMO-%' + """, + """ + UPDATE pick_order + SET targetDate = DATE_ADD(targetDate, INTERVAL $days DAY), + completeDate = DATE_ADD(completeDate, INTERVAL $days DAY), + releasedDate = DATE_ADD(releasedDate, INTERVAL $days DAY) + WHERE code LIKE 'DEMO-%' + """, + """ + UPDATE stock_in + SET orderDate = DATE_ADD(orderDate, INTERVAL $days DAY), + estimatedCompleteDate = DATE_ADD(estimatedCompleteDate, INTERVAL $days DAY), + completeDate = DATE_ADD(completeDate, INTERVAL $days DAY) + WHERE code LIKE 'DEMO-%' + """, + """ + UPDATE stock_in_line sil + JOIN stock_in si ON si.id = sil.stockInId + SET sil.expiryDate = DATE_ADD(sil.expiryDate, INTERVAL $days DAY), + sil.receiptDate = DATE_ADD(sil.receiptDate, INTERVAL $days DAY), + sil.productionDate = DATE_ADD(sil.productionDate, INTERVAL $days DAY), + sil.dnDate = DATE_ADD(sil.dnDate, INTERVAL $days DAY) + WHERE si.code LIKE 'DEMO-%' + OR sil.lotNo LIKE 'DEMO-LOT-%' + OR sil.itemNo LIKE 'DEMO-%' + """, + """ + UPDATE inventory_lot + SET expiryDate = DATE_ADD(expiryDate, INTERVAL $days DAY), + stockInDate = DATE_ADD(stockInDate, INTERVAL $days DAY), + productionDate = DATE_ADD(productionDate, INTERVAL $days DAY) + WHERE lotNo LIKE 'DEMO-LOT-%' + """, + """ + UPDATE stock_out + SET completeDate = DATE_ADD(completeDate, INTERVAL $days DAY) + WHERE deliveryOrderCode LIKE 'DEMO-%' + OR consoPickOrderCode LIKE 'DEMO-%' + """, + """ + UPDATE stock_out_line sol + JOIN stock_out so ON so.id = sol.stockOutId + SET sol.pickTime = DATE_ADD(sol.pickTime, INTERVAL $days DAY), + sol.startTime = DATE_ADD(sol.startTime, INTERVAL $days DAY), + sol.endTime = DATE_ADD(sol.endTime, INTERVAL $days DAY) + WHERE so.deliveryOrderCode LIKE 'DEMO-%' + OR so.consoPickOrderCode LIKE 'DEMO-%' + """, + """ + UPDATE stock_take + SET planStart = DATE_ADD(planStart, INTERVAL $days DAY), + planEnd = DATE_ADD(planEnd, INTERVAL $days DAY), + actualStart = DATE_ADD(actualStart, INTERVAL $days DAY), + actualEnd = DATE_ADD(actualEnd, INTERVAL $days DAY) + WHERE code LIKE 'DEMO-%' + """, + """ + UPDATE stocktakerecord r + JOIN stock_take st ON st.id = r.stockTakeId + SET r.date = DATE_ADD(r.date, INTERVAL $days DAY), + r.approverTime = DATE_ADD(r.approverTime, INTERVAL $days DAY), + r.stockTakeStartTime = DATE_ADD(r.stockTakeStartTime, INTERVAL $days DAY), + r.stockTakeEndTime = DATE_ADD(r.stockTakeEndTime, INTERVAL $days DAY), + r.expiredDate = DATE_ADD(r.expiredDate, INTERVAL $days DAY) + WHERE st.code LIKE 'DEMO-%' + OR r.itemCode LIKE 'DEMO-%' + OR r.lotNo LIKE 'DEMO-LOT-%' + """, + """ + UPDATE stock_ledger + SET date = DATE_ADD(date, INTERVAL $days DAY), + modified = DATE_ADD(modified, INTERVAL $days DAY) + WHERE itemCode LIKE 'DEMO-%' + AND (type IS NULL OR type <> 'OUT') + """, + ) + return statements.sumOf { jdbcTemplate.update(it.trimIndent()) } + } + + /** + * January / April / July / September ledger samples are fixed calendar days, not offsets from the seed day. + * Leave them in those months, and move them into the new year when the calendar rolls over. + */ + private fun rollQuarterlyLedgerIntoCurrentYear(): Int { + return jdbcTemplate.update( + """ + UPDATE stock_ledger + SET date = DATE_ADD(date, INTERVAL (YEAR(CURDATE()) - YEAR(date)) YEAR), + modified = CASE + WHEN modified IS NULL THEN NULL + ELSE DATE_ADD(modified, INTERVAL (YEAR(CURDATE()) - YEAR(modified)) YEAR) + END + WHERE itemCode LIKE 'DEMO-%' + AND type = 'OUT' + AND date IS NOT NULL + AND YEAR(date) < YEAR(CURDATE()) + """.trimIndent(), + ) + } + + private fun databaseName(jdbcUrl: String): String? { + val path = jdbcUrl.substringBefore('?').substringAfter("://") + val slash = path.indexOf('/') + if (slash < 0 || slash == path.lastIndex) return null + return path.substring(slash + 1).trim().ifEmpty { null } + } + + companion object { + private const val DEMO_DATABASE = "fpsms_demo" + const val ANCHOR_SETTING = "DEMO.dates.anchor" + } +} + +internal const val DEMO_DATE_SHIFT_MAX_DAYS = 2000L + +internal fun demoDateShiftDays( + anchor: LocalDate, + today: LocalDate, + maxDays: Long = DEMO_DATE_SHIFT_MAX_DAYS, +): DemoDateShift { + val days = ChronoUnit.DAYS.between(anchor, today) + return when { + days > maxDays -> DemoDateShift.TooLarge(days) + days > 0 -> DemoDateShift.Forward(days) + else -> DemoDateShift.None + } +} + +internal sealed class DemoDateShift { + data object None : DemoDateShift() + data class Forward(val days: Long) : DemoDateShift() + data class TooLarge(val days: Long) : DemoDateShift() +} diff --git a/src/main/java/com/ffii/fpsms/modules/common/demo/DemoSandboxSeeder.kt b/src/main/java/com/ffii/fpsms/modules/common/demo/DemoSandboxSeeder.kt index e10525ff..3aca25d3 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/demo/DemoSandboxSeeder.kt +++ b/src/main/java/com/ffii/fpsms/modules/common/demo/DemoSandboxSeeder.kt @@ -115,6 +115,7 @@ class DemoSandboxSeeder( private val deliveryOrderLineRepository: DeliveryOrderLineRepository, private val pickOrderRepository: PickOrderRepository, private val pickOrderLineRepository: PickOrderLineRepository, + private val demoDateKeeper: DemoDateKeeper, ) : ApplicationRunner { private val log = LoggerFactory.getLogger(DemoSandboxSeeder::class.java) @@ -200,8 +201,189 @@ class DemoSandboxSeeder( * If those rows are still on an earlier day, shift them to the current date. * Opening the dashboard does this, so a demo on a later day still has trucks to show. */ + /** Put the sample truck board back on today, tomorrow, and the day after. */ + fun realignDashboardTrucks() { + seedDashboardTrucks() + } + + /** + * Gives 「送貨訂單」 a row for each status on 2/F, 4/F, Truck X, 加單, and 補貨. + * Dates are set to today once, after [DemoDateKeeper] has shifted older rows. + * Later runs leave the dates alone so the daily shift keeps them on today. + */ + fun ensureDeliveryShowcase() { + jdbcTemplate.update( + """ + UPDATE delivery_order d + JOIN shop sup ON sup.code = 'DEMO-SUP' AND sup.deleted = 0 + SET d.supplierId = sup.id + WHERE d.code IN ('DEMO-DO-001', 'DEMO-DO-002', 'DEMO-DO-003') + AND d.supplierId IS NULL + """.trimIndent(), + ) + val already = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM settings WHERE name = ?", + Int::class.java, + SHOWCASE_MARKER, + ) ?: 0 + if (already > 0) return + + jdbcTemplate.update( + """ + UPDATE delivery_order + SET orderDate = CURDATE(), + estimatedArrivalDate = CURDATE(), + completeDate = CASE WHEN status = 'completed' THEN CURDATE() ELSE completeDate END + WHERE code IN ('DEMO-DO-001', 'DEMO-DO-002', 'DEMO-DO-003') + """.trimIndent(), + ) + jdbcTemplate.update( + """ + UPDATE settings + SET value = 'DEMO-SUP2' + WHERE name = 'DO.floor.suppliers.4F' + AND value IN ('DEMO-SUP', '') + """.trimIndent(), + ) + jdbcTemplate.update( + """ + INSERT INTO shop (code, name, type, contactName, contactNo, addr1, district, m18Id, m18LastModifyDate) + SELECT 'DEMO-SHP-X', '無車線門店', 'shop', '周先生', '51110009', '示範巷 1 號', '九龍', 910039, NOW() + WHERE NOT EXISTS (SELECT 1 FROM shop WHERE code = 'DEMO-SHP-X') + """.trimIndent(), + ) + jdbcTemplate.update( + """ + INSERT INTO truck (TruckLanceCode, DepartureTime, shopId, ShopName, ShopCode, LoadingSequence, Store_id) + SELECT 'DEMO-4F', '15:00:00', s.id, s.name, s.code, 1, '4F' + FROM shop s + WHERE s.code = 'DEMO-SHP2' AND s.deleted = 0 + AND NOT EXISTS (SELECT 1 FROM truck WHERE TruckLanceCode = 'DEMO-4F') + """.trimIndent(), + ) + listOf( + ShowcaseDo("DEMO-DO-4F-P", "DEMO-SUP2", "DEMO-SHP2", "pending", 0), + ShowcaseDo("DEMO-DO-4F-R", "DEMO-SUP2", "DEMO-SHP2", "receiving", 0), + ShowcaseDo("DEMO-DO-4F-C", "DEMO-SUP2", "DEMO-SHP2", "completed", 0), + ShowcaseDo("DEMO-DO-X-P", "DEMO-SUP", "DEMO-SHP-X", "pending", 0), + ShowcaseDo("DEMO-DO-X-R", "DEMO-SUP", "DEMO-SHP-X", "receiving", 0), + ShowcaseDo("DEMO-DO-X-C", "DEMO-SUP", "DEMO-SHP-X", "completed", 0), + ShowcaseDo("DEMO-DO-EX-P", "DEMO-SUP", "DEMO-SHP", "pending", 1), + ShowcaseDo("DEMO-DO-EX-R", "DEMO-SUP", "DEMO-SHP", "receiving", 1), + ShowcaseDo("DEMO-DO-EX-C", "DEMO-SUP", "DEMO-SHP", "completed", 1), + ).forEach { insertShowcaseDeliveryOrder(it) } + jdbcTemplate.update( + """ + UPDATE do_replenishment + SET deliveryDate = CURDATE() + WHERE code = 'DEMO-REPL-001' + """.trimIndent(), + ) + listOf("pending" to "DEMO-REPL-P", "processing" to "DEMO-REPL-R").forEach { (status, code) -> + val exists = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM do_replenishment WHERE code = ?", + Int::class.java, + code, + ) ?: 0 + if (exists > 0) return@forEach + jdbcTemplate.update( + """ + INSERT INTO do_replenishment ( + code, deliveryDate, sourceDoId, sourceDoCode, sourceDoLineId, sourceM18DataLogId, sourceM18Id, + itemId, itemNo, itemName, replenishQty, uomId, shopId, shopCode, shopName, + status, reason + ) + SELECT ?, CURDATE(), d.id, d.code, l.id, d.m18DataLogId, 910601, + l.itemId, l.itemNo, i.name, 2, l.uomId, d.shopId, s.code, s.name, + ?, '示範補貨' + FROM delivery_order d + JOIN delivery_order_line l ON l.deliveryOrderId = d.id + JOIN items i ON i.id = l.itemId + JOIN shop s ON s.id = d.shopId + WHERE d.code = 'DEMO-DO-003' + LIMIT 1 + """.trimIndent(), + code, + status, + ) + } + jdbcTemplate.update( + """ + INSERT INTO settings (name, value, category, type) + SELECT ?, 'loaded', 'DEMO', 'marker' + WHERE NOT EXISTS (SELECT 1 FROM settings WHERE name = ?) + """.trimIndent(), + SHOWCASE_MARKER, + SHOWCASE_MARKER, + ) + log.info("Demo delivery showcase loaded. Each 送貨訂單 tab has a row dated today.") + } + + private fun insertShowcaseDeliveryOrder(order: ShowcaseDo) { + val exists = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM delivery_order WHERE code = ?", + Int::class.java, + order.code, + ) ?: 0 + if (exists > 0) return + val lineStatus = when (order.status) { + "receiving" -> "picking" + else -> order.status + } + jdbcTemplate.update( + """ + INSERT INTO delivery_order ( + code, shopId, supplierId, currencyId, orderDate, estimatedArrivalDate, completeDate, + status, m18DataLogId, m18BeId, isExtra + ) + SELECT ?, shop.id, sup.id, src.currencyId, CURDATE(), CURDATE(), + CASE WHEN ? = 'completed' THEN CURDATE() ELSE NULL END, + ?, src.m18DataLogId, src.m18BeId, ? + FROM shop shop + JOIN shop sup ON sup.code = ? AND sup.deleted = 0 + JOIN ( + SELECT currencyId, m18DataLogId, m18BeId + FROM delivery_order + WHERE code = 'DEMO-DO-001' + LIMIT 1 + ) src + WHERE shop.code = ? AND shop.deleted = 0 + """.trimIndent(), + order.code, + order.status, + order.status, + order.extra, + order.supplierCode, + order.shopCode, + ) + jdbcTemplate.update( + """ + INSERT INTO delivery_order_line ( + deliveryOrderId, itemId, itemNo, uomId, uomIdM18, qty, qtyM18, status, m18DataLogId + ) + SELECT d.id, src.itemId, src.itemNo, src.uomId, src.uomIdM18, 4, 4, ?, src.m18DataLogId + FROM delivery_order d + JOIN delivery_order_line src + ON src.deliveryOrderId = (SELECT id FROM delivery_order WHERE code = 'DEMO-DO-001' LIMIT 1) + WHERE d.code = ? + AND NOT EXISTS (SELECT 1 FROM delivery_order_line l WHERE l.deliveryOrderId = d.id) + """.trimIndent(), + lineStatus, + order.code, + ) + } + + private data class ShowcaseDo( + val code: String, + val supplierCode: String, + val shopCode: String, + val status: String, + val extra: Int, + ) + @Synchronized fun alignDashboardTrucksToToday() { + demoDateKeeper.refresh() val anchored = jdbcTemplate.query( """ SELECT requiredDeliveryDate @@ -584,10 +766,10 @@ class DemoSandboxSeeder( BigDecimal("30"), "DEMO-LOT-LF1", today.plusDays(5), now.minusDays(2), ) - val doPending = deliveryOrder("DEMO-DO-001", customer, currency, dataLog, DeliveryOrderStatus.PENDING, now, null) + val doPending = deliveryOrder("DEMO-DO-001", supplier, customer, currency, dataLog, DeliveryOrderStatus.PENDING, now, null) deliveryLine(doPending, bun, pcs, dataLog, BigDecimal("12"), DeliveryOrderLineStatus.PENDING) - val doPicking = deliveryOrder("DEMO-DO-002", customer, currency, dataLog, DeliveryOrderStatus.RECEIVING, now, null) + val doPicking = deliveryOrder("DEMO-DO-002", supplier, customer, currency, dataLog, DeliveryOrderStatus.RECEIVING, now, null) deliveryLine(doPicking, loaf, pcs, dataLog, BigDecimal("4"), DeliveryOrderLineStatus.PICKING) val doPick = pickOrderRepository.save(PickOrder().apply { code = "DEMO-PK-DO2" @@ -600,7 +782,7 @@ class DemoSandboxSeeder( }) pickLine(doPick, loaf, pcs, BigDecimal("4"), PickOrderLineStatus.PENDING) - val doDone = deliveryOrder("DEMO-DO-003", customer, currency, dataLog, DeliveryOrderStatus.COMPLETED, now.minusDays(2), now.minusDays(1)) + val doDone = deliveryOrder("DEMO-DO-003", supplier, customer, currency, dataLog, DeliveryOrderStatus.COMPLETED, now.minusDays(2), now.minusDays(1)) deliveryLine(doDone, loaf, pcs, dataLog, BigDecimal("6"), DeliveryOrderLineStatus.COMPLETED) val donePick = pickOrderRepository.save(PickOrder().apply { code = "DEMO-PK-DO3" @@ -871,6 +1053,7 @@ class DemoSandboxSeeder( private fun deliveryOrder( code: String, + supplier: Shop, shop: Shop, currency: Currency, dataLog: M18DataLog, @@ -879,6 +1062,7 @@ class DemoSandboxSeeder( completeDate: LocalDateTime?, ) = deliveryOrderRepository.save(DeliveryOrder().apply { this.code = code + this.supplier = supplier this.shop = shop this.currency = currency this.orderDate = orderDate @@ -932,6 +1116,7 @@ class DemoSandboxSeeder( const val DEMO_DATABASE = "fpsms_demo" const val DEMO_USERNAME = "demo" const val DEMO_PASSWORD = "Demo2026!" + const val SHOWCASE_MARKER = "DEMO.do.showcase.v1" val DEMO_AUTHORITY_CODES = listOf("ADMIN", "STOCK", "PROD", "PACK", "REPORT_MGMT") } } diff --git a/src/main/java/com/ffii/fpsms/modules/dashboard/service/GoodsReceiptStatusService.kt b/src/main/java/com/ffii/fpsms/modules/dashboard/service/GoodsReceiptStatusService.kt index 82376fa9..23e390c0 100644 --- a/src/main/java/com/ffii/fpsms/modules/dashboard/service/GoodsReceiptStatusService.kt +++ b/src/main/java/com/ffii/fpsms/modules/dashboard/service/GoodsReceiptStatusService.kt @@ -80,12 +80,13 @@ class GoodsReceiptStatusService( agg.expectedNoOfDelivery = 1 } - // Receiving / IQC / Put-away (by stock-in receipt date) + // Receiving / IQC / Put-away (by stock-in receipt date). + // Job-order and other receipts have no purchase order; they are not supplier goods receipts. stockInLines.forEach { sil -> - val po = sil.purchaseOrder - val poId = po?.id - val poCode = po?.code - val supplier = po?.supplier + val po = sil.purchaseOrder ?: return@forEach + val poId = po.id + val poCode = po.code + val supplier = po.supplier val supplierId = supplier?.id val supplierCode = supplier?.code val supplierName = supplier?.name ?: "N/A" diff --git a/src/main/resources/application-db-demo.yml b/src/main/resources/application-db-demo.yml index a04761c1..b25a247d 100644 --- a/src/main/resources/application-db-demo.yml +++ b/src/main/resources/application-db-demo.yml @@ -8,6 +8,9 @@ spring: # Demo sandbox only. Do not point this profile at fpsmsdb. fpsms: demo-mode: true + # Slide DEMO- expiry and delivery dates forward so sample rows stay on today's screens. + demo: + date-refresh-cron: "0 10 0 * * *" monitoring: enabled: false diff --git a/src/test/kotlin/com/ffii/fpsms/modules/common/demo/DemoDateShiftTest.kt b/src/test/kotlin/com/ffii/fpsms/modules/common/demo/DemoDateShiftTest.kt new file mode 100644 index 00000000..ce3eee8b --- /dev/null +++ b/src/test/kotlin/com/ffii/fpsms/modules/common/demo/DemoDateShiftTest.kt @@ -0,0 +1,33 @@ +package com.ffii.fpsms.modules.common.demo + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import java.time.LocalDate + +class DemoDateShiftTest { + private val today = LocalDate.of(2026, 10, 6) + + @Test + fun same_day_does_not_shift() { + assertEquals(DemoDateShift.None, demoDateShiftDays(today, today)) + } + + @Test + fun future_anchor_does_not_shift() { + assertEquals(DemoDateShift.None, demoDateShiftDays(today.plusDays(2), today)) + } + + @Test + fun moves_forward_by_whole_days_since_the_anchor() { + val shift = demoDateShiftDays(today.minusDays(12), today) + assertEquals(DemoDateShift.Forward(12), shift) + } + + @Test + fun refuses_a_gap_large_enough_to_be_a_bad_anchor() { + val shift = demoDateShiftDays(today.minusDays(DEMO_DATE_SHIFT_MAX_DAYS + 1), today) + assertTrue(shift is DemoDateShift.TooLarge) + assertEquals(DEMO_DATE_SHIFT_MAX_DAYS + 1, (shift as DemoDateShift.TooLarge).days) + } +}