package models import ( "fmt" "odoo-go/pkg/orm" ) // initIrModel registers ir.model and ir.model.fields — Odoo's model metadata. // Mirrors: odoo/addons/base/models/ir_model.py // // These meta-models describe all other models at runtime. // They are the foundation for dynamic field access, view generation, and access control. func initIrModel() { // ir.model — Model metadata m := orm.NewModel("ir.model", orm.ModelOpts{ Description: "Models", Order: "model", RecName: "name", }) m.AddFields( orm.Char("name", orm.FieldOpts{String: "Model Description", Required: true, Translate: true}), orm.Char("model", orm.FieldOpts{String: "Model", Required: true, Index: true}), orm.Char("info", orm.FieldOpts{String: "Information"}), orm.One2many("field_id", "ir.model.fields", "model_id", orm.FieldOpts{String: "Fields"}), orm.One2many("access_ids", "ir.model.access", "model_id", orm.FieldOpts{String: "Access"}), orm.One2many("rule_ids", "ir.rule", "model_id", orm.FieldOpts{String: "Record Rules"}), orm.Char("state", orm.FieldOpts{String: "Type", Default: "base"}), orm.Boolean("transient", orm.FieldOpts{String: "Transient Model"}), orm.Many2many("group_ids", "res.groups", orm.FieldOpts{String: "Groups"}), ) // ir.model.fields — Field metadata f := orm.NewModel("ir.model.fields", orm.ModelOpts{ Description: "Fields", Order: "model_id, name", RecName: "name", }) f.AddFields( orm.Char("name", orm.FieldOpts{String: "Field Name", Required: true, Index: true}), orm.Char("field_description", orm.FieldOpts{String: "Field Label", Translate: true}), orm.Many2one("model_id", "ir.model", orm.FieldOpts{ String: "Model", Required: true, OnDelete: orm.OnDeleteCascade, Index: true, }), orm.Char("model", orm.FieldOpts{String: "Model Name", Related: "model_id.model"}), orm.Selection("ttype", []orm.SelectionItem{ {Value: "char", Label: "Char"}, {Value: "text", Label: "Text"}, {Value: "html", Label: "Html"}, {Value: "integer", Label: "Integer"}, {Value: "float", Label: "Float"}, {Value: "monetary", Label: "Monetary"}, {Value: "boolean", Label: "Boolean"}, {Value: "date", Label: "Date"}, {Value: "datetime", Label: "Datetime"}, {Value: "binary", Label: "Binary"}, {Value: "selection", Label: "Selection"}, {Value: "many2one", Label: "Many2one"}, {Value: "one2many", Label: "One2many"}, {Value: "many2many", Label: "Many2many"}, {Value: "reference", Label: "Reference"}, }, orm.FieldOpts{String: "Field Type", Required: true}), orm.Char("relation", orm.FieldOpts{String: "Related Model"}), orm.Char("relation_field", orm.FieldOpts{String: "Relation Field"}), orm.Boolean("required", orm.FieldOpts{String: "Required"}), orm.Boolean("readonly", orm.FieldOpts{String: "Readonly"}), orm.Boolean("index", orm.FieldOpts{String: "Indexed"}), orm.Boolean("store", orm.FieldOpts{String: "Stored", Default: true}), orm.Char("compute", orm.FieldOpts{String: "Compute"}), orm.Char("depends", orm.FieldOpts{String: "Dependencies"}), orm.Text("help", orm.FieldOpts{String: "Field Help"}), orm.Char("state", orm.FieldOpts{String: "Type", Default: "base"}), orm.Integer("size", orm.FieldOpts{String: "Size"}), orm.Char("on_delete", orm.FieldOpts{String: "On Delete", Default: "set null"}), orm.Boolean("translate", orm.FieldOpts{String: "Translatable"}), orm.Text("selection_ids", orm.FieldOpts{String: "Selection Options"}), orm.Char("copied", orm.FieldOpts{String: "Copied", Default: "1"}), orm.Many2many("group_ids", "res.groups", orm.FieldOpts{String: "Groups"}), ) // ir.module.category — Module categories for group organization orm.NewModel("ir.module.category", orm.ModelOpts{ Description: "Application", Order: "name", }).AddFields( orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}), orm.Many2one("parent_id", "ir.module.category", orm.FieldOpts{String: "Parent Application"}), orm.One2many("child_ids", "ir.module.category", "parent_id", orm.FieldOpts{String: "Child Applications"}), orm.Integer("sequence", orm.FieldOpts{String: "Sequence"}), orm.Boolean("visible", orm.FieldOpts{String: "Visible", Default: true}), ) } // initIrModelAccess registers ir.model.access — Object-level ACLs. // Mirrors: odoo/addons/base/models/ir_model.py class IrModelAccess // // Access is defined as CSV in each module: // // id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink // access_res_partner,res.partner,model_res_partner,base.group_user,1,1,1,0 func initIrModelAccess() { m := orm.NewModel("ir.model.access", orm.ModelOpts{ Description: "Access Controls", Order: "model_id, group_id", }) m.AddFields( orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Index: true}), orm.Many2one("model_id", "ir.model", orm.FieldOpts{ String: "Model", Required: true, OnDelete: orm.OnDeleteCascade, Index: true, }), orm.Many2one("group_id", "res.groups", orm.FieldOpts{ String: "Group", OnDelete: orm.OnDeleteRestrict, Index: true, }), orm.Boolean("perm_read", orm.FieldOpts{String: "Read Access"}), orm.Boolean("perm_write", orm.FieldOpts{String: "Write Access"}), orm.Boolean("perm_create", orm.FieldOpts{String: "Create Access"}), orm.Boolean("perm_unlink", orm.FieldOpts{String: "Delete Access"}), orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}), ) } // initIrRule registers ir.rule — Record-level access rules. // Mirrors: odoo/addons/base/models/ir_rule.py class IrRule // // Record rules add WHERE clause filters per user/group: // // Rule: domain = [('company_id', 'in', company_ids)] // → User can only see records of their companies func initIrRule() { m := orm.NewModel("ir.rule", orm.ModelOpts{ Description: "Record Rule", Order: "model_id, name", }) m.AddFields( orm.Char("name", orm.FieldOpts{String: "Name"}), orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}), orm.Many2one("model_id", "ir.model", orm.FieldOpts{ String: "Model", Required: true, OnDelete: orm.OnDeleteCascade, Index: true, }), orm.Many2many("groups", "res.groups", orm.FieldOpts{String: "Groups"}), orm.Text("domain_force", orm.FieldOpts{String: "Domain"}), orm.Boolean("perm_read", orm.FieldOpts{String: "Apply for Read", Default: true}), orm.Boolean("perm_write", orm.FieldOpts{String: "Apply for Write", Default: true}), orm.Boolean("perm_create", orm.FieldOpts{String: "Apply for Create", Default: true}), orm.Boolean("perm_unlink", orm.FieldOpts{String: "Apply for Delete", Default: true}), orm.Boolean("global", orm.FieldOpts{ String: "Global", Compute: "_compute_global", Store: true, Help: "If no group is specified, the rule is global and applied to everyone", }), ) } // initIrModelData registers ir.model.data — External identifiers (XML IDs). // Mirrors: odoo/addons/base/models/ir_model.py class IrModelData // // Maps module.xml_id → (model, res_id) for referencing records across modules. // Example: 'base.main_company' → res.company(1) func initIrModelData() { m := orm.NewModel("ir.model.data", orm.ModelOpts{ Description: "External Identifiers", Order: "module, name", RecName: "complete_name", }) m.AddFields( orm.Char("name", orm.FieldOpts{String: "External Identifier", Required: true, Index: true}), orm.Char("complete_name", orm.FieldOpts{String: "Complete ID", Compute: "_compute_complete_name"}), orm.Char("module", orm.FieldOpts{String: "Module", Required: true, Index: true, Default: ""}), orm.Char("model", orm.FieldOpts{String: "Model Name", Required: true}), orm.Integer("res_id", orm.FieldOpts{String: "Record ID", Index: true}), orm.Boolean("noupdate", orm.FieldOpts{String: "Non Updatable", Default: false}), ) } // initIrFilter registers ir.filters — Saved search filters (Favorites). // Mirrors: odoo/addons/base/models/ir_filters.py class IrFilters // // Filters are saved by the web client when a user bookmarks a search. // user_id = NULL means the filter is shared with everyone. func initIrFilter() { m := orm.NewModel("ir.filters", orm.ModelOpts{ Description: "Filters", Order: "model_id, name, id desc", RecName: "name", }) m.AddFields( orm.Char("name", orm.FieldOpts{String: "Filter Name", Required: true, Translate: true}), orm.Many2one("user_id", "res.users", orm.FieldOpts{ String: "User", OnDelete: orm.OnDeleteCascade, Help: "The user this filter is private to. When left empty the filter is shared.", }), orm.Text("domain", orm.FieldOpts{String: "Domain", Required: true, Default: "[]"}), orm.Text("context", orm.FieldOpts{String: "Context", Required: true, Default: "{}"}), orm.Text("sort", orm.FieldOpts{String: "Sort", Required: true, Default: "[]"}), orm.Char("model_id", orm.FieldOpts{ String: "Model", Required: true, Help: "Model name of the filtered view, e.g. 'res.partner'.", }), orm.Boolean("is_default", orm.FieldOpts{String: "Default Filter"}), orm.Many2one("action_id", "ir.act.window", orm.FieldOpts{ String: "Action", OnDelete: orm.OnDeleteCascade, Help: "The menu action this filter applies to.", }), orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}), ) // create_or_replace: Creates or updates a filter by (name, model_id, user_id, action_id). // Mirrors: odoo/addons/base/models/ir_filters.py IrFilters.create_or_replace() // Called by the web client when saving a favorite. m.RegisterMethod("create_or_replace", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { if len(args) == 0 { return nil, fmt.Errorf("ir.filters: create_or_replace requires a filter dict") } vals, ok := args[0].(orm.Values) if !ok { return nil, fmt.Errorf("ir.filters: create_or_replace expects Values argument") } env := rs.Env() name, _ := vals["name"].(string) modelID, _ := vals["model_id"].(string) // Build lookup query query := `SELECT id FROM ir_filters WHERE name = $1 AND model_id = $2` qArgs := []interface{}{name, modelID} idx := 3 // user_id if uid, ok := vals["user_id"]; ok && uid != nil { query += fmt.Sprintf(` AND user_id = $%d`, idx) qArgs = append(qArgs, uid) idx++ } else { query += ` AND user_id IS NULL` } // action_id if aid, ok := vals["action_id"]; ok && aid != nil { query += fmt.Sprintf(` AND action_id = $%d`, idx) qArgs = append(qArgs, aid) } else { query += ` AND action_id IS NULL` } query += ` LIMIT 1` var existingID int64 err := env.Tx().QueryRow(env.Ctx(), query, qArgs...).Scan(&existingID) if err == nil && existingID > 0 { // Update existing existing := rs.Browse(existingID) if err := existing.Write(vals); err != nil { return nil, err } return existingID, nil } // Create new created, err := env.Model("ir.filters").Create(vals) if err != nil { return nil, err } return created.ID(), nil }) } // initIrDefault registers ir.default — User-defined field defaults. // Mirrors: odoo/addons/base/models/ir_default.py class IrDefault // // Stores default values for specific fields, optionally scoped to a user or company. // Example: default value for sale.order.payment_term_id for company 1. func initIrDefault() { m := orm.NewModel("ir.default", orm.ModelOpts{ Description: "Default Values", Order: "id", }) m.AddFields( // In Python Odoo this is Many2one to ir.model.fields. // We use Char for now since ir.model.fields may not be fully populated. orm.Char("field_id", orm.FieldOpts{ String: "Field", Required: true, Index: true, Help: "Reference to the field, format: 'model_name.field_name'.", }), orm.Text("json_value", orm.FieldOpts{ String: "Default Value (JSON)", Help: "JSON-encoded default value for the field.", }), orm.Many2one("user_id", "res.users", orm.FieldOpts{ String: "User", OnDelete: orm.OnDeleteCascade, Help: "If set, this default only applies to this user.", }), orm.Many2one("company_id", "res.company", orm.FieldOpts{ String: "Company", OnDelete: orm.OnDeleteCascade, Help: "If set, this default only applies in this company.", }), orm.Char("condition", orm.FieldOpts{ String: "Condition", Help: "Optional condition for applying this default.", }), ) }