package org.vibeerp.demo import org.slf4j.LoggerFactory import org.springframework.boot.CommandLineRunner import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional import org.vibeerp.pbc.catalog.application.CreateItemCommand import org.vibeerp.pbc.catalog.application.ItemService import org.vibeerp.pbc.catalog.domain.ItemType import org.vibeerp.pbc.inventory.application.CreateLocationCommand import org.vibeerp.pbc.inventory.application.LocationService import org.vibeerp.pbc.inventory.application.StockBalanceService import org.vibeerp.pbc.inventory.domain.LocationType import org.vibeerp.pbc.orders.purchase.application.CreatePurchaseOrderCommand import org.vibeerp.pbc.orders.purchase.application.PurchaseOrderLineCommand import org.vibeerp.pbc.orders.purchase.application.PurchaseOrderService import org.vibeerp.pbc.orders.sales.application.CreateSalesOrderCommand import org.vibeerp.pbc.orders.sales.application.SalesOrderLineCommand import org.vibeerp.pbc.orders.sales.application.SalesOrderService import org.vibeerp.pbc.partners.application.CreatePartnerCommand import org.vibeerp.pbc.partners.application.PartnerService import org.vibeerp.pbc.partners.domain.PartnerType import org.vibeerp.pbc.production.application.CreateWorkOrderCommand import org.vibeerp.pbc.production.application.WorkOrderInputCommand import org.vibeerp.pbc.production.application.WorkOrderOperationCommand import org.vibeerp.pbc.production.application.WorkOrderService import org.vibeerp.platform.persistence.security.PrincipalContext import java.math.BigDecimal import java.time.LocalDate /** * One-shot demo data seeder matching the EBC-PP-001 work-order * management process from the reference printing company (昆明五彩印务). * * Gated behind `vibeerp.demo.seed=true` (dev profile only). * Idempotent via sentinel item check. * * The seeded data demonstrates the core flow from the doc: * A-010 Sales order confirmed (SO already in DRAFT, ready to confirm) * B-010 System auto-generates work orders from confirmed SO lines * B-020 Work order with BOM + routing (pre-seeded WO-PRINT-0001) * C-040 Material requisition (BOM inputs consumed on WO complete) * C-050 Material picking from warehouse (inventory ledger writes) * E-010 Shop floor tracking (shop-floor dashboard polls IN_PROGRESS WOs) */ @Component @ConditionalOnProperty(prefix = "vibeerp.demo", name = ["seed"], havingValue = "true") class DemoSeedRunner( private val itemService: ItemService, private val locationService: LocationService, private val stockBalanceService: StockBalanceService, private val partnerService: PartnerService, private val salesOrderService: SalesOrderService, private val purchaseOrderService: PurchaseOrderService, private val workOrderService: WorkOrderService, ) : CommandLineRunner { private val log = LoggerFactory.getLogger(DemoSeedRunner::class.java) @Transactional override fun run(vararg args: String?) { if (itemService.findByCode(SENTINEL) != null) { log.info("Demo seed: already present ({}); skipping", SENTINEL) return } log.info("Demo seed: populating printing-company dataset...") PrincipalContext.runAs("__demo_seed__") { seedItems() seedLocations() seedPartners() seedStock() seedWorkOrder() seedSalesOrders() seedPurchaseOrder() } log.info("Demo seed: done") } // ─── Catalog items (printing industry) ─────────────────────────── private fun seedItems() { // Raw materials item(SENTINEL, "80g A4 white card stock", ItemType.GOOD, "sheet") item("INK-4C", "4-color offset ink set (CMYK)", ItemType.GOOD, "kg") item("PLATE-CTP", "CTP printing plate", ItemType.GOOD, "ea") item("COVER-MATT", "Matt lamination film", ItemType.GOOD, "m2") // Finished goods item("BIZ-CARD-250", "Business cards, 250gsm coated, 100/box", ItemType.GOOD, "pack") item("BROCHURE-A5", "A5 tri-fold brochure, full color", ItemType.GOOD, "ea") item("POSTER-A3", "A3 promotional poster, glossy", ItemType.GOOD, "ea") } private fun item(code: String, name: String, type: ItemType, uom: String) { itemService.create(CreateItemCommand(code = code, name = name, description = null, itemType = type, baseUomCode = uom)) } // ─── Locations ─────────────────────────────────────────────────── private fun seedLocations() { location("WH-RAW", "Raw materials warehouse") location("WH-FG", "Finished goods warehouse") } private fun location(code: String, name: String) { locationService.create(CreateLocationCommand(code = code, name = name, type = LocationType.WAREHOUSE)) } // ─── Partners ──────────────────────────────────────────────────── private fun seedPartners() { partner("CUST-WCAD", "Wucai Advertising Co.", PartnerType.CUSTOMER, "info@wucai-ad.example") partner("CUST-GLOBE", "Globe Marketing Ltd.", PartnerType.CUSTOMER, "ops@globe.example") partner("SUPP-HZPAPER", "Huazhong Paper Co.", PartnerType.SUPPLIER, "sales@hzpaper.example") partner("SUPP-INKPRO", "InkPro Industries", PartnerType.SUPPLIER, "orders@inkpro.example") } private fun partner(code: String, name: String, type: PartnerType, email: String) { partnerService.create(CreatePartnerCommand(code = code, name = name, type = type, email = email)) } // ─── Opening stock ─────────────────────────────────────────────── private fun seedStock() { val raw = locationService.findByCode("WH-RAW")!! val fg = locationService.findByCode("WH-FG")!! stockBalanceService.adjust(SENTINEL, raw.id, BigDecimal("5000")) stockBalanceService.adjust("INK-4C", raw.id, BigDecimal("80")) stockBalanceService.adjust("PLATE-CTP", raw.id, BigDecimal("100")) stockBalanceService.adjust("COVER-MATT", raw.id, BigDecimal("500")) stockBalanceService.adjust("BIZ-CARD-250", fg.id, BigDecimal("200")) stockBalanceService.adjust("BROCHURE-A5", fg.id, BigDecimal("150")) stockBalanceService.adjust("POSTER-A3", fg.id, BigDecimal("50")) } // ─── Pre-seeded work order with BOM + routing ──────────────────── // // Matches the EBC-PP-001 flow: a production work order for // business cards with 3 BOM inputs and a 3-step routing // (CTP plate-making → offset printing → post-press finishing). // Left in DRAFT so the demo operator can start it, walk the // operations on the shop-floor dashboard, and complete it. private fun seedWorkOrder() { workOrderService.create( CreateWorkOrderCommand( code = "WO-PRINT-0001", outputItemCode = "BIZ-CARD-250", outputQuantity = BigDecimal("50"), dueDate = LocalDate.now().plusDays(3), inputs = listOf( WorkOrderInputCommand(lineNo = 1, itemCode = SENTINEL, quantityPerUnit = BigDecimal("10"), sourceLocationCode = "WH-RAW"), WorkOrderInputCommand(lineNo = 2, itemCode = "INK-4C", quantityPerUnit = BigDecimal("0.2"), sourceLocationCode = "WH-RAW"), WorkOrderInputCommand(lineNo = 3, itemCode = "PLATE-CTP", quantityPerUnit = BigDecimal("1"), sourceLocationCode = "WH-RAW"), ), operations = listOf( WorkOrderOperationCommand(lineNo = 1, operationCode = "CTP", workCenter = "CTP-ROOM-01", standardMinutes = BigDecimal("30")), WorkOrderOperationCommand(lineNo = 2, operationCode = "PRINT", workCenter = "PRESS-A", standardMinutes = BigDecimal("45")), WorkOrderOperationCommand(lineNo = 3, operationCode = "FINISH", workCenter = "BIND-01", standardMinutes = BigDecimal("20")), ), ), ) } // ─── Sales orders (DRAFT — confirm to auto-spawn work orders) ─── // // Two orders for different customers. Confirming either one // triggers SalesOrderConfirmedSubscriber in pbc-production, // which auto-creates one DRAFT work order per SO line (the // B-010 step from EBC-PP-001). The demo operator watches the // work order count jump on the dashboard after clicking Confirm. private fun seedSalesOrders() { salesOrderService.create( CreateSalesOrderCommand( code = "SO-2026-0001", partnerCode = "CUST-WCAD", orderDate = LocalDate.now(), currencyCode = "USD", lines = listOf( SalesOrderLineCommand(lineNo = 1, itemCode = "BIZ-CARD-250", quantity = BigDecimal("100"), unitPrice = BigDecimal("8.50"), currencyCode = "USD"), SalesOrderLineCommand(lineNo = 2, itemCode = "BROCHURE-A5", quantity = BigDecimal("500"), unitPrice = BigDecimal("2.20"), currencyCode = "USD"), ), ), ) salesOrderService.create( CreateSalesOrderCommand( code = "SO-2026-0002", partnerCode = "CUST-GLOBE", orderDate = LocalDate.now(), currencyCode = "USD", lines = listOf( SalesOrderLineCommand(lineNo = 1, itemCode = "POSTER-A3", quantity = BigDecimal("200"), unitPrice = BigDecimal("3.80"), currencyCode = "USD"), ), ), ) } // ─── Purchase order (DRAFT — confirm + receive to restock) ─────── private fun seedPurchaseOrder() { purchaseOrderService.create( CreatePurchaseOrderCommand( code = "PO-2026-0001", partnerCode = "SUPP-HZPAPER", orderDate = LocalDate.now(), expectedDate = LocalDate.now().plusDays(5), currencyCode = "USD", lines = listOf( PurchaseOrderLineCommand(lineNo = 1, itemCode = SENTINEL, quantity = BigDecimal("10000"), unitPrice = BigDecimal("0.03"), currencyCode = "USD"), PurchaseOrderLineCommand(lineNo = 2, itemCode = "INK-4C", quantity = BigDecimal("50"), unitPrice = BigDecimal("45.00"), currencyCode = "USD"), ), ), ) } companion object { const val SENTINEL: String = "PAPER-80G-A4" } }