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,371 @@
package models
import "odoo-go/pkg/orm"
// initStock registers all stock models.
// Mirrors: odoo/addons/stock/models/stock_warehouse.py,
// stock_location.py, stock_picking.py, stock_move.py,
// stock_move_line.py, stock_quant.py, stock_lot.py
func initStock() {
initStockWarehouse()
initStockLocation()
initStockPickingType()
initStockPicking()
initStockMove()
initStockMoveLine()
initStockQuant()
initStockLot()
}
// initStockWarehouse registers stock.warehouse.
// Mirrors: odoo/addons/stock/models/stock_warehouse.py
func initStockWarehouse() {
m := orm.NewModel("stock.warehouse", orm.ModelOpts{
Description: "Warehouse",
Order: "sequence, id",
RecName: "name",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Warehouse", Required: true, Index: true}),
orm.Char("code", orm.FieldOpts{String: "Short Name", Required: true, Size: 5}),
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
orm.Many2one("company_id", "res.company", orm.FieldOpts{
String: "Company", Required: true, Index: true,
}),
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{
String: "Address",
}),
orm.Many2one("lot_stock_id", "stock.location", orm.FieldOpts{
String: "Location Stock", Required: true, OnDelete: orm.OnDeleteRestrict,
}),
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
)
}
// initStockLocation registers stock.location.
// Mirrors: odoo/addons/stock/models/stock_location.py
func initStockLocation() {
m := orm.NewModel("stock.location", orm.ModelOpts{
Description: "Location",
Order: "complete_name, id",
RecName: "complete_name",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Location Name", Required: true, Translate: true}),
orm.Char("complete_name", orm.FieldOpts{
String: "Full Location Name", Compute: "_compute_complete_name", Store: true,
}),
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
orm.Selection("usage", []orm.SelectionItem{
{Value: "supplier", Label: "Vendor Location"},
{Value: "view", Label: "View"},
{Value: "internal", Label: "Internal Location"},
{Value: "customer", Label: "Customer Location"},
{Value: "inventory", Label: "Inventory Loss"},
{Value: "production", Label: "Production"},
{Value: "transit", Label: "Transit Location"},
}, orm.FieldOpts{String: "Location Type", Required: true, Default: "internal", Index: true}),
orm.Many2one("location_id", "stock.location", orm.FieldOpts{
String: "Parent Location", Index: true, OnDelete: orm.OnDeleteCascade,
}),
orm.Many2one("company_id", "res.company", orm.FieldOpts{
String: "Company", Index: true,
}),
orm.Many2one("removal_strategy_id", "product.removal", orm.FieldOpts{
String: "Removal Strategy",
}),
orm.Boolean("scrap_location", orm.FieldOpts{String: "Is a Scrap Location?"}),
orm.Boolean("return_location", orm.FieldOpts{String: "Is a Return Location?"}),
orm.Char("barcode", orm.FieldOpts{String: "Barcode", Index: true}),
)
}
// initStockPickingType registers stock.picking.type.
// Mirrors: odoo/addons/stock/models/stock_picking.py StockPickingType
func initStockPickingType() {
m := orm.NewModel("stock.picking.type", orm.ModelOpts{
Description: "Picking Type",
Order: "sequence, id",
RecName: "name",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Operation Type", Required: true, Translate: true}),
orm.Selection("code", []orm.SelectionItem{
{Value: "incoming", Label: "Receipt"},
{Value: "outgoing", Label: "Delivery"},
{Value: "internal", Label: "Internal Transfer"},
}, orm.FieldOpts{String: "Type of Operation", Required: true}),
orm.Char("sequence_code", orm.FieldOpts{String: "Code", Required: true}),
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
orm.Many2one("warehouse_id", "stock.warehouse", orm.FieldOpts{
String: "Warehouse", OnDelete: orm.OnDeleteCascade,
}),
orm.Many2one("default_location_src_id", "stock.location", orm.FieldOpts{
String: "Default Source Location",
}),
orm.Many2one("default_location_dest_id", "stock.location", orm.FieldOpts{
String: "Default Destination Location",
}),
orm.Many2one("company_id", "res.company", orm.FieldOpts{
String: "Company", Required: true, Index: true,
}),
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
orm.Boolean("show_operations", orm.FieldOpts{String: "Show Detailed Operations"}),
orm.Boolean("show_reserved", orm.FieldOpts{String: "Show Reserved"}),
)
}
// initStockPicking registers stock.picking — the transfer order.
// Mirrors: odoo/addons/stock/models/stock_picking.py StockPicking
func initStockPicking() {
m := orm.NewModel("stock.picking", orm.ModelOpts{
Description: "Transfer",
Order: "priority desc, scheduled_date asc, id desc",
RecName: "name",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{
String: "Reference", Default: "/", Required: true, Index: true, Readonly: true,
}),
orm.Selection("state", []orm.SelectionItem{
{Value: "draft", Label: "Draft"},
{Value: "waiting", Label: "Waiting Another Operation"},
{Value: "confirmed", Label: "Waiting"},
{Value: "assigned", Label: "Ready"},
{Value: "done", Label: "Done"},
{Value: "cancel", Label: "Cancelled"},
}, orm.FieldOpts{String: "Status", Default: "draft", Compute: "_compute_state", Store: true, Index: true}),
orm.Selection("priority", []orm.SelectionItem{
{Value: "0", Label: "Normal"},
{Value: "1", Label: "Urgent"},
}, orm.FieldOpts{String: "Priority", Default: "0", Index: true}),
orm.Many2one("picking_type_id", "stock.picking.type", orm.FieldOpts{
String: "Operation Type", Required: true, Index: true,
}),
orm.Many2one("location_id", "stock.location", orm.FieldOpts{
String: "Source Location", Required: true, Index: true,
}),
orm.Many2one("location_dest_id", "stock.location", orm.FieldOpts{
String: "Destination Location", Required: true, Index: true,
}),
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{
String: "Contact", Index: true,
}),
orm.Datetime("scheduled_date", orm.FieldOpts{String: "Scheduled Date", Required: true, Index: true}),
orm.Datetime("date_deadline", orm.FieldOpts{String: "Deadline"}),
orm.Datetime("date_done", orm.FieldOpts{String: "Date of Transfer", Readonly: true}),
orm.One2many("move_ids", "stock.move", "picking_id", orm.FieldOpts{
String: "Stock Moves",
}),
orm.Many2one("company_id", "res.company", orm.FieldOpts{
String: "Company", Required: true, Index: true,
}),
orm.Text("note", orm.FieldOpts{String: "Notes"}),
orm.Char("origin", orm.FieldOpts{String: "Source Document", Index: true}),
)
}
// initStockMove registers stock.move — individual product movements.
// Mirrors: odoo/addons/stock/models/stock_move.py
func initStockMove() {
m := orm.NewModel("stock.move", orm.ModelOpts{
Description: "Stock Move",
Order: "sequence, id",
RecName: "name",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Description", Required: true}),
orm.Char("reference", orm.FieldOpts{String: "Reference", Index: true}),
orm.Selection("state", []orm.SelectionItem{
{Value: "draft", Label: "New"},
{Value: "confirmed", Label: "Waiting Availability"},
{Value: "partially_available", Label: "Partially Available"},
{Value: "assigned", Label: "Available"},
{Value: "done", Label: "Done"},
{Value: "cancel", Label: "Cancelled"},
}, orm.FieldOpts{String: "Status", Default: "draft", Index: true}),
orm.Selection("priority", []orm.SelectionItem{
{Value: "0", Label: "Normal"},
{Value: "1", Label: "Urgent"},
}, orm.FieldOpts{String: "Priority", Default: "0"}),
orm.Many2one("product_id", "product.product", orm.FieldOpts{
String: "Product", Required: true, Index: true,
}),
orm.Float("product_uom_qty", orm.FieldOpts{String: "Demand", Required: true, Default: 1.0}),
orm.Many2one("product_uom", "uom.uom", orm.FieldOpts{
String: "UoM", Required: true,
}),
orm.Many2one("location_id", "stock.location", orm.FieldOpts{
String: "Source Location", Required: true, Index: true,
}),
orm.Many2one("location_dest_id", "stock.location", orm.FieldOpts{
String: "Destination Location", Required: true, Index: true,
}),
orm.Many2one("picking_id", "stock.picking", orm.FieldOpts{
String: "Transfer", Index: true,
}),
orm.Datetime("date", orm.FieldOpts{String: "Date Scheduled", Required: true, Index: true}),
orm.Datetime("date_deadline", orm.FieldOpts{String: "Deadline"}),
orm.Float("price_unit", orm.FieldOpts{String: "Unit Price"}),
orm.Many2one("company_id", "res.company", orm.FieldOpts{
String: "Company", Required: true, Index: true,
}),
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
orm.Char("origin", orm.FieldOpts{String: "Source Document"}),
)
}
// initStockMoveLine registers stock.move.line — detailed operations per lot/package.
// Mirrors: odoo/addons/stock/models/stock_move_line.py
func initStockMoveLine() {
m := orm.NewModel("stock.move.line", orm.ModelOpts{
Description: "Product Moves (Stock Move Line)",
Order: "result_package_id desc, id",
})
m.AddFields(
orm.Many2one("move_id", "stock.move", orm.FieldOpts{
String: "Stock Move", Index: true, OnDelete: orm.OnDeleteCascade,
}),
orm.Many2one("product_id", "product.product", orm.FieldOpts{
String: "Product", Required: true, Index: true,
}),
orm.Float("quantity", orm.FieldOpts{String: "Quantity", Required: true, Default: 0.0}),
orm.Many2one("product_uom_id", "uom.uom", orm.FieldOpts{
String: "Unit of Measure", Required: true,
}),
orm.Many2one("lot_id", "stock.lot", orm.FieldOpts{
String: "Lot/Serial Number", Index: true,
}),
orm.Many2one("package_id", "stock.quant.package", orm.FieldOpts{
String: "Source Package",
}),
orm.Many2one("result_package_id", "stock.quant.package", orm.FieldOpts{
String: "Destination Package",
}),
orm.Many2one("location_id", "stock.location", orm.FieldOpts{
String: "From", Required: true, Index: true,
}),
orm.Many2one("location_dest_id", "stock.location", orm.FieldOpts{
String: "To", Required: true, Index: true,
}),
orm.Many2one("picking_id", "stock.picking", orm.FieldOpts{
String: "Transfer", Index: true,
}),
orm.Many2one("company_id", "res.company", orm.FieldOpts{
String: "Company", Required: true, Index: true,
}),
orm.Datetime("date", orm.FieldOpts{String: "Date", Required: true}),
orm.Selection("state", []orm.SelectionItem{
{Value: "draft", Label: "New"},
{Value: "confirmed", Label: "Waiting"},
{Value: "assigned", Label: "Reserved"},
{Value: "done", Label: "Done"},
{Value: "cancel", Label: "Cancelled"},
}, orm.FieldOpts{String: "Status", Default: "draft"}),
)
}
// initStockQuant registers stock.quant — on-hand inventory quantities.
// Mirrors: odoo/addons/stock/models/stock_quant.py
func initStockQuant() {
m := orm.NewModel("stock.quant", orm.ModelOpts{
Description: "Quants",
Order: "removal_date, in_date, id",
})
m.AddFields(
orm.Many2one("product_id", "product.product", orm.FieldOpts{
String: "Product", Required: true, Index: true, OnDelete: orm.OnDeleteRestrict,
}),
orm.Many2one("location_id", "stock.location", orm.FieldOpts{
String: "Location", Required: true, Index: true, OnDelete: orm.OnDeleteRestrict,
}),
orm.Many2one("lot_id", "stock.lot", orm.FieldOpts{
String: "Lot/Serial Number", Index: true,
}),
orm.Float("quantity", orm.FieldOpts{String: "Quantity", Required: true, Default: 0.0}),
orm.Float("reserved_quantity", orm.FieldOpts{String: "Reserved Quantity", Required: true, Default: 0.0}),
orm.Many2one("company_id", "res.company", orm.FieldOpts{
String: "Company", Required: true, Index: true,
}),
orm.Datetime("in_date", orm.FieldOpts{String: "Incoming Date", Index: true}),
orm.Many2one("package_id", "stock.quant.package", orm.FieldOpts{
String: "Package",
}),
orm.Many2one("owner_id", "res.partner", orm.FieldOpts{
String: "Owner",
}),
orm.Datetime("removal_date", orm.FieldOpts{String: "Removal Date"}),
)
// stock.quant.package — physical packages / containers
orm.NewModel("stock.quant.package", orm.ModelOpts{
Description: "Packages",
Order: "name",
}).AddFields(
orm.Char("name", orm.FieldOpts{String: "Package Reference", Required: true, Index: true}),
orm.Many2one("package_type_id", "stock.package.type", orm.FieldOpts{
String: "Package Type",
}),
orm.Many2one("location_id", "stock.location", orm.FieldOpts{
String: "Location", Index: true,
}),
orm.Many2one("company_id", "res.company", orm.FieldOpts{
String: "Company", Index: true,
}),
orm.Many2one("owner_id", "res.partner", orm.FieldOpts{
String: "Owner",
}),
)
// stock.package.type — packaging types (box, pallet, etc.)
orm.NewModel("stock.package.type", orm.ModelOpts{
Description: "Package Type",
Order: "sequence, id",
}).AddFields(
orm.Char("name", orm.FieldOpts{String: "Package Type", Required: true}),
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 1}),
orm.Float("height", orm.FieldOpts{String: "Height"}),
orm.Float("width", orm.FieldOpts{String: "Width"}),
orm.Float("packaging_length", orm.FieldOpts{String: "Length"}),
orm.Float("max_weight", orm.FieldOpts{String: "Max Weight"}),
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
)
// product.removal — removal strategies (FIFO, LIFO, etc.)
orm.NewModel("product.removal", orm.ModelOpts{
Description: "Removal Strategy",
Order: "name",
}).AddFields(
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}),
orm.Char("method", orm.FieldOpts{String: "Method", Required: true}),
)
}
// initStockLot registers stock.lot — lot/serial number tracking.
// Mirrors: odoo/addons/stock/models/stock_lot.py
func initStockLot() {
m := orm.NewModel("stock.lot", orm.ModelOpts{
Description: "Lot/Serial",
Order: "name, id",
RecName: "name",
})
m.AddFields(
orm.Char("name", orm.FieldOpts{String: "Lot/Serial Number", Required: true, Index: true}),
orm.Many2one("product_id", "product.product", orm.FieldOpts{
String: "Product", Required: true, Index: true, OnDelete: orm.OnDeleteRestrict,
}),
orm.Many2one("company_id", "res.company", orm.FieldOpts{
String: "Company", Required: true, Index: true,
}),
orm.Text("note", orm.FieldOpts{String: "Description"}),
)
}