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>
258 lines
6.4 KiB
Go
258 lines
6.4 KiB
Go
package orm
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// Environment is the central context for all ORM operations.
|
|
// Mirrors: odoo/orm/environments.py Environment
|
|
//
|
|
// Odoo: self.env['res.partner'].search([('name', 'ilike', 'test')])
|
|
// Go: env.Model("res.partner").Search(And(Leaf("name", "ilike", "test")))
|
|
//
|
|
// An Environment wraps:
|
|
// - A database transaction (cursor in Odoo terms)
|
|
// - The current user ID
|
|
// - The current company ID
|
|
// - Context values (lang, tz, etc.)
|
|
type Environment struct {
|
|
ctx context.Context
|
|
pool *pgxpool.Pool
|
|
tx pgx.Tx
|
|
uid int64
|
|
companyID int64
|
|
su bool // sudo mode (bypass access checks)
|
|
context map[string]interface{}
|
|
cache *Cache
|
|
}
|
|
|
|
// EnvConfig configures a new Environment.
|
|
type EnvConfig struct {
|
|
Pool *pgxpool.Pool
|
|
UID int64
|
|
CompanyID int64
|
|
Context map[string]interface{}
|
|
}
|
|
|
|
// NewEnvironment creates a new ORM environment.
|
|
// Mirrors: odoo.api.Environment(cr, uid, context)
|
|
func NewEnvironment(ctx context.Context, cfg EnvConfig) (*Environment, error) {
|
|
tx, err := cfg.Pool.Begin(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("orm: begin transaction: %w", err)
|
|
}
|
|
|
|
envCtx := cfg.Context
|
|
if envCtx == nil {
|
|
envCtx = make(map[string]interface{})
|
|
}
|
|
|
|
return &Environment{
|
|
ctx: ctx,
|
|
pool: cfg.Pool,
|
|
tx: tx,
|
|
uid: cfg.UID,
|
|
companyID: cfg.CompanyID,
|
|
context: envCtx,
|
|
cache: NewCache(),
|
|
}, nil
|
|
}
|
|
|
|
// Model returns a Recordset bound to this environment for the given model.
|
|
// Mirrors: self.env['model.name']
|
|
func (env *Environment) Model(name string) *Recordset {
|
|
m := Registry.MustGet(name)
|
|
return &Recordset{
|
|
env: env,
|
|
model: m,
|
|
ids: nil,
|
|
}
|
|
}
|
|
|
|
// Ref returns a record by its XML ID (external identifier).
|
|
// Mirrors: self.env.ref('module.xml_id')
|
|
func (env *Environment) Ref(xmlID string) (*Recordset, error) {
|
|
// Query ir_model_data for the external ID
|
|
var resModel string
|
|
var resID int64
|
|
|
|
err := env.tx.QueryRow(env.ctx,
|
|
`SELECT model, res_id FROM ir_model_data WHERE module || '.' || name = $1 LIMIT 1`,
|
|
xmlID,
|
|
).Scan(&resModel, &resID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("orm: ref %q not found: %w", xmlID, err)
|
|
}
|
|
|
|
return env.Model(resModel).Browse(resID), nil
|
|
}
|
|
|
|
// UID returns the current user ID.
|
|
func (env *Environment) UID() int64 { return env.uid }
|
|
|
|
// CompanyID returns the current company ID.
|
|
func (env *Environment) CompanyID() int64 { return env.companyID }
|
|
|
|
// IsSuperuser returns true if this environment bypasses access checks.
|
|
func (env *Environment) IsSuperuser() bool { return env.su }
|
|
|
|
// Context returns the environment context (Odoo-style key-value context).
|
|
func (env *Environment) Context() map[string]interface{} { return env.context }
|
|
|
|
// Ctx returns the Go context.Context for database operations.
|
|
func (env *Environment) Ctx() context.Context { return env.ctx }
|
|
|
|
// Lang returns the language from context.
|
|
func (env *Environment) Lang() string {
|
|
if lang, ok := env.context["lang"].(string); ok {
|
|
return lang
|
|
}
|
|
return "en_US"
|
|
}
|
|
|
|
// Tx returns the underlying database transaction.
|
|
func (env *Environment) Tx() pgx.Tx { return env.tx }
|
|
|
|
// Sudo returns a new environment with superuser privileges.
|
|
// Mirrors: self.env['model'].sudo()
|
|
func (env *Environment) Sudo() *Environment {
|
|
return &Environment{
|
|
ctx: env.ctx,
|
|
pool: env.pool,
|
|
tx: env.tx,
|
|
uid: env.uid,
|
|
companyID: env.companyID,
|
|
su: true,
|
|
context: env.context,
|
|
cache: env.cache,
|
|
}
|
|
}
|
|
|
|
// WithUser returns a new environment for a different user.
|
|
// Mirrors: self.with_user(user_id)
|
|
func (env *Environment) WithUser(uid int64) *Environment {
|
|
return &Environment{
|
|
ctx: env.ctx,
|
|
pool: env.pool,
|
|
tx: env.tx,
|
|
uid: uid,
|
|
companyID: env.companyID,
|
|
su: false,
|
|
context: env.context,
|
|
cache: env.cache,
|
|
}
|
|
}
|
|
|
|
// WithCompany returns a new environment for a different company.
|
|
// Mirrors: self.with_company(company_id)
|
|
func (env *Environment) WithCompany(companyID int64) *Environment {
|
|
return &Environment{
|
|
ctx: env.ctx,
|
|
pool: env.pool,
|
|
tx: env.tx,
|
|
uid: env.uid,
|
|
companyID: companyID,
|
|
su: env.su,
|
|
context: env.context,
|
|
cache: env.cache,
|
|
}
|
|
}
|
|
|
|
// WithContext returns a new environment with additional context values.
|
|
// Mirrors: self.with_context(key=value)
|
|
func (env *Environment) WithContext(extra map[string]interface{}) *Environment {
|
|
merged := make(map[string]interface{}, len(env.context)+len(extra))
|
|
for k, v := range env.context {
|
|
merged[k] = v
|
|
}
|
|
for k, v := range extra {
|
|
merged[k] = v
|
|
}
|
|
return &Environment{
|
|
ctx: env.ctx,
|
|
pool: env.pool,
|
|
tx: env.tx,
|
|
uid: env.uid,
|
|
companyID: env.companyID,
|
|
su: env.su,
|
|
context: merged,
|
|
cache: env.cache,
|
|
}
|
|
}
|
|
|
|
// Commit commits the database transaction.
|
|
func (env *Environment) Commit() error {
|
|
return env.tx.Commit(env.ctx)
|
|
}
|
|
|
|
// Rollback rolls back the database transaction.
|
|
func (env *Environment) Rollback() error {
|
|
return env.tx.Rollback(env.ctx)
|
|
}
|
|
|
|
// Close rolls back uncommitted changes and releases the connection.
|
|
func (env *Environment) Close() error {
|
|
return env.tx.Rollback(env.ctx)
|
|
}
|
|
|
|
// --- Cache ---
|
|
|
|
// Cache is a per-environment record cache.
|
|
// Mirrors: odoo/orm/environments.py Transaction cache
|
|
type Cache struct {
|
|
// model_name → record_id → field_name → value
|
|
data map[string]map[int64]map[string]Value
|
|
}
|
|
|
|
// NewCache creates an empty cache.
|
|
func NewCache() *Cache {
|
|
return &Cache{
|
|
data: make(map[string]map[int64]map[string]Value),
|
|
}
|
|
}
|
|
|
|
// Get retrieves a cached value.
|
|
func (c *Cache) Get(model string, id int64, field string) (Value, bool) {
|
|
records, ok := c.data[model]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
fields, ok := records[id]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
val, ok := fields[field]
|
|
return val, ok
|
|
}
|
|
|
|
// Set stores a value in the cache.
|
|
func (c *Cache) Set(model string, id int64, field string, value Value) {
|
|
records, ok := c.data[model]
|
|
if !ok {
|
|
records = make(map[int64]map[string]Value)
|
|
c.data[model] = records
|
|
}
|
|
fields, ok := records[id]
|
|
if !ok {
|
|
fields = make(map[string]Value)
|
|
records[id] = fields
|
|
}
|
|
fields[field] = value
|
|
}
|
|
|
|
// Invalidate removes all cached values for a model.
|
|
func (c *Cache) Invalidate(model string) {
|
|
delete(c.data, model)
|
|
}
|
|
|
|
// InvalidateRecord removes cached values for a specific record.
|
|
func (c *Cache) InvalidateRecord(model string, id int64) {
|
|
if records, ok := c.data[model]; ok {
|
|
delete(records, id)
|
|
}
|
|
}
|