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>
153 lines
4.1 KiB
Go
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
|
|
}
|