Files
goodie/pkg/orm/inherits.go
Marc eb92a2e239 Complete ORM core: _inherits, Related write-back, Copy, constraints
Batch 1 (quick-fixes):
- Field.Copy attribute + IsCopyable() method for copy control
- Constraints now run in Write() (was only Create — bug fix)
- Readonly fields silently skipped in Write()
- active_test: auto-filter archived records in Search()

Batch 2 (Related field write-back):
- preprocessRelatedWrites() follows FK chain and writes to target model
- Enables Settings page to edit company name/address/etc.
- Loop protection via _write_related_depth context counter

Batch 3 (_inherits delegation):
- SetupAllInherits() generates Related fields from parent models
- preprocessInheritsCreate() auto-creates parent records on Create
- Declared on res.users, res.company, product.product
- Called in LoadModules before compute setup

Batch 4 (Copy method):
- Recordset.Copy(defaults) with blacklist, IsCopyable check
- M2M re-linking, rec_name "(copy)" suffix
- Replaces simplified copy case in server dispatch

Batch 5 (Onchange compute):
- RunOnchangeComputes() triggers dependent computes on field change
- Virtual record (ID=-1) with client values in cache
- Integrated into onchange RPC handler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 19:57:04 +02:00

153 lines
4.1 KiB
Go

package orm
import (
"fmt"
"log"
)
// SetupAllInherits generates Related fields for all models with _inherits declarations.
// Must be called after all models are registered but before computes are set up.
// Mirrors: odoo/orm/model_classes.py _add_inherited_fields()
func SetupAllInherits() {
for _, m := range Registry.Models() {
if len(m.inherits) == 0 {
continue
}
setupModelInherits(m)
}
}
func setupModelInherits(m *Model) {
for parentName, fkField := range m.inherits {
parent := Registry.Get(parentName)
if parent == nil {
log.Printf("inherits: parent model %q not found for %s", parentName, m.Name())
continue
}
// Verify FK field exists on child
fkDef := m.GetField(fkField)
if fkDef == nil {
log.Printf("inherits: FK field %q not found on %s", fkField, m.Name())
continue
}
// For each parent field, create a Related field on child if not already defined
inherited := 0
for _, fname := range parent.fieldOrder {
// Skip magic fields (child has its own)
if fname == "id" || fname == "display_name" ||
fname == "create_uid" || fname == "create_date" ||
fname == "write_uid" || fname == "write_date" {
continue
}
// Don't override fields already defined on child
if existing := m.GetField(fname); existing != nil {
continue
}
pf := parent.GetField(fname)
if pf == nil {
continue
}
// Create Related field: "fk_field.field_name"
relatedPath := fkField + "." + fname
inheritedField := &Field{
Name: fname,
Type: pf.Type,
String: pf.String,
Help: pf.Help,
Related: relatedPath,
Comodel: pf.Comodel,
InverseField: pf.InverseField,
Selection: pf.Selection,
CurrencyField: pf.CurrencyField,
Readonly: pf.Readonly,
Required: false, // Inherited fields are not required on child
model: m,
column: fname,
}
// Add to model's field maps
m.fields[fname] = inheritedField
m.allFields[fname] = inheritedField
m.fieldOrder = append(m.fieldOrder, fname)
inherited++
}
log.Printf("inherits: %s inherits %d fields from %s via %s",
m.Name(), inherited, parentName, fkField)
}
}
// preprocessInheritsCreate separates inherited field values and creates/updates parent records.
// Must be called in Create() BEFORE ApplyDefaults.
// Mirrors: odoo/orm/models.py create() _inherits handling
func preprocessInheritsCreate(env *Environment, m *Model, vals Values) error {
if len(m.inherits) == 0 {
return nil
}
for parentName, fkField := range m.inherits {
parent := Registry.Get(parentName)
if parent == nil {
continue
}
// Separate parent vals from child vals
parentVals := make(Values)
for fieldName, val := range vals {
// Check if this field belongs to parent (not child's own stored field)
childField := m.fields[fieldName]
parentField := parent.GetField(fieldName)
if parentField != nil && fieldName != fkField &&
(childField == nil || childField.Related != "") {
parentVals[fieldName] = val
delete(vals, fieldName)
}
}
// Check if FK is already provided
fkVal, hasFk := vals[fkField]
if hasFk && fkVal != nil {
// FK provided — update existing parent record with any inherited vals
fkID := int64(0)
switch v := fkVal.(type) {
case float64:
fkID = int64(v)
case int64:
fkID = v
case int32:
fkID = int64(v)
}
if fkID > 0 && len(parentVals) > 0 {
parentRS := env.Model(parentName).Browse(fkID)
if err := parentRS.Write(parentVals); err != nil {
return fmt.Errorf("inherits: update %s(%d): %w", parentName, fkID, err)
}
}
} else {
// No FK — create parent record first
if len(parentVals) == 0 {
// Need at least something for the parent (e.g., name)
// Check if name is in child vals but not moved to parent
if nameVal, ok := vals["name"]; ok {
parentVals["name"] = nameVal
}
}
parentRS := env.Model(parentName)
created, err := parentRS.Create(parentVals)
if err != nil {
return fmt.Errorf("inherits: create %s: %w", parentName, err)
}
vals[fkField] = created.ID()
}
}
return nil
}