package orm import ( "fmt" "strings" ) // Model defines an Odoo model (database table + business logic). // Mirrors: odoo/orm/models.py BaseModel // // Odoo: // // class ResPartner(models.Model): // _name = 'res.partner' // _description = 'Contact' // _order = 'name' // _rec_name = 'name' // // Go: // // NewModel("res.partner", ModelOpts{ // Description: "Contact", // Order: "name", // RecName: "name", // }) type Model struct { name string description string modelType ModelType table string // SQL table name order string // Default sort order recName string // Field used as display name inherit []string // _inherit: models to extend inherits map[string]string // _inherits: {parent_model: fk_field} auto bool // Auto-create/update table schema logAccess bool // Auto-create create_uid, write_uid, create_date, write_date // Fields fields map[string]*Field // Methods (registered business logic) methods map[string]interface{} // Access control checkCompany bool // Enforce multi-company record rules // Hooks BeforeCreate func(env *Environment, vals Values) error // Called before INSERT DefaultGet func(env *Environment, fields []string) Values // Dynamic defaults (e.g., from DB) Constraints []ConstraintFunc // Validation constraints Methods map[string]MethodFunc // Named business methods // SQL-level constraints SQLConstraints []SQLConstraint // Computed fields computes map[string]ComputeFunc // field_name → compute function dependencyMap map[string][]string // trigger_field → []computed_field_names // Onchange handlers // Maps field_name → handler that receives current vals and returns computed updates. // Mirrors: @api.onchange in Odoo. OnchangeHandlers map[string]func(env *Environment, vals Values) Values // Resolved parents []*Model // Resolved parent models from _inherit allFields map[string]*Field // Including fields from parents fieldOrder []string // Field creation order } // ModelOpts configures a new model. type ModelOpts struct { Description string Type ModelType Table string Order string RecName string Inherit []string Inherits map[string]string CheckCompany bool } // NewModel creates and registers a new model. // Mirrors: class MyModel(models.Model): _name = '...' func NewModel(name string, opts ModelOpts) *Model { m := &Model{ name: name, description: opts.Description, modelType: opts.Type, table: opts.Table, order: opts.Order, recName: opts.RecName, inherit: opts.Inherit, inherits: opts.Inherits, checkCompany: opts.CheckCompany, auto: opts.Type != ModelAbstract, logAccess: true, fields: make(map[string]*Field), methods: make(map[string]interface{}), allFields: make(map[string]*Field), } // Derive table name from model name if not specified. // Mirrors: odoo/orm/models.py BaseModel._table // 'res.partner' → 'res_partner' if m.table == "" && m.auto { m.table = strings.ReplaceAll(name, ".", "_") } if m.order == "" { m.order = "id" } if m.recName == "" { m.recName = "name" } // Add magic fields. // Mirrors: odoo/orm/models.py BaseModel.MAGIC_COLUMNS m.addMagicFields() Registry.Register(m) return m } // addMagicFields adds Odoo's automatic fields to every model. // Mirrors: odoo/orm/models.py MAGIC_COLUMNS + LOG_ACCESS_COLUMNS func (m *Model) addMagicFields() { // id is always present m.AddField(Integer("id", FieldOpts{ String: "ID", Readonly: true, })) // display_name: computed on-the-fly from rec_name, not stored in DB. // Mirrors: odoo/orm/models.py BaseModel.display_name (computed field on ALL models) m.AddField(Char("display_name", FieldOpts{ String: "Display Name", Compute: "_compute_display_name", })) if m.logAccess { m.AddField(Many2one("create_uid", "res.users", FieldOpts{ String: "Created by", Readonly: true, })) m.AddField(Datetime("create_date", FieldOpts{ String: "Created on", Readonly: true, })) m.AddField(Many2one("write_uid", "res.users", FieldOpts{ String: "Last Updated by", Readonly: true, })) m.AddField(Datetime("write_date", FieldOpts{ String: "Last Updated on", Readonly: true, })) } } // AddField adds a field to this model. func (m *Model) AddField(f *Field) *Model { f.model = m m.fields[f.Name] = f m.allFields[f.Name] = f m.fieldOrder = append(m.fieldOrder, f.Name) return m } // AddFields adds multiple fields at once. func (m *Model) AddFields(fields ...*Field) *Model { for _, f := range fields { m.AddField(f) } return m } // GetField returns a field by name, or nil. func (m *Model) GetField(name string) *Field { return m.allFields[name] } // Fields returns all fields of this model (including inherited). func (m *Model) Fields() map[string]*Field { return m.allFields } // StoredFields returns fields that have a database column. func (m *Model) StoredFields() []*Field { var result []*Field for _, name := range m.fieldOrder { f := m.fields[name] if f.IsStored() { result = append(result, f) } } return result } // Name returns the model name (e.g., "res.partner"). func (m *Model) Name() string { return m.name } // Table returns the SQL table name (e.g., "res_partner"). func (m *Model) Table() string { return m.table } // Description returns the human-readable model description. func (m *Model) Description() string { return m.description } // Order returns the default sort expression. func (m *Model) Order() string { return m.order } // RecName returns the field name used as display name. func (m *Model) RecName() string { return m.recName } // IsAbstract returns true if this model has no database table. func (m *Model) IsAbstract() bool { return m.modelType == ModelAbstract } // IsTransient returns true if this is a wizard/temporary model. func (m *Model) IsTransient() bool { return m.modelType == ModelTransient } // SQLConstraint represents a SQL-level constraint on a model's table. // Mirrors: _sql_constraints in Odoo. type SQLConstraint struct { Name string // Constraint name Definition string // SQL definition (e.g., "UNIQUE(name, company_id)") Message string // Error message } // AddSQLConstraint registers a SQL-level constraint on this model. func (m *Model) AddSQLConstraint(name, definition, message string) { m.SQLConstraints = append(m.SQLConstraints, SQLConstraint{Name: name, Definition: definition, Message: message}) } // ConstraintFunc validates a recordset. Returns error if constraint violated. // Mirrors: @api.constrains in Odoo. type ConstraintFunc func(rs *Recordset) error // MethodFunc is a callable business method on a model. // Mirrors: Odoo model methods called via RPC. type MethodFunc func(rs *Recordset, args ...interface{}) (interface{}, error) // AddConstraint registers a validation constraint. func (m *Model) AddConstraint(fn ConstraintFunc) *Model { m.Constraints = append(m.Constraints, fn) return m } // RegisterMethod registers a named business logic method on this model. // Mirrors: Odoo's method definitions on model classes. func (m *Model) RegisterMethod(name string, fn MethodFunc) *Model { if m.Methods == nil { m.Methods = make(map[string]MethodFunc) } m.Methods[name] = fn return m } // RegisterOnchange registers an onchange handler for a field. // When the field changes on the client, the handler is called with the current // record values and returns computed field updates. // Mirrors: @api.onchange('field_name') in Odoo. func (m *Model) RegisterOnchange(fieldName string, handler func(env *Environment, vals Values) Values) { if m.OnchangeHandlers == nil { m.OnchangeHandlers = make(map[string]func(env *Environment, vals Values) Values) } m.OnchangeHandlers[fieldName] = handler } // Extend extends this model with additional fields (like _inherit in Odoo). // Mirrors: class MyModelExt(models.Model): _inherit = 'res.partner' func (m *Model) Extend(fields ...*Field) *Model { for _, f := range fields { m.AddField(f) } return m } // CreateTableSQL generates the CREATE TABLE statement for this model. // Mirrors: odoo/orm/models.py BaseModel._table_exist / init() func (m *Model) CreateTableSQL() string { if !m.auto { return "" } var cols []string cols = append(cols, `"id" SERIAL PRIMARY KEY`) for _, name := range m.fieldOrder { f := m.fields[name] if name == "id" || !f.IsStored() { continue } sqlType := f.SQLType() if sqlType == "" { continue } col := fmt.Sprintf(" %q %s", f.Column(), sqlType) if f.Required { col += " NOT NULL" } cols = append(cols, col) } return fmt.Sprintf("CREATE TABLE IF NOT EXISTS %q (\n%s\n)", m.table, strings.Join(cols, ",\n"), ) } // ForeignKeySQL generates ALTER TABLE statements for foreign keys. func (m *Model) ForeignKeySQL() []string { var stmts []string for _, name := range m.fieldOrder { f := m.fields[name] if f.Type != TypeMany2one || f.Comodel == "" { continue } if name == "create_uid" || name == "write_uid" { continue // Skip magic fields, added later } comodel := Registry.Get(f.Comodel) if comodel == nil { continue } onDelete := "SET NULL" switch f.OnDelete { case OnDeleteRestrict: onDelete = "RESTRICT" case OnDeleteCascade: onDelete = "CASCADE" } stmt := fmt.Sprintf( `ALTER TABLE %q ADD CONSTRAINT %q FOREIGN KEY (%q) REFERENCES %q ("id") ON DELETE %s`, m.table, fmt.Sprintf("%s_%s_fkey", m.table, f.Column()), f.Column(), comodel.Table(), onDelete, ) stmts = append(stmts, stmt) } return stmts } // IndexSQL generates CREATE INDEX statements. func (m *Model) IndexSQL() []string { var stmts []string for _, name := range m.fieldOrder { f := m.fields[name] if !f.Index || !f.IsStored() || name == "id" { continue } stmt := fmt.Sprintf( `CREATE INDEX IF NOT EXISTS %q ON %q (%q)`, fmt.Sprintf("%s_%s_index", m.table, f.Column()), m.table, f.Column(), ) stmts = append(stmts, stmt) } return stmts } // Many2manyTableSQL generates junction table DDL for Many2many fields. func (m *Model) Many2manyTableSQL() []string { var stmts []string for _, name := range m.fieldOrder { f := m.fields[name] if f.Type != TypeMany2many { continue } comodel := Registry.Get(f.Comodel) if comodel == nil { continue } // Determine junction table name. // Mirrors: odoo/orm/fields_relational.py Many2many._table_name relation := f.Relation if relation == "" { // Default: alphabetically sorted model tables joined by underscore t1, t2 := m.table, comodel.Table() if t1 > t2 { t1, t2 = t2, t1 } relation = fmt.Sprintf("%s_%s_rel", t1, t2) } col1 := f.Column1 if col1 == "" { col1 = m.table + "_id" } col2 := f.Column2 if col2 == "" { col2 = comodel.Table() + "_id" } // Self-referential M2M: ensure distinct column names if col1 == col2 { col1 = m.table + "_src_id" col2 = m.table + "_dst_id" } stmt := fmt.Sprintf( "CREATE TABLE IF NOT EXISTS %q (\n"+ " %q integer NOT NULL REFERENCES %q(id) ON DELETE CASCADE,\n"+ " %q integer NOT NULL REFERENCES %q(id) ON DELETE CASCADE,\n"+ " PRIMARY KEY (%q, %q)\n"+ ")", relation, col1, m.table, col2, comodel.Table(), col1, col2, ) stmts = append(stmts, stmt) } return stmts }