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
|
// 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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user