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>
This commit is contained in:
152
pkg/orm/inherits.go
Normal file
152
pkg/orm/inherits.go
Normal file
@@ -0,0 +1,152 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user