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:
5
addons/stock/models/init.go
Normal file
5
addons/stock/models/init.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initStock()
|
||||
}
|
||||
371
addons/stock/models/stock.go
Normal file
371
addons/stock/models/stock.go
Normal 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"}),
|
||||
)
|
||||
}
|
||||
22
addons/stock/module.go
Normal file
22
addons/stock/module.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Package stock implements Odoo's inventory & warehouse management module.
|
||||
// Mirrors: odoo/addons/stock/__manifest__.py
|
||||
package stock
|
||||
|
||||
import (
|
||||
"odoo-go/addons/stock/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "stock",
|
||||
Description: "Inventory",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Inventory/Inventory",
|
||||
Depends: []string{"base", "product"},
|
||||
Application: true,
|
||||
Installable: true,
|
||||
Sequence: 20,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user