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:
173
pkg/server/views.go
Normal file
173
pkg/server/views.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"odoo-go/pkg/orm"
|
||||
)
|
||||
|
||||
// handleGetViews implements the get_views method.
|
||||
// Mirrors: odoo/addons/base/models/ir_ui_view.py get_views()
|
||||
func handleGetViews(env *orm.Environment, model string, params CallKWParams) (interface{}, *RPCError) {
|
||||
// Parse views list: [[false, "list"], [false, "form"], [false, "search"]]
|
||||
var viewRequests [][]interface{}
|
||||
if len(params.Args) > 0 {
|
||||
if vr, ok := params.Args[0].([]interface{}); ok {
|
||||
viewRequests = make([][]interface{}, len(vr))
|
||||
for i, v := range vr {
|
||||
if pair, ok := v.([]interface{}); ok {
|
||||
viewRequests[i] = pair
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also check kwargs
|
||||
if viewRequests == nil {
|
||||
if vr, ok := params.KW["views"].([]interface{}); ok {
|
||||
viewRequests = make([][]interface{}, len(vr))
|
||||
for i, v := range vr {
|
||||
if pair, ok := v.([]interface{}); ok {
|
||||
viewRequests[i] = pair
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
views := make(map[string]interface{})
|
||||
for _, vr := range viewRequests {
|
||||
if len(vr) < 2 {
|
||||
continue
|
||||
}
|
||||
viewType, _ := vr[1].(string)
|
||||
if viewType == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to load from ir_ui_view table
|
||||
arch := loadViewArch(env, model, viewType)
|
||||
if arch == "" {
|
||||
// Generate default view
|
||||
arch = generateDefaultView(model, viewType)
|
||||
}
|
||||
|
||||
views[viewType] = map[string]interface{}{
|
||||
"arch": arch,
|
||||
"type": viewType,
|
||||
"model": model,
|
||||
"view_id": 0,
|
||||
"field_parent": false,
|
||||
}
|
||||
}
|
||||
|
||||
// Build models dict with field metadata
|
||||
models := map[string]interface{}{
|
||||
model: map[string]interface{}{
|
||||
"fields": fieldsGetForModel(model),
|
||||
},
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"views": views,
|
||||
"models": models,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// loadViewArch tries to load a view from the ir_ui_view table.
|
||||
func loadViewArch(env *orm.Environment, model, viewType string) string {
|
||||
var arch string
|
||||
err := env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT arch FROM ir_ui_view WHERE model = $1 AND type = $2 AND active = true ORDER BY priority LIMIT 1`,
|
||||
model, viewType,
|
||||
).Scan(&arch)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return arch
|
||||
}
|
||||
|
||||
// generateDefaultView creates a minimal view XML for a model.
|
||||
func generateDefaultView(modelName, viewType string) string {
|
||||
m := orm.Registry.Get(modelName)
|
||||
if m == nil {
|
||||
return fmt.Sprintf("<%s><field name=\"id\"/></%s>", viewType, viewType)
|
||||
}
|
||||
|
||||
switch viewType {
|
||||
case "list", "tree":
|
||||
return generateDefaultListView(m)
|
||||
case "form":
|
||||
return generateDefaultFormView(m)
|
||||
case "search":
|
||||
return generateDefaultSearchView(m)
|
||||
case "kanban":
|
||||
return generateDefaultKanbanView(m)
|
||||
default:
|
||||
return fmt.Sprintf("<%s><field name=\"id\"/></%s>", viewType, viewType)
|
||||
}
|
||||
}
|
||||
|
||||
func generateDefaultListView(m *orm.Model) string {
|
||||
var fields []string
|
||||
count := 0
|
||||
for _, f := range m.Fields() {
|
||||
if f.Name == "id" || !f.IsStored() || f.Name == "create_uid" || f.Name == "write_uid" ||
|
||||
f.Name == "create_date" || f.Name == "write_date" || f.Type == orm.TypeBinary {
|
||||
continue
|
||||
}
|
||||
fields = append(fields, fmt.Sprintf(`<field name="%s"/>`, f.Name))
|
||||
count++
|
||||
if count >= 8 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("<list>\n %s\n</list>", strings.Join(fields, "\n "))
|
||||
}
|
||||
|
||||
func generateDefaultFormView(m *orm.Model) string {
|
||||
var fields []string
|
||||
for _, f := range m.Fields() {
|
||||
if f.Name == "id" || f.Name == "create_uid" || f.Name == "write_uid" ||
|
||||
f.Name == "create_date" || f.Name == "write_date" || f.Type == orm.TypeBinary {
|
||||
continue
|
||||
}
|
||||
if f.Type == orm.TypeOne2many || f.Type == orm.TypeMany2many {
|
||||
continue // Skip relational fields in default form
|
||||
}
|
||||
fields = append(fields, fmt.Sprintf(` <field name="%s"/>`, f.Name))
|
||||
if len(fields) >= 20 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("<form>\n <sheet>\n <group>\n%s\n </group>\n </sheet>\n</form>",
|
||||
strings.Join(fields, "\n"))
|
||||
}
|
||||
|
||||
func generateDefaultSearchView(m *orm.Model) string {
|
||||
var fields []string
|
||||
// Add name field if it exists
|
||||
if f := m.GetField("name"); f != nil {
|
||||
fields = append(fields, `<field name="name"/>`)
|
||||
}
|
||||
if f := m.GetField("email"); f != nil {
|
||||
fields = append(fields, `<field name="email"/>`)
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
fields = append(fields, `<field name="id"/>`)
|
||||
}
|
||||
return fmt.Sprintf("<search>\n %s\n</search>", strings.Join(fields, "\n "))
|
||||
}
|
||||
|
||||
func generateDefaultKanbanView(m *orm.Model) string {
|
||||
nameField := "name"
|
||||
if f := m.GetField("name"); f == nil {
|
||||
nameField = "id"
|
||||
}
|
||||
return fmt.Sprintf(`<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="%s"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>`, nameField)
|
||||
}
|
||||
Reference in New Issue
Block a user