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>
70 lines
2.1 KiB
Go
70 lines
2.1 KiB
Go
package orm
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// NextByCode generates the next value for a sequence identified by its code.
|
|
// Mirrors: odoo/addons/base/models/ir_sequence.py IrSequence.next_by_code()
|
|
//
|
|
// Uses PostgreSQL FOR UPDATE to ensure atomic increment under concurrency.
|
|
// Format: prefix + LPAD(number, padding, '0') + suffix
|
|
// Supports date interpolation in prefix/suffix: %(year)s, %(month)s, %(day)s
|
|
func NextByCode(env *Environment, code string) (string, error) {
|
|
var id int64
|
|
var prefix, suffix string
|
|
var numberNext, numberIncrement, padding int
|
|
|
|
err := env.tx.QueryRow(env.ctx, `
|
|
SELECT id, COALESCE(prefix, ''), COALESCE(suffix, ''), number_next, number_increment, padding
|
|
FROM ir_sequence
|
|
WHERE code = $1 AND active = true
|
|
ORDER BY id
|
|
LIMIT 1
|
|
FOR UPDATE
|
|
`, code).Scan(&id, &prefix, &suffix, &numberNext, &numberIncrement, &padding)
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf("orm: sequence %q not found: %w", code, err)
|
|
}
|
|
|
|
// Format the sequence value
|
|
result := FormatSequence(prefix, suffix, numberNext, padding)
|
|
|
|
// Increment for next call
|
|
_, err = env.tx.Exec(env.ctx, `
|
|
UPDATE ir_sequence SET number_next = number_next + $1 WHERE id = $2
|
|
`, numberIncrement, id)
|
|
if err != nil {
|
|
return "", fmt.Errorf("orm: sequence %q increment failed: %w", code, err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// FormatSequence formats a sequence number with prefix, suffix, and zero-padding.
|
|
func FormatSequence(prefix, suffix string, number, padding int) string {
|
|
prefix = InterpolateDate(prefix)
|
|
suffix = InterpolateDate(suffix)
|
|
|
|
numStr := fmt.Sprintf("%d", number)
|
|
if padding > 0 && len(numStr) < padding {
|
|
numStr = strings.Repeat("0", padding-len(numStr)) + numStr
|
|
}
|
|
|
|
return prefix + numStr + suffix
|
|
}
|
|
|
|
// InterpolateDate replaces Odoo-style date placeholders in a string.
|
|
// Supports: %(year)s, %(month)s, %(day)s, %(y)s (2-digit year)
|
|
func InterpolateDate(s string) string {
|
|
now := time.Now()
|
|
s = strings.ReplaceAll(s, "%(year)s", now.Format("2006"))
|
|
s = strings.ReplaceAll(s, "%(y)s", now.Format("06"))
|
|
s = strings.ReplaceAll(s, "%(month)s", now.Format("01"))
|
|
s = strings.ReplaceAll(s, "%(day)s", now.Format("02"))
|
|
return s
|
|
}
|