Add _inherit (ExtendModel) + Inverse fields + sale extends partner
ORM: - ExtendModel(name) retrieves existing model for extension (mirrors Python _inherit without _name). Panics on missing model. - RegisterInverse(fieldName, fn) convenience for computed write-back - Inverse field handling in Write(): caches new value, calls inverse method so computed fields can be written back Sale module: - Extends res.partner with sale_order_ids (O2M) and sale_order_count (computed) via ExtendModel — demonstrates real _inherit pattern Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -276,6 +276,39 @@ func (m *Model) Extend(fields ...*Field) *Model {
|
||||
return m
|
||||
}
|
||||
|
||||
// ExtendModel retrieves an existing model for extension by another module.
|
||||
// Mirrors: Python Odoo's _inherit = 'model.name' (without _name).
|
||||
//
|
||||
// This is the formal mechanism for cross-module model extension. It provides:
|
||||
// 1. Explicit intent (extending vs creating)
|
||||
// 2. Panic on missing model (catch registration order bugs early)
|
||||
// 3. Python Odoo compatibility pattern
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// m := orm.ExtendModel("res.partner")
|
||||
// m.AddFields(orm.Char("industry", orm.FieldOpts{String: "Industry"}))
|
||||
// m.RegisterMethod("action_custom", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { ... })
|
||||
func ExtendModel(name string) *Model {
|
||||
m := Registry.Get(name)
|
||||
if m == nil {
|
||||
panic(fmt.Sprintf("orm: cannot extend unregistered model %q", name))
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// RegisterInverse registers an inverse method for a computed field.
|
||||
// The method is auto-named "_inverse_<fieldName>" and linked to the field's
|
||||
// Inverse property so that Write() calls it automatically.
|
||||
// Mirrors: odoo/orm/fields.py Field.inverse
|
||||
func (m *Model) RegisterInverse(fieldName string, fn MethodFunc) {
|
||||
inverseName := "_inverse_" + fieldName
|
||||
m.RegisterMethod(inverseName, fn)
|
||||
if f := m.GetField(fieldName); f != nil {
|
||||
f.Inverse = inverseName
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTableSQL generates the CREATE TABLE statement for this model.
|
||||
// Mirrors: odoo/orm/models.py BaseModel._table_exist / init()
|
||||
func (m *Model) CreateTableSQL() string {
|
||||
|
||||
@@ -393,6 +393,31 @@ func (rs *Recordset) Write(vals Values) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle inverse fields (write-back for computed fields).
|
||||
// Mirrors: odoo/orm/fields.py Field.inverse
|
||||
//
|
||||
// When a computed field with an Inverse method is written to, the inverse
|
||||
// method is called so it can propagate the value to the underlying fields.
|
||||
// In Python Odoo, the inverse method reads self.field_name from cache (which
|
||||
// has the new value). We replicate this by caching the new value before calling.
|
||||
for fieldName := range vals {
|
||||
f := m.GetField(fieldName)
|
||||
if f == nil || f.Inverse == "" {
|
||||
continue
|
||||
}
|
||||
inverseMethod, ok := m.Methods[f.Inverse]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Cache the new value so the inverse method can read it via Get()
|
||||
for _, id := range rs.ids {
|
||||
rs.env.cache.Set(m.Name(), id, fieldName, vals[fieldName])
|
||||
}
|
||||
if _, err := inverseMethod(rs); err != nil {
|
||||
return fmt.Errorf("orm: inverse %s.%s: %w", m.name, f.Inverse, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger recompute for stored computed fields that depend on written fields
|
||||
if err := TriggerRecompute(rs, vals); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user