Odoo ERP ported to Go — complete backend + original OWL frontend

Full port of Odoo's ERP system from Python to Go, with the original
Odoo JavaScript frontend (OWL framework) running against the Go server.

Backend (10,691 LoC Go):
- Custom ORM: CRUD, domains→SQL with JOINs, computed fields, sequences
- 93 models across 14 modules (base, account, sale, stock, purchase, hr,
  project, crm, fleet, product, l10n_de, google_address/translate/calendar)
- Auth with bcrypt + session cookies
- Setup wizard (company, SKR03 chart, admin, demo data)
- Double-entry bookkeeping constraint
- Sale→Invoice workflow (confirm SO → generate invoice → post)
- SKR03 chart of accounts (110 accounts) + German taxes (USt/VSt)
- Record rules (multi-company filter)
- Google integrations as opt-in modules (Maps, Translate, Calendar)

Frontend:
- Odoo's original OWL webclient (503 JS modules, 378 XML templates)
- JS transpiled via Odoo's js_transpiler (ES modules → odoo.define)
- SCSS compiled to CSS (675KB) via dart-sass
- XML templates compiled to registerTemplate() JS calls
- Static file serving from Odoo source addons
- Login page, session management, menu navigation
- Contacts list view renders with real data from PostgreSQL

Infrastructure:
- 14MB single binary (CGO_ENABLED=0)
- Docker Compose (Go server + PostgreSQL 16)
- Zero phone-home (no outbound calls to odoo.com)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc
2026-03-31 01:45:09 +02:00
commit 0ed29fe2fd
90 changed files with 12133 additions and 0 deletions

228
addons/base/models/ir_ui.go Normal file
View File

@@ -0,0 +1,228 @@
package models
import "odoo-go/pkg/orm"
// initIrUI registers UI infrastructure models.
// Mirrors: odoo/addons/base/models/ir_ui_menu.py, ir_ui_view.py, ir_actions.py
func initIrUI() {
initIrUIMenu()
initIrUIView()
initIrActions()
initIrSequence()
initIrAttachment()
initReportPaperformat()
}
// initIrUIMenu registers ir.ui.menu — Application menu structure.
// Mirrors: odoo/addons/base/models/ir_ui_menu.py class IrUiMenu
func initIrUIMenu() {
m := orm.NewModel("ir.ui.menu", orm.ModelOpts{
Description: "Menu",
Order: "sequence, id",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Menu", Required: true, Translate: true}),
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
orm.Many2one("parent_id", "ir.ui.menu", orm.FieldOpts{String: "Parent Menu", Index: true}),
orm.One2many("child_id", "ir.ui.menu", "parent_id", orm.FieldOpts{String: "Child Menus"}),
orm.Char("complete_name", orm.FieldOpts{String: "Full Path", Compute: "_compute_complete_name"}),
orm.Many2many("groups_id", "res.groups", orm.FieldOpts{String: "Groups"}),
orm.Char("web_icon", orm.FieldOpts{String: "Web Icon File"}),
orm.Char("action", orm.FieldOpts{String: "Action"}),
)
}
// initIrUIView registers ir.ui.view — UI view definitions.
// Mirrors: odoo/addons/base/models/ir_ui_view.py class View
func initIrUIView() {
m := orm.NewModel("ir.ui.view", orm.ModelOpts{
Description: "View",
Order: "priority, name, id",
RecName: "name",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "View Name", Required: true}),
orm.Selection("type", []orm.SelectionItem{
{Value: "tree", Label: "Tree"},
{Value: "form", Label: "Form"},
{Value: "graph", Label: "Graph"},
{Value: "pivot", Label: "Pivot"},
{Value: "calendar", Label: "Calendar"},
{Value: "gantt", Label: "Gantt"},
{Value: "kanban", Label: "Kanban"},
{Value: "search", Label: "Search"},
{Value: "qweb", Label: "QWeb"},
{Value: "list", Label: "List"},
{Value: "activity", Label: "Activity"},
}, orm.FieldOpts{String: "View Type"}),
orm.Char("model", orm.FieldOpts{String: "Model", Index: true}),
orm.Integer("priority", orm.FieldOpts{String: "Sequence", Default: 16}),
orm.Text("arch", orm.FieldOpts{String: "View Architecture"}),
orm.Text("arch_db", orm.FieldOpts{String: "Arch Blob", Translate: true}),
orm.Many2one("inherit_id", "ir.ui.view", orm.FieldOpts{String: "Inherited View"}),
orm.One2many("inherit_children_ids", "ir.ui.view", "inherit_id", orm.FieldOpts{String: "Views which inherit from this one"}),
orm.Char("key", orm.FieldOpts{String: "Key", Index: true}),
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
orm.Selection("mode", []orm.SelectionItem{
{Value: "primary", Label: "Base view"},
{Value: "extension", Label: "Extension View"},
}, orm.FieldOpts{String: "View inheritance mode", Default: "primary"}),
orm.Many2many("groups_id", "res.groups", orm.FieldOpts{String: "Groups"}),
)
}
// initIrActions registers ir.actions.* — Action definitions.
// Mirrors: odoo/addons/base/models/ir_actions.py
func initIrActions() {
// ir.actions.act_window — Window actions (open a view)
m := orm.NewModel("ir.actions.act_window", orm.ModelOpts{
Description: "Action Window",
Table: "ir_act_window",
Order: "name",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Action Name", Required: true, Translate: true}),
orm.Char("type", orm.FieldOpts{String: "Action Type", Default: "ir.actions.act_window"}),
orm.Char("res_model", orm.FieldOpts{String: "Model", Required: true}),
orm.Selection("view_mode", []orm.SelectionItem{
{Value: "tree", Label: "Tree"},
{Value: "form", Label: "Form"},
{Value: "tree,form", Label: "Tree, Form"},
{Value: "form,tree", Label: "Form, Tree"},
{Value: "kanban", Label: "Kanban"},
{Value: "kanban,tree,form", Label: "Kanban, Tree, Form"},
}, orm.FieldOpts{String: "View Mode", Default: "tree,form"}),
orm.Integer("res_id", orm.FieldOpts{String: "Record ID"}),
orm.Char("domain", orm.FieldOpts{String: "Domain Value"}),
orm.Char("context", orm.FieldOpts{String: "Context Value", Default: "{}"}),
orm.Integer("limit", orm.FieldOpts{String: "Limit", Default: 80}),
orm.Char("search_view_id", orm.FieldOpts{String: "Search View Ref"}),
orm.Char("target", orm.FieldOpts{String: "Target Window", Default: "current"}),
orm.Boolean("auto_search", orm.FieldOpts{String: "Auto Search", Default: true}),
orm.Many2many("groups_id", "res.groups", orm.FieldOpts{String: "Groups"}),
orm.Char("help", orm.FieldOpts{String: "Action Description"}),
orm.Many2one("binding_model_id", "ir.model", orm.FieldOpts{String: "Binding Model"}),
)
// ir.actions.server — Server actions (execute Python/code)
srv := orm.NewModel("ir.actions.server", orm.ModelOpts{
Description: "Server Actions",
Table: "ir_act_server",
Order: "sequence, name",
})
srv.AddFields(
orm.Char("name", orm.FieldOpts{String: "Action Name", Required: true, Translate: true}),
orm.Char("type", orm.FieldOpts{String: "Action Type", Default: "ir.actions.server"}),
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 5}),
orm.Many2one("model_id", "ir.model", orm.FieldOpts{String: "Model", Required: true, OnDelete: orm.OnDeleteCascade}),
orm.Char("model_name", orm.FieldOpts{String: "Model Name", Related: "model_id.model"}),
orm.Selection("state", []orm.SelectionItem{
{Value: "code", Label: "Execute Code"},
{Value: "object_write", Label: "Update Record"},
{Value: "object_create", Label: "Create Record"},
{Value: "multi", Label: "Execute Several Actions"},
}, orm.FieldOpts{String: "Action To Do", Default: "code", Required: true}),
orm.Text("code", orm.FieldOpts{String: "Code"}),
orm.Many2many("groups_id", "res.groups", orm.FieldOpts{String: "Groups"}),
)
}
// initIrSequence registers ir.sequence — Automatic numbering.
// Mirrors: odoo/addons/base/models/ir_sequence.py
func initIrSequence() {
m := orm.NewModel("ir.sequence", orm.ModelOpts{
Description: "Sequence",
Order: "name",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
orm.Char("code", orm.FieldOpts{String: "Sequence Code"}),
orm.Selection("implementation", []orm.SelectionItem{
{Value: "standard", Label: "Standard"},
{Value: "no_gap", Label: "No gap"},
}, orm.FieldOpts{String: "Implementation", Default: "standard", Required: true}),
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
orm.Char("prefix", orm.FieldOpts{String: "Prefix"}),
orm.Char("suffix", orm.FieldOpts{String: "Suffix"}),
orm.Integer("number_next", orm.FieldOpts{String: "Next Number", Default: 1, Required: true}),
orm.Integer("number_increment", orm.FieldOpts{String: "Step", Default: 1, Required: true}),
orm.Integer("padding", orm.FieldOpts{String: "Sequence Size", Default: 0, Required: true}),
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
orm.Boolean("use_date_range", orm.FieldOpts{String: "Use subsequences per date_range"}),
)
}
// initIrAttachment registers ir.attachment — File storage.
// Mirrors: odoo/addons/base/models/ir_attachment.py
func initIrAttachment() {
m := orm.NewModel("ir.attachment", orm.ModelOpts{
Description: "Attachment",
Order: "id desc",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
orm.Char("description", orm.FieldOpts{String: "Description"}),
orm.Char("res_model", orm.FieldOpts{String: "Resource Model", Index: true}),
orm.Integer("res_id", orm.FieldOpts{String: "Resource ID", Index: true}),
orm.Char("res_field", orm.FieldOpts{String: "Resource Field"}),
orm.Char("res_name", orm.FieldOpts{String: "Resource Name"}),
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
orm.Selection("type", []orm.SelectionItem{
{Value: "url", Label: "URL"},
{Value: "binary", Label: "File"},
}, orm.FieldOpts{String: "Type", Default: "binary", Required: true}),
orm.Char("url", orm.FieldOpts{String: "Url"}),
orm.Binary("datas", orm.FieldOpts{String: "File Content"}),
orm.Char("store_fname", orm.FieldOpts{String: "Stored Filename"}),
orm.Integer("file_size", orm.FieldOpts{String: "File Size"}),
orm.Char("checksum", orm.FieldOpts{String: "Checksum/SHA1", Size: 40, Index: true}),
orm.Char("mimetype", orm.FieldOpts{String: "Mime Type"}),
orm.Boolean("public", orm.FieldOpts{String: "Is public document"}),
)
}
// initReportPaperformat registers report.paperformat.
// Mirrors: odoo/addons/base/models/report_paperformat.py
func initReportPaperformat() {
m := orm.NewModel("report.paperformat", orm.ModelOpts{
Description: "Paper Format Config",
Order: "name",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
orm.Boolean("default", orm.FieldOpts{String: "Default paper format"}),
orm.Selection("format", []orm.SelectionItem{
{Value: "A0", Label: "A0 (841 x 1189 mm)"},
{Value: "A1", Label: "A1 (594 x 841 mm)"},
{Value: "A2", Label: "A2 (420 x 594 mm)"},
{Value: "A3", Label: "A3 (297 x 420 mm)"},
{Value: "A4", Label: "A4 (210 x 297 mm)"},
{Value: "A5", Label: "A5 (148 x 210 mm)"},
{Value: "Legal", Label: "Legal (216 x 356 mm)"},
{Value: "Letter", Label: "Letter (216 x 279 mm)"},
{Value: "custom", Label: "Custom"},
}, orm.FieldOpts{String: "Paper size", Default: "A4"}),
orm.Integer("margin_top", orm.FieldOpts{String: "Top Margin (mm)", Default: 40}),
orm.Integer("margin_bottom", orm.FieldOpts{String: "Bottom Margin (mm)", Default: 20}),
orm.Integer("margin_left", orm.FieldOpts{String: "Left Margin (mm)", Default: 7}),
orm.Integer("margin_right", orm.FieldOpts{String: "Right Margin (mm)", Default: 7}),
orm.Integer("page_height", orm.FieldOpts{String: "Page height (mm)"}),
orm.Integer("page_width", orm.FieldOpts{String: "Page width (mm)"}),
orm.Selection("orientation", []orm.SelectionItem{
{Value: "Landscape", Label: "Landscape"},
{Value: "Portrait", Label: "Portrait"},
}, orm.FieldOpts{String: "Orientation", Default: "Portrait"}),
orm.Integer("header_spacing", orm.FieldOpts{String: "Header spacing (mm)"}),
orm.Integer("dpi", orm.FieldOpts{String: "Output DPI", Default: 90, Required: true}),
orm.Boolean("disable_shrinking", orm.FieldOpts{String: "Disable smart shrinking"}),
)
}