Performance: - O2M formatO2MFields: N+1 → single batched query per O2M field (collect parent IDs, WHERE inverse IN (...), group by parent) Code dedup: - domain.go compileSimpleCondition: 80 lines → 12 lines by delegating to compileQualifiedCondition (eliminates duplicate operator handling) - db.go SeedWithSetup: 170-line monolith → 5 focused sub-functions (seedCurrencyAndCountry, seedCompanyAndAdmin, seedJournalsAndSequences, seedChartOfAccounts, resetSequences) Quality: - Magic numbers (80, 200, 8) replaced with named constants - Net -34 lines Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1619 lines
58 KiB
Go
1619 lines
58 KiB
Go
// Package service provides database and model services.
|
|
// Mirrors: odoo/service/db.py
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
l10n_de "odoo-go/addons/l10n_de"
|
|
"odoo-go/pkg/orm"
|
|
"odoo-go/pkg/tools"
|
|
)
|
|
|
|
// InitDatabase creates all tables for registered models.
|
|
// Mirrors: odoo/modules/loading.py load_module_graph() → model._setup_base()
|
|
func InitDatabase(ctx context.Context, pool *pgxpool.Pool) error {
|
|
tx, err := pool.Begin(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("db: begin: %w", err)
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
models := orm.Registry.Models()
|
|
|
|
// Phase 1: Create tables
|
|
for name, m := range models {
|
|
if m.IsAbstract() {
|
|
continue
|
|
}
|
|
sql := m.CreateTableSQL()
|
|
if sql == "" {
|
|
continue
|
|
}
|
|
log.Printf("db: creating table for %s", name)
|
|
if _, err := tx.Exec(ctx, sql); err != nil {
|
|
return fmt.Errorf("db: create table %s: %w", name, err)
|
|
}
|
|
}
|
|
|
|
// Phase 2: Add foreign keys (after all tables exist, each in savepoint)
|
|
for name, m := range models {
|
|
if m.IsAbstract() {
|
|
continue
|
|
}
|
|
for _, sql := range m.ForeignKeySQL() {
|
|
sp, spErr := tx.Begin(ctx)
|
|
if spErr != nil {
|
|
continue
|
|
}
|
|
wrappedSQL := fmt.Sprintf(
|
|
`DO $$ BEGIN %s; EXCEPTION WHEN duplicate_object THEN NULL; END $$`,
|
|
sql,
|
|
)
|
|
if _, err := sp.Exec(ctx, wrappedSQL); err != nil {
|
|
log.Printf("db: warning: FK for %s: %v", name, err)
|
|
sp.Rollback(ctx)
|
|
} else {
|
|
log.Printf("db: adding FK for %s", name)
|
|
sp.Commit(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Phase 3: Create indexes (each in savepoint)
|
|
for _, m := range models {
|
|
if m.IsAbstract() {
|
|
continue
|
|
}
|
|
for _, sql := range m.IndexSQL() {
|
|
sp, spErr := tx.Begin(ctx)
|
|
if spErr != nil {
|
|
continue
|
|
}
|
|
if _, err := sp.Exec(ctx, sql); err != nil {
|
|
log.Printf("db: warning: index: %v", err)
|
|
sp.Rollback(ctx)
|
|
} else {
|
|
sp.Commit(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Phase 4: Create Many2many junction tables (each in savepoint to avoid aborting tx)
|
|
for _, m := range models {
|
|
if m.IsAbstract() {
|
|
continue
|
|
}
|
|
for _, sql := range m.Many2manyTableSQL() {
|
|
sp, spErr := tx.Begin(ctx)
|
|
if spErr != nil {
|
|
continue
|
|
}
|
|
if _, err := sp.Exec(ctx, sql); err != nil {
|
|
log.Printf("db: warning: m2m table: %v", err)
|
|
sp.Rollback(ctx)
|
|
} else {
|
|
sp.Commit(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Phase 4b: Add unique constraint on ir_config_parameter.key for ON CONFLICT support.
|
|
// Mirrors: odoo/addons/base/models/ir_config_parameter.py _sql_constraints
|
|
sp, spErr := tx.Begin(ctx)
|
|
if spErr == nil {
|
|
if _, err := sp.Exec(ctx, `CREATE UNIQUE INDEX IF NOT EXISTS ir_config_parameter_key_uniq ON ir_config_parameter (key)`); err != nil {
|
|
sp.Rollback(ctx)
|
|
} else {
|
|
sp.Commit(ctx)
|
|
}
|
|
}
|
|
|
|
// Phase 4c: SQL constraints
|
|
// Mirrors: _sql_constraints in Odoo models
|
|
for name, m := range models {
|
|
if m.IsAbstract() {
|
|
continue
|
|
}
|
|
for _, sc := range m.SQLConstraints {
|
|
constraintName := strings.ReplaceAll(name, ".", "_") + "_" + sc.Name
|
|
query := fmt.Sprintf(
|
|
`DO $$ BEGIN
|
|
ALTER TABLE %q ADD CONSTRAINT %s %s;
|
|
EXCEPTION WHEN duplicate_object THEN NULL;
|
|
END $$`,
|
|
m.Table(), constraintName, sc.Definition,
|
|
)
|
|
sp, spErr := tx.Begin(ctx)
|
|
if spErr != nil {
|
|
continue
|
|
}
|
|
if _, err := sp.Exec(ctx, query); err != nil {
|
|
log.Printf("db: constraint %s: %v", constraintName, err)
|
|
sp.Rollback(ctx)
|
|
} else {
|
|
sp.Commit(ctx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Phase 5: Seed ir_model and ir_model_fields with model metadata.
|
|
// This is critical because ir.rule joins through ir_model to find rules for a model.
|
|
// Mirrors: odoo/modules/loading.py load_module_graph() → _setup_base()
|
|
seedIrModelMetadata(ctx, tx, models)
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return fmt.Errorf("db: commit: %w", err)
|
|
}
|
|
|
|
log.Printf("db: schema initialized for %d models", len(models))
|
|
return nil
|
|
}
|
|
|
|
// seedIrModelMetadata populates ir_model and ir_model_fields for all registered models.
|
|
// Each model gets a row in ir_model; each field gets a row in ir_model_fields.
|
|
// Uses ON CONFLICT DO NOTHING so it's safe to call on every startup.
|
|
func seedIrModelMetadata(ctx context.Context, tx pgx.Tx, models map[string]*orm.Model) {
|
|
// Check if ir_model table exists (it should, but guard against ordering issues)
|
|
var exists bool
|
|
err := tx.QueryRow(ctx,
|
|
`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ir_model')`,
|
|
).Scan(&exists)
|
|
if err != nil || !exists {
|
|
log.Println("db: ir_model table does not exist yet, skipping metadata seed")
|
|
return
|
|
}
|
|
|
|
// Also check ir_model_fields
|
|
var fieldsExists bool
|
|
err = tx.QueryRow(ctx,
|
|
`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ir_model_fields')`,
|
|
).Scan(&fieldsExists)
|
|
if err != nil || !fieldsExists {
|
|
log.Println("db: ir_model_fields table does not exist yet, skipping metadata seed")
|
|
return
|
|
}
|
|
|
|
modelCount := 0
|
|
fieldCount := 0
|
|
|
|
for name, m := range models {
|
|
if m.IsAbstract() {
|
|
continue
|
|
}
|
|
|
|
// Check if this model already exists in ir_model
|
|
var modelID int64
|
|
sp, spErr := tx.Begin(ctx)
|
|
if spErr != nil {
|
|
continue
|
|
}
|
|
err := sp.QueryRow(ctx,
|
|
`SELECT id FROM ir_model WHERE model = $1`, name,
|
|
).Scan(&modelID)
|
|
if err != nil {
|
|
// Model doesn't exist yet — insert it
|
|
sp.Rollback(ctx)
|
|
sp2, spErr2 := tx.Begin(ctx)
|
|
if spErr2 != nil {
|
|
continue
|
|
}
|
|
err = sp2.QueryRow(ctx,
|
|
`INSERT INTO ir_model (model, name, info, state, transient)
|
|
VALUES ($1, $2, $3, 'base', $4)
|
|
RETURNING id`,
|
|
name, m.Description(), m.Description(), m.IsTransient(),
|
|
).Scan(&modelID)
|
|
if err != nil {
|
|
sp2.Rollback(ctx)
|
|
continue
|
|
}
|
|
sp2.Commit(ctx)
|
|
modelCount++
|
|
} else {
|
|
sp.Commit(ctx)
|
|
}
|
|
|
|
if modelID == 0 {
|
|
continue
|
|
}
|
|
|
|
// INSERT into ir_model_fields for each field
|
|
for fieldName, field := range m.Fields() {
|
|
if fieldName == "id" || fieldName == "display_name" {
|
|
continue
|
|
}
|
|
|
|
// Check if this field already exists
|
|
sp, spErr := tx.Begin(ctx)
|
|
if spErr != nil {
|
|
continue
|
|
}
|
|
var fieldExists bool
|
|
err := sp.QueryRow(ctx,
|
|
`SELECT EXISTS(SELECT 1 FROM ir_model_fields WHERE model_id = $1 AND name = $2)`,
|
|
modelID, fieldName,
|
|
).Scan(&fieldExists)
|
|
if err != nil {
|
|
sp.Rollback(ctx)
|
|
continue
|
|
}
|
|
if fieldExists {
|
|
sp.Commit(ctx)
|
|
continue
|
|
}
|
|
sp.Commit(ctx)
|
|
|
|
sp2, spErr2 := tx.Begin(ctx)
|
|
if spErr2 != nil {
|
|
continue
|
|
}
|
|
_, err = sp2.Exec(ctx,
|
|
`INSERT INTO ir_model_fields (model_id, name, field_description, ttype, state, store)
|
|
VALUES ($1, $2, $3, $4, 'base', $5)`,
|
|
modelID, fieldName, field.String, field.Type.String(), field.IsStored(),
|
|
)
|
|
if err != nil {
|
|
sp2.Rollback(ctx)
|
|
} else {
|
|
sp2.Commit(ctx)
|
|
fieldCount++
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Printf("db: seeded ir_model metadata: %d models, %d fields", modelCount, fieldCount)
|
|
}
|
|
|
|
// NeedsSetup checks if the database requires initial setup.
|
|
func NeedsSetup(ctx context.Context, pool *pgxpool.Pool) bool {
|
|
var count int
|
|
err := pool.QueryRow(ctx, `SELECT COUNT(*) FROM res_company`).Scan(&count)
|
|
return err != nil || count == 0
|
|
}
|
|
|
|
// SetupConfig holds parameters for the setup wizard.
|
|
type SetupConfig struct {
|
|
CompanyName string
|
|
Street string
|
|
Zip string
|
|
City string
|
|
CountryCode string
|
|
CountryName string
|
|
PhoneCode string
|
|
Email string
|
|
Phone string
|
|
VAT string
|
|
Chart string // "skr03", "skr04", "none"
|
|
AdminLogin string
|
|
AdminPassword string // Already hashed with bcrypt
|
|
DemoData bool
|
|
}
|
|
|
|
// SeedWithSetup initializes the database with user-provided configuration.
|
|
func SeedWithSetup(ctx context.Context, pool *pgxpool.Pool, cfg SetupConfig) error {
|
|
tx, err := pool.Begin(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("db: begin: %w", err)
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
log.Printf("db: seeding database for %q...", cfg.CompanyName)
|
|
|
|
if err := seedCurrencyAndCountry(ctx, tx, cfg); err != nil {
|
|
return err
|
|
}
|
|
if err := seedCompanyAndAdmin(ctx, tx, cfg); err != nil {
|
|
return err
|
|
}
|
|
if err := seedJournalsAndSequences(ctx, tx); err != nil {
|
|
return err
|
|
}
|
|
seedChartOfAccounts(ctx, tx, cfg)
|
|
seedStockData(ctx, tx)
|
|
seedViews(ctx, tx)
|
|
seedActions(ctx, tx)
|
|
seedMenus(ctx, tx)
|
|
|
|
// Settings record (res.config.settings needs at least one record to display)
|
|
tx.Exec(ctx, `INSERT INTO res_config_settings (id, company_id, show_effect, create_uid, write_uid)
|
|
VALUES (1, 1, true, 1, 1) ON CONFLICT (id) DO NOTHING`)
|
|
|
|
seedSystemParams(ctx, tx)
|
|
seedLanguages(ctx, tx)
|
|
seedTranslations(ctx, tx)
|
|
|
|
if cfg.DemoData {
|
|
seedDemoData(ctx, tx)
|
|
}
|
|
|
|
resetSequences(ctx, tx)
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return fmt.Errorf("db: commit seed: %w", err)
|
|
}
|
|
|
|
log.Printf("db: database seeded successfully for %q", cfg.CompanyName)
|
|
return nil
|
|
}
|
|
|
|
// seedCurrencyAndCountry seeds the default currency (EUR) and country.
|
|
func seedCurrencyAndCountry(ctx context.Context, tx pgx.Tx, cfg SetupConfig) error {
|
|
_, err := tx.Exec(ctx, `
|
|
INSERT INTO res_currency (id, name, symbol, decimal_places, rounding, active, "position")
|
|
VALUES (1, 'EUR', '€', 2, 0.01, true, 'after')
|
|
ON CONFLICT (id) DO NOTHING`)
|
|
if err != nil {
|
|
return fmt.Errorf("db: seed currency: %w", err)
|
|
}
|
|
|
|
_, err = tx.Exec(ctx, `
|
|
INSERT INTO res_country (id, name, code, phone_code)
|
|
VALUES (1, $1, $2, $3)
|
|
ON CONFLICT (id) DO NOTHING`, cfg.CountryName, cfg.CountryCode, cfg.PhoneCode)
|
|
if err != nil {
|
|
return fmt.Errorf("db: seed country: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// seedCompanyAndAdmin seeds the company partner, company, admin partner, and admin user.
|
|
func seedCompanyAndAdmin(ctx context.Context, tx pgx.Tx, cfg SetupConfig) error {
|
|
_, err := tx.Exec(ctx, `
|
|
INSERT INTO res_partner (id, name, is_company, active, type, lang, email, phone, street, zip, city, country_id, vat)
|
|
VALUES (1, $1, true, true, 'contact', 'de_DE', $2, $3, $4, $5, $6, 1, $7)
|
|
ON CONFLICT (id) DO NOTHING`,
|
|
cfg.CompanyName, cfg.Email, cfg.Phone, cfg.Street, cfg.Zip, cfg.City, cfg.VAT)
|
|
if err != nil {
|
|
return fmt.Errorf("db: seed company partner: %w", err)
|
|
}
|
|
|
|
_, err = tx.Exec(ctx, `
|
|
INSERT INTO res_company (id, name, partner_id, currency_id, country_id, active, sequence, street, zip, city, email, phone, vat)
|
|
VALUES (1, $1, 1, 1, 1, true, 10, $2, $3, $4, $5, $6, $7)
|
|
ON CONFLICT (id) DO NOTHING`,
|
|
cfg.CompanyName, cfg.Street, cfg.Zip, cfg.City, cfg.Email, cfg.Phone, cfg.VAT)
|
|
if err != nil {
|
|
return fmt.Errorf("db: seed company: %w", err)
|
|
}
|
|
|
|
adminName := "Administrator"
|
|
_, err = tx.Exec(ctx, `
|
|
INSERT INTO res_partner (id, name, is_company, active, type, email, lang)
|
|
VALUES (2, $1, false, true, 'contact', $2, 'de_DE')
|
|
ON CONFLICT (id) DO NOTHING`, adminName, cfg.AdminLogin)
|
|
if err != nil {
|
|
return fmt.Errorf("db: seed admin partner: %w", err)
|
|
}
|
|
|
|
_, err = tx.Exec(ctx, `
|
|
INSERT INTO res_users (id, login, password, active, partner_id, company_id)
|
|
VALUES (1, $1, $2, true, 2, 1)
|
|
ON CONFLICT (id) DO NOTHING`, cfg.AdminLogin, cfg.AdminPassword)
|
|
if err != nil {
|
|
return fmt.Errorf("db: seed admin user: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// seedJournalsAndSequences seeds accounting journals and IR sequences.
|
|
func seedJournalsAndSequences(ctx context.Context, tx pgx.Tx) error {
|
|
_, err := tx.Exec(ctx, `
|
|
INSERT INTO account_journal (id, name, code, type, company_id, active, sequence) VALUES
|
|
(1, 'Ausgangsrechnungen', 'INV', 'sale', 1, true, 10),
|
|
(2, 'Eingangsrechnungen', 'BILL', 'purchase', 1, true, 20),
|
|
(3, 'Bank', 'BNK1', 'bank', 1, true, 30),
|
|
(4, 'Kasse', 'CSH1', 'cash', 1, true, 40),
|
|
(5, 'Sonstige', 'MISC', 'general', 1, true, 50)
|
|
ON CONFLICT (id) DO NOTHING`)
|
|
if err != nil {
|
|
return fmt.Errorf("db: seed journals: %w", err)
|
|
}
|
|
|
|
_, err = tx.Exec(ctx, `
|
|
INSERT INTO ir_sequence (id, name, code, prefix, padding, number_next, number_increment, active, implementation) VALUES
|
|
(1, 'Buchungssatz', 'account.move', 'MISC/', 4, 1, 1, true, 'standard'),
|
|
(2, 'Ausgangsrechnung', 'account.move.out_invoice', 'RE/%(year)s/', 4, 1, 1, true, 'standard'),
|
|
(3, 'Eingangsrechnung', 'account.move.in_invoice', 'ER/%(year)s/', 4, 1, 1, true, 'standard'),
|
|
(4, 'Angebot', 'sale.order', 'AG', 4, 1, 1, true, 'standard'),
|
|
(5, 'Bestellung', 'purchase.order', 'BE', 4, 1, 1, true, 'standard')
|
|
ON CONFLICT (id) DO NOTHING`)
|
|
if err != nil {
|
|
return fmt.Errorf("db: seed sequences: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// seedChartOfAccounts seeds the chart of accounts and tax definitions if configured.
|
|
func seedChartOfAccounts(ctx context.Context, tx pgx.Tx, cfg SetupConfig) {
|
|
if cfg.Chart != "skr03" && cfg.Chart != "skr04" {
|
|
return
|
|
}
|
|
// Currently only SKR03 is implemented
|
|
for _, acc := range l10n_de.SKR03Accounts {
|
|
tx.Exec(ctx, `
|
|
INSERT INTO account_account (code, name, account_type, company_id, reconcile)
|
|
VALUES ($1, $2, $3, 1, $4) ON CONFLICT DO NOTHING`,
|
|
acc.Code, acc.Name, acc.AccountType, acc.Reconcile)
|
|
}
|
|
log.Printf("db: seeded %d SKR03 accounts", len(l10n_de.SKR03Accounts))
|
|
|
|
for _, tax := range l10n_de.SKR03Taxes {
|
|
tx.Exec(ctx, `
|
|
INSERT INTO account_tax (name, amount, type_tax_use, amount_type, company_id, active, sequence, is_base_affected)
|
|
VALUES ($1, $2, $3, 'percent', 1, true, 1, true) ON CONFLICT DO NOTHING`,
|
|
tax.Name, tax.Amount, tax.TypeUse)
|
|
}
|
|
log.Printf("db: seeded %d German tax definitions", len(l10n_de.SKR03Taxes))
|
|
}
|
|
|
|
// resetSequences resets all auto-increment sequences to their current max values.
|
|
func resetSequences(ctx context.Context, tx pgx.Tx) {
|
|
seqs := []string{
|
|
"res_currency", "res_country", "res_partner", "res_company",
|
|
"res_users", "ir_sequence", "account_journal", "account_account",
|
|
"account_tax", "sale_order", "sale_order_line", "account_move",
|
|
"ir_act_window", "ir_model_data", "ir_ui_menu",
|
|
"stock_location", "stock_picking_type", "stock_warehouse",
|
|
"crm_stage", "crm_lead",
|
|
"ir_config_parameter",
|
|
"ir_translation", "ir_act_report", "res_lang",
|
|
"product_template", "product_product",
|
|
"hr_department", "hr_employee",
|
|
"project_project",
|
|
}
|
|
for _, table := range seqs {
|
|
tx.Exec(ctx, fmt.Sprintf(
|
|
`SELECT setval('%s_id_seq', GREATEST((SELECT COALESCE(MAX(id),0) FROM %q), 1))`,
|
|
table, table))
|
|
}
|
|
}
|
|
|
|
// seedStockData creates stock locations, picking types, and a default warehouse.
|
|
// Mirrors: odoo/addons/stock/data/stock_data.xml
|
|
func seedStockData(ctx context.Context, tx pgx.Tx) {
|
|
log.Println("db: seeding stock reference data...")
|
|
|
|
// Stock locations (hierarchical, mirroring Odoo's stock_data.xml)
|
|
tx.Exec(ctx, `
|
|
INSERT INTO stock_location (id, name, complete_name, usage, active, location_id, company_id) VALUES
|
|
(1, 'Physical Locations', 'Physical Locations', 'view', true, NULL, 1),
|
|
(2, 'Partner Locations', 'Partner Locations', 'view', true, NULL, 1),
|
|
(3, 'Virtual Locations', 'Virtual Locations', 'view', true, NULL, 1),
|
|
(4, 'WH', 'Physical Locations/WH', 'internal', true, 1, 1),
|
|
(5, 'Customers', 'Partner Locations/Customers', 'customer', true, 2, 1),
|
|
(6, 'Vendors', 'Partner Locations/Vendors', 'supplier', true, 2, 1)
|
|
ON CONFLICT (id) DO NOTHING`)
|
|
|
|
// Default warehouse (must come before picking types due to FK)
|
|
tx.Exec(ctx, `
|
|
INSERT INTO stock_warehouse (id, name, code, active, company_id, lot_stock_id, sequence)
|
|
VALUES (1, 'Main Warehouse', 'WH', true, 1, 4, 10)
|
|
ON CONFLICT (id) DO NOTHING`)
|
|
|
|
// Stock picking types (operation types for receipts, deliveries, internal transfers)
|
|
tx.Exec(ctx, `
|
|
INSERT INTO stock_picking_type (id, name, code, sequence_code, active, warehouse_id, company_id, sequence,
|
|
default_location_src_id, default_location_dest_id) VALUES
|
|
(1, 'Receipts', 'incoming', 'IN', true, 1, 1, 1, 6, 4),
|
|
(2, 'Delivery Orders', 'outgoing', 'OUT', true, 1, 1, 2, 4, 5),
|
|
(3, 'Internal Transfers', 'internal', 'INT', true, 1, 1, 3, 4, 4)
|
|
ON CONFLICT (id) DO NOTHING`)
|
|
|
|
log.Println("db: stock reference data seeded (6 locations, 3 picking types, 1 warehouse)")
|
|
}
|
|
|
|
// seedViews creates UI views for key models.
|
|
func seedViews(ctx context.Context, tx pgx.Tx) {
|
|
log.Println("db: seeding UI views...")
|
|
|
|
tx.Exec(ctx, `INSERT INTO ir_ui_view (name, model, type, arch, priority, active, mode) VALUES
|
|
('partner.list', 'res.partner', 'list', '<list>
|
|
<field name="name"/>
|
|
<field name="email"/>
|
|
<field name="phone"/>
|
|
<field name="city"/>
|
|
<field name="country_id"/>
|
|
<field name="is_company"/>
|
|
</list>', 16, true, 'primary'),
|
|
('partner.form', 'res.partner', 'form', '<form>
|
|
<sheet>
|
|
<div class="oe_title"><h1><field name="name" placeholder="Name"/></h1></div>
|
|
<group>
|
|
<group>
|
|
<field name="is_company"/>
|
|
<field name="type"/>
|
|
<field name="email"/>
|
|
<field name="phone"/>
|
|
<field name="mobile"/>
|
|
<field name="website"/>
|
|
</group>
|
|
<group>
|
|
<field name="street"/>
|
|
<field name="street2"/>
|
|
<field name="zip"/>
|
|
<field name="city"/>
|
|
<field name="country_id"/>
|
|
<field name="vat"/>
|
|
<field name="lang"/>
|
|
</group>
|
|
</group>
|
|
<notebook>
|
|
<page string="Internal Notes">
|
|
<field name="comment"/>
|
|
</page>
|
|
</notebook>
|
|
</sheet>
|
|
</form>', 16, true, 'primary'),
|
|
('partner.search', 'res.partner', 'search', '<search>
|
|
<field name="name"/>
|
|
<field name="email"/>
|
|
<field name="phone"/>
|
|
<field name="city"/>
|
|
</search>', 16, true, 'primary'),
|
|
|
|
('invoice.list', 'account.move', 'list', '<list>
|
|
<field name="name"/>
|
|
<field name="partner_id"/>
|
|
<field name="invoice_date"/>
|
|
<field name="date"/>
|
|
<field name="move_type"/>
|
|
<field name="state"/>
|
|
<field name="amount_total"/>
|
|
</list>', 16, true, 'primary'),
|
|
|
|
('sale.list', 'sale.order', 'list', '<list>
|
|
<field name="name"/>
|
|
<field name="partner_id"/>
|
|
<field name="date_order"/>
|
|
<field name="state"/>
|
|
<field name="amount_total"/>
|
|
</list>', 16, true, 'primary'),
|
|
|
|
('lead.list', 'crm.lead', 'list', '<list>
|
|
<field name="name"/>
|
|
<field name="partner_name"/>
|
|
<field name="email_from"/>
|
|
<field name="phone"/>
|
|
<field name="stage_id"/>
|
|
<field name="expected_revenue"/>
|
|
<field name="user_id"/>
|
|
</list>', 16, true, 'primary'),
|
|
|
|
('partner.kanban', 'res.partner', 'kanban', '<kanban>
|
|
<templates>
|
|
<t t-name="card">
|
|
<div class="oe_kanban_global_click">
|
|
<strong><field name="name"/></strong>
|
|
<div><field name="email"/></div>
|
|
<div><field name="phone"/></div>
|
|
<div><field name="city"/></div>
|
|
</div>
|
|
</t>
|
|
</templates>
|
|
</kanban>', 16, true, 'primary'),
|
|
|
|
('lead.kanban', 'crm.lead', 'kanban', '<kanban default_group_by="stage_id">
|
|
<templates>
|
|
<t t-name="card">
|
|
<div class="oe_kanban_global_click">
|
|
<strong><field name="name"/></strong>
|
|
<div><field name="partner_name"/></div>
|
|
<div>Revenue: <field name="expected_revenue"/></div>
|
|
</div>
|
|
</t>
|
|
</templates>
|
|
</kanban>', 16, true, 'primary'),
|
|
|
|
('sale.form', 'sale.order', 'form', '<form>
|
|
<header>
|
|
<button name="action_confirm" string="Confirm" type="object" class="btn-primary" invisible="state != ''draft''"/>
|
|
<button name="create_invoices" string="Create Invoice" type="object" class="btn-primary" invisible="state != ''sale''"/>
|
|
<button name="action_cancel" string="Cancel" type="object" invisible="state not in (''draft'',''sale'')"/>
|
|
<button name="action_draft" string="Reset to Draft" type="object" invisible="state != ''cancel''"/>
|
|
<field name="state" widget="statusbar" clickable="1"/>
|
|
</header>
|
|
<sheet>
|
|
<div class="oe_title"><h1><field name="name"/></h1></div>
|
|
<group>
|
|
<group>
|
|
<field name="partner_id"/>
|
|
<field name="date_order"/>
|
|
<field name="company_id"/>
|
|
</group>
|
|
<group>
|
|
<field name="currency_id"/>
|
|
<field name="payment_term_id"/>
|
|
<field name="pricelist_id"/>
|
|
</group>
|
|
</group>
|
|
<notebook>
|
|
<page string="Order Lines">
|
|
<field name="order_line">
|
|
<list editable="bottom">
|
|
<field name="product_id"/>
|
|
<field name="name"/>
|
|
<field name="product_uom_qty"/>
|
|
<field name="price_unit"/>
|
|
<field name="price_subtotal"/>
|
|
</list>
|
|
</field>
|
|
</page>
|
|
</notebook>
|
|
<group>
|
|
<group>
|
|
<field name="amount_untaxed"/>
|
|
<field name="amount_tax"/>
|
|
<field name="amount_total"/>
|
|
</group>
|
|
</group>
|
|
</sheet>
|
|
</form>', 16, true, 'primary'),
|
|
|
|
('invoice.form', 'account.move', 'form', '<form>
|
|
<header>
|
|
<button name="action_post" string="Post" type="object" class="btn-primary" invisible="state != ''draft''"/>
|
|
<button name="action_register_payment" string="Register Payment" type="object" class="btn-primary" invisible="state != ''posted'' or payment_state == ''paid''"/>
|
|
<button name="button_cancel" string="Cancel" type="object" invisible="state != ''draft''"/>
|
|
<button name="button_draft" string="Reset to Draft" type="object" invisible="state != ''cancel''"/>
|
|
<field name="state" widget="statusbar" clickable="1"/>
|
|
</header>
|
|
<sheet>
|
|
<div class="oe_title"><h1><field name="name"/></h1></div>
|
|
<group>
|
|
<group>
|
|
<field name="partner_id"/>
|
|
<field name="journal_id"/>
|
|
<field name="company_id"/>
|
|
<field name="move_type"/>
|
|
</group>
|
|
<group>
|
|
<field name="date"/>
|
|
<field name="invoice_date"/>
|
|
<field name="invoice_date_due"/>
|
|
<field name="currency_id"/>
|
|
<field name="payment_state"/>
|
|
</group>
|
|
</group>
|
|
<notebook>
|
|
<page string="Invoice Lines">
|
|
<field name="invoice_line_ids">
|
|
<list>
|
|
<field name="name"/>
|
|
<field name="quantity"/>
|
|
<field name="price_unit"/>
|
|
<field name="balance"/>
|
|
</list>
|
|
</field>
|
|
</page>
|
|
</notebook>
|
|
<group>
|
|
<group>
|
|
<field name="amount_untaxed"/>
|
|
<field name="amount_tax"/>
|
|
<field name="amount_total"/>
|
|
<field name="amount_residual"/>
|
|
</group>
|
|
</group>
|
|
</sheet>
|
|
</form>', 16, true, 'primary'),
|
|
|
|
('purchase.list', 'purchase.order', 'list', '<list>
|
|
<field name="name"/>
|
|
<field name="partner_id"/>
|
|
<field name="date_order"/>
|
|
<field name="state"/>
|
|
<field name="amount_total"/>
|
|
</list>', 16, true, 'primary'),
|
|
|
|
('employee.list', 'hr.employee', 'list', '<list>
|
|
<field name="name"/>
|
|
<field name="department_id"/>
|
|
<field name="job_id"/>
|
|
<field name="work_email"/>
|
|
<field name="company_id"/>
|
|
</list>', 16, true, 'primary'),
|
|
|
|
('project.list', 'project.project', 'list', '<list>
|
|
<field name="name"/>
|
|
<field name="partner_id"/>
|
|
<field name="company_id"/>
|
|
<field name="active"/>
|
|
</list>', 16, true, 'primary'),
|
|
|
|
('lead.form', 'crm.lead', 'form', '<form>
|
|
<header>
|
|
<button name="action_set_won" string="Won" type="object" class="btn-primary" invisible="stage_id != false"/>
|
|
<button name="action_set_lost" string="Lost" type="object" invisible="stage_id != false"/>
|
|
<field name="stage_id" widget="statusbar" options="{''clickable'': ''1''}"/>
|
|
</header>
|
|
<sheet>
|
|
<div class="oe_title"><h1><field name="name" placeholder="Opportunity..."/></h1></div>
|
|
<group>
|
|
<group>
|
|
<field name="partner_id"/>
|
|
<field name="email_from"/>
|
|
<field name="phone"/>
|
|
<field name="user_id"/>
|
|
</group>
|
|
<group>
|
|
<field name="expected_revenue"/>
|
|
<field name="probability"/>
|
|
<field name="date_deadline"/>
|
|
<field name="priority"/>
|
|
<field name="company_id"/>
|
|
</group>
|
|
</group>
|
|
<notebook>
|
|
<page string="Notes">
|
|
<field name="description"/>
|
|
</page>
|
|
</notebook>
|
|
</sheet>
|
|
</form>', 10, true, 'primary'),
|
|
|
|
('purchase.form', 'purchase.order', 'form', '<form>
|
|
<header>
|
|
<button name="button_confirm" string="Confirm Order" type="object" class="btn-primary" invisible="state != ''draft''"/>
|
|
<field name="state" widget="statusbar" clickable="1"/>
|
|
</header>
|
|
<sheet>
|
|
<div class="oe_title"><h1><field name="name"/></h1></div>
|
|
<group>
|
|
<group>
|
|
<field name="partner_id"/>
|
|
<field name="date_order"/>
|
|
<field name="company_id"/>
|
|
</group>
|
|
<group>
|
|
<field name="currency_id"/>
|
|
</group>
|
|
</group>
|
|
<notebook>
|
|
<page string="Products">
|
|
<field name="order_line">
|
|
<list editable="bottom">
|
|
<field name="product_id"/>
|
|
<field name="name"/>
|
|
<field name="product_qty"/>
|
|
<field name="price_unit"/>
|
|
<field name="price_subtotal"/>
|
|
</list>
|
|
</field>
|
|
</page>
|
|
</notebook>
|
|
<group>
|
|
<group class="oe_subtotal_footer">
|
|
<field name="amount_untaxed"/>
|
|
<field name="amount_tax"/>
|
|
<field name="amount_total" class="oe_subtotal_footer_separator"/>
|
|
</group>
|
|
</group>
|
|
</sheet>
|
|
</form>', 10, true, 'primary'),
|
|
|
|
('employee.form', 'hr.employee', 'form', '<form>
|
|
<sheet>
|
|
<div class="oe_title"><h1><field name="name" placeholder="Employee Name"/></h1></div>
|
|
<group>
|
|
<group>
|
|
<field name="job_id"/>
|
|
<field name="department_id"/>
|
|
<field name="parent_id"/>
|
|
<field name="coach_id"/>
|
|
</group>
|
|
<group>
|
|
<field name="work_email"/>
|
|
<field name="work_phone"/>
|
|
<field name="mobile_phone"/>
|
|
<field name="company_id"/>
|
|
</group>
|
|
</group>
|
|
<notebook>
|
|
<page string="Personal Information">
|
|
<group>
|
|
<group>
|
|
<field name="gender"/>
|
|
<field name="birthday"/>
|
|
<field name="marital"/>
|
|
</group>
|
|
<group>
|
|
<field name="identification_id"/>
|
|
<field name="country_id"/>
|
|
</group>
|
|
</group>
|
|
</page>
|
|
</notebook>
|
|
</sheet>
|
|
</form>', 10, true, 'primary'),
|
|
|
|
('project.form', 'project.project', 'form', '<form>
|
|
<sheet>
|
|
<div class="oe_title"><h1><field name="name" placeholder="Project Name"/></h1></div>
|
|
<group>
|
|
<group>
|
|
<field name="partner_id"/>
|
|
<field name="user_id"/>
|
|
<field name="company_id"/>
|
|
</group>
|
|
<group>
|
|
<field name="date_start"/>
|
|
<field name="date"/>
|
|
<field name="active"/>
|
|
</group>
|
|
</group>
|
|
<notebook>
|
|
<page string="Description">
|
|
<field name="description"/>
|
|
</page>
|
|
</notebook>
|
|
</sheet>
|
|
</form>', 10, true, 'primary')
|
|
ON CONFLICT DO NOTHING`)
|
|
|
|
// Settings form view
|
|
tx.Exec(ctx, `INSERT INTO ir_ui_view (name, model, type, arch, priority, active, mode) VALUES
|
|
('res.config.settings.form', 'res.config.settings', 'form', '<form string="Settings" class="oe_form_configuration">
|
|
<header>
|
|
<button name="execute" string="Save" type="object" class="btn-primary"/>
|
|
<button string="Discard" special="cancel" class="btn-secondary"/>
|
|
</header>
|
|
<div class="o_setting_container">
|
|
<div class="settings">
|
|
<div class="app_settings_block">
|
|
<h2>Company</h2>
|
|
<div class="row mt16 o_settings_container">
|
|
<div class="col-12 col-lg-6 o_setting_box">
|
|
<div class="o_setting_left_pane"/>
|
|
<div class="o_setting_right_pane">
|
|
<label for="company_name"/>
|
|
<div class="text-muted">Your company name</div>
|
|
<field name="company_name"/>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-6 o_setting_box">
|
|
<div class="o_setting_left_pane"/>
|
|
<div class="o_setting_right_pane">
|
|
<label for="company_email"/>
|
|
<field name="company_email"/>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-6 o_setting_box">
|
|
<div class="o_setting_left_pane"/>
|
|
<div class="o_setting_right_pane">
|
|
<label for="company_phone"/>
|
|
<field name="company_phone"/>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-6 o_setting_box">
|
|
<div class="o_setting_left_pane"/>
|
|
<div class="o_setting_right_pane">
|
|
<label for="company_website"/>
|
|
<field name="company_website"/>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-6 o_setting_box">
|
|
<div class="o_setting_left_pane"/>
|
|
<div class="o_setting_right_pane">
|
|
<label for="company_vat"/>
|
|
<div class="text-muted">Tax ID / VAT number</div>
|
|
<field name="company_vat"/>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-6 o_setting_box">
|
|
<div class="o_setting_left_pane"/>
|
|
<div class="o_setting_right_pane">
|
|
<label for="company_currency_id"/>
|
|
<field name="company_currency_id"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<h2>Address</h2>
|
|
<div class="row mt16 o_settings_container">
|
|
<div class="col-12 col-lg-6 o_setting_box">
|
|
<div class="o_setting_left_pane"/>
|
|
<div class="o_setting_right_pane">
|
|
<label for="company_street"/>
|
|
<field name="company_street"/>
|
|
<field name="company_street2"/>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-6 o_setting_box">
|
|
<div class="o_setting_left_pane"/>
|
|
<div class="o_setting_right_pane">
|
|
<label for="company_zip"/>
|
|
<field name="company_zip"/>
|
|
<label for="company_city"/>
|
|
<field name="company_city"/>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-6 o_setting_box">
|
|
<div class="o_setting_left_pane"/>
|
|
<div class="o_setting_right_pane">
|
|
<label for="company_country_id"/>
|
|
<field name="company_country_id"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<h2>Features</h2>
|
|
<div class="row mt16 o_settings_container">
|
|
<div class="col-12 col-lg-6 o_setting_box">
|
|
<div class="o_setting_left_pane">
|
|
<field name="group_multi_company"/>
|
|
</div>
|
|
<div class="o_setting_right_pane">
|
|
<label for="group_multi_company"/>
|
|
<div class="text-muted">Manage multiple companies</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-6 o_setting_box">
|
|
<div class="o_setting_left_pane">
|
|
<field name="module_base_import"/>
|
|
</div>
|
|
<div class="o_setting_right_pane">
|
|
<label for="module_base_import"/>
|
|
<div class="text-muted">Import records from CSV/Excel files</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-lg-6 o_setting_box">
|
|
<div class="o_setting_left_pane">
|
|
<field name="show_effect"/>
|
|
</div>
|
|
<div class="o_setting_right_pane">
|
|
<label for="show_effect"/>
|
|
<div class="text-muted">Show animation effects</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<field name="company_id" invisible="1"/>
|
|
</form>', 10, true, 'primary')
|
|
ON CONFLICT DO NOTHING`)
|
|
|
|
// Admin list views for Settings > Technical
|
|
tx.Exec(ctx, `INSERT INTO ir_ui_view (name, model, type, arch, priority, active, mode) VALUES
|
|
('company.list', 'res.company', 'list', '<list>
|
|
<field name="name"/>
|
|
<field name="partner_id"/>
|
|
<field name="currency_id"/>
|
|
<field name="email"/>
|
|
<field name="phone"/>
|
|
</list>', 16, true, 'primary'),
|
|
('users.list', 'res.users', 'list', '<list>
|
|
<field name="name"/>
|
|
<field name="login"/>
|
|
<field name="company_id"/>
|
|
<field name="active"/>
|
|
</list>', 16, true, 'primary'),
|
|
('config_parameter.list', 'ir.config_parameter', 'list', '<list>
|
|
<field name="key"/>
|
|
<field name="value"/>
|
|
</list>', 16, true, 'primary'),
|
|
('ui_view.list', 'ir.ui.view', 'list', '<list>
|
|
<field name="name"/>
|
|
<field name="model"/>
|
|
<field name="type"/>
|
|
<field name="priority"/>
|
|
<field name="active"/>
|
|
</list>', 16, true, 'primary'),
|
|
('ui_menu.list', 'ir.ui.menu', 'list', '<list>
|
|
<field name="name"/>
|
|
<field name="parent_id"/>
|
|
<field name="sequence"/>
|
|
<field name="action"/>
|
|
</list>', 16, true, 'primary')
|
|
ON CONFLICT DO NOTHING`)
|
|
|
|
log.Println("db: UI views seeded")
|
|
}
|
|
|
|
// seedActions inserts action records into ir_act_window and their XML IDs into ir_model_data.
|
|
// Mirrors: odoo/addons/base/data/ir_actions_data.xml, contacts/data/..., account/data/..., etc.
|
|
func seedActions(ctx context.Context, tx pgx.Tx) {
|
|
log.Println("db: seeding actions...")
|
|
|
|
// Action definitions: id, name, res_model, view_mode, domain, context, target, limit, res_id
|
|
type actionDef struct {
|
|
ID int
|
|
Name string
|
|
ResModel string
|
|
ViewMode string
|
|
Domain string
|
|
Context string
|
|
Target string
|
|
Limit int
|
|
ResID int
|
|
// XML ID parts
|
|
Module string
|
|
XMLName string
|
|
}
|
|
|
|
actions := []actionDef{
|
|
{1, "Contacts", "res.partner", "list,kanban,form", "[]", "{}", "current", 80, 0, "contacts", "action_contacts"},
|
|
{2, "Invoices", "account.move", "list,form", `[("move_type","in",["out_invoice","out_refund"])]`, "{}", "current", 80, 0, "account", "action_move_out_invoice_type"},
|
|
{3, "Sale Orders", "sale.order", "list,form", "[]", "{}", "current", 80, 0, "sale", "action_quotations_with_onboarding"},
|
|
{4, "CRM Pipeline", "crm.lead", "kanban,list,form", "[]", "{}", "current", 80, 0, "crm", "crm_lead_all_pipeline"},
|
|
{5, "Transfers", "stock.picking", "list,form", "[]", "{}", "current", 80, 0, "stock", "action_picking_tree_all"},
|
|
{6, "Products", "product.template", "list,form", "[]", "{}", "current", 80, 0, "stock", "action_product_template"},
|
|
{7, "Purchase Orders", "purchase.order", "list,form", "[]", "{}", "current", 80, 0, "purchase", "action_purchase_orders"},
|
|
{8, "Employees", "hr.employee", "list,form", "[]", "{}", "current", 80, 0, "hr", "action_hr_employee"},
|
|
{9, "Departments", "hr.department", "list,form", "[]", "{}", "current", 80, 0, "hr", "action_hr_department"},
|
|
{10, "Projects", "project.project", "list,form", "[]", "{}", "current", 80, 0, "project", "action_project"},
|
|
{11, "Tasks", "project.task", "list,form", "[]", "{}", "current", 80, 0, "project", "action_project_task"},
|
|
{12, "Vehicles", "fleet.vehicle", "list,form", "[]", "{}", "current", 80, 0, "fleet", "action_fleet_vehicle"},
|
|
{100, "Settings", "res.config.settings", "form", "[]", "{}", "current", 80, 1, "base", "action_general_configuration"},
|
|
{104, "Companies", "res.company", "list,form", "[]", "{}", "current", 80, 0, "base", "action_res_company_form"},
|
|
{101, "Users", "res.users", "list,form", "[]", "{}", "current", 80, 0, "base", "action_res_users"},
|
|
{102, "Sequences", "ir.sequence", "list,form", "[]", "{}", "current", 80, 0, "base", "ir_sequence_form"},
|
|
{103, "Change My Preferences", "res.users", "form", "[]", "{}", "new", 80, 0, "base", "action_res_users_my"},
|
|
{105, "Groups", "res.groups", "list,form", "[]", "{}", "current", 80, 0, "base", "action_res_groups"},
|
|
{106, "Logging", "ir.logging", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_logging"},
|
|
{107, "System Parameters", "ir.config_parameter", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_config_parameter"},
|
|
{108, "Scheduled Actions", "ir.cron", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_cron"},
|
|
{109, "Views", "ir.ui.view", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_ui_view"},
|
|
{110, "Actions", "ir.actions.act_window", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_act_window"},
|
|
{111, "Menus", "ir.ui.menu", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_ui_menu"},
|
|
{112, "Access Rights", "ir.model.access", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_model_access"},
|
|
{113, "Record Rules", "ir.rule", "list,form", "[]", "{}", "current", 80, 0, "base", "action_ir_rule"},
|
|
}
|
|
|
|
for _, a := range actions {
|
|
tx.Exec(ctx, `
|
|
INSERT INTO ir_act_window (id, name, type, res_model, view_mode, res_id, domain, context, target, "limit")
|
|
VALUES ($1, $2, 'ir.actions.act_window', $3, $4, $5, $6, $7, $8, $9)
|
|
ON CONFLICT (id) DO NOTHING`,
|
|
a.ID, a.Name, a.ResModel, a.ViewMode, a.ResID, a.Domain, a.Context, a.Target, a.Limit)
|
|
|
|
tx.Exec(ctx, `
|
|
INSERT INTO ir_model_data (module, name, model, res_id, noupdate)
|
|
VALUES ($1, $2, 'ir.actions.act_window', $3, true)
|
|
ON CONFLICT DO NOTHING`,
|
|
a.Module, a.XMLName, a.ID)
|
|
}
|
|
|
|
log.Printf("db: seeded %d actions with XML IDs", len(actions))
|
|
}
|
|
|
|
// seedMenus creates menu records in ir_ui_menu and their XML IDs in ir_model_data.
|
|
// Mirrors: odoo/addons/*/data/*_menu.xml — menus are loaded from XML data files.
|
|
//
|
|
// The action field stores Odoo reference format: "ir.actions.act_window,<id>"
|
|
// pointing to the action IDs seeded by seedActions().
|
|
// parent_id is NULL for top-level app menus, or references the parent menu ID.
|
|
func seedMenus(ctx context.Context, tx pgx.Tx) {
|
|
log.Println("db: seeding menus...")
|
|
|
|
type menuDef struct {
|
|
ID int
|
|
Name string
|
|
ParentID *int // nil = top-level
|
|
Sequence int
|
|
Action string // "ir.actions.act_window,<id>" or ""
|
|
WebIcon string // FontAwesome icon for top-level menus
|
|
// XML ID parts for ir_model_data
|
|
Module string
|
|
XMLName string
|
|
}
|
|
|
|
// Helper to create a pointer to int
|
|
p := func(id int) *int { return &id }
|
|
|
|
menus := []menuDef{
|
|
// ── Contacts ──────────────────────────────────────────────
|
|
{1, "Contacts", nil, 10, "ir.actions.act_window,1", "fa-address-book,#71639e,#FFFFFF", "contacts", "menu_contacts"},
|
|
{10, "Contacts", p(1), 10, "ir.actions.act_window,1", "", "contacts", "menu_contacts_list"},
|
|
|
|
// ── Invoicing ────────────────────────────────────────────
|
|
{2, "Invoicing", nil, 20, "ir.actions.act_window,2", "fa-book,#71639e,#FFFFFF", "account", "menu_finance"},
|
|
{20, "Invoices", p(2), 10, "ir.actions.act_window,2", "", "account", "menu_finance_invoices"},
|
|
|
|
// ── Sales ────────────────────────────────────────────────
|
|
{3, "Sales", nil, 30, "ir.actions.act_window,3", "fa-bar-chart,#71639e,#FFFFFF", "sale", "menu_sale_root"},
|
|
{30, "Orders", p(3), 10, "ir.actions.act_window,3", "", "sale", "menu_sale_orders"},
|
|
|
|
// ── CRM ──────────────────────────────────────────────────
|
|
{4, "CRM", nil, 40, "ir.actions.act_window,4", "fa-star,#71639e,#FFFFFF", "crm", "menu_crm_root"},
|
|
{40, "Pipeline", p(4), 10, "ir.actions.act_window,4", "", "crm", "menu_crm_pipeline"},
|
|
|
|
// ── Inventory ────────────────────────────────────────────
|
|
{5, "Inventory", nil, 50, "ir.actions.act_window,5", "fa-cubes,#71639e,#FFFFFF", "stock", "menu_stock_root"},
|
|
{50, "Transfers", p(5), 10, "ir.actions.act_window,5", "", "stock", "menu_stock_transfers"},
|
|
{51, "Products", p(5), 20, "ir.actions.act_window,6", "", "stock", "menu_stock_products"},
|
|
|
|
// ── Purchase ─────────────────────────────────────────────
|
|
{6, "Purchase", nil, 60, "ir.actions.act_window,7", "fa-shopping-cart,#71639e,#FFFFFF", "purchase", "menu_purchase_root"},
|
|
{60, "Purchase Orders", p(6), 10, "ir.actions.act_window,7", "", "purchase", "menu_purchase_orders"},
|
|
|
|
// ── Employees / HR ───────────────────────────────────────
|
|
{7, "Employees", nil, 70, "ir.actions.act_window,8", "fa-users,#71639e,#FFFFFF", "hr", "menu_hr_root"},
|
|
{70, "Employees", p(7), 10, "ir.actions.act_window,8", "", "hr", "menu_hr_employees"},
|
|
{71, "Departments", p(7), 20, "ir.actions.act_window,9", "", "hr", "menu_hr_departments"},
|
|
|
|
// ── Project ──────────────────────────────────────────────
|
|
{8, "Project", nil, 80, "ir.actions.act_window,10", "fa-puzzle-piece,#71639e,#FFFFFF", "project", "menu_project_root"},
|
|
{80, "Projects", p(8), 10, "ir.actions.act_window,10", "", "project", "menu_projects"},
|
|
{81, "Tasks", p(8), 20, "ir.actions.act_window,11", "", "project", "menu_project_tasks"},
|
|
|
|
// ── Fleet ────────────────────────────────────────────────
|
|
{9, "Fleet", nil, 90, "ir.actions.act_window,12", "fa-car,#71639e,#FFFFFF", "fleet", "menu_fleet_root"},
|
|
{90, "Vehicles", p(9), 10, "ir.actions.act_window,12", "", "fleet", "menu_fleet_vehicles"},
|
|
|
|
// ── Settings ─────────────────────────────────────────────
|
|
{100, "Settings", nil, 100, "ir.actions.act_window,100", "fa-cog,#71639e,#FFFFFF", "base", "menu_administration"},
|
|
{101, "Users & Companies", p(100), 10, "", "", "base", "menu_users"},
|
|
{110, "Users", p(101), 10, "ir.actions.act_window,101", "", "base", "menu_action_res_users"},
|
|
{111, "Companies", p(101), 20, "ir.actions.act_window,104", "", "base", "menu_action_res_company_form"},
|
|
{112, "Groups", p(101), 30, "ir.actions.act_window,105", "", "base", "menu_action_res_groups"},
|
|
|
|
{102, "Technical", p(100), 20, "", "", "base", "menu_custom"},
|
|
|
|
// Database Structure
|
|
{120, "Database Structure", p(102), 10, "", "", "base", "menu_custom_database_structure"},
|
|
|
|
// Sequences & Identifiers
|
|
{122, "Sequences", p(102), 15, "ir.actions.act_window,102", "", "base", "menu_custom_sequences"},
|
|
|
|
// Parameters
|
|
{125, "Parameters", p(102), 20, "", "", "base", "menu_custom_parameters"},
|
|
{126, "System Parameters", p(125), 10, "ir.actions.act_window,107", "", "base", "menu_ir_config_parameter"},
|
|
|
|
// Scheduled Actions
|
|
{128, "Automation", p(102), 25, "", "", "base", "menu_custom_automation"},
|
|
{129, "Scheduled Actions", p(128), 10, "ir.actions.act_window,108", "", "base", "menu_ir_cron"},
|
|
|
|
// User Interface
|
|
{130, "User Interface", p(102), 30, "", "", "base", "menu_custom_user_interface"},
|
|
{131, "Views", p(130), 10, "ir.actions.act_window,109", "", "base", "menu_ir_ui_view"},
|
|
{132, "Actions", p(130), 20, "ir.actions.act_window,110", "", "base", "menu_ir_act_window"},
|
|
{133, "Menus", p(130), 30, "ir.actions.act_window,111", "", "base", "menu_ir_ui_menu"},
|
|
|
|
// Security
|
|
{135, "Security", p(102), 40, "", "", "base", "menu_custom_security"},
|
|
{136, "Access Rights", p(135), 10, "ir.actions.act_window,112", "", "base", "menu_ir_model_access"},
|
|
{137, "Record Rules", p(135), 20, "ir.actions.act_window,113", "", "base", "menu_ir_rule"},
|
|
|
|
// Logging
|
|
{140, "Logging", p(102), 50, "ir.actions.act_window,106", "", "base", "menu_ir_logging"},
|
|
}
|
|
|
|
for _, m := range menus {
|
|
// Insert the menu record
|
|
var actionVal *string
|
|
if m.Action != "" {
|
|
actionVal = &m.Action
|
|
}
|
|
var webIconVal *string
|
|
if m.WebIcon != "" {
|
|
webIconVal = &m.WebIcon
|
|
}
|
|
|
|
tx.Exec(ctx, `
|
|
INSERT INTO ir_ui_menu (id, name, parent_id, sequence, action, web_icon, active)
|
|
VALUES ($1, $2, $3, $4, $5, $6, true)
|
|
ON CONFLICT (id) DO NOTHING`,
|
|
m.ID, m.Name, m.ParentID, m.Sequence, actionVal, webIconVal)
|
|
|
|
// Insert the XML ID into ir_model_data
|
|
tx.Exec(ctx, `
|
|
INSERT INTO ir_model_data (module, name, model, res_id, noupdate)
|
|
VALUES ($1, $2, 'ir.ui.menu', $3, true)
|
|
ON CONFLICT DO NOTHING`,
|
|
m.Module, m.XMLName, m.ID)
|
|
}
|
|
|
|
log.Printf("db: seeded %d menus with XML IDs", len(menus))
|
|
}
|
|
|
|
// seedDemoData creates example records for testing.
|
|
func seedDemoData(ctx context.Context, tx pgx.Tx) {
|
|
log.Println("db: loading demo data...")
|
|
|
|
// Demo customers
|
|
tx.Exec(ctx, `INSERT INTO res_partner (name, is_company, active, type, email, city, lang) VALUES
|
|
('Müller Bau GmbH', true, true, 'contact', 'info@mueller-bau.de', 'München', 'de_DE'),
|
|
('Schmidt & Söhne KG', true, true, 'contact', 'kontakt@schmidt-soehne.de', 'Hamburg', 'de_DE'),
|
|
('Weber Elektro AG', true, true, 'contact', 'info@weber-elektro.de', 'Frankfurt', 'de_DE'),
|
|
('Fischer Metallbau', true, true, 'contact', 'office@fischer-metall.de', 'Stuttgart', 'de_DE'),
|
|
('Hoffmann IT Services', true, true, 'contact', 'hello@hoffmann-it.de', 'Berlin', 'de_DE')
|
|
ON CONFLICT DO NOTHING`)
|
|
|
|
// Demo contacts (employees of customers)
|
|
tx.Exec(ctx, `INSERT INTO res_partner (name, is_company, active, type, email, phone, lang) VALUES
|
|
('Thomas Müller', false, true, 'contact', 'thomas@mueller-bau.de', '+49 89 1234567', 'de_DE'),
|
|
('Anna Schmidt', false, true, 'contact', 'anna@schmidt-soehne.de', '+49 40 9876543', 'de_DE'),
|
|
('Peter Weber', false, true, 'contact', 'peter@weber-elektro.de', '+49 69 5551234', 'de_DE')
|
|
ON CONFLICT DO NOTHING`)
|
|
|
|
// Demo sale orders
|
|
tx.Exec(ctx, `INSERT INTO sale_order (name, partner_id, company_id, currency_id, state, date_order, amount_untaxed, amount_total) VALUES
|
|
('AG0001', 3, 1, 1, 'sale', '2026-03-15 10:00:00', 18100, 21539),
|
|
('AG0002', 4, 1, 1, 'draft', '2026-03-20 14:30:00', 6000, 7140),
|
|
('AG0003', 5, 1, 1, 'sale', '2026-03-25 09:15:00', 11700, 13923)
|
|
ON CONFLICT DO NOTHING`)
|
|
|
|
// Demo sale order lines
|
|
tx.Exec(ctx, `INSERT INTO sale_order_line (order_id, name, product_uom_qty, price_unit, sequence) VALUES
|
|
((SELECT id FROM sale_order WHERE name='AG0001'), 'Baustelleneinrichtung', 1, 12500, 10),
|
|
((SELECT id FROM sale_order WHERE name='AG0001'), 'Erdarbeiten', 3, 2800, 20),
|
|
((SELECT id FROM sale_order WHERE name='AG0002'), 'Beratung IT-Infrastruktur', 40, 150, 10),
|
|
((SELECT id FROM sale_order WHERE name='AG0003'), 'Elektroinstallation', 1, 8500, 10),
|
|
((SELECT id FROM sale_order WHERE name='AG0003'), 'Material Kabel/Dosen', 1, 3200, 20)
|
|
ON CONFLICT DO NOTHING`)
|
|
|
|
// Demo invoices (account.move)
|
|
tx.Exec(ctx, `INSERT INTO account_move (name, move_type, state, date, invoice_date, partner_id, journal_id, company_id, currency_id, amount_total, amount_untaxed) VALUES
|
|
('RE/2026/0001', 'out_invoice', 'posted', '2026-03-10', '2026-03-10', 3, 1, 1, 1, 14875, 12500),
|
|
('RE/2026/0002', 'out_invoice', 'draft', '2026-03-20', '2026-03-20', 4, 1, 1, 1, 7140, 6000),
|
|
('RE/2026/0003', 'out_invoice', 'posted', '2026-03-25', '2026-03-25', 5, 1, 1, 1, 13923, 11700)
|
|
ON CONFLICT DO NOTHING`)
|
|
|
|
// CRM pipeline stages
|
|
tx.Exec(ctx, `INSERT INTO crm_stage (id, name, sequence, fold, is_won) VALUES
|
|
(1, 'New', 1, false, false),
|
|
(2, 'Qualified', 2, false, false),
|
|
(3, 'Proposition', 3, false, false),
|
|
(4, 'Won', 4, false, true)
|
|
ON CONFLICT (id) DO NOTHING`)
|
|
|
|
// CRM demo leads (partner IDs 3-5 are the first three demo companies seeded above)
|
|
tx.Exec(ctx, `INSERT INTO crm_lead (name, type, stage_id, partner_id, expected_revenue, company_id, currency_id, active, state, priority) VALUES
|
|
('Website Redesign', 'opportunity', 1, 3, 15000, 1, 1, true, 'open', '0'),
|
|
('ERP Implementation', 'opportunity', 2, 4, 45000, 1, 1, true, 'open', '0'),
|
|
('Cloud Migration', 'opportunity', 3, 5, 28000, 1, 1, true, 'open', '0')
|
|
ON CONFLICT DO NOTHING`)
|
|
|
|
// Products (templates + variants)
|
|
tx.Exec(ctx, `INSERT INTO product_template (id, name, type, list_price, standard_price, sale_ok, purchase_ok, active) VALUES
|
|
(1, 'Server Hosting', 'service', 50.00, 30.00, true, false, true),
|
|
(2, 'Consulting Hours', 'service', 150.00, 80.00, true, false, true),
|
|
(3, 'Laptop', 'consu', 1200.00, 800.00, true, true, true),
|
|
(4, 'Monitor 27"', 'consu', 450.00, 300.00, true, true, true),
|
|
(5, 'Office Chair', 'consu', 350.00, 200.00, true, true, true)
|
|
ON CONFLICT (id) DO NOTHING`)
|
|
|
|
tx.Exec(ctx, `INSERT INTO product_product (id, product_tmpl_id, active, default_code) VALUES
|
|
(1, 1, true, 'SRV-HOST'),
|
|
(2, 2, true, 'SRV-CONS'),
|
|
(3, 3, true, 'HW-LAPTOP'),
|
|
(4, 4, true, 'HW-MON27'),
|
|
(5, 5, true, 'HW-CHAIR')
|
|
ON CONFLICT (id) DO NOTHING`)
|
|
|
|
// HR Departments
|
|
tx.Exec(ctx, `INSERT INTO hr_department (id, name, company_id) VALUES
|
|
(1, 'Management', 1),
|
|
(2, 'IT', 1),
|
|
(3, 'Sales', 1)
|
|
ON CONFLICT (id) DO NOTHING`)
|
|
|
|
// HR Employees
|
|
tx.Exec(ctx, `INSERT INTO hr_employee (id, name, department_id, company_id, work_email) VALUES
|
|
(1, 'Marc Bauer', 1, 1, 'marc@bauer-bau.de'),
|
|
(2, 'Anna Schmidt', 2, 1, 'anna@bauer-bau.de'),
|
|
(3, 'Peter Weber', 3, 1, 'peter@bauer-bau.de')
|
|
ON CONFLICT (id) DO NOTHING`)
|
|
|
|
// Project task stages
|
|
tx.Exec(ctx, `INSERT INTO project_task_type (id, name, sequence, fold) VALUES
|
|
(1, 'New', 1, false),
|
|
(2, 'In Progress', 5, false),
|
|
(3, 'Done', 10, true),
|
|
(4, 'Cancelled', 20, true)
|
|
ON CONFLICT (id) DO NOTHING`)
|
|
|
|
// Projects
|
|
tx.Exec(ctx, `INSERT INTO project_project (id, name, partner_id, company_id, active) VALUES
|
|
(1, 'Website Redesign', 5, 1, true),
|
|
(2, 'Office Migration', 3, 1, true)
|
|
ON CONFLICT (id) DO NOTHING`)
|
|
|
|
log.Println("db: demo data loaded (8 contacts, 3 sale orders, 3 invoices, 4 CRM stages, 3 CRM leads, 5 products, 3 departments, 3 employees, 2 projects)")
|
|
}
|
|
|
|
// SeedBaseData is the legacy function — redirects to setup with defaults.
|
|
// Used when running without the setup wizard (e.g., Docker auto-start).
|
|
func SeedBaseData(ctx context.Context, pool *pgxpool.Pool) error {
|
|
if !NeedsSetup(ctx, pool) {
|
|
log.Println("db: base data already exists, skipping seed")
|
|
return nil
|
|
}
|
|
|
|
adminHash, _ := tools.HashPassword("admin")
|
|
return SeedWithSetup(ctx, pool, SetupConfig{
|
|
CompanyName: "My Company",
|
|
Street: "",
|
|
Zip: "",
|
|
City: "",
|
|
CountryCode: "DE",
|
|
CountryName: "Germany",
|
|
PhoneCode: "49",
|
|
Email: "admin@example.com",
|
|
Chart: "skr03",
|
|
AdminLogin: "admin",
|
|
AdminPassword: adminHash,
|
|
DemoData: false,
|
|
})
|
|
}
|
|
|
|
// seedSystemParams inserts default system parameters into ir_config_parameter.
|
|
// Mirrors: odoo/addons/base/data/ir_config_parameter_data.xml
|
|
func seedSystemParams(ctx context.Context, tx pgx.Tx) {
|
|
log.Println("db: seeding system parameters...")
|
|
|
|
// Ensure unique constraint on key column for ON CONFLICT to work
|
|
tx.Exec(ctx, `CREATE UNIQUE INDEX IF NOT EXISTS ir_config_parameter_key_uniq ON ir_config_parameter (key)`)
|
|
|
|
// Generate a random UUID for database.uuid
|
|
dbUUID := generateUUID()
|
|
|
|
params := []struct {
|
|
key string
|
|
value string
|
|
}{
|
|
{"web.base.url", "http://localhost:8069"},
|
|
{"database.uuid", dbUUID},
|
|
{"report.url", "http://localhost:8069"},
|
|
{"base.login_cooldown_after", "10"},
|
|
{"base.login_cooldown_duration", "60"},
|
|
}
|
|
|
|
for _, p := range params {
|
|
tx.Exec(ctx,
|
|
`INSERT INTO ir_config_parameter (key, value) VALUES ($1, $2)
|
|
ON CONFLICT (key) DO NOTHING`, p.key, p.value)
|
|
}
|
|
|
|
log.Printf("db: seeded %d system parameters", len(params))
|
|
}
|
|
|
|
// seedLanguages inserts English and German language entries into res_lang.
|
|
// Mirrors: odoo/addons/base/data/res_lang_data.xml
|
|
func seedLanguages(ctx context.Context, tx pgx.Tx) {
|
|
log.Println("db: seeding languages...")
|
|
|
|
// English (US) — default language
|
|
tx.Exec(ctx, `
|
|
INSERT INTO res_lang (name, code, iso_code, url_code, active, direction, date_format, time_format, decimal_point, thousands_sep, week_start, grouping)
|
|
VALUES ('English (US)', 'en_US', 'en', 'en', true, 'ltr', '%%m/%%d/%%Y', '%%H:%%M:%%S', '.', ',', '7', '[3,0]')
|
|
ON CONFLICT DO NOTHING`)
|
|
|
|
// German (Germany)
|
|
tx.Exec(ctx, `
|
|
INSERT INTO res_lang (name, code, iso_code, url_code, active, direction, date_format, time_format, decimal_point, thousands_sep, week_start, grouping)
|
|
VALUES ('German / Deutsch', 'de_DE', 'de', 'de', true, 'ltr', '%%d.%%m.%%Y', '%%H:%%M:%%S', ',', '.', '1', '[3,0]')
|
|
ON CONFLICT DO NOTHING`)
|
|
|
|
log.Println("db: languages seeded (en_US, de_DE)")
|
|
}
|
|
|
|
// seedTranslations inserts German translations for core UI terms into ir_translation.
|
|
// Mirrors: odoo/addons/base/i18n/de.po (partially)
|
|
//
|
|
// These translations are loaded by the web client via /web/webclient/translations
|
|
// and used to display UI elements in German.
|
|
func seedTranslations(ctx context.Context, tx pgx.Tx) {
|
|
log.Println("db: seeding German translations...")
|
|
|
|
translations := []struct {
|
|
src, value, module string
|
|
}{
|
|
// Navigation & App names
|
|
{"Contacts", "Kontakte", "contacts"},
|
|
{"Invoicing", "Rechnungen", "account"},
|
|
{"Sales", "Verkauf", "sale"},
|
|
{"Purchase", "Einkauf", "purchase"},
|
|
{"Inventory", "Lager", "stock"},
|
|
{"Employees", "Mitarbeiter", "hr"},
|
|
{"Project", "Projekt", "project"},
|
|
{"Settings", "Einstellungen", "base"},
|
|
{"Apps", "Apps", "base"},
|
|
{"Discuss", "Diskussion", "base"},
|
|
{"Calendar", "Kalender", "base"},
|
|
{"Dashboard", "Dashboard", "base"},
|
|
{"Fleet", "Fuhrpark", "fleet"},
|
|
{"CRM", "CRM", "crm"},
|
|
|
|
// Common field labels
|
|
{"Name", "Name", "base"},
|
|
{"Email", "E-Mail", "base"},
|
|
{"Phone", "Telefon", "base"},
|
|
{"Mobile", "Mobil", "base"},
|
|
{"Company", "Unternehmen", "base"},
|
|
{"Partner", "Partner", "base"},
|
|
{"Active", "Aktiv", "base"},
|
|
{"Date", "Datum", "base"},
|
|
{"Status", "Status", "base"},
|
|
{"Total", "Gesamt", "base"},
|
|
{"Amount", "Betrag", "account"},
|
|
{"Description", "Beschreibung", "base"},
|
|
{"Reference", "Referenz", "base"},
|
|
{"Notes", "Notizen", "base"},
|
|
{"Tags", "Schlagwörter", "base"},
|
|
{"Type", "Typ", "base"},
|
|
{"Country", "Land", "base"},
|
|
{"City", "Stadt", "base"},
|
|
{"Street", "Straße", "base"},
|
|
{"Zip", "PLZ", "base"},
|
|
{"Website", "Webseite", "base"},
|
|
{"Language", "Sprache", "base"},
|
|
{"Currency", "Währung", "base"},
|
|
{"Sequence", "Reihenfolge", "base"},
|
|
{"Priority", "Priorität", "base"},
|
|
{"Color", "Farbe", "base"},
|
|
{"Image", "Bild", "base"},
|
|
{"Attachment", "Anhang", "base"},
|
|
{"Category", "Kategorie", "base"},
|
|
{"Title", "Titel", "base"},
|
|
|
|
// Buttons & Actions
|
|
{"Save", "Speichern", "web"},
|
|
{"Discard", "Verwerfen", "web"},
|
|
{"New", "Neu", "web"},
|
|
{"Edit", "Bearbeiten", "web"},
|
|
{"Delete", "Löschen", "web"},
|
|
{"Archive", "Archivieren", "web"},
|
|
{"Unarchive", "Dearchivieren", "web"},
|
|
{"Duplicate", "Duplizieren", "web"},
|
|
{"Import", "Importieren", "web"},
|
|
{"Export", "Exportieren", "web"},
|
|
{"Print", "Drucken", "web"},
|
|
{"Confirm", "Bestätigen", "web"},
|
|
{"Cancel", "Abbrechen", "web"},
|
|
{"Close", "Schließen", "web"},
|
|
{"Apply", "Anwenden", "web"},
|
|
{"Ok", "Ok", "web"},
|
|
{"Yes", "Ja", "web"},
|
|
{"No", "Nein", "web"},
|
|
{"Send", "Senden", "web"},
|
|
{"Refresh", "Aktualisieren", "web"},
|
|
{"Actions", "Aktionen", "web"},
|
|
{"Action", "Aktion", "web"},
|
|
{"Create", "Erstellen", "web"},
|
|
|
|
// Search & Filters
|
|
{"Search...", "Suchen...", "web"},
|
|
{"Filters", "Filter", "web"},
|
|
{"Group By", "Gruppieren nach", "web"},
|
|
{"Favorites", "Favoriten", "web"},
|
|
{"Custom Filter", "Benutzerdefinierter Filter", "web"},
|
|
|
|
// Status values
|
|
{"Draft", "Entwurf", "base"},
|
|
{"Posted", "Gebucht", "account"},
|
|
{"Cancelled", "Storniert", "base"},
|
|
{"Confirmed", "Bestätigt", "base"},
|
|
{"Done", "Erledigt", "base"},
|
|
{"In Progress", "In Bearbeitung", "base"},
|
|
{"Waiting", "Wartend", "base"},
|
|
{"Sent", "Gesendet", "base"},
|
|
{"Paid", "Bezahlt", "account"},
|
|
{"Open", "Offen", "base"},
|
|
{"Locked", "Gesperrt", "base"},
|
|
|
|
// View types & navigation
|
|
{"List", "Liste", "web"},
|
|
{"Form", "Formular", "web"},
|
|
{"Kanban", "Kanban", "web"},
|
|
{"Graph", "Grafik", "web"},
|
|
{"Pivot", "Pivot", "web"},
|
|
{"Map", "Karte", "web"},
|
|
{"Activity", "Aktivität", "web"},
|
|
|
|
// Accounting terms
|
|
{"Invoice", "Rechnung", "account"},
|
|
{"Invoices", "Rechnungen", "account"},
|
|
{"Bill", "Eingangsrechnung", "account"},
|
|
{"Bills", "Eingangsrechnungen", "account"},
|
|
{"Payment", "Zahlung", "account"},
|
|
{"Payments", "Zahlungen", "account"},
|
|
{"Journal", "Journal", "account"},
|
|
{"Journals", "Journale", "account"},
|
|
{"Account", "Konto", "account"},
|
|
{"Tax", "Steuer", "account"},
|
|
{"Taxes", "Steuern", "account"},
|
|
{"Untaxed Amount", "Nettobetrag", "account"},
|
|
{"Tax Amount", "Steuerbetrag", "account"},
|
|
{"Total Amount", "Gesamtbetrag", "account"},
|
|
{"Due Date", "Fälligkeitsdatum", "account"},
|
|
{"Journal Entry", "Buchungssatz", "account"},
|
|
{"Journal Entries", "Buchungssätze", "account"},
|
|
{"Credit Note", "Gutschrift", "account"},
|
|
|
|
// Sales terms
|
|
{"Quotation", "Angebot", "sale"},
|
|
{"Quotations", "Angebote", "sale"},
|
|
{"Sales Order", "Verkaufsauftrag", "sale"},
|
|
{"Sales Orders", "Verkaufsaufträge", "sale"},
|
|
{"Customer", "Kunde", "sale"},
|
|
{"Customers", "Kunden", "sale"},
|
|
{"Unit Price", "Stückpreis", "sale"},
|
|
{"Quantity", "Menge", "sale"},
|
|
{"Ordered Quantity", "Bestellte Menge", "sale"},
|
|
{"Delivered Quantity", "Gelieferte Menge", "sale"},
|
|
|
|
// Purchase terms
|
|
{"Purchase Order", "Bestellung", "purchase"},
|
|
{"Purchase Orders", "Bestellungen", "purchase"},
|
|
{"Vendor", "Lieferant", "purchase"},
|
|
{"Vendors", "Lieferanten", "purchase"},
|
|
{"Request for Quotation", "Angebotsanfrage", "purchase"},
|
|
|
|
// Inventory terms
|
|
{"Product", "Produkt", "stock"},
|
|
{"Products", "Produkte", "stock"},
|
|
{"Warehouse", "Lager", "stock"},
|
|
{"Location", "Lagerort", "stock"},
|
|
{"Delivery", "Lieferung", "stock"},
|
|
{"Receipt", "Wareneingang", "stock"},
|
|
{"Picking", "Kommissionierung", "stock"},
|
|
{"Stock", "Bestand", "stock"},
|
|
|
|
// HR terms
|
|
{"Employee", "Mitarbeiter", "hr"},
|
|
{"Department", "Abteilung", "hr"},
|
|
{"Job Position", "Stelle", "hr"},
|
|
{"Contract", "Vertrag", "hr"},
|
|
|
|
// Time & date
|
|
{"Today", "Heute", "web"},
|
|
{"Yesterday", "Gestern", "web"},
|
|
{"This Week", "Diese Woche", "web"},
|
|
{"This Month", "Dieser Monat", "web"},
|
|
{"This Year", "Dieses Jahr", "web"},
|
|
{"Last 7 Days", "Letzte 7 Tage", "web"},
|
|
{"Last 30 Days", "Letzte 30 Tage", "web"},
|
|
{"Last 365 Days", "Letzte 365 Tage", "web"},
|
|
|
|
// Misc UI
|
|
{"Loading...", "Wird geladen...", "web"},
|
|
{"No records found", "Keine Einträge gefunden", "web"},
|
|
{"Are you sure?", "Sind Sie sicher?", "web"},
|
|
{"Warning", "Warnung", "web"},
|
|
{"Error", "Fehler", "web"},
|
|
{"Success", "Erfolg", "web"},
|
|
{"Information", "Information", "web"},
|
|
{"Powered by", "Betrieben von", "web"},
|
|
{"My Profile", "Mein Profil", "web"},
|
|
{"Log out", "Abmelden", "web"},
|
|
{"Preferences", "Einstellungen", "web"},
|
|
{"Documentation", "Dokumentation", "web"},
|
|
{"Support", "Support", "web"},
|
|
{"Shortcuts", "Tastenkürzel", "web"},
|
|
}
|
|
|
|
count := 0
|
|
for _, t := range translations {
|
|
_, err := tx.Exec(ctx,
|
|
`INSERT INTO ir_translation (name, lang, type, src, value, module, state)
|
|
VALUES ('code', $1, 'code', $2, $3, $4, 'translated')
|
|
ON CONFLICT DO NOTHING`,
|
|
"de_DE", t.src, t.value, t.module)
|
|
if err == nil {
|
|
count++
|
|
}
|
|
}
|
|
|
|
log.Printf("db: seeded %d German translations", count)
|
|
}
|
|
|
|
// generateUUID creates a random UUID v4 string.
|
|
func generateUUID() string {
|
|
b := make([]byte, 16)
|
|
rand.Read(b)
|
|
// Set UUID version 4 bits
|
|
b[6] = (b[6] & 0x0f) | 0x40
|
|
b[8] = (b[8] & 0x3f) | 0x80
|
|
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
|
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
|
}
|
|
|