Files
goodie/pkg/orm/model.go
Marc 0ed29fe2fd 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>
2026-03-31 01:45:09 +02:00

379 lines
9.7 KiB
Go

package orm
import (
"fmt"
"strings"
)
// Model defines an Odoo model (database table + business logic).
// Mirrors: odoo/orm/models.py BaseModel
//
// Odoo:
//
// class ResPartner(models.Model):
// _name = 'res.partner'
// _description = 'Contact'
// _order = 'name'
// _rec_name = 'name'
//
// Go:
//
// NewModel("res.partner", ModelOpts{
// Description: "Contact",
// Order: "name",
// RecName: "name",
// })
type Model struct {
name string
description string
modelType ModelType
table string // SQL table name
order string // Default sort order
recName string // Field used as display name
inherit []string // _inherit: models to extend
inherits map[string]string // _inherits: {parent_model: fk_field}
auto bool // Auto-create/update table schema
logAccess bool // Auto-create create_uid, write_uid, create_date, write_date
// Fields
fields map[string]*Field
// Methods (registered business logic)
methods map[string]interface{}
// Access control
checkCompany bool // Enforce multi-company record rules
// Hooks
BeforeCreate func(env *Environment, vals Values) error // Called before INSERT
Constraints []ConstraintFunc // Validation constraints
Methods map[string]MethodFunc // Named business methods
// Computed fields
computes map[string]ComputeFunc // field_name → compute function
dependencyMap map[string][]string // trigger_field → []computed_field_names
// Resolved
parents []*Model // Resolved parent models from _inherit
allFields map[string]*Field // Including fields from parents
fieldOrder []string // Field creation order
}
// ModelOpts configures a new model.
type ModelOpts struct {
Description string
Type ModelType
Table string
Order string
RecName string
Inherit []string
Inherits map[string]string
CheckCompany bool
}
// NewModel creates and registers a new model.
// Mirrors: class MyModel(models.Model): _name = '...'
func NewModel(name string, opts ModelOpts) *Model {
m := &Model{
name: name,
description: opts.Description,
modelType: opts.Type,
table: opts.Table,
order: opts.Order,
recName: opts.RecName,
inherit: opts.Inherit,
inherits: opts.Inherits,
checkCompany: opts.CheckCompany,
auto: opts.Type != ModelAbstract,
logAccess: true,
fields: make(map[string]*Field),
methods: make(map[string]interface{}),
allFields: make(map[string]*Field),
}
// Derive table name from model name if not specified.
// Mirrors: odoo/orm/models.py BaseModel._table
// 'res.partner' → 'res_partner'
if m.table == "" && m.auto {
m.table = strings.ReplaceAll(name, ".", "_")
}
if m.order == "" {
m.order = "id"
}
if m.recName == "" {
m.recName = "name"
}
// Add magic fields.
// Mirrors: odoo/orm/models.py BaseModel.MAGIC_COLUMNS
m.addMagicFields()
Registry.Register(m)
return m
}
// addMagicFields adds Odoo's automatic fields to every model.
// Mirrors: odoo/orm/models.py MAGIC_COLUMNS + LOG_ACCESS_COLUMNS
func (m *Model) addMagicFields() {
// id is always present
m.AddField(Integer("id", FieldOpts{
String: "ID",
Readonly: true,
}))
if m.logAccess {
m.AddField(Many2one("create_uid", "res.users", FieldOpts{
String: "Created by",
Readonly: true,
}))
m.AddField(Datetime("create_date", FieldOpts{
String: "Created on",
Readonly: true,
}))
m.AddField(Many2one("write_uid", "res.users", FieldOpts{
String: "Last Updated by",
Readonly: true,
}))
m.AddField(Datetime("write_date", FieldOpts{
String: "Last Updated on",
Readonly: true,
}))
}
}
// AddField adds a field to this model.
func (m *Model) AddField(f *Field) *Model {
f.model = m
m.fields[f.Name] = f
m.allFields[f.Name] = f
m.fieldOrder = append(m.fieldOrder, f.Name)
return m
}
// AddFields adds multiple fields at once.
func (m *Model) AddFields(fields ...*Field) *Model {
for _, f := range fields {
m.AddField(f)
}
return m
}
// GetField returns a field by name, or nil.
func (m *Model) GetField(name string) *Field {
return m.allFields[name]
}
// Fields returns all fields of this model (including inherited).
func (m *Model) Fields() map[string]*Field {
return m.allFields
}
// StoredFields returns fields that have a database column.
func (m *Model) StoredFields() []*Field {
var result []*Field
for _, name := range m.fieldOrder {
f := m.fields[name]
if f.IsStored() {
result = append(result, f)
}
}
return result
}
// Name returns the model name (e.g., "res.partner").
func (m *Model) Name() string { return m.name }
// Table returns the SQL table name (e.g., "res_partner").
func (m *Model) Table() string { return m.table }
// Description returns the human-readable model description.
func (m *Model) Description() string { return m.description }
// Order returns the default sort expression.
func (m *Model) Order() string { return m.order }
// RecName returns the field name used as display name.
func (m *Model) RecName() string { return m.recName }
// IsAbstract returns true if this model has no database table.
func (m *Model) IsAbstract() bool { return m.modelType == ModelAbstract }
// IsTransient returns true if this is a wizard/temporary model.
func (m *Model) IsTransient() bool { return m.modelType == ModelTransient }
// ConstraintFunc validates a recordset. Returns error if constraint violated.
// Mirrors: @api.constrains in Odoo.
type ConstraintFunc func(rs *Recordset) error
// MethodFunc is a callable business method on a model.
// Mirrors: Odoo model methods called via RPC.
type MethodFunc func(rs *Recordset, args ...interface{}) (interface{}, error)
// AddConstraint registers a validation constraint.
func (m *Model) AddConstraint(fn ConstraintFunc) *Model {
m.Constraints = append(m.Constraints, fn)
return m
}
// RegisterMethod registers a named business logic method on this model.
// Mirrors: Odoo's method definitions on model classes.
func (m *Model) RegisterMethod(name string, fn MethodFunc) *Model {
if m.Methods == nil {
m.Methods = make(map[string]MethodFunc)
}
m.Methods[name] = fn
return m
}
// Extend extends this model with additional fields (like _inherit in Odoo).
// Mirrors: class MyModelExt(models.Model): _inherit = 'res.partner'
func (m *Model) Extend(fields ...*Field) *Model {
for _, f := range fields {
m.AddField(f)
}
return m
}
// CreateTableSQL generates the CREATE TABLE statement for this model.
// Mirrors: odoo/orm/models.py BaseModel._table_exist / init()
func (m *Model) CreateTableSQL() string {
if !m.auto {
return ""
}
var cols []string
cols = append(cols, `"id" SERIAL PRIMARY KEY`)
for _, name := range m.fieldOrder {
f := m.fields[name]
if name == "id" || !f.IsStored() {
continue
}
sqlType := f.SQLType()
if sqlType == "" {
continue
}
col := fmt.Sprintf(" %q %s", f.Column(), sqlType)
if f.Required {
col += " NOT NULL"
}
cols = append(cols, col)
}
return fmt.Sprintf("CREATE TABLE IF NOT EXISTS %q (\n%s\n)",
m.table,
strings.Join(cols, ",\n"),
)
}
// ForeignKeySQL generates ALTER TABLE statements for foreign keys.
func (m *Model) ForeignKeySQL() []string {
var stmts []string
for _, name := range m.fieldOrder {
f := m.fields[name]
if f.Type != TypeMany2one || f.Comodel == "" {
continue
}
if name == "create_uid" || name == "write_uid" {
continue // Skip magic fields, added later
}
comodel := Registry.Get(f.Comodel)
if comodel == nil {
continue
}
onDelete := "SET NULL"
switch f.OnDelete {
case OnDeleteRestrict:
onDelete = "RESTRICT"
case OnDeleteCascade:
onDelete = "CASCADE"
}
stmt := fmt.Sprintf(
`ALTER TABLE %q ADD CONSTRAINT %q FOREIGN KEY (%q) REFERENCES %q ("id") ON DELETE %s`,
m.table,
fmt.Sprintf("%s_%s_fkey", m.table, f.Column()),
f.Column(),
comodel.Table(),
onDelete,
)
stmts = append(stmts, stmt)
}
return stmts
}
// IndexSQL generates CREATE INDEX statements.
func (m *Model) IndexSQL() []string {
var stmts []string
for _, name := range m.fieldOrder {
f := m.fields[name]
if !f.Index || !f.IsStored() || name == "id" {
continue
}
stmt := fmt.Sprintf(
`CREATE INDEX IF NOT EXISTS %q ON %q (%q)`,
fmt.Sprintf("%s_%s_index", m.table, f.Column()),
m.table,
f.Column(),
)
stmts = append(stmts, stmt)
}
return stmts
}
// Many2manyTableSQL generates junction table DDL for Many2many fields.
func (m *Model) Many2manyTableSQL() []string {
var stmts []string
for _, name := range m.fieldOrder {
f := m.fields[name]
if f.Type != TypeMany2many {
continue
}
comodel := Registry.Get(f.Comodel)
if comodel == nil {
continue
}
// Determine junction table name.
// Mirrors: odoo/orm/fields_relational.py Many2many._table_name
relation := f.Relation
if relation == "" {
// Default: alphabetically sorted model tables joined by underscore
t1, t2 := m.table, comodel.Table()
if t1 > t2 {
t1, t2 = t2, t1
}
relation = fmt.Sprintf("%s_%s_rel", t1, t2)
}
col1 := f.Column1
if col1 == "" {
col1 = m.table + "_id"
}
col2 := f.Column2
if col2 == "" {
col2 = comodel.Table() + "_id"
}
// Self-referential M2M: ensure distinct column names
if col1 == col2 {
col1 = m.table + "_src_id"
col2 = m.table + "_dst_id"
}
stmt := fmt.Sprintf(
"CREATE TABLE IF NOT EXISTS %q (\n"+
" %q integer NOT NULL REFERENCES %q(id) ON DELETE CASCADE,\n"+
" %q integer NOT NULL REFERENCES %q(id) ON DELETE CASCADE,\n"+
" PRIMARY KEY (%q, %q)\n"+
")",
relation, col1, m.table, col2, comodel.Table(), col1, col2,
)
stmts = append(stmts, stmt)
}
return stmts
}