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:
@@ -61,6 +61,23 @@ func initResUsers() {
|
|||||||
Help: "External user with limited access (portal/public)",
|
Help: "External user with limited access (portal/public)",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// -- Methods --
|
||||||
|
|
||||||
|
// action_get returns the "Change My Preferences" action for the current user.
|
||||||
|
// Mirrors: odoo/addons/base/models/res_users.py Users.action_get()
|
||||||
|
m.RegisterMethod("action_get", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||||
|
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": rs.Env().UID(),
|
||||||
|
"context": map[string]interface{}{},
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// initResGroups registers the res.groups model.
|
// initResGroups registers the res.groups model.
|
||||||
|
|||||||
BIN
odoo-server
BIN
odoo-server
Binary file not shown.
@@ -629,20 +629,6 @@ func (s *Server) dispatchORM(env *orm.Environment, params CallKWParams) (interfa
|
|||||||
case "activity_format":
|
case "activity_format":
|
||||||
return []interface{}{}, nil
|
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":
|
case "action_archive":
|
||||||
ids := parseIDs(params.Args)
|
ids := parseIDs(params.Args)
|
||||||
if len(ids) > 0 {
|
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
|
// Format date/datetime fields to Odoo's expected string format
|
||||||
formatDateFields(model, records)
|
formatDateFields(model, records)
|
||||||
|
|
||||||
|
// Convert SQL NULLs to Odoo-expected defaults (false, 0, etc.)
|
||||||
|
normalizeNullFields(model, records)
|
||||||
|
|
||||||
if records == nil {
|
if records == nil {
|
||||||
records = []orm.Values{}
|
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
|
// Format date/datetime fields to Odoo's expected string format
|
||||||
formatDateFields(model, records)
|
formatDateFields(model, records)
|
||||||
|
|
||||||
|
// Convert SQL NULLs to Odoo-expected defaults (false, 0, etc.)
|
||||||
|
normalizeNullFields(model, records)
|
||||||
|
|
||||||
if records == nil {
|
if records == nil {
|
||||||
records = []orm.Values{}
|
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.
|
// formatDateFields converts date/datetime values to Odoo's expected string format.
|
||||||
func formatDateFields(model string, records []orm.Values) {
|
func formatDateFields(model string, records []orm.Values) {
|
||||||
m := orm.Registry.Get(model)
|
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))
|
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)
|
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)
|
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)
|
seedMenus(ctx, tx)
|
||||||
|
|
||||||
// 13. Demo data
|
// 14. Demo data
|
||||||
if cfg.DemoData {
|
if cfg.DemoData {
|
||||||
seedDemoData(ctx, tx)
|
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{
|
seqs := []string{
|
||||||
"res_currency", "res_country", "res_partner", "res_company",
|
"res_currency", "res_country", "res_partner", "res_company",
|
||||||
"res_users", "ir_sequence", "account_journal", "account_account",
|
"res_users", "ir_sequence", "account_journal", "account_account",
|
||||||
"account_tax", "sale_order", "sale_order_line", "account_move",
|
"account_tax", "sale_order", "sale_order_line", "account_move",
|
||||||
"ir_act_window", "ir_model_data", "ir_ui_menu",
|
"ir_act_window", "ir_model_data", "ir_ui_menu",
|
||||||
|
"stock_location", "stock_picking_type", "stock_warehouse",
|
||||||
}
|
}
|
||||||
for _, table := range seqs {
|
for _, table := range seqs {
|
||||||
tx.Exec(ctx, fmt.Sprintf(
|
tx.Exec(ctx, fmt.Sprintf(
|
||||||
@@ -285,6 +289,40 @@ func SeedWithSetup(ctx context.Context, pool *pgxpool.Pool, cfg SetupConfig) err
|
|||||||
return nil
|
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.
|
// seedViews creates UI views for key models.
|
||||||
func seedViews(ctx context.Context, tx pgx.Tx) {
|
func seedViews(ctx context.Context, tx pgx.Tx) {
|
||||||
log.Println("db: seeding UI views...")
|
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"},
|
{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"},
|
{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"},
|
{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 {
|
for _, a := range actions {
|
||||||
|
|||||||
Reference in New Issue
Block a user