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:
Marc
2026-04-02 19:57:04 +02:00
parent b57176de2f
commit eb92a2e239
9 changed files with 477 additions and 18 deletions

View File

@@ -60,6 +60,9 @@ type Field struct {
// Translation
Translate bool // Field supports multi-language
// Copy
Copy bool // Whether this field is copied on record duplication
// Groups (access control)
Groups string // Comma-separated group XML IDs
@@ -97,6 +100,27 @@ func (f *Field) IsStored() bool {
return f.Type.IsStored()
}
// IsCopyable returns true if this field should be copied on record duplication.
// Mirrors: odoo/orm/fields.py Field.copy
func (f *Field) IsCopyable() bool {
// If explicitly set, use that
if f.Copy {
return true
}
// Defaults: non-copyable for computed, O2M, id, timestamps
if f.Name == "id" || f.Name == "create_uid" || f.Name == "create_date" ||
f.Name == "write_uid" || f.Name == "write_date" || f.Name == "password" {
return false
}
if f.Compute != "" && !f.Store {
return false
}
if f.Type == TypeOne2many {
return false
}
return true
}
// IsRelational returns true for relational field types.
func (f *Field) IsRelational() bool {
return f.Type.IsRelational()
@@ -130,6 +154,7 @@ type FieldOpts struct {
OnDelete OnDelete
CurrencyField string
Translate bool
Copy bool
Groups string
}
@@ -160,6 +185,7 @@ func newField(name string, typ FieldType, opts FieldOpts) *Field {
OnDelete: opts.OnDelete,
CurrencyField: opts.CurrencyField,
Translate: opts.Translate,
Copy: opts.Copy,
Groups: opts.Groups,
column: name,
}