- Improved auto-generated list/form/search views with priority fields, two-column form layout, statusbar widget, notebook for O2M fields - Enhanced fields_get with currency_field, compute, related metadata - Fixed session handling: handleSessionInfo/handleSessionCheck use real session from cookie instead of hardcoded values - Added read_progress_bar and activity_format RPC stubs - Improved bootstrap translations with lang_parameters - Added "contacts" to session modules list Server starts successfully: 14 modules, 93 models, 378 XML templates, 503 JS modules transpiled — all from local frontend/ directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
244 lines
5.9 KiB
Go
244 lines
5.9 KiB
Go
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
|
|
}
|
|
}
|
|
}
|
|
}
|