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.debrand
| @@ -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).` | |||
| @@ -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<DemoSandboxSeeder>, | |||
| ) : 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() | |||
| } | |||
| @@ -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") | |||
| } | |||
| } | |||
| @@ -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" | |||
| @@ -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 | |||
| @@ -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) | |||
| } | |||
| } | |||