package server import ( "fmt" "time" "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 (regular search_read) or kwargs (web_search_read) domain := parseDomain(params.Args) if domain == nil { if domainRaw, ok := params.KW["domain"].([]interface{}); ok && len(domainRaw) > 0 { domain = parseDomain([]interface{}{domainRaw}) } } // 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, respecting count_limit for optimization. // Mirrors: odoo/addons/web/models/models.py web_search_read() count_limit parameter countLimit := int64(0) if v, ok := params.KW["count_limit"].(float64); ok { countLimit = int64(v) } count, err := rs.SearchCount(domain) if err != nil { return nil, &RPCError{Code: -32000, Message: err.Error()} } if countLimit > 0 && count > countLimit { count = countLimit } // 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) // Format date/datetime fields to Odoo's expected string format formatDateFields(model, records) 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) // Always include id hasID := false for _, f := range fields { if f == "id" { hasID = true break } } if !hasID { fields = append([]string{"id"}, fields...) } 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) // Format date/datetime fields to Odoo's expected string format formatDateFields(model, records) 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)} } } } } } // formatDateFields converts date/datetime values to Odoo's expected string format. func formatDateFields(model string, records []orm.Values) { m := orm.Registry.Get(model) if m == nil { return } for _, rec := range records { for fieldName, val := range rec { f := m.GetField(fieldName) if f == nil { continue } if f.Type == orm.TypeDate || f.Type == orm.TypeDatetime { switch v := val.(type) { case time.Time: if f.Type == orm.TypeDate { rec[fieldName] = v.Format("2006-01-02") } else { rec[fieldName] = v.Format("2006-01-02 15:04:05") } case string: // Already a string, might need reformatting if t, err := time.Parse(time.RFC3339, v); err == nil { if f.Type == orm.TypeDate { rec[fieldName] = t.Format("2006-01-02") } else { rec[fieldName] = t.Format("2006-01-02 15:04:05") } } } } // Also convert boolean fields: Go nil → Odoo false if f.Type == orm.TypeBoolean && val == nil { rec[fieldName] = false } } } }