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

204
pkg/orm/compute.go Normal file
View File

@@ -0,0 +1,204 @@
package orm
import "fmt"
// ComputeFunc is a function that computes field values for a recordset.
// Mirrors: @api.depends decorated methods in Odoo.
//
// The function receives a singleton recordset and must return a Values map
// with the computed field values.
//
// Example:
//
// func computeAmount(rs *Recordset) (Values, error) {
// total := 0.0
// // ... sum line amounts ...
// return Values{"amount_total": total}, nil
// }
type ComputeFunc func(rs *Recordset) (Values, error)
// RegisterCompute registers a compute function for a field.
// The same function can compute multiple fields (call RegisterCompute for each).
func (m *Model) RegisterCompute(fieldName string, fn ComputeFunc) {
if m.computes == nil {
m.computes = make(map[string]ComputeFunc)
}
m.computes[fieldName] = fn
}
// SetupComputes builds the reverse dependency map for this model.
// Called after all modules are loaded.
//
// For each field with Depends, creates a mapping:
//
// trigger_field → []computed_field_names
//
// So when trigger_field is written, we know which computes to re-run.
func (m *Model) SetupComputes() {
m.dependencyMap = make(map[string][]string)
for _, name := range m.fieldOrder {
f := m.fields[name]
if f.Compute == "" || len(f.Depends) == 0 {
continue
}
for _, dep := range f.Depends {
m.dependencyMap[dep] = append(m.dependencyMap[dep], name)
}
}
}
// RunStoredComputes runs compute functions for stored computed fields
// and merges results into vals. Called before INSERT in Create().
func RunStoredComputes(m *Model, env *Environment, id int64, vals Values) error {
if len(m.computes) == 0 {
return nil
}
// Collect stored computed fields that have registered functions
seen := make(map[string]bool) // Track compute functions already called (by field name)
for _, name := range m.fieldOrder {
f := m.fields[name]
if f.Compute == "" || !f.Store {
continue
}
fn, ok := m.computes[name]
if !ok {
continue
}
// Deduplicate: same function may compute multiple fields
if seen[f.Compute] {
continue
}
seen[f.Compute] = true
// Create a temporary recordset for the computation
rs := &Recordset{env: env, model: m, ids: []int64{id}}
computed, err := fn(rs)
if err != nil {
return fmt.Errorf("orm: compute %s.%s: %w", m.name, name, err)
}
// Merge computed values
for k, v := range computed {
cf := m.GetField(k)
if cf != nil && cf.Store && cf.Compute != "" {
vals[k] = v
}
}
}
return nil
}
// TriggerRecompute re-computes stored fields that depend on written fields.
// Called after UPDATE in Write().
func TriggerRecompute(rs *Recordset, writtenFields Values) error {
m := rs.model
if len(m.dependencyMap) == 0 || len(m.computes) == 0 {
return nil
}
// Find which computed fields need re-computation
toRecompute := make(map[string]bool)
for fieldName := range writtenFields {
for _, computedField := range m.dependencyMap[fieldName] {
toRecompute[computedField] = true
}
}
if len(toRecompute) == 0 {
return nil
}
// Run computes for each record
seen := make(map[string]bool)
for _, id := range rs.IDs() {
singleton := rs.Browse(id)
recomputedVals := make(Values)
for fieldName := range toRecompute {
f := m.GetField(fieldName)
if f == nil || !f.Store {
continue
}
fn, ok := m.computes[fieldName]
if !ok {
continue
}
if seen[f.Compute] {
continue
}
seen[f.Compute] = true
computed, err := fn(singleton)
if err != nil {
return fmt.Errorf("orm: recompute %s.%s: %w", m.name, fieldName, err)
}
for k, v := range computed {
cf := m.GetField(k)
if cf != nil && cf.Store && cf.Compute != "" {
recomputedVals[k] = v
}
}
}
// Write recomputed values directly to DB (bypass hooks to avoid infinite loop)
if len(recomputedVals) > 0 {
if err := writeDirectNohook(rs.env, m, id, recomputedVals); err != nil {
return err
}
}
// Reset seen for next record
seen = make(map[string]bool)
}
return nil
}
// writeDirectNohook writes values directly without triggering hooks or recomputes.
func writeDirectNohook(env *Environment, m *Model, id int64, vals Values) error {
var setClauses []string
var args []interface{}
idx := 1
for k, v := range vals {
f := m.GetField(k)
if f == nil || !f.IsStored() {
continue
}
setClauses = append(setClauses, fmt.Sprintf("%q = $%d", f.Column(), idx))
args = append(args, v)
idx++
}
if len(setClauses) == 0 {
return nil
}
args = append(args, id)
query := fmt.Sprintf(
`UPDATE %q SET %s WHERE "id" = $%d`,
m.Table(), joinStrings(setClauses, ", "), idx,
)
_, err := env.tx.Exec(env.ctx, query, args...)
return err
}
func joinStrings(s []string, sep string) string {
result := ""
for i, str := range s {
if i > 0 {
result += sep
}
result += str
}
return result
}
// SetupAllComputes calls SetupComputes on all registered models.
func SetupAllComputes() {
for _, m := range Registry.Models() {
m.SetupComputes()
}
}