package server import ( "fmt" "odoo-go/pkg/orm" ) // handleWebSearchRead implements the web_search_read method. // Mirrors: odoo/addons/web/models/models.py web_search_read() // Returns {length: N, records: [...]} instead of just records. func handleWebSearchRead(env *orm.Environment, model string, params CallKWParams) (interface{}, *RPCError) { rs := env.Model(model) // Parse domain from first arg domain := parseDomain(params.Args) // Parse specification from kwargs spec, _ := params.KW["specification"].(map[string]interface{}) fields := specToFields(spec) // Always include id hasID := false for _, f := range fields { if f == "id" { hasID = true break } } if !hasID { fields = append([]string{"id"}, fields...) } // Parse offset, limit, order offset := 0 limit := 80 order := "" if v, ok := params.KW["offset"].(float64); ok { offset = int(v) } if v, ok := params.KW["limit"].(float64); ok { limit = int(v) } if v, ok := params.KW["order"].(string); ok { order = v } // Get total count count, err := rs.SearchCount(domain) if err != nil { return nil, &RPCError{Code: -32000, Message: err.Error()} } // Search with offset/limit found, err := rs.Search(domain, orm.SearchOpts{ Offset: offset, Limit: limit, Order: order, }) if err != nil { return nil, &RPCError{Code: -32000, Message: err.Error()} } var records []orm.Values if !found.IsEmpty() { records, err = found.Read(fields) if err != nil { return nil, &RPCError{Code: -32000, Message: err.Error()} } } // Format M2O fields as {id, display_name} when spec requests it formatM2OFields(env, model, records, spec) if records == nil { records = []orm.Values{} } return map[string]interface{}{ "length": count, "records": records, }, nil } // handleWebRead implements the web_read method. // Mirrors: odoo/addons/web/models/models.py web_read() func handleWebRead(env *orm.Environment, model string, params CallKWParams) (interface{}, *RPCError) { ids := parseIDs(params.Args) if len(ids) == 0 { return []orm.Values{}, nil } spec, _ := params.KW["specification"].(map[string]interface{}) fields := specToFields(spec) rs := env.Model(model) records, err := rs.Browse(ids...).Read(fields) if err != nil { return nil, &RPCError{Code: -32000, Message: err.Error()} } formatM2OFields(env, model, records, spec) if records == nil { records = []orm.Values{} } return records, nil } // specToFields extracts field names from a specification dict. // {"name": {}, "partner_id": {"fields": {"display_name": {}}}} → ["name", "partner_id"] func specToFields(spec map[string]interface{}) []string { if len(spec) == 0 { return nil } fields := make([]string, 0, len(spec)) for name := range spec { fields = append(fields, name) } return fields } // formatM2OFields converts Many2one field values from raw int to {id, display_name}. func formatM2OFields(env *orm.Environment, modelName string, records []orm.Values, spec map[string]interface{}) { m := orm.Registry.Get(modelName) if m == nil || spec == nil { return } for _, rec := range records { for fieldName, fieldSpec := range spec { f := m.GetField(fieldName) if f == nil || f.Type != orm.TypeMany2one { continue } // Accept any spec entry for M2O fields (even empty {} means include it) _, ok := fieldSpec.(map[string]interface{}) if !ok { continue } // Get the raw FK ID rawID := rec[fieldName] fkID := int64(0) switch v := rawID.(type) { case int64: fkID = v case int32: fkID = int64(v) case float64: fkID = int64(v) } if fkID == 0 { rec[fieldName] = false // Odoo convention for empty M2O continue } // Fetch display_name from comodel — return as [id, "name"] array if f.Comodel != "" { coRS := env.Model(f.Comodel).Browse(fkID) names, err := coRS.NameGet() if err == nil && len(names) > 0 { rec[fieldName] = []interface{}{fkID, names[fkID]} } else { rec[fieldName] = []interface{}{fkID, fmt.Sprintf("%s,%d", f.Comodel, fkID)} } } } } }