Fetch O2M child records in web_read/web_search_read responses

When a view specification requests O2M fields (e.g., order_line on
sale.order), the response now includes the child records as an array
of dicts. Searches child model by inverse_field = parent_id, reads
requested sub-fields, and formats M2O/dates/nulls on children too.

This makes inline list views in form notebooks show actual data:
- Sale order lines (product, qty, price) in sale.order form
- Invoice lines in account.move form
- Any O2M field with specification in get_views response

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc
2026-04-02 21:43:42 +02:00
parent e8fe84b913
commit fbe65af951

View File

@@ -86,6 +86,9 @@ func handleWebSearchRead(env *orm.Environment, model string, params CallKWParams
// Format M2O fields as {id, display_name} when spec requests it // Format M2O fields as {id, display_name} when spec requests it
formatM2OFields(env, model, records, spec) formatM2OFields(env, model, records, spec)
// Fetch O2M fields (child records) based on specification
formatO2MFields(env, model, records, spec)
// Format date/datetime fields to Odoo's expected string format // Format date/datetime fields to Odoo's expected string format
formatDateFields(model, records) formatDateFields(model, records)
@@ -133,6 +136,9 @@ func handleWebRead(env *orm.Environment, model string, params CallKWParams) (int
formatM2OFields(env, model, records, spec) formatM2OFields(env, model, records, spec)
// Fetch O2M fields (child records) based on specification
formatO2MFields(env, model, records, spec)
// Format date/datetime fields to Odoo's expected string format // Format date/datetime fields to Odoo's expected string format
formatDateFields(model, records) formatDateFields(model, records)
@@ -145,6 +151,87 @@ func handleWebRead(env *orm.Environment, model string, params CallKWParams) (int
return records, nil return records, nil
} }
// formatO2MFields fetches One2many child records and adds them to the response.
// Mirrors: odoo/addons/web/models/models.py web_read() O2M handling
func formatO2MFields(env *orm.Environment, modelName string, records []orm.Values, spec map[string]interface{}) {
m := orm.Registry.Get(modelName)
if m == nil || spec == nil {
return
}
for fieldName, fieldSpec := range spec {
f := m.GetField(fieldName)
if f == nil || f.Type != orm.TypeOne2many {
continue
}
// Check if spec requests sub-fields
specMap, ok := fieldSpec.(map[string]interface{})
if !ok {
continue
}
subFieldsSpec, _ := specMap["fields"].(map[string]interface{})
// Determine which fields to read from child model
var childFields []string
if len(subFieldsSpec) > 0 {
childFields = []string{"id"}
for name := range subFieldsSpec {
if name != "id" {
childFields = append(childFields, name)
}
}
}
comodel := f.Comodel
inverseField := f.InverseField
if comodel == "" || inverseField == "" {
continue
}
for _, rec := range records {
parentID, ok := rec["id"].(int64)
if !ok {
if pid, ok := rec["id"].(int32); ok {
parentID = int64(pid)
}
}
if parentID == 0 {
rec[fieldName] = []interface{}{}
continue
}
// Search child records where inverse_field = parent_id
childRS := env.Model(comodel)
domain := orm.And(orm.Leaf(inverseField, "=", parentID))
found, err := childRS.Search(domain, orm.SearchOpts{Limit: 200})
if err != nil || found.IsEmpty() {
rec[fieldName] = []interface{}{}
continue
}
// Read child records
childRecords, err := found.Read(childFields)
if err != nil {
rec[fieldName] = []interface{}{}
continue
}
// Format child records (M2O fields, dates, nulls)
formatM2OFields(env, comodel, childRecords, subFieldsSpec)
formatDateFields(comodel, childRecords)
normalizeNullFields(comodel, childRecords)
// Convert to []interface{} for JSON
lines := make([]interface{}, len(childRecords))
for i, cr := range childRecords {
lines[i] = cr
}
rec[fieldName] = lines
}
}
}
// specToFields extracts field names from a specification dict. // specToFields extracts field names from a specification dict.
// {"name": {}, "partner_id": {"fields": {"display_name": {}}}} → ["name", "partner_id"] // {"name": {}, "partner_id": {"fields": {"display_name": {}}}} → ["name", "partner_id"]
func specToFields(spec map[string]interface{}) []string { func specToFields(spec map[string]interface{}) []string {