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:
16
addons/base/models/init.go
Normal file
16
addons/base/models/init.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Package models registers all base module models.
|
||||
package models
|
||||
|
||||
// Init registers all models for the base module.
|
||||
// Called by the module loader in dependency order.
|
||||
func Init() {
|
||||
initIrUI() // ir.ui.menu, ir.ui.view, ir.actions, ir.sequence, ir.attachment, report.paperformat
|
||||
initResCurrency() // res.currency, res.country, res.country.state
|
||||
initResCompany() // res.company (needs res.currency, res.country)
|
||||
initResPartner() // res.partner, res.partner.category, res.partner.title, res.partner.bank, res.bank
|
||||
initResUsers() // res.users, res.groups (needs res.partner, res.company)
|
||||
initIrModel() // ir.model, ir.model.fields, ir.module.category
|
||||
initIrModelAccess()
|
||||
initIrRule()
|
||||
initIrModelData()
|
||||
}
|
||||
175
addons/base/models/ir_model.go
Normal file
175
addons/base/models/ir_model.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package models
|
||||
|
||||
import "odoo-go/pkg/orm"
|
||||
|
||||
// initIrModel registers ir.model and ir.model.fields — Odoo's model metadata.
|
||||
// Mirrors: odoo/addons/base/models/ir_model.py
|
||||
//
|
||||
// These meta-models describe all other models at runtime.
|
||||
// They are the foundation for dynamic field access, view generation, and access control.
|
||||
func initIrModel() {
|
||||
// ir.model — Model metadata
|
||||
m := orm.NewModel("ir.model", orm.ModelOpts{
|
||||
Description: "Models",
|
||||
Order: "model",
|
||||
RecName: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Model Description", Required: true, Translate: true}),
|
||||
orm.Char("model", orm.FieldOpts{String: "Model", Required: true, Index: true}),
|
||||
orm.Char("info", orm.FieldOpts{String: "Information"}),
|
||||
orm.One2many("field_id", "ir.model.fields", "model_id", orm.FieldOpts{String: "Fields"}),
|
||||
orm.One2many("access_ids", "ir.model.access", "model_id", orm.FieldOpts{String: "Access"}),
|
||||
orm.One2many("rule_ids", "ir.rule", "model_id", orm.FieldOpts{String: "Record Rules"}),
|
||||
orm.Char("state", orm.FieldOpts{String: "Type", Default: "base"}),
|
||||
orm.Boolean("transient", orm.FieldOpts{String: "Transient Model"}),
|
||||
orm.Many2many("group_ids", "res.groups", orm.FieldOpts{String: "Groups"}),
|
||||
)
|
||||
|
||||
// ir.model.fields — Field metadata
|
||||
f := orm.NewModel("ir.model.fields", orm.ModelOpts{
|
||||
Description: "Fields",
|
||||
Order: "model_id, name",
|
||||
RecName: "name",
|
||||
})
|
||||
|
||||
f.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Field Name", Required: true, Index: true}),
|
||||
orm.Char("field_description", orm.FieldOpts{String: "Field Label", Translate: true}),
|
||||
orm.Many2one("model_id", "ir.model", orm.FieldOpts{
|
||||
String: "Model", Required: true, OnDelete: orm.OnDeleteCascade, Index: true,
|
||||
}),
|
||||
orm.Char("model", orm.FieldOpts{String: "Model Name", Related: "model_id.model"}),
|
||||
orm.Selection("ttype", []orm.SelectionItem{
|
||||
{Value: "char", Label: "Char"},
|
||||
{Value: "text", Label: "Text"},
|
||||
{Value: "html", Label: "Html"},
|
||||
{Value: "integer", Label: "Integer"},
|
||||
{Value: "float", Label: "Float"},
|
||||
{Value: "monetary", Label: "Monetary"},
|
||||
{Value: "boolean", Label: "Boolean"},
|
||||
{Value: "date", Label: "Date"},
|
||||
{Value: "datetime", Label: "Datetime"},
|
||||
{Value: "binary", Label: "Binary"},
|
||||
{Value: "selection", Label: "Selection"},
|
||||
{Value: "many2one", Label: "Many2one"},
|
||||
{Value: "one2many", Label: "One2many"},
|
||||
{Value: "many2many", Label: "Many2many"},
|
||||
{Value: "reference", Label: "Reference"},
|
||||
}, orm.FieldOpts{String: "Field Type", Required: true}),
|
||||
orm.Char("relation", orm.FieldOpts{String: "Related Model"}),
|
||||
orm.Char("relation_field", orm.FieldOpts{String: "Relation Field"}),
|
||||
orm.Boolean("required", orm.FieldOpts{String: "Required"}),
|
||||
orm.Boolean("readonly", orm.FieldOpts{String: "Readonly"}),
|
||||
orm.Boolean("index", orm.FieldOpts{String: "Indexed"}),
|
||||
orm.Boolean("store", orm.FieldOpts{String: "Stored", Default: true}),
|
||||
orm.Char("compute", orm.FieldOpts{String: "Compute"}),
|
||||
orm.Char("depends", orm.FieldOpts{String: "Dependencies"}),
|
||||
orm.Text("help", orm.FieldOpts{String: "Field Help"}),
|
||||
orm.Char("state", orm.FieldOpts{String: "Type", Default: "base"}),
|
||||
orm.Integer("size", orm.FieldOpts{String: "Size"}),
|
||||
orm.Char("on_delete", orm.FieldOpts{String: "On Delete", Default: "set null"}),
|
||||
orm.Boolean("translate", orm.FieldOpts{String: "Translatable"}),
|
||||
orm.Text("selection_ids", orm.FieldOpts{String: "Selection Options"}),
|
||||
orm.Char("copied", orm.FieldOpts{String: "Copied", Default: "1"}),
|
||||
orm.Many2many("group_ids", "res.groups", orm.FieldOpts{String: "Groups"}),
|
||||
)
|
||||
|
||||
// ir.module.category — Module categories for group organization
|
||||
orm.NewModel("ir.module.category", orm.ModelOpts{
|
||||
Description: "Application",
|
||||
Order: "name",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}),
|
||||
orm.Many2one("parent_id", "ir.module.category", orm.FieldOpts{String: "Parent Application"}),
|
||||
orm.One2many("child_ids", "ir.module.category", "parent_id", orm.FieldOpts{String: "Child Applications"}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence"}),
|
||||
orm.Boolean("visible", orm.FieldOpts{String: "Visible", Default: true}),
|
||||
)
|
||||
}
|
||||
|
||||
// initIrModelAccess registers ir.model.access — Object-level ACLs.
|
||||
// Mirrors: odoo/addons/base/models/ir_model.py class IrModelAccess
|
||||
//
|
||||
// Access is defined as CSV in each module:
|
||||
//
|
||||
// id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
// access_res_partner,res.partner,model_res_partner,base.group_user,1,1,1,0
|
||||
func initIrModelAccess() {
|
||||
m := orm.NewModel("ir.model.access", orm.ModelOpts{
|
||||
Description: "Access Controls",
|
||||
Order: "model_id, group_id",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Index: true}),
|
||||
orm.Many2one("model_id", "ir.model", orm.FieldOpts{
|
||||
String: "Model", Required: true, OnDelete: orm.OnDeleteCascade, Index: true,
|
||||
}),
|
||||
orm.Many2one("group_id", "res.groups", orm.FieldOpts{
|
||||
String: "Group", OnDelete: orm.OnDeleteRestrict, Index: true,
|
||||
}),
|
||||
orm.Boolean("perm_read", orm.FieldOpts{String: "Read Access"}),
|
||||
orm.Boolean("perm_write", orm.FieldOpts{String: "Write Access"}),
|
||||
orm.Boolean("perm_create", orm.FieldOpts{String: "Create Access"}),
|
||||
orm.Boolean("perm_unlink", orm.FieldOpts{String: "Delete Access"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
)
|
||||
}
|
||||
|
||||
// initIrRule registers ir.rule — Record-level access rules.
|
||||
// Mirrors: odoo/addons/base/models/ir_rule.py class IrRule
|
||||
//
|
||||
// Record rules add WHERE clause filters per user/group:
|
||||
//
|
||||
// Rule: domain = [('company_id', 'in', company_ids)]
|
||||
// → User can only see records of their companies
|
||||
func initIrRule() {
|
||||
m := orm.NewModel("ir.rule", orm.ModelOpts{
|
||||
Description: "Record Rule",
|
||||
Order: "model_id, name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Many2one("model_id", "ir.model", orm.FieldOpts{
|
||||
String: "Model", Required: true, OnDelete: orm.OnDeleteCascade, Index: true,
|
||||
}),
|
||||
orm.Many2many("groups", "res.groups", orm.FieldOpts{String: "Groups"}),
|
||||
orm.Text("domain_force", orm.FieldOpts{String: "Domain"}),
|
||||
orm.Boolean("perm_read", orm.FieldOpts{String: "Apply for Read", Default: true}),
|
||||
orm.Boolean("perm_write", orm.FieldOpts{String: "Apply for Write", Default: true}),
|
||||
orm.Boolean("perm_create", orm.FieldOpts{String: "Apply for Create", Default: true}),
|
||||
orm.Boolean("perm_unlink", orm.FieldOpts{String: "Apply for Delete", Default: true}),
|
||||
orm.Boolean("global", orm.FieldOpts{
|
||||
String: "Global",
|
||||
Compute: "_compute_global",
|
||||
Store: true,
|
||||
Help: "If no group is specified, the rule is global and applied to everyone",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// initIrModelData registers ir.model.data — External identifiers (XML IDs).
|
||||
// Mirrors: odoo/addons/base/models/ir_model.py class IrModelData
|
||||
//
|
||||
// Maps module.xml_id → (model, res_id) for referencing records across modules.
|
||||
// Example: 'base.main_company' → res.company(1)
|
||||
func initIrModelData() {
|
||||
m := orm.NewModel("ir.model.data", orm.ModelOpts{
|
||||
Description: "External Identifiers",
|
||||
Order: "module, name",
|
||||
RecName: "complete_name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "External Identifier", Required: true, Index: true}),
|
||||
orm.Char("complete_name", orm.FieldOpts{String: "Complete ID", Compute: "_compute_complete_name"}),
|
||||
orm.Char("module", orm.FieldOpts{String: "Module", Required: true, Index: true, Default: ""}),
|
||||
orm.Char("model", orm.FieldOpts{String: "Model Name", Required: true}),
|
||||
orm.Integer("res_id", orm.FieldOpts{String: "Record ID", Index: true}),
|
||||
orm.Boolean("noupdate", orm.FieldOpts{String: "Non Updatable", Default: false}),
|
||||
)
|
||||
}
|
||||
228
addons/base/models/ir_ui.go
Normal file
228
addons/base/models/ir_ui.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package models
|
||||
|
||||
import "odoo-go/pkg/orm"
|
||||
|
||||
// initIrUI registers UI infrastructure models.
|
||||
// Mirrors: odoo/addons/base/models/ir_ui_menu.py, ir_ui_view.py, ir_actions.py
|
||||
|
||||
func initIrUI() {
|
||||
initIrUIMenu()
|
||||
initIrUIView()
|
||||
initIrActions()
|
||||
initIrSequence()
|
||||
initIrAttachment()
|
||||
initReportPaperformat()
|
||||
}
|
||||
|
||||
// initIrUIMenu registers ir.ui.menu — Application menu structure.
|
||||
// Mirrors: odoo/addons/base/models/ir_ui_menu.py class IrUiMenu
|
||||
func initIrUIMenu() {
|
||||
m := orm.NewModel("ir.ui.menu", orm.ModelOpts{
|
||||
Description: "Menu",
|
||||
Order: "sequence, id",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Menu", Required: true, Translate: true}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
orm.Many2one("parent_id", "ir.ui.menu", orm.FieldOpts{String: "Parent Menu", Index: true}),
|
||||
orm.One2many("child_id", "ir.ui.menu", "parent_id", orm.FieldOpts{String: "Child Menus"}),
|
||||
orm.Char("complete_name", orm.FieldOpts{String: "Full Path", Compute: "_compute_complete_name"}),
|
||||
orm.Many2many("groups_id", "res.groups", orm.FieldOpts{String: "Groups"}),
|
||||
orm.Char("web_icon", orm.FieldOpts{String: "Web Icon File"}),
|
||||
orm.Char("action", orm.FieldOpts{String: "Action"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initIrUIView registers ir.ui.view — UI view definitions.
|
||||
// Mirrors: odoo/addons/base/models/ir_ui_view.py class View
|
||||
func initIrUIView() {
|
||||
m := orm.NewModel("ir.ui.view", orm.ModelOpts{
|
||||
Description: "View",
|
||||
Order: "priority, name, id",
|
||||
RecName: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "View Name", Required: true}),
|
||||
orm.Selection("type", []orm.SelectionItem{
|
||||
{Value: "tree", Label: "Tree"},
|
||||
{Value: "form", Label: "Form"},
|
||||
{Value: "graph", Label: "Graph"},
|
||||
{Value: "pivot", Label: "Pivot"},
|
||||
{Value: "calendar", Label: "Calendar"},
|
||||
{Value: "gantt", Label: "Gantt"},
|
||||
{Value: "kanban", Label: "Kanban"},
|
||||
{Value: "search", Label: "Search"},
|
||||
{Value: "qweb", Label: "QWeb"},
|
||||
{Value: "list", Label: "List"},
|
||||
{Value: "activity", Label: "Activity"},
|
||||
}, orm.FieldOpts{String: "View Type"}),
|
||||
orm.Char("model", orm.FieldOpts{String: "Model", Index: true}),
|
||||
orm.Integer("priority", orm.FieldOpts{String: "Sequence", Default: 16}),
|
||||
orm.Text("arch", orm.FieldOpts{String: "View Architecture"}),
|
||||
orm.Text("arch_db", orm.FieldOpts{String: "Arch Blob", Translate: true}),
|
||||
orm.Many2one("inherit_id", "ir.ui.view", orm.FieldOpts{String: "Inherited View"}),
|
||||
orm.One2many("inherit_children_ids", "ir.ui.view", "inherit_id", orm.FieldOpts{String: "Views which inherit from this one"}),
|
||||
orm.Char("key", orm.FieldOpts{String: "Key", Index: true}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Selection("mode", []orm.SelectionItem{
|
||||
{Value: "primary", Label: "Base view"},
|
||||
{Value: "extension", Label: "Extension View"},
|
||||
}, orm.FieldOpts{String: "View inheritance mode", Default: "primary"}),
|
||||
orm.Many2many("groups_id", "res.groups", orm.FieldOpts{String: "Groups"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initIrActions registers ir.actions.* — Action definitions.
|
||||
// Mirrors: odoo/addons/base/models/ir_actions.py
|
||||
func initIrActions() {
|
||||
// ir.actions.act_window — Window actions (open a view)
|
||||
m := orm.NewModel("ir.actions.act_window", orm.ModelOpts{
|
||||
Description: "Action Window",
|
||||
Table: "ir_act_window",
|
||||
Order: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Action Name", Required: true, Translate: true}),
|
||||
orm.Char("type", orm.FieldOpts{String: "Action Type", Default: "ir.actions.act_window"}),
|
||||
orm.Char("res_model", orm.FieldOpts{String: "Model", Required: true}),
|
||||
orm.Selection("view_mode", []orm.SelectionItem{
|
||||
{Value: "tree", Label: "Tree"},
|
||||
{Value: "form", Label: "Form"},
|
||||
{Value: "tree,form", Label: "Tree, Form"},
|
||||
{Value: "form,tree", Label: "Form, Tree"},
|
||||
{Value: "kanban", Label: "Kanban"},
|
||||
{Value: "kanban,tree,form", Label: "Kanban, Tree, Form"},
|
||||
}, orm.FieldOpts{String: "View Mode", Default: "tree,form"}),
|
||||
orm.Integer("res_id", orm.FieldOpts{String: "Record ID"}),
|
||||
orm.Char("domain", orm.FieldOpts{String: "Domain Value"}),
|
||||
orm.Char("context", orm.FieldOpts{String: "Context Value", Default: "{}"}),
|
||||
orm.Integer("limit", orm.FieldOpts{String: "Limit", Default: 80}),
|
||||
orm.Char("search_view_id", orm.FieldOpts{String: "Search View Ref"}),
|
||||
orm.Char("target", orm.FieldOpts{String: "Target Window", Default: "current"}),
|
||||
orm.Boolean("auto_search", orm.FieldOpts{String: "Auto Search", Default: true}),
|
||||
orm.Many2many("groups_id", "res.groups", orm.FieldOpts{String: "Groups"}),
|
||||
orm.Char("help", orm.FieldOpts{String: "Action Description"}),
|
||||
orm.Many2one("binding_model_id", "ir.model", orm.FieldOpts{String: "Binding Model"}),
|
||||
)
|
||||
|
||||
// ir.actions.server — Server actions (execute Python/code)
|
||||
srv := orm.NewModel("ir.actions.server", orm.ModelOpts{
|
||||
Description: "Server Actions",
|
||||
Table: "ir_act_server",
|
||||
Order: "sequence, name",
|
||||
})
|
||||
|
||||
srv.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Action Name", Required: true, Translate: true}),
|
||||
orm.Char("type", orm.FieldOpts{String: "Action Type", Default: "ir.actions.server"}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 5}),
|
||||
orm.Many2one("model_id", "ir.model", orm.FieldOpts{String: "Model", Required: true, OnDelete: orm.OnDeleteCascade}),
|
||||
orm.Char("model_name", orm.FieldOpts{String: "Model Name", Related: "model_id.model"}),
|
||||
orm.Selection("state", []orm.SelectionItem{
|
||||
{Value: "code", Label: "Execute Code"},
|
||||
{Value: "object_write", Label: "Update Record"},
|
||||
{Value: "object_create", Label: "Create Record"},
|
||||
{Value: "multi", Label: "Execute Several Actions"},
|
||||
}, orm.FieldOpts{String: "Action To Do", Default: "code", Required: true}),
|
||||
orm.Text("code", orm.FieldOpts{String: "Code"}),
|
||||
orm.Many2many("groups_id", "res.groups", orm.FieldOpts{String: "Groups"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initIrSequence registers ir.sequence — Automatic numbering.
|
||||
// Mirrors: odoo/addons/base/models/ir_sequence.py
|
||||
func initIrSequence() {
|
||||
m := orm.NewModel("ir.sequence", orm.ModelOpts{
|
||||
Description: "Sequence",
|
||||
Order: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
|
||||
orm.Char("code", orm.FieldOpts{String: "Sequence Code"}),
|
||||
orm.Selection("implementation", []orm.SelectionItem{
|
||||
{Value: "standard", Label: "Standard"},
|
||||
{Value: "no_gap", Label: "No gap"},
|
||||
}, orm.FieldOpts{String: "Implementation", Default: "standard", Required: true}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Char("prefix", orm.FieldOpts{String: "Prefix"}),
|
||||
orm.Char("suffix", orm.FieldOpts{String: "Suffix"}),
|
||||
orm.Integer("number_next", orm.FieldOpts{String: "Next Number", Default: 1, Required: true}),
|
||||
orm.Integer("number_increment", orm.FieldOpts{String: "Step", Default: 1, Required: true}),
|
||||
orm.Integer("padding", orm.FieldOpts{String: "Sequence Size", Default: 0, Required: true}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
|
||||
orm.Boolean("use_date_range", orm.FieldOpts{String: "Use subsequences per date_range"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initIrAttachment registers ir.attachment — File storage.
|
||||
// Mirrors: odoo/addons/base/models/ir_attachment.py
|
||||
func initIrAttachment() {
|
||||
m := orm.NewModel("ir.attachment", orm.ModelOpts{
|
||||
Description: "Attachment",
|
||||
Order: "id desc",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
|
||||
orm.Char("description", orm.FieldOpts{String: "Description"}),
|
||||
orm.Char("res_model", orm.FieldOpts{String: "Resource Model", Index: true}),
|
||||
orm.Integer("res_id", orm.FieldOpts{String: "Resource ID", Index: true}),
|
||||
orm.Char("res_field", orm.FieldOpts{String: "Resource Field"}),
|
||||
orm.Char("res_name", orm.FieldOpts{String: "Resource Name"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
|
||||
orm.Selection("type", []orm.SelectionItem{
|
||||
{Value: "url", Label: "URL"},
|
||||
{Value: "binary", Label: "File"},
|
||||
}, orm.FieldOpts{String: "Type", Default: "binary", Required: true}),
|
||||
orm.Char("url", orm.FieldOpts{String: "Url"}),
|
||||
orm.Binary("datas", orm.FieldOpts{String: "File Content"}),
|
||||
orm.Char("store_fname", orm.FieldOpts{String: "Stored Filename"}),
|
||||
orm.Integer("file_size", orm.FieldOpts{String: "File Size"}),
|
||||
orm.Char("checksum", orm.FieldOpts{String: "Checksum/SHA1", Size: 40, Index: true}),
|
||||
orm.Char("mimetype", orm.FieldOpts{String: "Mime Type"}),
|
||||
orm.Boolean("public", orm.FieldOpts{String: "Is public document"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initReportPaperformat registers report.paperformat.
|
||||
// Mirrors: odoo/addons/base/models/report_paperformat.py
|
||||
func initReportPaperformat() {
|
||||
m := orm.NewModel("report.paperformat", orm.ModelOpts{
|
||||
Description: "Paper Format Config",
|
||||
Order: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
|
||||
orm.Boolean("default", orm.FieldOpts{String: "Default paper format"}),
|
||||
orm.Selection("format", []orm.SelectionItem{
|
||||
{Value: "A0", Label: "A0 (841 x 1189 mm)"},
|
||||
{Value: "A1", Label: "A1 (594 x 841 mm)"},
|
||||
{Value: "A2", Label: "A2 (420 x 594 mm)"},
|
||||
{Value: "A3", Label: "A3 (297 x 420 mm)"},
|
||||
{Value: "A4", Label: "A4 (210 x 297 mm)"},
|
||||
{Value: "A5", Label: "A5 (148 x 210 mm)"},
|
||||
{Value: "Legal", Label: "Legal (216 x 356 mm)"},
|
||||
{Value: "Letter", Label: "Letter (216 x 279 mm)"},
|
||||
{Value: "custom", Label: "Custom"},
|
||||
}, orm.FieldOpts{String: "Paper size", Default: "A4"}),
|
||||
orm.Integer("margin_top", orm.FieldOpts{String: "Top Margin (mm)", Default: 40}),
|
||||
orm.Integer("margin_bottom", orm.FieldOpts{String: "Bottom Margin (mm)", Default: 20}),
|
||||
orm.Integer("margin_left", orm.FieldOpts{String: "Left Margin (mm)", Default: 7}),
|
||||
orm.Integer("margin_right", orm.FieldOpts{String: "Right Margin (mm)", Default: 7}),
|
||||
orm.Integer("page_height", orm.FieldOpts{String: "Page height (mm)"}),
|
||||
orm.Integer("page_width", orm.FieldOpts{String: "Page width (mm)"}),
|
||||
orm.Selection("orientation", []orm.SelectionItem{
|
||||
{Value: "Landscape", Label: "Landscape"},
|
||||
{Value: "Portrait", Label: "Portrait"},
|
||||
}, orm.FieldOpts{String: "Orientation", Default: "Portrait"}),
|
||||
orm.Integer("header_spacing", orm.FieldOpts{String: "Header spacing (mm)"}),
|
||||
orm.Integer("dpi", orm.FieldOpts{String: "Output DPI", Default: 90, Required: true}),
|
||||
orm.Boolean("disable_shrinking", orm.FieldOpts{String: "Disable smart shrinking"}),
|
||||
)
|
||||
}
|
||||
71
addons/base/models/res_company.go
Normal file
71
addons/base/models/res_company.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package models
|
||||
|
||||
import "odoo-go/pkg/orm"
|
||||
|
||||
// initResCompany registers the res.company model.
|
||||
// Mirrors: odoo/addons/base/models/res_company.py class Company
|
||||
//
|
||||
// In Odoo, companies are central to multi-company support.
|
||||
// Every record can be scoped to a company via company_id.
|
||||
func initResCompany() {
|
||||
m := orm.NewModel("res.company", orm.ModelOpts{
|
||||
Description: "Companies",
|
||||
Order: "sequence, name",
|
||||
})
|
||||
|
||||
// -- Identity --
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Company Name", Required: true}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Many2one("parent_id", "res.company", orm.FieldOpts{String: "Parent Company"}),
|
||||
orm.One2many("child_ids", "res.company", "parent_id", orm.FieldOpts{String: "Child Companies"}),
|
||||
)
|
||||
|
||||
// -- Contact (delegates to partner) --
|
||||
// In Odoo: _inherits = {'res.partner': 'partner_id'}
|
||||
// We use explicit fields instead of delegation for clarity.
|
||||
m.AddFields(
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{
|
||||
String: "Partner", Required: true, OnDelete: orm.OnDeleteRestrict,
|
||||
}),
|
||||
orm.Char("street", orm.FieldOpts{String: "Street"}),
|
||||
orm.Char("street2", orm.FieldOpts{String: "Street2"}),
|
||||
orm.Char("zip", orm.FieldOpts{String: "Zip"}),
|
||||
orm.Char("city", orm.FieldOpts{String: "City"}),
|
||||
orm.Many2one("state_id", "res.country.state", orm.FieldOpts{String: "State"}),
|
||||
orm.Many2one("country_id", "res.country", orm.FieldOpts{String: "Country"}),
|
||||
orm.Char("email", orm.FieldOpts{String: "Email"}),
|
||||
orm.Char("phone", orm.FieldOpts{String: "Phone"}),
|
||||
orm.Char("mobile", orm.FieldOpts{String: "Mobile"}),
|
||||
orm.Char("website", orm.FieldOpts{String: "Website"}),
|
||||
orm.Char("vat", orm.FieldOpts{String: "Tax ID"}),
|
||||
orm.Char("company_registry", orm.FieldOpts{String: "Company ID (Registry)"}),
|
||||
)
|
||||
|
||||
// -- Currency --
|
||||
m.AddFields(
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{
|
||||
String: "Currency", Required: true,
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Display --
|
||||
m.AddFields(
|
||||
orm.Binary("logo", orm.FieldOpts{String: "Company Logo"}),
|
||||
orm.Char("color", orm.FieldOpts{String: "Color"}),
|
||||
orm.Integer("font_color", orm.FieldOpts{String: "Font Color"}),
|
||||
)
|
||||
|
||||
// -- Report Layout --
|
||||
m.AddFields(
|
||||
orm.Many2one("paperformat_id", "report.paperformat", orm.FieldOpts{String: "Paper Format"}),
|
||||
orm.HTML("report_header", orm.FieldOpts{String: "Company Tagline"}),
|
||||
orm.HTML("report_footer", orm.FieldOpts{String: "Report Footer"}),
|
||||
)
|
||||
|
||||
// -- Fiscal --
|
||||
m.AddFields(
|
||||
orm.Many2one("account_fiscal_country_id", "res.country", orm.FieldOpts{String: "Fiscal Country"}),
|
||||
)
|
||||
}
|
||||
79
addons/base/models/res_currency.go
Normal file
79
addons/base/models/res_currency.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package models
|
||||
|
||||
import "odoo-go/pkg/orm"
|
||||
|
||||
// initResCurrency registers currency models.
|
||||
// Mirrors: odoo/addons/base/models/res_currency.py
|
||||
|
||||
func initResCurrency() {
|
||||
// res.currency — Currency definition
|
||||
m := orm.NewModel("res.currency", orm.ModelOpts{
|
||||
Description: "Currency",
|
||||
Order: "name",
|
||||
RecName: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Currency", Required: true, Size: 3}),
|
||||
orm.Char("full_name", orm.FieldOpts{String: "Name"}),
|
||||
orm.Char("symbol", orm.FieldOpts{String: "Symbol", Required: true, Size: 4}),
|
||||
orm.Integer("decimal_places", orm.FieldOpts{String: "Decimal Places"}),
|
||||
orm.Float("rounding", orm.FieldOpts{String: "Rounding Factor"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Selection("position", []orm.SelectionItem{
|
||||
{Value: "after", Label: "After Amount"},
|
||||
{Value: "before", Label: "Before Amount"},
|
||||
}, orm.FieldOpts{String: "Symbol Position", Default: "after"}),
|
||||
orm.Float("rate", orm.FieldOpts{String: "Current Rate", Compute: "_compute_current_rate"}),
|
||||
orm.One2many("rate_ids", "res.currency.rate", "currency_id", orm.FieldOpts{String: "Rates"}),
|
||||
)
|
||||
|
||||
// res.currency.rate — Exchange rates
|
||||
rate := orm.NewModel("res.currency.rate", orm.ModelOpts{
|
||||
Description: "Currency Rate",
|
||||
Order: "name desc",
|
||||
RecName: "name",
|
||||
})
|
||||
|
||||
rate.AddFields(
|
||||
orm.Date("name", orm.FieldOpts{String: "Date", Required: true, Index: true, Default: "today"}),
|
||||
orm.Float("rate", orm.FieldOpts{String: "Rate", Required: true}),
|
||||
orm.Float("inverse_company_rate", orm.FieldOpts{String: "Inverse Rate"}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{
|
||||
String: "Currency", Required: true, OnDelete: orm.OnDeleteCascade,
|
||||
}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
|
||||
)
|
||||
|
||||
// res.country — Country
|
||||
// Mirrors: odoo/addons/base/models/res_country.py
|
||||
country := orm.NewModel("res.country", orm.ModelOpts{
|
||||
Description: "Country",
|
||||
Order: "name",
|
||||
})
|
||||
|
||||
country.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Country Name", Required: true, Translate: true}),
|
||||
orm.Char("code", orm.FieldOpts{String: "Country Code", Size: 2, Required: true}),
|
||||
orm.Char("phone_code", orm.FieldOpts{String: "Country Calling Code"}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency"}),
|
||||
orm.Binary("image", orm.FieldOpts{String: "Flag"}),
|
||||
orm.One2many("state_ids", "res.country.state", "country_id", orm.FieldOpts{String: "States"}),
|
||||
orm.Char("address_format", orm.FieldOpts{String: "Layout in Reports"}),
|
||||
orm.Char("vat_label", orm.FieldOpts{String: "Vat Label", Translate: true}),
|
||||
)
|
||||
|
||||
// res.country.state — Country state/province
|
||||
state := orm.NewModel("res.country.state", orm.ModelOpts{
|
||||
Description: "Country state",
|
||||
Order: "code",
|
||||
})
|
||||
|
||||
state.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "State Name", Required: true}),
|
||||
orm.Char("code", orm.FieldOpts{String: "State Code", Required: true, Size: 3}),
|
||||
orm.Many2one("country_id", "res.country", orm.FieldOpts{
|
||||
String: "Country", Required: true, OnDelete: orm.OnDeleteCascade,
|
||||
}),
|
||||
)
|
||||
}
|
||||
158
addons/base/models/res_partner.go
Normal file
158
addons/base/models/res_partner.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package models
|
||||
|
||||
import "odoo-go/pkg/orm"
|
||||
|
||||
// initResPartner registers the res.partner model.
|
||||
// Mirrors: odoo/addons/base/models/res_partner.py class Partner(models.Model)
|
||||
//
|
||||
// res.partner is the central contact model in Odoo. It stores:
|
||||
// - Companies and individuals
|
||||
// - Customers, vendors, employees (via type)
|
||||
// - Addresses (street, city, zip, country)
|
||||
// - Communication (email, phone, website)
|
||||
func initResPartner() {
|
||||
m := orm.NewModel("res.partner", orm.ModelOpts{
|
||||
Description: "Contact",
|
||||
Order: "name, id",
|
||||
RecName: "name",
|
||||
})
|
||||
|
||||
// -- Identity --
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Index: true}),
|
||||
orm.Char("display_name", orm.FieldOpts{String: "Display Name", Compute: "_compute_display_name", Store: true}),
|
||||
orm.Char("ref", orm.FieldOpts{String: "Reference", Index: true}),
|
||||
orm.Selection("type", []orm.SelectionItem{
|
||||
{Value: "contact", Label: "Contact"},
|
||||
{Value: "invoice", Label: "Invoice Address"},
|
||||
{Value: "delivery", Label: "Delivery Address"},
|
||||
{Value: "other", Label: "Other Address"},
|
||||
{Value: "private", Label: "Private Address"},
|
||||
}, orm.FieldOpts{String: "Address Type", Default: "contact"}),
|
||||
orm.Boolean("is_company", orm.FieldOpts{String: "Is a Company", Default: false}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Char("lang", orm.FieldOpts{String: "Language", Default: "en_US"}),
|
||||
orm.Char("tz", orm.FieldOpts{String: "Timezone"}),
|
||||
)
|
||||
|
||||
// -- Address --
|
||||
m.AddFields(
|
||||
orm.Char("street", orm.FieldOpts{String: "Street"}),
|
||||
orm.Char("street2", orm.FieldOpts{String: "Street2"}),
|
||||
orm.Char("zip", orm.FieldOpts{String: "Zip"}),
|
||||
orm.Char("city", orm.FieldOpts{String: "City"}),
|
||||
orm.Many2one("state_id", "res.country.state", orm.FieldOpts{String: "State"}),
|
||||
orm.Many2one("country_id", "res.country", orm.FieldOpts{String: "Country"}),
|
||||
)
|
||||
|
||||
// -- Communication --
|
||||
m.AddFields(
|
||||
orm.Char("email", orm.FieldOpts{String: "Email"}),
|
||||
orm.Char("phone", orm.FieldOpts{String: "Phone"}),
|
||||
orm.Char("mobile", orm.FieldOpts{String: "Mobile"}),
|
||||
orm.Char("website", orm.FieldOpts{String: "Website"}),
|
||||
orm.Char("vat", orm.FieldOpts{String: "Tax ID", Index: true}),
|
||||
)
|
||||
|
||||
// -- Company --
|
||||
m.AddFields(
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company", Index: true}),
|
||||
orm.Char("company_name", orm.FieldOpts{String: "Company Name"}),
|
||||
orm.Char("company_registry", orm.FieldOpts{String: "Company Registry"}),
|
||||
)
|
||||
|
||||
// -- Relationships --
|
||||
m.AddFields(
|
||||
orm.Many2one("parent_id", "res.partner", orm.FieldOpts{String: "Related Company", Index: true}),
|
||||
orm.One2many("child_ids", "res.partner", "parent_id", orm.FieldOpts{String: "Contact & Addresses"}),
|
||||
orm.Many2many("category_ids", "res.partner.category", orm.FieldOpts{String: "Tags"}),
|
||||
orm.Many2one("user_id", "res.users", orm.FieldOpts{String: "Salesperson"}),
|
||||
orm.Many2one("title", "res.partner.title", orm.FieldOpts{String: "Title"}),
|
||||
)
|
||||
|
||||
// -- Commercial --
|
||||
m.AddFields(
|
||||
orm.Many2one("commercial_partner_id", "res.partner", orm.FieldOpts{
|
||||
String: "Commercial Entity", Compute: "_compute_commercial_partner", Store: true,
|
||||
}),
|
||||
orm.Selection("customer_rank", []orm.SelectionItem{
|
||||
{Value: "0", Label: "None"},
|
||||
{Value: "1", Label: "Customer"},
|
||||
}, orm.FieldOpts{String: "Customer Rank", Default: "0"}),
|
||||
orm.Selection("supplier_rank", []orm.SelectionItem{
|
||||
{Value: "0", Label: "None"},
|
||||
{Value: "1", Label: "Vendor"},
|
||||
}, orm.FieldOpts{String: "Vendor Rank", Default: "0"}),
|
||||
)
|
||||
|
||||
// -- Banking --
|
||||
m.AddFields(
|
||||
orm.One2many("bank_ids", "res.partner.bank", "partner_id", orm.FieldOpts{String: "Bank Accounts"}),
|
||||
)
|
||||
|
||||
// -- Notes --
|
||||
m.AddFields(
|
||||
orm.Text("comment", orm.FieldOpts{String: "Notes"}),
|
||||
orm.Binary("image_1920", orm.FieldOpts{String: "Image"}),
|
||||
)
|
||||
|
||||
// --- Supporting models ---
|
||||
|
||||
// res.partner.category — Contact tags
|
||||
// Mirrors: odoo/addons/base/models/res_partner.py class PartnerCategory
|
||||
cat := orm.NewModel("res.partner.category", orm.ModelOpts{
|
||||
Description: "Contact Tag",
|
||||
Order: "name",
|
||||
})
|
||||
cat.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Tag Name", Required: true}),
|
||||
orm.Integer("color", orm.FieldOpts{String: "Color Index"}),
|
||||
orm.Many2one("parent_id", "res.partner.category", orm.FieldOpts{String: "Parent Category"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
)
|
||||
|
||||
// res.partner.title — Contact titles (Mr., Mrs., etc.)
|
||||
// Mirrors: odoo/addons/base/models/res_partner.py class PartnerTitle
|
||||
title := orm.NewModel("res.partner.title", orm.ModelOpts{
|
||||
Description: "Partner Title",
|
||||
Order: "name",
|
||||
})
|
||||
title.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Title", Required: true, Translate: true}),
|
||||
orm.Char("shortcut", orm.FieldOpts{String: "Abbreviation", Translate: true}),
|
||||
)
|
||||
|
||||
// res.partner.bank — Bank accounts
|
||||
// Mirrors: odoo/addons/base/models/res_bank.py class ResPartnerBank
|
||||
bank := orm.NewModel("res.partner.bank", orm.ModelOpts{
|
||||
Description: "Bank Accounts",
|
||||
Order: "id",
|
||||
RecName: "acc_number",
|
||||
})
|
||||
bank.AddFields(
|
||||
orm.Char("acc_number", orm.FieldOpts{String: "Account Number", Required: true}),
|
||||
orm.Char("sanitized_acc_number", orm.FieldOpts{String: "Sanitized Account Number", Compute: "_compute_sanitized"}),
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{String: "Account Holder", Required: true, OnDelete: orm.OnDeleteCascade}),
|
||||
orm.Many2one("bank_id", "res.bank", orm.FieldOpts{String: "Bank"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
|
||||
orm.Char("acc_holder_name", orm.FieldOpts{String: "Account Holder Name"}),
|
||||
)
|
||||
|
||||
// res.bank — Bank directory
|
||||
resBank := orm.NewModel("res.bank", orm.ModelOpts{
|
||||
Description: "Bank",
|
||||
Order: "name",
|
||||
})
|
||||
resBank.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
|
||||
orm.Char("bic", orm.FieldOpts{String: "Bank Identifier Code", Index: true}),
|
||||
orm.Char("street", orm.FieldOpts{String: "Street"}),
|
||||
orm.Char("street2", orm.FieldOpts{String: "Street2"}),
|
||||
orm.Char("zip", orm.FieldOpts{String: "Zip"}),
|
||||
orm.Char("city", orm.FieldOpts{String: "City"}),
|
||||
orm.Many2one("country", "res.country", orm.FieldOpts{String: "Country"}),
|
||||
orm.Char("email", orm.FieldOpts{String: "Email"}),
|
||||
orm.Char("phone", orm.FieldOpts{String: "Phone"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
)
|
||||
}
|
||||
101
addons/base/models/res_users.go
Normal file
101
addons/base/models/res_users.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package models
|
||||
|
||||
import "odoo-go/pkg/orm"
|
||||
|
||||
// initResUsers registers the res.users model.
|
||||
// Mirrors: odoo/addons/base/models/res_users.py class Users
|
||||
//
|
||||
// In Odoo, res.users inherits from res.partner via _inherits.
|
||||
// Every user has a linked partner record for contact info.
|
||||
func initResUsers() {
|
||||
m := orm.NewModel("res.users", orm.ModelOpts{
|
||||
Description: "Users",
|
||||
Order: "login",
|
||||
})
|
||||
|
||||
// -- Authentication --
|
||||
m.AddFields(
|
||||
orm.Char("login", orm.FieldOpts{String: "Login", Required: true, Index: true}),
|
||||
orm.Char("password", orm.FieldOpts{String: "Password"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
)
|
||||
|
||||
// -- Partner link (Odoo: _inherits = {'res.partner': 'partner_id'}) --
|
||||
m.AddFields(
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{
|
||||
String: "Related Partner", Required: true, OnDelete: orm.OnDeleteRestrict,
|
||||
}),
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Related: "partner_id.name"}),
|
||||
orm.Char("email", orm.FieldOpts{String: "Email", Related: "partner_id.email"}),
|
||||
)
|
||||
|
||||
// -- Company --
|
||||
m.AddFields(
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2many("company_ids", "res.company", orm.FieldOpts{String: "Allowed Companies"}),
|
||||
)
|
||||
|
||||
// -- Groups / Permissions --
|
||||
m.AddFields(
|
||||
orm.Many2many("groups_id", "res.groups", orm.FieldOpts{String: "Groups"}),
|
||||
)
|
||||
|
||||
// -- Preferences --
|
||||
m.AddFields(
|
||||
orm.Char("lang", orm.FieldOpts{String: "Language", Default: "en_US"}),
|
||||
orm.Char("tz", orm.FieldOpts{String: "Timezone", Default: "UTC"}),
|
||||
orm.Selection("notification_type", []orm.SelectionItem{
|
||||
{Value: "email", Label: "Handle by Emails"},
|
||||
{Value: "inbox", Label: "Handle in Odoo"},
|
||||
}, orm.FieldOpts{String: "Notification", Default: "email"}),
|
||||
orm.Binary("image_1920", orm.FieldOpts{String: "Avatar"}),
|
||||
orm.Char("signature", orm.FieldOpts{String: "Email Signature"}),
|
||||
)
|
||||
|
||||
// -- Status --
|
||||
m.AddFields(
|
||||
orm.Boolean("share", orm.FieldOpts{
|
||||
String: "Share User", Compute: "_compute_share", Store: true,
|
||||
Help: "External user with limited access (portal/public)",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// initResGroups registers the res.groups model.
|
||||
// Mirrors: odoo/addons/base/models/res_users.py class Groups
|
||||
//
|
||||
// Groups define permission sets. Users belong to groups.
|
||||
// Groups can imply other groups (hierarchy).
|
||||
func initResGroups() {
|
||||
m := orm.NewModel("res.groups", orm.ModelOpts{
|
||||
Description: "Access Groups",
|
||||
Order: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}),
|
||||
orm.Text("comment", orm.FieldOpts{String: "Comment"}),
|
||||
orm.Many2one("category_id", "ir.module.category", orm.FieldOpts{String: "Application"}),
|
||||
orm.Char("color", orm.FieldOpts{String: "Color Index"}),
|
||||
orm.Char("full_name", orm.FieldOpts{String: "Group Name", Compute: "_compute_full_name"}),
|
||||
orm.Boolean("share", orm.FieldOpts{String: "Share Group", Default: false}),
|
||||
)
|
||||
|
||||
// -- Relationships --
|
||||
m.AddFields(
|
||||
orm.Many2many("users", "res.users", orm.FieldOpts{String: "Users"}),
|
||||
orm.Many2many("implied_ids", "res.groups", orm.FieldOpts{
|
||||
String: "Inherits",
|
||||
Help: "Users of this group automatically inherit those groups",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Access Control --
|
||||
m.AddFields(
|
||||
orm.One2many("model_access", "ir.model.access", "group_id", orm.FieldOpts{String: "Access Controls"}),
|
||||
orm.One2many("rule_groups", "ir.rule", "group_id", orm.FieldOpts{String: "Rules"}),
|
||||
orm.Many2many("menu_access", "ir.ui.menu", orm.FieldOpts{String: "Access Menu"}),
|
||||
)
|
||||
}
|
||||
33
addons/base/module.go
Normal file
33
addons/base/module.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Package base implements the 'base' module — the foundation of Odoo.
|
||||
// Mirrors: odoo/addons/base/__manifest__.py
|
||||
//
|
||||
// The base module provides core models that every other module depends on:
|
||||
// - res.partner (contacts)
|
||||
// - res.users (system users)
|
||||
// - res.company (companies)
|
||||
// - res.currency (currencies)
|
||||
// - ir.module.module (installed modules)
|
||||
// - ir.model (model metadata)
|
||||
// - ir.model.access (access control)
|
||||
// - ir.rule (record rules)
|
||||
// - ir.model.data (external identifiers)
|
||||
package base
|
||||
|
||||
import (
|
||||
"odoo-go/addons/base/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "base",
|
||||
Description: "Base module — core models and infrastructure",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Hidden",
|
||||
Depends: nil, // base has no dependencies
|
||||
Application: false,
|
||||
Installable: true,
|
||||
Sequence: 0,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user