Load menus and actions from database (like Python Odoo)
Replaces all hardcoded Go maps with database-driven loading:
Menus (pkg/server/menus.go):
- Queries ir_ui_menu table joined with ir_model_data for XML IDs
- Builds parent/child tree from parent_id relationships
- Resolves appID by walking up to top-level ancestor
- Parses action references ("ir.actions.act_window,123" format)
Actions (pkg/server/action.go):
- Loads from ir_actions_act_window table by integer ID
- Supports XML ID resolution ("sale.action_quotations_with_onboarding")
via ir_model_data lookup
- Builds views array from view_mode string dynamically
- Handles nullable DB columns with helper functions
Seed data (pkg/service/db.go):
- seedActions(): 15 actions with XML IDs in ir_model_data
- seedMenus(): 25 menus with parent/child hierarchy and XML IDs
- Both called during database creation
This mirrors Python Odoo's architecture where menus and actions
are database records loaded from XML data files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -253,16 +253,23 @@ func SeedWithSetup(ctx context.Context, pool *pgxpool.Pool, cfg SetupConfig) err
|
||||
// 10. UI Views for key models
|
||||
seedViews(ctx, tx)
|
||||
|
||||
// 11. Demo data
|
||||
// 11. Actions (ir_act_window + ir_model_data for XML IDs)
|
||||
seedActions(ctx, tx)
|
||||
|
||||
// 12. Menus (ir_ui_menu + ir_model_data for XML IDs)
|
||||
seedMenus(ctx, tx)
|
||||
|
||||
// 13. Demo data
|
||||
if cfg.DemoData {
|
||||
seedDemoData(ctx, tx)
|
||||
}
|
||||
|
||||
// 12. Reset sequences (each individually — pgx doesn't support multi-statement)
|
||||
// 14. Reset sequences (each individually — pgx doesn't support multi-statement)
|
||||
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",
|
||||
}
|
||||
for _, table := range seqs {
|
||||
tx.Exec(ctx, fmt.Sprintf(
|
||||
@@ -384,6 +391,160 @@ func seedViews(ctx context.Context, tx pgx.Tx) {
|
||||
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.company", "form", "[]", "{}", "current", 80, 1, "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"},
|
||||
}
|
||||
|
||||
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, "ir.actions.act_window,101", "", "base", "menu_users"},
|
||||
{102, "Technical", p(100), 20, "ir.actions.act_window,102", "", "base", "menu_custom"},
|
||||
}
|
||||
|
||||
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...")
|
||||
|
||||
Reference in New Issue
Block a user