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

57
pkg/server/fields_get.go Normal file
View File

@@ -0,0 +1,57 @@
package server
import "odoo-go/pkg/orm"
// fieldsGetForModel returns field metadata for a model.
// Mirrors: odoo/orm/models.py BaseModel.fields_get()
func fieldsGetForModel(modelName string) map[string]interface{} {
m := orm.Registry.Get(modelName)
if m == nil {
return map[string]interface{}{}
}
result := make(map[string]interface{})
for name, f := range m.Fields() {
fieldInfo := map[string]interface{}{
"name": name,
"type": f.Type.String(),
"string": f.String,
"help": f.Help,
"readonly": f.Readonly,
"required": f.Required,
"searchable": f.IsStored(),
"sortable": f.IsStored(),
"store": f.IsStored(),
"manual": false,
"depends": f.Depends,
"groupable": f.IsStored() && f.Type != orm.TypeText && f.Type != orm.TypeHTML,
"exportable": true,
"change_default": false,
}
// Relational fields
if f.Comodel != "" {
fieldInfo["relation"] = f.Comodel
}
if f.InverseField != "" {
fieldInfo["relation_field"] = f.InverseField
}
// Selection
if f.Type == orm.TypeSelection && len(f.Selection) > 0 {
sel := make([][]string, len(f.Selection))
for i, item := range f.Selection {
sel[i] = []string{item.Value, item.Label}
}
fieldInfo["selection"] = sel
}
// Domain & context defaults
fieldInfo["domain"] = "[]"
fieldInfo["context"] = "{}"
result[name] = fieldInfo
}
return result
}