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:
@@ -196,6 +196,74 @@ func joinStrings(s []string, sep string) string {
|
||||
return result
|
||||
}
|
||||
|
||||
// RunOnchangeComputes runs computed fields that depend on changed fields.
|
||||
// Used by the onchange RPC to recompute values when form fields change.
|
||||
// Mirrors: odoo/orm/models.py BaseModel._onchange_eval()
|
||||
//
|
||||
// Creates a virtual record (ID=-1) with the client's values in cache,
|
||||
// then runs any compute functions whose dependencies include the changed fields.
|
||||
func RunOnchangeComputes(m *Model, env *Environment, currentVals Values, changedFields []string) Values {
|
||||
result := make(Values)
|
||||
if m == nil || len(m.dependencyMap) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
// Find which computed fields need recomputation
|
||||
toRecompute := make(map[string]bool)
|
||||
for _, fname := range changedFields {
|
||||
if deps, ok := m.dependencyMap[fname]; ok {
|
||||
for _, computedField := range deps {
|
||||
toRecompute[computedField] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(toRecompute) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
// Populate cache with current form values so computes can read them
|
||||
fakeID := int64(-1)
|
||||
for k, v := range currentVals {
|
||||
env.cache.Set(m.Name(), fakeID, k, v)
|
||||
}
|
||||
defer env.cache.InvalidateRecord(m.Name(), fakeID)
|
||||
|
||||
// Create virtual recordset
|
||||
rs := &Recordset{env: env, model: m, ids: []int64{fakeID}}
|
||||
|
||||
// Track which compute functions we've already called (multiple fields can share one function)
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for fieldName := range toRecompute {
|
||||
f := m.GetField(fieldName)
|
||||
if f == nil || f.Compute == "" {
|
||||
continue
|
||||
}
|
||||
// Don't call the same compute function twice
|
||||
if seen[f.Compute] {
|
||||
continue
|
||||
}
|
||||
seen[f.Compute] = true
|
||||
|
||||
fn, ok := m.computes[fieldName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
computed, err := fn(rs)
|
||||
if err != nil {
|
||||
// Non-fatal: skip failed computes during onchange
|
||||
continue
|
||||
}
|
||||
for k, v := range computed {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SetupAllComputes calls SetupComputes on all registered models.
|
||||
func SetupAllComputes() {
|
||||
for _, m := range Registry.Models() {
|
||||
|
||||
Reference in New Issue
Block a user