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:
@@ -1,6 +1,41 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
|
import "odoo-go/pkg/orm"
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
initSaleOrder()
|
initSaleOrder()
|
||||||
initSaleOrderLine()
|
initSaleOrderLine()
|
||||||
|
initResPartnerSaleExtension()
|
||||||
|
}
|
||||||
|
|
||||||
|
// initResPartnerSaleExtension extends res.partner with sale-specific fields.
|
||||||
|
// Mirrors: odoo/addons/sale/models/res_partner.py
|
||||||
|
//
|
||||||
|
// class ResPartner(models.Model):
|
||||||
|
// _inherit = 'res.partner'
|
||||||
|
// sale_order_count = fields.Integer(compute='_compute_sale_order_count')
|
||||||
|
// sale_order_ids = fields.One2many('sale.order', 'partner_id', string='Sales Orders')
|
||||||
|
func initResPartnerSaleExtension() {
|
||||||
|
partner := orm.ExtendModel("res.partner")
|
||||||
|
partner.AddFields(
|
||||||
|
orm.One2many("sale_order_ids", "sale.order", "partner_id", orm.FieldOpts{
|
||||||
|
String: "Sales Orders",
|
||||||
|
}),
|
||||||
|
orm.Integer("sale_order_count", orm.FieldOpts{
|
||||||
|
String: "Sale Order Count",
|
||||||
|
Compute: "_compute_sale_order_count",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
partner.RegisterCompute("sale_order_count", func(rs *orm.Recordset) (orm.Values, error) {
|
||||||
|
env := rs.Env()
|
||||||
|
partnerID := rs.IDs()[0]
|
||||||
|
var count int
|
||||||
|
err := env.Tx().QueryRow(env.Ctx(),
|
||||||
|
`SELECT COUNT(*) FROM sale_order WHERE partner_id = $1`, partnerID,
|
||||||
|
).Scan(&count)
|
||||||
|
if err != nil {
|
||||||
|
count = 0
|
||||||
|
}
|
||||||
|
return orm.Values{"sale_order_count": count}, nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,6 +276,39 @@ func (m *Model) Extend(fields ...*Field) *Model {
|
|||||||
return m
|
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.
|
// CreateTableSQL generates the CREATE TABLE statement for this model.
|
||||||
// Mirrors: odoo/orm/models.py BaseModel._table_exist / init()
|
// Mirrors: odoo/orm/models.py BaseModel._table_exist / init()
|
||||||
func (m *Model) CreateTableSQL() string {
|
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
|
// Trigger recompute for stored computed fields that depend on written fields
|
||||||
if err := TriggerRecompute(rs, vals); err != nil {
|
if err := TriggerRecompute(rs, vals); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in New Issue
Block a user