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:
@@ -2,7 +2,9 @@ package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// handleLoadBreadcrumbs returns breadcrumb data for the current navigation path.
|
||||
@@ -22,8 +24,12 @@ func (s *Server) handleLoadBreadcrumbs(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeJSONRPC(w, req.ID, []interface{}{}, nil)
|
||||
}
|
||||
|
||||
// handleActionLoad loads an action definition by ID.
|
||||
// handleActionLoad loads an action definition by ID from the database.
|
||||
// Mirrors: odoo/addons/web/controllers/action.py Action.load()
|
||||
//
|
||||
// The action_id can be:
|
||||
// - An integer (database ID): SELECT directly from ir_act_window
|
||||
// - A string (XML ID like "base.action_res_users"): resolve via ir_model_data first
|
||||
func (s *Server) handleActionLoad(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
@@ -42,258 +48,162 @@ func (s *Server) handleActionLoad(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
json.Unmarshal(req.Params, ¶ms)
|
||||
|
||||
// Parse action_id from params (can be float64 from JSON or string)
|
||||
actionID := 0
|
||||
ctx := r.Context()
|
||||
var actionID int64
|
||||
|
||||
switch v := params.ActionID.(type) {
|
||||
case float64:
|
||||
actionID = int(v)
|
||||
actionID = int64(v)
|
||||
case string:
|
||||
// Try to parse numeric string
|
||||
for _, c := range v {
|
||||
if c >= '0' && c <= '9' {
|
||||
actionID = actionID*10 + int(c-'0')
|
||||
// Try numeric string first
|
||||
if id, ok := parseNumericString(v); ok {
|
||||
actionID = id
|
||||
} else {
|
||||
actionID = 0
|
||||
break
|
||||
// XML ID: "module.name" → look up in ir_model_data
|
||||
parts := strings.SplitN(v, ".", 2)
|
||||
if len(parts) == 2 {
|
||||
err := s.pool.QueryRow(ctx,
|
||||
`SELECT res_id FROM ir_model_data
|
||||
WHERE module = $1 AND name = $2 AND model = 'ir.actions.act_window'`,
|
||||
parts[0], parts[1]).Scan(&actionID)
|
||||
if err != nil {
|
||||
s.writeJSONRPC(w, req.ID, nil, &RPCError{
|
||||
Code: -32000,
|
||||
Message: fmt.Sprintf("Action not found: %s", v),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Action definitions by ID
|
||||
actions := map[int]map[string]interface{}{
|
||||
1: {
|
||||
"id": 1,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Contacts",
|
||||
"res_model": "res.partner",
|
||||
"view_mode": "list,kanban,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "kanban"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "contacts.action_contacts",
|
||||
},
|
||||
2: {
|
||||
"id": 2,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Invoices",
|
||||
"res_model": "account.move",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": `[("move_type","in",["out_invoice","out_refund"])]`,
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "account.action_move_out_invoice_type",
|
||||
},
|
||||
3: {
|
||||
"id": 3,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Sale Orders",
|
||||
"res_model": "sale.order",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "sale.action_quotations_with_onboarding",
|
||||
},
|
||||
4: {
|
||||
"id": 4,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "CRM Pipeline",
|
||||
"res_model": "crm.lead",
|
||||
"view_mode": "kanban,list,form",
|
||||
"views": [][]interface{}{{nil, "kanban"}, {nil, "list"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "crm.crm_lead_all_pipeline",
|
||||
},
|
||||
5: {
|
||||
"id": 5,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Transfers",
|
||||
"res_model": "stock.picking",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "stock.action_picking_tree_all",
|
||||
},
|
||||
6: {
|
||||
"id": 6,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Products",
|
||||
"res_model": "product.template",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "stock.action_product_template",
|
||||
},
|
||||
7: {
|
||||
"id": 7,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Purchase Orders",
|
||||
"res_model": "purchase.order",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "purchase.action_purchase_orders",
|
||||
},
|
||||
8: {
|
||||
"id": 8,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Employees",
|
||||
"res_model": "hr.employee",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "hr.action_hr_employee",
|
||||
},
|
||||
9: {
|
||||
"id": 9,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Departments",
|
||||
"res_model": "hr.department",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "hr.action_hr_department",
|
||||
},
|
||||
10: {
|
||||
"id": 10,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Projects",
|
||||
"res_model": "project.project",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "project.action_project",
|
||||
},
|
||||
11: {
|
||||
"id": 11,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Tasks",
|
||||
"res_model": "project.task",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "project.action_project_task",
|
||||
},
|
||||
12: {
|
||||
"id": 12,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Vehicles",
|
||||
"res_model": "fleet.vehicle",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "fleet.action_fleet_vehicle",
|
||||
},
|
||||
100: {
|
||||
"id": 100,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Settings",
|
||||
"res_model": "res.company",
|
||||
"res_id": 1,
|
||||
"view_mode": "form",
|
||||
"views": [][]interface{}{{nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "base.action_res_company_form",
|
||||
},
|
||||
101: {
|
||||
"id": 101,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Users",
|
||||
"res_model": "res.users",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "base.action_res_users",
|
||||
},
|
||||
102: {
|
||||
"id": 102,
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Sequences",
|
||||
"res_model": "ir.sequence",
|
||||
"view_mode": "list,form",
|
||||
"views": [][]interface{}{{nil, "list"}, {nil, "form"}},
|
||||
"search_view_id": false,
|
||||
"domain": "[]",
|
||||
"context": "{}",
|
||||
"target": "current",
|
||||
"limit": 80,
|
||||
"help": "",
|
||||
"xml_id": "base.ir_sequence_form",
|
||||
},
|
||||
if actionID == 0 {
|
||||
s.writeJSONRPC(w, req.ID, nil, &RPCError{
|
||||
Code: -32000,
|
||||
Message: "Invalid action_id",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
action, ok := actions[actionID]
|
||||
if !ok {
|
||||
// Default to Contacts if unknown action ID
|
||||
action = actions[1]
|
||||
// Load action from ir_act_window table
|
||||
var (
|
||||
id int64
|
||||
name string
|
||||
actType string
|
||||
resModel string
|
||||
viewMode string
|
||||
resID *int64
|
||||
domain *string
|
||||
actCtx *string
|
||||
target *string
|
||||
limit *int
|
||||
searchVID *string
|
||||
help *string
|
||||
)
|
||||
|
||||
err := s.pool.QueryRow(ctx,
|
||||
`SELECT id, name, type, res_model, view_mode,
|
||||
res_id, domain, context, target, "limit",
|
||||
search_view_id, help
|
||||
FROM ir_act_window WHERE id = $1`, actionID,
|
||||
).Scan(&id, &name, &actType, &resModel, &viewMode,
|
||||
&resID, &domain, &actCtx, &target, &limit,
|
||||
&searchVID, &help)
|
||||
|
||||
if err != nil {
|
||||
s.writeJSONRPC(w, req.ID, nil, &RPCError{
|
||||
Code: -32000,
|
||||
Message: fmt.Sprintf("Action %d not found", actionID),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Look up xml_id from ir_model_data
|
||||
xmlID := ""
|
||||
_ = s.pool.QueryRow(ctx,
|
||||
`SELECT module || '.' || name FROM ir_model_data
|
||||
WHERE model = 'ir.actions.act_window' AND res_id = $1
|
||||
LIMIT 1`, id).Scan(&xmlID)
|
||||
|
||||
// Build views array from view_mode string (e.g. "list,kanban,form" → [[nil,"list"],[nil,"kanban"],[nil,"form"]])
|
||||
views := buildViewsFromMode(viewMode)
|
||||
|
||||
// Assemble action response in the format the webclient expects
|
||||
action := map[string]interface{}{
|
||||
"id": id,
|
||||
"type": coalesce(actType, "ir.actions.act_window"),
|
||||
"name": name,
|
||||
"res_model": resModel,
|
||||
"view_mode": coalesce(viewMode, "list,form"),
|
||||
"views": views,
|
||||
"search_view_id": false,
|
||||
"domain": coalesce(deref(domain), "[]"),
|
||||
"context": coalesce(deref(actCtx), "{}"),
|
||||
"target": coalesce(deref(target), "current"),
|
||||
"limit": coalesceInt(limit, 80),
|
||||
"help": deref(help),
|
||||
"xml_id": xmlID,
|
||||
}
|
||||
|
||||
// Include res_id if set (for single-record actions like Settings)
|
||||
if resID != nil && *resID != 0 {
|
||||
action["res_id"] = *resID
|
||||
}
|
||||
|
||||
s.writeJSONRPC(w, req.ID, action, nil)
|
||||
}
|
||||
|
||||
// parseNumericString tries to parse a string as a positive integer.
|
||||
func parseNumericString(s string) (int64, bool) {
|
||||
if len(s) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
var n int64
|
||||
for _, c := range s {
|
||||
if c < '0' || c > '9' {
|
||||
return 0, false
|
||||
}
|
||||
n = n*10 + int64(c-'0')
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
|
||||
// buildViewsFromMode converts a view_mode string like "list,kanban,form"
|
||||
// into the webclient format: [[nil,"list"],[nil,"kanban"],[nil,"form"]].
|
||||
func buildViewsFromMode(viewMode string) [][]interface{} {
|
||||
modes := strings.Split(viewMode, ",")
|
||||
views := make([][]interface{}, 0, len(modes))
|
||||
for _, m := range modes {
|
||||
m = strings.TrimSpace(m)
|
||||
if m != "" {
|
||||
views = append(views, []interface{}{nil, m})
|
||||
}
|
||||
}
|
||||
return views
|
||||
}
|
||||
|
||||
// coalesce returns the first non-empty string.
|
||||
func coalesce(vals ...string) string {
|
||||
for _, v := range vals {
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// coalesceInt returns *p if non-nil, otherwise the fallback.
|
||||
func coalesceInt(p *int, fallback int) int {
|
||||
if p != nil {
|
||||
return *p
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// deref returns the value of a string pointer, or "" if nil.
|
||||
func deref(p *string) string {
|
||||
if p != nil {
|
||||
return *p
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1,22 +1,117 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// menuRow holds a single row from the ir_ui_menu table joined with ir_model_data.
|
||||
type menuRow struct {
|
||||
ID int
|
||||
Name string
|
||||
ParentID *int // NULL for top-level menus
|
||||
Sequence int
|
||||
Action *string // e.g. "ir.actions.act_window,1"
|
||||
WebIcon *string
|
||||
XMLID string // e.g. "contacts.menu_contacts" from ir_model_data
|
||||
}
|
||||
|
||||
// handleLoadMenus returns the menu tree for the webclient.
|
||||
// Mirrors: odoo/addons/web/controllers/home.py Home.web_load_menus()
|
||||
//
|
||||
// Loads menus from the ir_ui_menu table, builds the parent/child tree,
|
||||
// and returns the JSON map keyed by string IDs that the webclient expects.
|
||||
func (s *Server) handleLoadMenus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=3600")
|
||||
|
||||
// Build menu tree from database or hardcoded defaults
|
||||
menus := map[string]interface{}{
|
||||
"root": map[string]interface{}{
|
||||
ctx := r.Context()
|
||||
menus, err := s.loadWebMenus(ctx)
|
||||
if err != nil {
|
||||
log.Printf("menus: error loading menus: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(menus)
|
||||
}
|
||||
|
||||
// loadWebMenus queries ir_ui_menu and builds the JSON structure the webclient expects.
|
||||
// Mirrors: odoo/addons/base/models/ir_ui_menu.py IrUiMenu.load_web_menus()
|
||||
func (s *Server) loadWebMenus(ctx context.Context) (map[string]interface{}, error) {
|
||||
// Query all active menus, joined with ir_model_data for the xmlid.
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT m.id, m.name, m.parent_id, m.sequence, m.action, m.web_icon,
|
||||
COALESCE(d.module || '.' || d.name, '') AS xmlid
|
||||
FROM ir_ui_menu m
|
||||
LEFT JOIN ir_model_data d
|
||||
ON d.model = 'ir.ui.menu' AND d.res_id = m.id
|
||||
WHERE m.active = true
|
||||
ORDER BY m.sequence, m.id`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query ir_ui_menu: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var allMenus []menuRow
|
||||
for rows.Next() {
|
||||
var mr menuRow
|
||||
if err := rows.Scan(&mr.ID, &mr.Name, &mr.ParentID, &mr.Sequence, &mr.Action, &mr.WebIcon, &mr.XMLID); err != nil {
|
||||
return nil, fmt.Errorf("scan menu row: %w", err)
|
||||
}
|
||||
allMenus = append(allMenus, mr)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate menu rows: %w", err)
|
||||
}
|
||||
|
||||
// Build children map: parent_id → list of child IDs
|
||||
childrenOf := make(map[int][]int)
|
||||
for _, m := range allMenus {
|
||||
if m.ParentID != nil {
|
||||
childrenOf[*m.ParentID] = append(childrenOf[*m.ParentID], m.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Find the root app ID for each menu by walking up to the top-level ancestor.
|
||||
// Top-level menus (parent_id IS NULL) are their own appID.
|
||||
menuByID := make(map[int]*menuRow, len(allMenus))
|
||||
for i := range allMenus {
|
||||
menuByID[allMenus[i].ID] = &allMenus[i]
|
||||
}
|
||||
|
||||
appIDOf := func(id int) int {
|
||||
cur := id
|
||||
for {
|
||||
m, ok := menuByID[cur]
|
||||
if !ok || m.ParentID == nil {
|
||||
return cur
|
||||
}
|
||||
cur = *m.ParentID
|
||||
}
|
||||
}
|
||||
|
||||
// Build the result map
|
||||
result := make(map[string]interface{}, len(allMenus)+1)
|
||||
|
||||
// Collect top-level menu IDs (parent_id IS NULL) for the root entry
|
||||
var rootChildren []int
|
||||
for _, m := range allMenus {
|
||||
if m.ParentID == nil {
|
||||
rootChildren = append(rootChildren, m.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// "root" pseudo-entry
|
||||
result["root"] = map[string]interface{}{
|
||||
"id": "root",
|
||||
"name": "root",
|
||||
"children": []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 100},
|
||||
"children": rootChildren,
|
||||
"appID": false,
|
||||
"xmlid": "",
|
||||
"actionID": false,
|
||||
@@ -26,354 +121,57 @@ func (s *Server) handleLoadMenus(w http.ResponseWriter, r *http.Request) {
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
// Contacts
|
||||
"1": map[string]interface{}{
|
||||
"id": 1,
|
||||
"name": "Contacts",
|
||||
"children": []int{10},
|
||||
"appID": 1,
|
||||
"xmlid": "contacts.menu_contacts",
|
||||
"actionID": 1,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": "fa-address-book,#71639e,#FFFFFF",
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"10": map[string]interface{}{
|
||||
"id": 10,
|
||||
"name": "Contacts",
|
||||
"children": []int{},
|
||||
"appID": 1,
|
||||
"xmlid": "contacts.menu_contacts_list",
|
||||
"actionID": 1,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
// Invoicing
|
||||
"2": map[string]interface{}{
|
||||
"id": 2,
|
||||
"name": "Invoicing",
|
||||
"children": []int{20},
|
||||
"appID": 2,
|
||||
"xmlid": "account.menu_finance",
|
||||
"actionID": 2,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": "fa-book,#71639e,#FFFFFF",
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"20": map[string]interface{}{
|
||||
"id": 20,
|
||||
"name": "Invoices",
|
||||
"children": []int{},
|
||||
"appID": 2,
|
||||
"xmlid": "account.menu_finance_invoices",
|
||||
"actionID": 2,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
// Sales
|
||||
"3": map[string]interface{}{
|
||||
"id": 3,
|
||||
"name": "Sales",
|
||||
"children": []int{30},
|
||||
"appID": 3,
|
||||
"xmlid": "sale.menu_sale_root",
|
||||
"actionID": 3,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": "fa-bar-chart,#71639e,#FFFFFF",
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"30": map[string]interface{}{
|
||||
"id": 30,
|
||||
"name": "Orders",
|
||||
"children": []int{},
|
||||
"appID": 3,
|
||||
"xmlid": "sale.menu_sale_orders",
|
||||
"actionID": 3,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
// CRM
|
||||
"4": map[string]interface{}{
|
||||
"id": 4,
|
||||
"name": "CRM",
|
||||
"children": []int{40},
|
||||
"appID": 4,
|
||||
"xmlid": "crm.menu_crm_root",
|
||||
"actionID": 4,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": "fa-star,#71639e,#FFFFFF",
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"40": map[string]interface{}{
|
||||
"id": 40,
|
||||
"name": "Pipeline",
|
||||
"children": []int{},
|
||||
"appID": 4,
|
||||
"xmlid": "crm.menu_crm_pipeline",
|
||||
"actionID": 4,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
// Inventory / Stock
|
||||
"5": map[string]interface{}{
|
||||
"id": 5,
|
||||
"name": "Inventory",
|
||||
"children": []int{50, 51},
|
||||
"appID": 5,
|
||||
"xmlid": "stock.menu_stock_root",
|
||||
"actionID": 5,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": "fa-cubes,#71639e,#FFFFFF",
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"50": map[string]interface{}{
|
||||
"id": 50,
|
||||
"name": "Transfers",
|
||||
"children": []int{},
|
||||
"appID": 5,
|
||||
"xmlid": "stock.menu_stock_transfers",
|
||||
"actionID": 5,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"51": map[string]interface{}{
|
||||
"id": 51,
|
||||
"name": "Products",
|
||||
"children": []int{},
|
||||
"appID": 5,
|
||||
"xmlid": "stock.menu_stock_products",
|
||||
"actionID": 6,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
// Purchase
|
||||
"6": map[string]interface{}{
|
||||
"id": 6,
|
||||
"name": "Purchase",
|
||||
"children": []int{60},
|
||||
"appID": 6,
|
||||
"xmlid": "purchase.menu_purchase_root",
|
||||
"actionID": 7,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": "fa-shopping-cart,#71639e,#FFFFFF",
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"60": map[string]interface{}{
|
||||
"id": 60,
|
||||
"name": "Purchase Orders",
|
||||
"children": []int{},
|
||||
"appID": 6,
|
||||
"xmlid": "purchase.menu_purchase_orders",
|
||||
"actionID": 7,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
// Employees / HR
|
||||
"7": map[string]interface{}{
|
||||
"id": 7,
|
||||
"name": "Employees",
|
||||
"children": []int{70, 71},
|
||||
"appID": 7,
|
||||
"xmlid": "hr.menu_hr_root",
|
||||
"actionID": 8,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": "fa-users,#71639e,#FFFFFF",
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"70": map[string]interface{}{
|
||||
"id": 70,
|
||||
"name": "Employees",
|
||||
"children": []int{},
|
||||
"appID": 7,
|
||||
"xmlid": "hr.menu_hr_employees",
|
||||
"actionID": 8,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"71": map[string]interface{}{
|
||||
"id": 71,
|
||||
"name": "Departments",
|
||||
"children": []int{},
|
||||
"appID": 7,
|
||||
"xmlid": "hr.menu_hr_departments",
|
||||
"actionID": 9,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
// Project
|
||||
"8": map[string]interface{}{
|
||||
"id": 8,
|
||||
"name": "Project",
|
||||
"children": []int{80, 81},
|
||||
"appID": 8,
|
||||
"xmlid": "project.menu_project_root",
|
||||
"actionID": 10,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": "fa-puzzle-piece,#71639e,#FFFFFF",
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"80": map[string]interface{}{
|
||||
"id": 80,
|
||||
"name": "Projects",
|
||||
"children": []int{},
|
||||
"appID": 8,
|
||||
"xmlid": "project.menu_projects",
|
||||
"actionID": 10,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"81": map[string]interface{}{
|
||||
"id": 81,
|
||||
"name": "Tasks",
|
||||
"children": []int{},
|
||||
"appID": 8,
|
||||
"xmlid": "project.menu_project_tasks",
|
||||
"actionID": 11,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
// Fleet
|
||||
"9": map[string]interface{}{
|
||||
"id": 9,
|
||||
"name": "Fleet",
|
||||
"children": []int{90},
|
||||
"appID": 9,
|
||||
"xmlid": "fleet.menu_fleet_root",
|
||||
"actionID": 12,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": "fa-car,#71639e,#FFFFFF",
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"90": map[string]interface{}{
|
||||
"id": 90,
|
||||
"name": "Vehicles",
|
||||
"children": []int{},
|
||||
"appID": 9,
|
||||
"xmlid": "fleet.menu_fleet_vehicles",
|
||||
"actionID": 12,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
// Settings
|
||||
"100": map[string]interface{}{
|
||||
"id": 100,
|
||||
"name": "Settings",
|
||||
"children": []int{101, 102},
|
||||
"appID": 100,
|
||||
"xmlid": "base.menu_administration",
|
||||
"actionID": 100,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": "fa-cog,#71639e,#FFFFFF",
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"101": map[string]interface{}{
|
||||
"id": 101,
|
||||
"name": "Users & Companies",
|
||||
"children": []int{},
|
||||
"appID": 100,
|
||||
"xmlid": "base.menu_users",
|
||||
"actionID": 101,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
"102": map[string]interface{}{
|
||||
"id": 102,
|
||||
"name": "Technical",
|
||||
"children": []int{},
|
||||
"appID": 100,
|
||||
"xmlid": "base.menu_custom",
|
||||
"actionID": 102,
|
||||
"actionModel": "ir.actions.act_window",
|
||||
"actionPath": false,
|
||||
"webIcon": nil,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
},
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(menus)
|
||||
// Each menu entry
|
||||
for _, m := range allMenus {
|
||||
children := childrenOf[m.ID]
|
||||
if children == nil {
|
||||
children = []int{}
|
||||
}
|
||||
|
||||
actionID, actionModel := parseMenuAction(m.Action)
|
||||
appID := appIDOf(m.ID)
|
||||
|
||||
var webIcon interface{}
|
||||
if m.WebIcon != nil && *m.WebIcon != "" {
|
||||
webIcon = *m.WebIcon
|
||||
}
|
||||
|
||||
result[strconv.Itoa(m.ID)] = map[string]interface{}{
|
||||
"id": m.ID,
|
||||
"name": m.Name,
|
||||
"children": children,
|
||||
"appID": appID,
|
||||
"xmlid": m.XMLID,
|
||||
"actionID": actionID,
|
||||
"actionModel": actionModel,
|
||||
"actionPath": false,
|
||||
"webIcon": webIcon,
|
||||
"webIconData": nil,
|
||||
"webIconDataMimetype": nil,
|
||||
"backgroundImage": nil,
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// parseMenuAction parses the Odoo action reference format "ir.actions.act_window,123"
|
||||
// into an action ID (int or false) and action model (string or false).
|
||||
// Mirrors: odoo/addons/base/models/ir_ui_menu.py _compute_action()
|
||||
func parseMenuAction(action *string) (interface{}, interface{}) {
|
||||
if action == nil || *action == "" {
|
||||
return false, false
|
||||
}
|
||||
parts := strings.SplitN(*action, ",", 2)
|
||||
if len(parts) != 2 {
|
||||
return false, false
|
||||
}
|
||||
model := parts[0]
|
||||
id, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
return id, model
|
||||
}
|
||||
|
||||
@@ -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