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. | 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: | In the log, expect: | ||||
| `Demo database login cannot read live schema fpsmsdb (MySQL error 1142).` | `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 deliveryOrderLineRepository: DeliveryOrderLineRepository, | ||||
| private val pickOrderRepository: PickOrderRepository, | private val pickOrderRepository: PickOrderRepository, | ||||
| private val pickOrderLineRepository: PickOrderLineRepository, | private val pickOrderLineRepository: PickOrderLineRepository, | ||||
| private val demoDateKeeper: DemoDateKeeper, | |||||
| ) : ApplicationRunner { | ) : ApplicationRunner { | ||||
| private val log = LoggerFactory.getLogger(DemoSandboxSeeder::class.java) | 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. | * 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. | * 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 | @Synchronized | ||||
| fun alignDashboardTrucksToToday() { | fun alignDashboardTrucksToToday() { | ||||
| demoDateKeeper.refresh() | |||||
| val anchored = jdbcTemplate.query( | val anchored = jdbcTemplate.query( | ||||
| """ | """ | ||||
| SELECT requiredDeliveryDate | SELECT requiredDeliveryDate | ||||
| @@ -584,10 +766,10 @@ class DemoSandboxSeeder( | |||||
| BigDecimal("30"), "DEMO-LOT-LF1", today.plusDays(5), now.minusDays(2), | 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) | 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) | deliveryLine(doPicking, loaf, pcs, dataLog, BigDecimal("4"), DeliveryOrderLineStatus.PICKING) | ||||
| val doPick = pickOrderRepository.save(PickOrder().apply { | val doPick = pickOrderRepository.save(PickOrder().apply { | ||||
| code = "DEMO-PK-DO2" | code = "DEMO-PK-DO2" | ||||
| @@ -600,7 +782,7 @@ class DemoSandboxSeeder( | |||||
| }) | }) | ||||
| pickLine(doPick, loaf, pcs, BigDecimal("4"), PickOrderLineStatus.PENDING) | 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) | deliveryLine(doDone, loaf, pcs, dataLog, BigDecimal("6"), DeliveryOrderLineStatus.COMPLETED) | ||||
| val donePick = pickOrderRepository.save(PickOrder().apply { | val donePick = pickOrderRepository.save(PickOrder().apply { | ||||
| code = "DEMO-PK-DO3" | code = "DEMO-PK-DO3" | ||||
| @@ -871,6 +1053,7 @@ class DemoSandboxSeeder( | |||||
| private fun deliveryOrder( | private fun deliveryOrder( | ||||
| code: String, | code: String, | ||||
| supplier: Shop, | |||||
| shop: Shop, | shop: Shop, | ||||
| currency: Currency, | currency: Currency, | ||||
| dataLog: M18DataLog, | dataLog: M18DataLog, | ||||
| @@ -879,6 +1062,7 @@ class DemoSandboxSeeder( | |||||
| completeDate: LocalDateTime?, | completeDate: LocalDateTime?, | ||||
| ) = deliveryOrderRepository.save(DeliveryOrder().apply { | ) = deliveryOrderRepository.save(DeliveryOrder().apply { | ||||
| this.code = code | this.code = code | ||||
| this.supplier = supplier | |||||
| this.shop = shop | this.shop = shop | ||||
| this.currency = currency | this.currency = currency | ||||
| this.orderDate = orderDate | this.orderDate = orderDate | ||||
| @@ -932,6 +1116,7 @@ class DemoSandboxSeeder( | |||||
| const val DEMO_DATABASE = "fpsms_demo" | const val DEMO_DATABASE = "fpsms_demo" | ||||
| const val DEMO_USERNAME = "demo" | const val DEMO_USERNAME = "demo" | ||||
| const val DEMO_PASSWORD = "Demo2026!" | const val DEMO_PASSWORD = "Demo2026!" | ||||
| const val SHOWCASE_MARKER = "DEMO.do.showcase.v1" | |||||
| val DEMO_AUTHORITY_CODES = listOf("ADMIN", "STOCK", "PROD", "PACK", "REPORT_MGMT") | val DEMO_AUTHORITY_CODES = listOf("ADMIN", "STOCK", "PROD", "PACK", "REPORT_MGMT") | ||||
| } | } | ||||
| } | } | ||||
| @@ -80,12 +80,13 @@ class GoodsReceiptStatusService( | |||||
| agg.expectedNoOfDelivery = 1 | 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 -> | 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 supplierId = supplier?.id | ||||
| val supplierCode = supplier?.code | val supplierCode = supplier?.code | ||||
| val supplierName = supplier?.name ?: "N/A" | val supplierName = supplier?.name ?: "N/A" | ||||
| @@ -8,6 +8,9 @@ spring: | |||||
| # Demo sandbox only. Do not point this profile at fpsmsdb. | # Demo sandbox only. Do not point this profile at fpsmsdb. | ||||
| fpsms: | fpsms: | ||||
| demo-mode: true | 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: | monitoring: | ||||
| enabled: false | 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) | |||||
| } | |||||
| } | |||||