Files
goodie/pkg/modules/graph.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

116 lines
2.5 KiB
Go

package modules
import (
"fmt"
"odoo-go/pkg/orm"
)
// ResolveDependencies returns modules in topological order (dependencies first).
// Mirrors: odoo/modules/module_graph.py Graph.add_modules()
//
// Uses Kahn's algorithm for topological sort.
func ResolveDependencies(moduleNames []string) ([]string, error) {
// Build adjacency list and in-degree map
inDegree := make(map[string]int)
dependents := make(map[string][]string) // module → modules that depend on it
// Initialize all requested modules
for _, name := range moduleNames {
if _, exists := inDegree[name]; !exists {
inDegree[name] = 0
}
}
// Build graph from dependencies
for _, name := range moduleNames {
m := Get(name)
if m == nil {
return nil, fmt.Errorf("modules: %q not found", name)
}
for _, dep := range m.Depends {
// Ensure dependency is in our set
if _, exists := inDegree[dep]; !exists {
inDegree[dep] = 0
}
inDegree[name]++
dependents[dep] = append(dependents[dep], name)
}
}
// Kahn's algorithm
var queue []string
for name, degree := range inDegree {
if degree == 0 {
queue = append(queue, name)
}
}
var sorted []string
for len(queue) > 0 {
// Pop first element
current := queue[0]
queue = queue[1:]
sorted = append(sorted, current)
// Reduce in-degree for dependents
for _, dep := range dependents[current] {
inDegree[dep]--
if inDegree[dep] == 0 {
queue = append(queue, dep)
}
}
}
// Check for circular dependencies
if len(sorted) != len(inDegree) {
var circular []string
for name, degree := range inDegree {
if degree > 0 {
circular = append(circular, name)
}
}
return nil, fmt.Errorf("modules: circular dependency detected among: %v", circular)
}
return sorted, nil
}
// LoadModules initializes all modules in dependency order.
// Mirrors: odoo/modules/loading.py load_modules()
func LoadModules(moduleNames []string) error {
sorted, err := ResolveDependencies(moduleNames)
if err != nil {
return err
}
// Phase 1: Call Init() for each module (registers models and fields)
for _, name := range sorted {
m := Get(name)
if m == nil {
continue
}
if m.Init != nil {
m.Init()
}
}
// Phase 2: Call PostInit() after all models are registered
for _, name := range sorted {
m := Get(name)
if m == nil {
continue
}
if m.PostInit != nil {
m.PostInit()
}
}
// Phase 3: Generate Related fields for _inherits delegation
orm.SetupAllInherits()
// Phase 4: Build computed field dependency maps
orm.SetupAllComputes()
return nil
}