Simplify: batch M2O, deduplicate formatting, clean dead code
Code reuse: - formatRecordsForWeb() consolidates 4-call formatting sequence (was duplicated in handleWebSearchRead + handleWebRead) - ToRecordID() public alias for cross-package ID extraction Performance: - M2O NameGet now batched: collect all FK IDs per field, single NameGet per comodel instead of per-record (N+1 → 1) Quality: - normalizeNullFields: 30-line switch (all returning false) → 5 lines - domain.go: remove unused _ = subParams assignment - Net -14 lines Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -198,11 +198,10 @@ func (dc *DomainCompiler) compileNodes(domain Domain, pos int) (string, error) {
|
||||
case domainGroup:
|
||||
// domainGroup wraps a sub-domain as a single node.
|
||||
// Compile it recursively as a full domain.
|
||||
subSQL, subParams, err := dc.compileDomainGroup(Domain(n))
|
||||
subSQL, _, err := dc.compileDomainGroup(Domain(n))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_ = subParams // params already appended inside compileDomainGroup
|
||||
return subSQL, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -269,6 +269,9 @@ func ParseCommands(raw interface{}) ([]Command, bool) {
|
||||
return cmds, true
|
||||
}
|
||||
|
||||
// ToRecordID extracts an int64 ID from various numeric types. Public alias.
|
||||
func ToRecordID(v interface{}) (int64, bool) { return toInt64(v) }
|
||||
|
||||
func toInt64(v interface{}) (int64, bool) {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
|
||||
@@ -83,17 +83,7 @@ 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)
|
||||
|
||||
// Convert SQL NULLs to Odoo-expected defaults (false, 0, etc.)
|
||||
normalizeNullFields(model, records)
|
||||
formatRecordsForWeb(env, model, records, spec)
|
||||
|
||||
if records == nil {
|
||||
records = []orm.Values{}
|
||||
@@ -134,16 +124,7 @@ func handleWebRead(env *orm.Environment, model string, params CallKWParams) (int
|
||||
return nil, &RPCError{Code: -32000, Message: err.Error()}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Convert SQL NULLs to Odoo-expected defaults (false, 0, etc.)
|
||||
normalizeNullFields(model, records)
|
||||
formatRecordsForWeb(env, model, records, spec)
|
||||
|
||||
if records == nil {
|
||||
records = []orm.Values{}
|
||||
@@ -151,6 +132,14 @@ func handleWebRead(env *orm.Environment, model string, params CallKWParams) (int
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// formatRecordsForWeb applies all web-client formatting in one call.
|
||||
func formatRecordsForWeb(env *orm.Environment, model string, records []orm.Values, spec map[string]interface{}) {
|
||||
formatM2OFields(env, model, records, spec)
|
||||
formatO2MFields(env, model, records, spec)
|
||||
formatDateFields(model, records)
|
||||
normalizeNullFields(model, records)
|
||||
}
|
||||
|
||||
// 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{}) {
|
||||
@@ -252,80 +241,75 @@ func formatM2OFields(env *orm.Environment, modelName string, records []orm.Value
|
||||
return
|
||||
}
|
||||
|
||||
// Batch M2O resolution: collect all FK IDs per (fieldName, comodel), one NameGet per comodel
|
||||
type m2oKey struct{ field, comodel string }
|
||||
idSets := make(map[m2oKey]map[int64]bool)
|
||||
|
||||
for fieldName, fieldSpec := range spec {
|
||||
f := m.GetField(fieldName)
|
||||
if f == nil || f.Type != orm.TypeMany2one || f.Comodel == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := fieldSpec.(map[string]interface{}); !ok {
|
||||
continue
|
||||
}
|
||||
key := m2oKey{fieldName, f.Comodel}
|
||||
idSets[key] = make(map[int64]bool)
|
||||
for _, rec := range records {
|
||||
if fkID, ok := orm.ToRecordID(rec[fieldName]); ok && fkID > 0 {
|
||||
idSets[key][fkID] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One NameGet per comodel (batched)
|
||||
nameCache := make(map[m2oKey]map[int64]string)
|
||||
for key, ids := range idSets {
|
||||
if len(ids) == 0 {
|
||||
continue
|
||||
}
|
||||
var idSlice []int64
|
||||
for id := range ids {
|
||||
idSlice = append(idSlice, id)
|
||||
}
|
||||
coRS := env.Model(key.comodel).Browse(idSlice...)
|
||||
names, err := coRS.NameGet()
|
||||
if err == nil {
|
||||
nameCache[key] = names
|
||||
}
|
||||
}
|
||||
|
||||
// Apply resolved names to records
|
||||
for _, rec := range records {
|
||||
for fieldName, fieldSpec := range spec {
|
||||
f := m.GetField(fieldName)
|
||||
if f == nil || f.Type != orm.TypeMany2one {
|
||||
if f == nil || f.Type != orm.TypeMany2one || f.Comodel == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Accept any spec entry for M2O fields (even empty {} means include it)
|
||||
_, ok := fieldSpec.(map[string]interface{})
|
||||
if !ok {
|
||||
if _, ok := fieldSpec.(map[string]interface{}); !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
|
||||
fkID, ok := orm.ToRecordID(rec[fieldName])
|
||||
if !ok || fkID == 0 {
|
||||
rec[fieldName] = false
|
||||
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 {
|
||||
key := m2oKey{fieldName, f.Comodel}
|
||||
if names, ok := nameCache[key]; ok {
|
||||
rec[fieldName] = []interface{}{fkID, names[fkID]}
|
||||
} else {
|
||||
rec[fieldName] = []interface{}{fkID, fmt.Sprintf("%s,%d", f.Comodel, fkID)}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeNullFields converts SQL NULL (Go nil) values to Odoo-expected defaults.
|
||||
// In Python Odoo, empty fields return False (JSON false), not null.
|
||||
// Without this, the webclient may render "null" as literal text.
|
||||
// Mirrors: odoo/orm/fields.py convert_to_read() behaviour
|
||||
// normalizeNullFields converts SQL NULL (Go nil) to Odoo's false convention.
|
||||
// Without this, the webclient renders "null" as literal text.
|
||||
func normalizeNullFields(model string, records []orm.Values) {
|
||||
m := orm.Registry.Get(model)
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
for _, rec := range records {
|
||||
for fieldName, val := range rec {
|
||||
if val != nil {
|
||||
continue
|
||||
}
|
||||
f := m.GetField(fieldName)
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
switch f.Type {
|
||||
case orm.TypeChar, orm.TypeText, orm.TypeHTML, orm.TypeSelection:
|
||||
rec[fieldName] = false
|
||||
case orm.TypeMany2one:
|
||||
rec[fieldName] = false
|
||||
case orm.TypeInteger, orm.TypeFloat, orm.TypeMonetary:
|
||||
rec[fieldName] = false
|
||||
case orm.TypeBoolean:
|
||||
rec[fieldName] = false
|
||||
case orm.TypeDate, orm.TypeDatetime:
|
||||
rec[fieldName] = false
|
||||
default:
|
||||
if val == nil {
|
||||
rec[fieldName] = false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user