package orm import "fmt" // ComputeFunc is a function that computes field values for a recordset. // Mirrors: @api.depends decorated methods in Odoo. // // The function receives a singleton recordset and must return a Values map // with the computed field values. // // Example: // // func computeAmount(rs *Recordset) (Values, error) { // total := 0.0 // // ... sum line amounts ... // return Values{"amount_total": total}, nil // } type ComputeFunc func(rs *Recordset) (Values, error) // RegisterCompute registers a compute function for a field. // The same function can compute multiple fields (call RegisterCompute for each). func (m *Model) RegisterCompute(fieldName string, fn ComputeFunc) { if m.computes == nil { m.computes = make(map[string]ComputeFunc) } m.computes[fieldName] = fn } // SetupComputes builds the reverse dependency map for this model. // Called after all modules are loaded. // // For each field with Depends, creates a mapping: // // trigger_field → []computed_field_names // // So when trigger_field is written, we know which computes to re-run. func (m *Model) SetupComputes() { m.dependencyMap = make(map[string][]string) for _, name := range m.fieldOrder { f := m.fields[name] if f.Compute == "" || len(f.Depends) == 0 { continue } for _, dep := range f.Depends { m.dependencyMap[dep] = append(m.dependencyMap[dep], name) } } } // RunStoredComputes runs compute functions for stored computed fields // and merges results into vals. Called before INSERT in Create(). func RunStoredComputes(m *Model, env *Environment, id int64, vals Values) error { if len(m.computes) == 0 { return nil } // Collect stored computed fields that have registered functions seen := make(map[string]bool) // Track compute functions already called (by field name) for _, name := range m.fieldOrder { f := m.fields[name] if f.Compute == "" || !f.Store { continue } fn, ok := m.computes[name] if !ok { continue } // Deduplicate: same function may compute multiple fields if seen[f.Compute] { continue } seen[f.Compute] = true // Create a temporary recordset for the computation rs := &Recordset{env: env, model: m, ids: []int64{id}} computed, err := fn(rs) if err != nil { return fmt.Errorf("orm: compute %s.%s: %w", m.name, name, err) } // Merge computed values for k, v := range computed { cf := m.GetField(k) if cf != nil && cf.Store && cf.Compute != "" { vals[k] = v } } } return nil } // TriggerRecompute re-computes stored fields that depend on written fields. // Called after UPDATE in Write(). func TriggerRecompute(rs *Recordset, writtenFields Values) error { m := rs.model if len(m.dependencyMap) == 0 || len(m.computes) == 0 { return nil } // Find which computed fields need re-computation toRecompute := make(map[string]bool) for fieldName := range writtenFields { for _, computedField := range m.dependencyMap[fieldName] { toRecompute[computedField] = true } } if len(toRecompute) == 0 { return nil } // Run computes for each record seen := make(map[string]bool) for _, id := range rs.IDs() { singleton := rs.Browse(id) recomputedVals := make(Values) for fieldName := range toRecompute { f := m.GetField(fieldName) if f == nil || !f.Store { continue } fn, ok := m.computes[fieldName] if !ok { continue } if seen[f.Compute] { continue } seen[f.Compute] = true computed, err := fn(singleton) if err != nil { return fmt.Errorf("orm: recompute %s.%s: %w", m.name, fieldName, err) } for k, v := range computed { cf := m.GetField(k) if cf != nil && cf.Store && cf.Compute != "" { recomputedVals[k] = v } } } // Write recomputed values directly to DB (bypass hooks to avoid infinite loop) if len(recomputedVals) > 0 { if err := writeDirectNohook(rs.env, m, id, recomputedVals); err != nil { return err } } // Reset seen for next record seen = make(map[string]bool) } return nil } // writeDirectNohook writes values directly without triggering hooks or recomputes. func writeDirectNohook(env *Environment, m *Model, id int64, vals Values) error { var setClauses []string var args []interface{} idx := 1 for k, v := range vals { f := m.GetField(k) if f == nil || !f.IsStored() { continue } setClauses = append(setClauses, fmt.Sprintf("%q = $%d", f.Column(), idx)) args = append(args, v) idx++ } if len(setClauses) == 0 { return nil } args = append(args, id) query := fmt.Sprintf( `UPDATE %q SET %s WHERE "id" = $%d`, m.Table(), joinStrings(setClauses, ", "), idx, ) _, err := env.tx.Exec(env.ctx, query, args...) return err } func joinStrings(s []string, sep string) string { result := "" for i, str := range s { if i > 0 { result += sep } result += str } return result } // SetupAllComputes calls SetupComputes on all registered models. func SetupAllComputes() { for _, m := range Registry.Models() { m.SetupComputes() } }