Fix null→false, stock seed data, action_get on model, FK order
- Normalize null DB values to false (Odoo convention) in web_search_read and web_read responses - Seed stock reference data: 6 locations, 1 warehouse, 3 picking types - Fix FK order: warehouse must be created before picking types - Move action_get from hardcoded dispatcher to res.users RegisterMethod - Add action_res_users_my (ID 103) to seedActions - Remove hardcoded action_get case from dispatchORM Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -629,20 +629,6 @@ func (s *Server) dispatchORM(env *orm.Environment, params CallKWParams) (interfa
|
||||
case "activity_format":
|
||||
return []interface{}{}, nil
|
||||
|
||||
case "action_get":
|
||||
// res.users.action_get() — returns the preference action
|
||||
// Mirrors: odoo/addons/base/models/res_users.py action_get()
|
||||
return map[string]interface{}{
|
||||
"type": "ir.actions.act_window",
|
||||
"name": "Change My Preferences",
|
||||
"res_model": "res.users",
|
||||
"view_mode": "form",
|
||||
"views": [][]interface{}{{false, "form"}},
|
||||
"target": "new",
|
||||
"res_id": env.UID(),
|
||||
"context": map[string]interface{}{},
|
||||
}, nil
|
||||
|
||||
case "action_archive":
|
||||
ids := parseIDs(params.Args)
|
||||
if len(ids) > 0 {
|
||||
|
||||
@@ -89,6 +89,9 @@ func handleWebSearchRead(env *orm.Environment, model string, params CallKWParams
|
||||
// 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)
|
||||
|
||||
if records == nil {
|
||||
records = []orm.Values{}
|
||||
}
|
||||
@@ -133,6 +136,9 @@ func handleWebRead(env *orm.Environment, model string, params CallKWParams) (int
|
||||
// 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)
|
||||
|
||||
if records == nil {
|
||||
records = []orm.Values{}
|
||||
}
|
||||
@@ -203,6 +209,42 @@ func formatM2OFields(env *orm.Environment, modelName string, records []orm.Value
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
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:
|
||||
rec[fieldName] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// formatDateFields converts date/datetime values to Odoo's expected string format.
|
||||
func formatDateFields(model string, records []orm.Values) {
|
||||
m := orm.Registry.Get(model)
|
||||
|
||||
@@ -250,26 +250,30 @@ func SeedWithSetup(ctx context.Context, pool *pgxpool.Pool, cfg SetupConfig) err
|
||||
log.Printf("db: seeded %d German tax definitions", len(l10n_de.SKR03Taxes))
|
||||
}
|
||||
|
||||
// 10. UI Views for key models
|
||||
// 10. Stock reference data (locations, picking types, warehouse)
|
||||
seedStockData(ctx, tx)
|
||||
|
||||
// 11. UI Views for key models
|
||||
seedViews(ctx, tx)
|
||||
|
||||
// 11. Actions (ir_act_window + ir_model_data for XML IDs)
|
||||
// 12. Actions (ir_act_window + ir_model_data for XML IDs)
|
||||
seedActions(ctx, tx)
|
||||
|
||||
// 12. Menus (ir_ui_menu + ir_model_data for XML IDs)
|
||||
// 13. Menus (ir_ui_menu + ir_model_data for XML IDs)
|
||||
seedMenus(ctx, tx)
|
||||
|
||||
// 13. Demo data
|
||||
// 14. Demo data
|
||||
if cfg.DemoData {
|
||||
seedDemoData(ctx, tx)
|
||||
}
|
||||
|
||||
// 14. Reset sequences (each individually — pgx doesn't support multi-statement)
|
||||
// 15. Reset sequences (each individually — pgx doesn't support multi-statement)
|
||||
seqs := []string{
|
||||
"res_currency", "res_country", "res_partner", "res_company",
|
||||
"res_users", "ir_sequence", "account_journal", "account_account",
|
||||
"account_tax", "sale_order", "sale_order_line", "account_move",
|
||||
"ir_act_window", "ir_model_data", "ir_ui_menu",
|
||||
"stock_location", "stock_picking_type", "stock_warehouse",
|
||||
}
|
||||
for _, table := range seqs {
|
||||
tx.Exec(ctx, fmt.Sprintf(
|
||||
@@ -285,6 +289,40 @@ func SeedWithSetup(ctx context.Context, pool *pgxpool.Pool, cfg SetupConfig) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// seedStockData creates stock locations, picking types, and a default warehouse.
|
||||
// Mirrors: odoo/addons/stock/data/stock_data.xml
|
||||
func seedStockData(ctx context.Context, tx pgx.Tx) {
|
||||
log.Println("db: seeding stock reference data...")
|
||||
|
||||
// Stock locations (hierarchical, mirroring Odoo's stock_data.xml)
|
||||
tx.Exec(ctx, `
|
||||
INSERT INTO stock_location (id, name, complete_name, usage, active, location_id, company_id) VALUES
|
||||
(1, 'Physical Locations', 'Physical Locations', 'view', true, NULL, 1),
|
||||
(2, 'Partner Locations', 'Partner Locations', 'view', true, NULL, 1),
|
||||
(3, 'Virtual Locations', 'Virtual Locations', 'view', true, NULL, 1),
|
||||
(4, 'WH', 'Physical Locations/WH', 'internal', true, 1, 1),
|
||||
(5, 'Customers', 'Partner Locations/Customers', 'customer', true, 2, 1),
|
||||
(6, 'Vendors', 'Partner Locations/Vendors', 'supplier', true, 2, 1)
|
||||
ON CONFLICT (id) DO NOTHING`)
|
||||
|
||||
// Default warehouse (must come before picking types due to FK)
|
||||
tx.Exec(ctx, `
|
||||
INSERT INTO stock_warehouse (id, name, code, active, company_id, lot_stock_id, sequence)
|
||||
VALUES (1, 'Main Warehouse', 'WH', true, 1, 4, 10)
|
||||
ON CONFLICT (id) DO NOTHING`)
|
||||
|
||||
// Stock picking types (operation types for receipts, deliveries, internal transfers)
|
||||
tx.Exec(ctx, `
|
||||
INSERT INTO stock_picking_type (id, name, code, sequence_code, active, warehouse_id, company_id, sequence,
|
||||
default_location_src_id, default_location_dest_id) VALUES
|
||||
(1, 'Receipts', 'incoming', 'IN', true, 1, 1, 1, 6, 4),
|
||||
(2, 'Delivery Orders', 'outgoing', 'OUT', true, 1, 1, 2, 4, 5),
|
||||
(3, 'Internal Transfers', 'internal', 'INT', true, 1, 1, 3, 4, 4)
|
||||
ON CONFLICT (id) DO NOTHING`)
|
||||
|
||||
log.Println("db: stock reference data seeded (6 locations, 3 picking types, 1 warehouse)")
|
||||
}
|
||||
|
||||
// seedViews creates UI views for key models.
|
||||
func seedViews(ctx context.Context, tx pgx.Tx) {
|
||||
log.Println("db: seeding UI views...")
|
||||
@@ -428,6 +466,7 @@ func seedActions(ctx context.Context, tx pgx.Tx) {
|
||||
{100, "Settings", "res.company", "form", "[]", "{}", "current", 80, 1, "base", "action_res_company_form"},
|
||||
{101, "Users", "res.users", "list,form", "[]", "{}", "current", 80, 0, "base", "action_res_users"},
|
||||
{102, "Sequences", "ir.sequence", "list,form", "[]", "{}", "current", 80, 0, "base", "ir_sequence_form"},
|
||||
{103, "Change My Preferences", "res.users", "form", "[]", "{}", "new", 80, 0, "base", "action_res_users_my"},
|
||||
}
|
||||
|
||||
for _, a := range actions {
|
||||
|
||||
Reference in New Issue
Block a user