From 7f1ea8ba72f0eaa09d42db8d966490d9865196c6 Mon Sep 17 00:00:00 2001 From: Victor Broman Date: Fri, 6 Feb 2026 19:21:32 +0100 Subject: [PATCH] Adds seed script --- cmd/seed/main.go | 224 +++++++++++++++++++++++++++++++++++++++++++++++ erp.db-wal | Bin 453232 -> 0 bytes 2 files changed, 224 insertions(+) create mode 100644 cmd/seed/main.go delete mode 100644 erp.db-wal diff --git a/cmd/seed/main.go b/cmd/seed/main.go new file mode 100644 index 0000000..7b40d55 --- /dev/null +++ b/cmd/seed/main.go @@ -0,0 +1,224 @@ +package main + +import ( + "fmt" + "log" + "math/rand" + "os" + "strconv" + "time" + + "erp_system/internal/database" + + _ "modernc.org/sqlite" +) + +func main() { + dbPath := "erp.db" + if len(os.Args) > 1 { + dbPath = os.Args[1] + } + + log.Printf("Connecting to database at %s...", dbPath) + // We can use the internal database package to initialize (runs migrations) + db, err := database.Initialize(dbPath) + if err != nil { + log.Fatalf("Failed to initialize database: %v", err) + } + defer db.Close() + + // Configuration for generation + const ( + numCustomers = 500 + maxOrdersPerCust = 15 + maxLinesPerOrder = 8 + numJournalEntries = 100 + ) + + log.Println("Starting data generation...") + start := time.Now() + + tx, err := db.Begin() + if err != nil { + log.Fatalf("Failed to begin transaction: %v", err) + } + + // 1. Customers + customerIDs := make([]int64, 0, numCustomers) + for i := 0; i < numCustomers; i++ { + name := fmt.Sprintf("Customer %s %s", randomString(5), randomString(7)) + email := fmt.Sprintf("%s.%s@example.com", randomString(4), randomString(5)) + phone := fmt.Sprintf("555-%04d", rand.Intn(10000)) + address := fmt.Sprintf("%d %s St, City %s", rand.Intn(999), randomString(6), randomString(4)) + + res, err := tx.Exec("INSERT INTO customers (name, email, phone, address) VALUES (?, ?, ?, ?)", name, email, phone, address) + if err != nil { + tx.Rollback() + log.Fatalf("Failed to insert customer: %v", err) + } + id, _ := res.LastInsertId() + customerIDs = append(customerIDs, id) + } + log.Printf("Generated %d customers", numCustomers) + + // 2. Orders and Lines and Invoices + var orderCount, lineCount, invoiceCount int + for _, custID := range customerIDs { + numOrders := rand.Intn(maxOrdersPerCust) + for j := 0; j < numOrders; j++ { + // Create Order + status := "draft" + r := rand.Float32() + if r > 0.7 { + status = "fulfilled" + } else if r > 0.4 { + status = "confirmed" + } else if r > 0.3 { + status = "cancelled" + } + + // Random date in last year + orderDate := time.Now().AddDate(0, 0, -rand.Intn(365)) + + res, err := tx.Exec("INSERT INTO orders (customer_id, status, order_date, notes) VALUES (?, ?, ?, ?)", + custID, status, orderDate.Format("2006-01-02"), "Generated order") + if err != nil { + tx.Rollback() + log.Fatalf("Failed to insert order: %v", err) + } + orderID, _ := res.LastInsertId() + orderCount++ + + // Create Order Lines + numLines := rand.Intn(maxLinesPerOrder) + 1 + var totalAmount float64 + for k := 0; k < numLines; k++ { + qty := float64(rand.Intn(10) + 1) + price := float64(rand.Intn(100) + 10) + desc := fmt.Sprintf("Product %s-%d", randomString(3), rand.Intn(100)) + lineTotal := qty * price + totalAmount += lineTotal + + _, err := tx.Exec("INSERT INTO order_lines (order_id, description, quantity, unit_price, line_total) VALUES (?, ?, ?, ?, ?)", + orderID, desc, qty, price, lineTotal) + if err != nil { + tx.Rollback() + log.Fatalf("Failed to insert order line: %v", err) + } + lineCount++ + } + + // Update Order Total + _, err = tx.Exec("UPDATE orders SET total_amount = ? WHERE id = ?", totalAmount, orderID) + if err != nil { + tx.Rollback() + log.Fatalf("Failed to update order total: %v", err) + } + + // Create Invoice if order is confirmed or fulfilled + if status == "confirmed" || status == "fulfilled" { + invoiceStatus := "pending" + if rand.Float32() > 0.5 { + invoiceStatus = "paid" + } + + invNum := fmt.Sprintf("INV-%d-%d", orderID, rand.Intn(10000)) + dueDate := orderDate.AddDate(0, 1, 0) // Due in 30 days + var paidDate interface{} = nil + if invoiceStatus == "paid" { + pd := dueDate.AddDate(0, 0, -rand.Intn(10)) + paidDate = pd.Format("2006-01-02") + } + + _, err := tx.Exec("INSERT INTO invoices (order_id, customer_id, invoice_number, status, amount, due_date, paid_date) VALUES (?, ?, ?, ?, ?, ?, ?)", + orderID, custID, invNum, invoiceStatus, totalAmount, dueDate.Format("2006-01-02"), paidDate) + if err != nil { + tx.Rollback() + log.Fatalf("Failed to insert invoice: %v", err) + } + invoiceCount++ + } + } + } + log.Printf("Generated %d orders, %d lines, %d invoices", orderCount, lineCount, invoiceCount) + + // 3. Journal Entries + // Fetch account IDs first + rows, err := tx.Query("SELECT id, code, type FROM gl_accounts") + if err != nil { + tx.Rollback() + log.Fatalf("Failed to fetch accounts: %v", err) + } + defer rows.Close() + + type Account struct { + ID int64 + Code string + Type string + } + var accounts []Account + for rows.Next() { + var a Account + if err := rows.Scan(&a.ID, &a.Code, &a.Type); err != nil { + log.Fatalf("Failed to scan account: %v", err) + } + accounts = append(accounts, a) + } + + if len(accounts) >= 2 { + for i := 0; i < numJournalEntries; i++ { + date := time.Now().AddDate(0, 0, -rand.Intn(60)).Format("2006-01-02") + desc := fmt.Sprintf("Journal Entry %d", i+1) + + res, err := tx.Exec("INSERT INTO journal_entries (entry_date, description, reference) VALUES (?, ?, ?)", + date, desc, "GEN-"+strconv.Itoa(i)) + if err != nil { + tx.Rollback() + log.Fatalf("Failed to insert journal entry: %v", err) + } + jeID, _ := res.LastInsertId() + + // Simple balanced entry: 2 lines + amount := float64(rand.Intn(5000) + 100) + + // Pick 2 random accounts + idx1 := rand.Intn(len(accounts)) + idx2 := rand.Intn(len(accounts)) + for idx1 == idx2 { + idx2 = rand.Intn(len(accounts)) + } + + // Debit line + _, err = tx.Exec("INSERT INTO journal_lines (journal_entry_id, account_id, debit, credit) VALUES (?, ?, ?, ?)", + jeID, accounts[idx1].ID, amount, 0) + if err != nil { + tx.Rollback() + log.Fatalf("Failed to insert journal line 1: %v", err) + } + + // Credit line + _, err = tx.Exec("INSERT INTO journal_lines (journal_entry_id, account_id, debit, credit) VALUES (?, ?, ?, ?)", + jeID, accounts[idx2].ID, 0, amount) + if err != nil { + tx.Rollback() + log.Fatalf("Failed to insert journal line 2: %v", err) + } + } + } + log.Printf("Generated %d journal entries", numJournalEntries) + + if err := tx.Commit(); err != nil { + log.Fatalf("Failed to commit transaction: %v", err) + } + + log.Printf("Seeding completed in %v", time.Since(start)) +} + +func randomString(n int) string { + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]rune, n) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} diff --git a/erp.db-wal b/erp.db-wal deleted file mode 100644 index 6f8332a09317da9225344e2d918d9bf8364df093..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 453232 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 vcd#IU00IagfB*srAb