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:
Marc
2026-03-31 01:45:09 +02:00
commit 0ed29fe2fd
90 changed files with 12133 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
package models
import "odoo-go/pkg/orm"
// initProductCategory registers product.category — product classification tree.
// Mirrors: odoo/addons/product/models/product_category.py
func initProductCategory() {
m := orm.NewModel("product.category", orm.ModelOpts{
Description: "Product Category",
Order: "name",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}),
orm.Many2one("parent_id", "product.category", orm.FieldOpts{
String: "Parent Category", Index: true, OnDelete: orm.OnDeleteCascade,
}),
orm.Many2one("property_account_income_categ_id", "account.account", orm.FieldOpts{
String: "Income Account",
Help: "This account will be used when validating a customer invoice.",
}),
orm.Many2one("property_account_expense_categ_id", "account.account", orm.FieldOpts{
String: "Expense Account",
Help: "This account will be used when validating a vendor bill.",
}),
)
}
// initUoM registers uom.category and uom.uom — units of measure.
// Mirrors: odoo/addons/product/models/product_uom.py
func initUoM() {
// uom.category — groups compatible units (e.g., Weight, Volume)
orm.NewModel("uom.category", orm.ModelOpts{
Description: "Product UoM Categories",
Order: "name",
}).AddFields(
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}),
)
// uom.uom — individual units of measure
m := orm.NewModel("uom.uom", orm.ModelOpts{
Description: "Product Unit of Measure",
Order: "name",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Unit of Measure", Required: true, Translate: true}),
orm.Many2one("category_id", "uom.category", orm.FieldOpts{
String: "Category", Required: true, OnDelete: orm.OnDeleteCascade,
Help: "Conversion between Units of Measure can only occur if they belong to the same category.",
}),
orm.Float("factor", orm.FieldOpts{
String: "Ratio", Required: true, Default: 1.0,
Help: "How much bigger or smaller this unit is compared to the reference unit of measure for this category.",
}),
orm.Selection("uom_type", []orm.SelectionItem{
{Value: "bigger", Label: "Bigger than the reference Unit of Measure"},
{Value: "reference", Label: "Reference Unit of Measure for this category"},
{Value: "smaller", Label: "Smaller than the reference Unit of Measure"},
}, orm.FieldOpts{String: "Type", Required: true, Default: "reference"}),
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
orm.Float("rounding", orm.FieldOpts{
String: "Rounding Precision", Required: true, Default: 0.01,
Help: "The computed quantity will be a multiple of this value. Use 1.0 for a Unit of Measure that cannot be further split.",
}),
)
}
// initProductTemplate registers product.template — the base product definition.
// Mirrors: odoo/addons/product/models/product_template.py
func initProductTemplate() {
m := orm.NewModel("product.template", orm.ModelOpts{
Description: "Product Template",
Order: "name",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Index: true, Translate: true}),
orm.Selection("type", []orm.SelectionItem{
{Value: "consu", Label: "Consumable"},
{Value: "service", Label: "Service"},
{Value: "storable", Label: "Storable Product"},
}, orm.FieldOpts{String: "Product Type", Required: true, Default: "consu"}),
orm.Float("list_price", orm.FieldOpts{String: "Sales Price", Default: 1.0}),
orm.Float("standard_price", orm.FieldOpts{String: "Cost"}),
orm.Many2one("categ_id", "product.category", orm.FieldOpts{
String: "Product Category", Required: true,
}),
orm.Many2one("uom_id", "uom.uom", orm.FieldOpts{
String: "Unit of Measure", Required: true,
}),
orm.Boolean("sale_ok", orm.FieldOpts{String: "Can be Sold", Default: true}),
orm.Boolean("purchase_ok", orm.FieldOpts{String: "Can be Purchased", Default: true}),
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
orm.Text("description", orm.FieldOpts{String: "Description", Translate: true}),
orm.Many2many("taxes_id", "account.tax", orm.FieldOpts{
String: "Customer Taxes",
Help: "Default taxes used when selling the product.",
}),
orm.Many2many("supplier_taxes_id", "account.tax", orm.FieldOpts{
String: "Vendor Taxes",
Help: "Default taxes used when buying the product.",
}),
)
}
// initProductProduct registers product.product — a concrete product variant.
// Mirrors: odoo/addons/product/models/product_product.py
func initProductProduct() {
m := orm.NewModel("product.product", orm.ModelOpts{
Description: "Product",
Order: "default_code, name, id",
})
m.AddFields(
orm.Many2one("product_tmpl_id", "product.template", orm.FieldOpts{
String: "Product Template", Required: true, OnDelete: orm.OnDeleteCascade, Index: true,
}),
orm.Char("default_code", orm.FieldOpts{String: "Internal Reference", Index: true}),
orm.Char("barcode", orm.FieldOpts{String: "Barcode", Index: true}),
orm.Float("volume", orm.FieldOpts{String: "Volume", Help: "The volume in m3."}),
orm.Float("weight", orm.FieldOpts{String: "Weight", Help: "The weight in kg."}),
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
)
}
// initProductPricelist registers product.pricelist — price lists for customers.
// Mirrors: odoo/addons/product/models/product_pricelist.py
func initProductPricelist() {
m := orm.NewModel("product.pricelist", orm.ModelOpts{
Description: "Pricelist",
Order: "sequence, id",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Pricelist Name", Required: true, Translate: true}),
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency", Required: true}),
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 16}),
orm.One2many("item_ids", "product.pricelist.item", "pricelist_id", orm.FieldOpts{String: "Pricelist Rules"}),
)
// product.pricelist.item — Price rules
orm.NewModel("product.pricelist.item", orm.ModelOpts{
Description: "Pricelist Rule",
Order: "applied_on, min_quantity desc, categ_id desc, id desc",
}).AddFields(
orm.Many2one("pricelist_id", "product.pricelist", orm.FieldOpts{
String: "Pricelist", Required: true, OnDelete: orm.OnDeleteCascade, Index: true,
}),
orm.Many2one("product_tmpl_id", "product.template", orm.FieldOpts{String: "Product"}),
orm.Many2one("categ_id", "product.category", orm.FieldOpts{String: "Product Category"}),
orm.Float("min_quantity", orm.FieldOpts{String: "Min. Quantity"}),
orm.Selection("applied_on", []orm.SelectionItem{
{Value: "3_global", Label: "All Products"},
{Value: "2_product_category", Label: "Product Category"},
{Value: "1_product", Label: "Product"},
{Value: "0_product_variant", Label: "Product Variant"},
}, orm.FieldOpts{String: "Apply On", Default: "3_global", Required: true}),
orm.Selection("compute_price", []orm.SelectionItem{
{Value: "fixed", Label: "Fixed Price"},
{Value: "percentage", Label: "Discount"},
{Value: "formula", Label: "Formula"},
}, orm.FieldOpts{String: "Computation", Default: "fixed", Required: true}),
orm.Float("fixed_price", orm.FieldOpts{String: "Fixed Price"}),
orm.Float("percent_price", orm.FieldOpts{String: "Percentage Price"}),
orm.Date("date_start", orm.FieldOpts{String: "Start Date"}),
orm.Date("date_end", orm.FieldOpts{String: "End Date"}),
)
}