package orm import ( "fmt" "time" ) // Field defines a model field. // Mirrors: odoo/orm/fields.py Field class // // Odoo field declaration: // // name = fields.Char(string='Name', required=True, index=True) // // Go equivalent: // // Char("name", FieldOpts{String: "Name", Required: true, Index: true}) type Field struct { Name string // Technical name (e.g., "name", "partner_id") Type FieldType // Field type (TypeChar, TypeMany2one, etc.) String string // Human-readable label // Constraints Required bool Readonly bool Index bool // Create database index Unique bool // Unique constraint (not in Odoo, but useful) Size int // Max length for Char fields Default Value // Default value or function name Help string // Tooltip / help text // Computed fields // Mirrors: odoo/orm/fields.py Field.compute, Field.inverse, Field.depends Compute string // Method name to compute this field Inverse string // Method name to write back computed value Depends []string // Field names this compute depends on Store bool // Store computed value in DB (default: false for computed) Precompute bool // Compute before first DB flush // Related fields // Mirrors: odoo/orm/fields.py Field.related Related string // Dot-separated path (e.g., "partner_id.name") // Selection Selection []SelectionItem // Relational fields // Mirrors: odoo/orm/fields_relational.py Comodel string // Target model name (e.g., "res.company") InverseField string // For One2many: field name in comodel pointing back Relation string // For Many2many: junction table name Column1 string // For Many2many: column name for this model's FK Column2 string // For Many2many: column name for comodel's FK Domain Domain // Default domain filter for relational fields OnDelete OnDelete // For Many2one: what happens on target deletion // Monetary CurrencyField string // Field name holding the currency_id // Translation Translate bool // Field supports multi-language // Copy Copy bool // Whether this field is copied on record duplication // Groups (access control) Groups string // Comma-separated group XML IDs // Internal model *Model // Back-reference to owning model column string // SQL column name (usually same as Name) } // Column returns the SQL column name for this field. func (f *Field) Column() string { if f.column != "" { return f.column } return f.Name } // SQLType returns the PostgreSQL type for this field. func (f *Field) SQLType() string { if f.Type == TypeChar && f.Size > 0 { return fmt.Sprintf("varchar(%d)", f.Size) } return f.Type.SQLType() } // IsStored returns true if this field has a database column. func (f *Field) IsStored() bool { // Computed fields without Store are not stored if f.Compute != "" && !f.Store { return false } // Related fields without Store are not stored if f.Related != "" && !f.Store { return false } return f.Type.IsStored() } // IsCopyable returns true if this field should be copied on record duplication. // Mirrors: odoo/orm/fields.py Field.copy func (f *Field) IsCopyable() bool { // If explicitly set, use that if f.Copy { return true } // Defaults: non-copyable for computed, O2M, id, timestamps if f.Name == "id" || f.Name == "create_uid" || f.Name == "create_date" || f.Name == "write_uid" || f.Name == "write_date" || f.Name == "password" { return false } if f.Compute != "" && !f.Store { return false } if f.Type == TypeOne2many { return false } return true } // IsRelational returns true for relational field types. func (f *Field) IsRelational() bool { return f.Type.IsRelational() } // --- Field constructors --- // Mirror Odoo's fields.Char(), fields.Integer(), etc. // FieldOpts holds optional parameters for field constructors. type FieldOpts struct { String string Required bool Readonly bool Index bool Size int Default Value Help string Compute string Inverse string Depends []string Store bool Precompute bool Related string Selection []SelectionItem Comodel string InverseField string Relation string Column1 string Column2 string Domain Domain OnDelete OnDelete CurrencyField string Translate bool Copy bool Groups string } func newField(name string, typ FieldType, opts FieldOpts) *Field { f := &Field{ Name: name, Type: typ, String: opts.String, Required: opts.Required, Readonly: opts.Readonly, Index: opts.Index, Size: opts.Size, Default: opts.Default, Help: opts.Help, Compute: opts.Compute, Inverse: opts.Inverse, Depends: opts.Depends, Store: opts.Store, Precompute: opts.Precompute, Related: opts.Related, Selection: opts.Selection, Comodel: opts.Comodel, InverseField: opts.InverseField, Relation: opts.Relation, Column1: opts.Column1, Column2: opts.Column2, Domain: opts.Domain, OnDelete: opts.OnDelete, CurrencyField: opts.CurrencyField, Translate: opts.Translate, Copy: opts.Copy, Groups: opts.Groups, column: name, } if f.String == "" { f.String = name } return f } // Char creates a character field. Mirrors: fields.Char func Char(name string, opts FieldOpts) *Field { return newField(name, TypeChar, opts) } // Text creates a text field. Mirrors: fields.Text func Text(name string, opts FieldOpts) *Field { return newField(name, TypeText, opts) } // HTML creates an HTML field. Mirrors: fields.Html func HTML(name string, opts FieldOpts) *Field { return newField(name, TypeHTML, opts) } // Integer creates an integer field. Mirrors: fields.Integer func Integer(name string, opts FieldOpts) *Field { return newField(name, TypeInteger, opts) } // Float creates a float field. Mirrors: fields.Float func Float(name string, opts FieldOpts) *Field { return newField(name, TypeFloat, opts) } // Monetary creates a monetary field. Mirrors: fields.Monetary func Monetary(name string, opts FieldOpts) *Field { return newField(name, TypeMonetary, opts) } // Boolean creates a boolean field. Mirrors: fields.Boolean func Boolean(name string, opts FieldOpts) *Field { return newField(name, TypeBoolean, opts) } // Date creates a date field. Mirrors: fields.Date func Date(name string, opts FieldOpts) *Field { return newField(name, TypeDate, opts) } // Datetime creates a datetime field. Mirrors: fields.Datetime func Datetime(name string, opts FieldOpts) *Field { return newField(name, TypeDatetime, opts) } // Binary creates a binary field. Mirrors: fields.Binary func Binary(name string, opts FieldOpts) *Field { return newField(name, TypeBinary, opts) } // Selection creates a selection field. Mirrors: fields.Selection func Selection(name string, selection []SelectionItem, opts FieldOpts) *Field { opts.Selection = selection return newField(name, TypeSelection, opts) } // Json creates a JSON field. Mirrors: fields.Json func Json(name string, opts FieldOpts) *Field { return newField(name, TypeJson, opts) } // Many2one creates a many-to-one relational field. Mirrors: fields.Many2one func Many2one(name string, comodel string, opts FieldOpts) *Field { opts.Comodel = comodel if opts.OnDelete == "" { opts.OnDelete = OnDeleteSetNull } f := newField(name, TypeMany2one, opts) f.Index = true // M2O fields are always indexed in Odoo return f } // One2many creates a one-to-many relational field. Mirrors: fields.One2many func One2many(name string, comodel string, inverseField string, opts FieldOpts) *Field { opts.Comodel = comodel opts.InverseField = inverseField return newField(name, TypeOne2many, opts) } // Many2many creates a many-to-many relational field. Mirrors: fields.Many2many func Many2many(name string, comodel string, opts FieldOpts) *Field { opts.Comodel = comodel return newField(name, TypeMany2many, opts) } // --- Default & Validation Helpers --- // ResolveDefault returns the concrete default value for this field. // Handles: literal values, "today" sentinel for date/datetime fields. func (f *Field) ResolveDefault() Value { if f.Default == nil { return nil } switch v := f.Default.(type) { case string: if v == "today" && (f.Type == TypeDate || f.Type == TypeDatetime) { return time.Now().Format("2006-01-02") } return v case bool, int, int64, float64: return v default: return v } } // ApplyDefaults sets default values on vals for any stored field that is // missing from vals and has a non-nil Default. // Mirrors: odoo/orm/models.py BaseModel._add_missing_default_values() func ApplyDefaults(m *Model, vals Values) { for _, name := range m.fieldOrder { f := m.fields[name] if name == "id" || !f.IsStored() || f.Default == nil { continue } if _, exists := vals[name]; exists { continue } if resolved := f.ResolveDefault(); resolved != nil { vals[name] = resolved } } } // ValidateRequired checks that all required stored fields have a non-nil value in vals. // Returns an error describing the first missing required field. // Mirrors: odoo/orm/models.py BaseModel._check_required() func ValidateRequired(m *Model, vals Values, isCreate bool) error { for _, name := range m.fieldOrder { f := m.fields[name] if !f.Required || !f.IsStored() || name == "id" { continue } // Magic fields are auto-set if name == "create_uid" || name == "write_uid" || name == "create_date" || name == "write_date" { continue } if isCreate { val, exists := vals[name] if !exists || val == nil { return fmt.Errorf("orm: field '%s' is required on %s", name, m.Name()) } } // On write: only check if the field is explicitly set to nil if !isCreate { val, exists := vals[name] if exists && val == nil { return fmt.Errorf("orm: field '%s' cannot be set to null on %s (required)", name, m.Name()) } } } return nil } // JunctionTable returns the M2M junction table name for this field. func (f *Field) JunctionTable() string { if f.Relation != "" { return f.Relation } if f.model == nil || f.Comodel == "" { return "" } comodel := Registry.Get(f.Comodel) if comodel == nil { return "" } t1, t2 := f.model.Table(), comodel.Table() if t1 > t2 { t1, t2 = t2, t1 } return fmt.Sprintf("%s_%s_rel", t1, t2) } // JunctionCol1 returns this model's FK column in the junction table. func (f *Field) JunctionCol1() string { if f.Column1 != "" { return f.Column1 } if f.model == nil { return "" } col := f.model.Table() + "_id" // Self-referential comodel := Registry.Get(f.Comodel) if comodel != nil && f.model.Table() == comodel.Table() { col = f.model.Table() + "_src_id" } return col } // JunctionCol2 returns the comodel's FK column in the junction table. func (f *Field) JunctionCol2() string { if f.Column2 != "" { return f.Column2 } comodel := Registry.Get(f.Comodel) if comodel == nil { return "" } col := comodel.Table() + "_id" if f.model != nil && f.model.Table() == comodel.Table() { col = comodel.Table() + "_dst_id" } return col }