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>
174 lines
4.4 KiB
Go
174 lines
4.4 KiB
Go
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)
|
|
}
|