DemoSeedRunner.kt
10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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"
}
}