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:
@@ -86,6 +86,9 @@ func handleWebSearchRead(env *orm.Environment, model string, params CallKWParams
|
||||
// Format M2O fields as {id, display_name} when spec requests it
|
||||
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
|
||||
formatDateFields(model, records)
|
||||
|
||||
@@ -133,6 +136,9 @@ func handleWebRead(env *orm.Environment, model string, params CallKWParams) (int
|
||||
|
||||
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
|
||||
formatDateFields(model, records)
|
||||
|
||||
@@ -145,6 +151,87 @@ func handleWebRead(env *orm.Environment, model string, params CallKWParams) (int
|
||||
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.
|
||||
// {"name": {}, "partner_id": {"fields": {"display_name": {}}}} → ["name", "partner_id"]
|
||||
func specToFields(spec map[string]interface{}) []string {
|
||||
|
||||
Reference in New Issue
Block a user