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:
204
addons/account/models/account_account.go
Normal file
204
addons/account/models/account_account.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package models
|
||||
|
||||
import "odoo-go/pkg/orm"
|
||||
|
||||
// initAccountAccount registers the chart of accounts.
|
||||
// Mirrors: odoo/addons/account/models/account_account.py
|
||||
|
||||
func initAccountAccount() {
|
||||
// account.account — Chart of Accounts
|
||||
m := orm.NewModel("account.account", orm.ModelOpts{
|
||||
Description: "Account",
|
||||
Order: "code, company_id",
|
||||
RecName: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Account Name", Required: true, Index: true, Translate: true}),
|
||||
orm.Char("code", orm.FieldOpts{String: "Code", Required: true, Size: 64, Index: true}),
|
||||
orm.Selection("account_type", []orm.SelectionItem{
|
||||
// Balance Sheet
|
||||
{Value: "asset_receivable", Label: "Receivable"},
|
||||
{Value: "asset_cash", Label: "Bank and Cash"},
|
||||
{Value: "asset_current", Label: "Current Assets"},
|
||||
{Value: "asset_non_current", Label: "Non-current Assets"},
|
||||
{Value: "asset_prepayments", Label: "Prepayments"},
|
||||
{Value: "asset_fixed", Label: "Fixed Assets"},
|
||||
{Value: "liability_payable", Label: "Payable"},
|
||||
{Value: "liability_credit_card", Label: "Credit Card"},
|
||||
{Value: "liability_current", Label: "Current Liabilities"},
|
||||
{Value: "liability_non_current", Label: "Non-current Liabilities"},
|
||||
{Value: "equity", Label: "Equity"},
|
||||
{Value: "equity_unaffected", Label: "Current Year Earnings"},
|
||||
// P&L
|
||||
{Value: "income", Label: "Income"},
|
||||
{Value: "income_other", Label: "Other Income"},
|
||||
{Value: "expense", Label: "Expenses"},
|
||||
{Value: "expense_depreciation", Label: "Depreciation"},
|
||||
{Value: "expense_direct_cost", Label: "Cost of Revenue"},
|
||||
// Special
|
||||
{Value: "off_balance", Label: "Off-Balance Sheet"},
|
||||
}, orm.FieldOpts{String: "Type", Required: true}),
|
||||
orm.Boolean("reconcile", orm.FieldOpts{String: "Allow Reconciliation"}),
|
||||
orm.Boolean("non_trade", orm.FieldOpts{String: "Non-Trade Receivable/Payable"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Account Currency"}),
|
||||
orm.Many2many("tax_ids", "account.tax", orm.FieldOpts{String: "Default Taxes"}),
|
||||
orm.Many2many("tag_ids", "account.account.tag", orm.FieldOpts{String: "Tags"}),
|
||||
orm.Text("note", orm.FieldOpts{String: "Internal Notes"}),
|
||||
orm.Boolean("deprecated", orm.FieldOpts{String: "Deprecated"}),
|
||||
orm.Boolean("include_initial_balance", orm.FieldOpts{String: "Include Initial Balance"}),
|
||||
)
|
||||
|
||||
// account.account.tag — Account tags for reporting
|
||||
orm.NewModel("account.account.tag", orm.ModelOpts{
|
||||
Description: "Account Tag",
|
||||
Order: "name",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Tag Name", Required: true, Translate: true}),
|
||||
orm.Selection("applicability", []orm.SelectionItem{
|
||||
{Value: "accounts", Label: "Accounts"},
|
||||
{Value: "taxes", Label: "Taxes"},
|
||||
{Value: "products", Label: "Products"},
|
||||
}, orm.FieldOpts{String: "Applicability", Default: "accounts", Required: true}),
|
||||
orm.Many2one("country_id", "res.country", orm.FieldOpts{String: "Country"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Char("color", orm.FieldOpts{String: "Color Index"}),
|
||||
)
|
||||
|
||||
// account.group — Account groups for hierarchy
|
||||
orm.NewModel("account.group", orm.ModelOpts{
|
||||
Description: "Account Group",
|
||||
Order: "code_prefix_start",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Account Group", Required: true, Translate: true}),
|
||||
orm.Char("code_prefix_start", orm.FieldOpts{String: "Code Prefix Start"}),
|
||||
orm.Char("code_prefix_end", orm.FieldOpts{String: "Code Prefix End"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company", Required: true}),
|
||||
)
|
||||
}
|
||||
|
||||
// initAccountTax registers tax models.
|
||||
// Mirrors: odoo/addons/account/models/account_tax.py
|
||||
func initAccountTax() {
|
||||
m := orm.NewModel("account.tax", orm.ModelOpts{
|
||||
Description: "Tax",
|
||||
Order: "sequence, id",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Tax Name", Required: true, Translate: true}),
|
||||
orm.Selection("type_tax_use", []orm.SelectionItem{
|
||||
{Value: "sale", Label: "Sales"},
|
||||
{Value: "purchase", Label: "Purchases"},
|
||||
{Value: "none", Label: "None"},
|
||||
}, orm.FieldOpts{String: "Tax Type", Required: true, Default: "sale"}),
|
||||
orm.Selection("amount_type", []orm.SelectionItem{
|
||||
{Value: "group", Label: "Group of Taxes"},
|
||||
{Value: "fixed", Label: "Fixed"},
|
||||
{Value: "percent", Label: "Percentage of Price"},
|
||||
{Value: "division", Label: "Percentage of Price Tax Included"},
|
||||
}, orm.FieldOpts{String: "Tax Computation", Required: true, Default: "percent"}),
|
||||
orm.Float("amount", orm.FieldOpts{String: "Amount", Required: true}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 1}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company", Required: true}),
|
||||
orm.Boolean("price_include", orm.FieldOpts{String: "Included in Price"}),
|
||||
orm.Boolean("include_base_amount", orm.FieldOpts{String: "Affect Base of Subsequent Taxes"}),
|
||||
orm.Boolean("is_base_affected", orm.FieldOpts{String: "Base Affected by Previous Taxes", Default: true}),
|
||||
orm.Many2many("children_tax_ids", "account.tax", orm.FieldOpts{String: "Children Taxes"}),
|
||||
orm.Char("description", orm.FieldOpts{String: "Label on Invoices"}),
|
||||
orm.Many2one("tax_group_id", "account.tax.group", orm.FieldOpts{String: "Tax Group"}),
|
||||
orm.Many2one("country_id", "res.country", orm.FieldOpts{String: "Country"}),
|
||||
// Repartition lines define how tax amounts are distributed to accounts
|
||||
orm.One2many("invoice_repartition_line_ids", "account.tax.repartition.line", "tax_id", orm.FieldOpts{String: "Invoice Repartition"}),
|
||||
orm.One2many("refund_repartition_line_ids", "account.tax.repartition.line", "tax_id", orm.FieldOpts{String: "Refund Repartition"}),
|
||||
)
|
||||
|
||||
// account.tax.group — Tax group for reports
|
||||
orm.NewModel("account.tax.group", orm.ModelOpts{
|
||||
Description: "Tax Group",
|
||||
Order: "sequence, name",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
orm.Many2one("country_id", "res.country", orm.FieldOpts{String: "Country"}),
|
||||
)
|
||||
|
||||
// account.tax.repartition.line — How tax is split across accounts
|
||||
// Mirrors: odoo/addons/account/models/account_tax.py AccountTaxRepartitionLine
|
||||
orm.NewModel("account.tax.repartition.line", orm.ModelOpts{
|
||||
Description: "Tax Repartition Line",
|
||||
Order: "sequence, id",
|
||||
}).AddFields(
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 1}),
|
||||
orm.Float("factor_percent", orm.FieldOpts{String: "Percentage", Default: 100, Required: true}),
|
||||
orm.Selection("repartition_type", []orm.SelectionItem{
|
||||
{Value: "base", Label: "Base"},
|
||||
{Value: "tax", Label: "Tax"},
|
||||
}, orm.FieldOpts{String: "Based On", Default: "tax", Required: true}),
|
||||
orm.Many2one("account_id", "account.account", orm.FieldOpts{String: "Account"}),
|
||||
orm.Many2one("tax_id", "account.tax", orm.FieldOpts{String: "Tax", OnDelete: orm.OnDeleteCascade}),
|
||||
orm.Many2many("tag_ids", "account.account.tag", orm.FieldOpts{String: "Tax Grids"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
|
||||
orm.Selection("document_type", []orm.SelectionItem{
|
||||
{Value: "invoice", Label: "Invoice"},
|
||||
{Value: "refund", Label: "Refund"},
|
||||
}, orm.FieldOpts{String: "Document Type"}),
|
||||
orm.Boolean("use_in_tax_closing", orm.FieldOpts{String: "Tax Closing Entry", Default: true}),
|
||||
)
|
||||
|
||||
// account.fiscal.position — Tax mapping per country/partner
|
||||
// Mirrors: odoo/addons/account/models/account_fiscal_position.py
|
||||
initAccountFiscalPosition()
|
||||
}
|
||||
|
||||
func initAccountFiscalPosition() {
|
||||
m := orm.NewModel("account.fiscal.position", orm.ModelOpts{
|
||||
Description: "Fiscal Position",
|
||||
Order: "sequence",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Fiscal Position", Required: true, Translate: true}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company", Required: true}),
|
||||
orm.One2many("tax_ids", "account.fiscal.position.tax", "position_id", orm.FieldOpts{String: "Tax Mapping"}),
|
||||
orm.One2many("account_ids", "account.fiscal.position.account", "position_id", orm.FieldOpts{String: "Account Mapping"}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence"}),
|
||||
orm.Boolean("auto_apply", orm.FieldOpts{String: "Detect Automatically"}),
|
||||
orm.Many2one("country_id", "res.country", orm.FieldOpts{String: "Country"}),
|
||||
orm.Many2one("country_group_id", "res.country.group", orm.FieldOpts{String: "Country Group"}),
|
||||
orm.Boolean("vat_required", orm.FieldOpts{String: "VAT required"}),
|
||||
orm.Text("note", orm.FieldOpts{String: "Notes", Translate: true}),
|
||||
)
|
||||
|
||||
// Fiscal position tax mapping
|
||||
orm.NewModel("account.fiscal.position.tax", orm.ModelOpts{
|
||||
Description: "Tax Mapping",
|
||||
}).AddFields(
|
||||
orm.Many2one("position_id", "account.fiscal.position", orm.FieldOpts{String: "Fiscal Position", Required: true, OnDelete: orm.OnDeleteCascade}),
|
||||
orm.Many2one("tax_src_id", "account.tax", orm.FieldOpts{String: "Tax on Product", Required: true}),
|
||||
orm.Many2one("tax_dest_id", "account.tax", orm.FieldOpts{String: "Tax to Apply"}),
|
||||
)
|
||||
|
||||
// Fiscal position account mapping
|
||||
orm.NewModel("account.fiscal.position.account", orm.ModelOpts{
|
||||
Description: "Account Mapping",
|
||||
}).AddFields(
|
||||
orm.Many2one("position_id", "account.fiscal.position", orm.FieldOpts{String: "Fiscal Position", Required: true, OnDelete: orm.OnDeleteCascade}),
|
||||
orm.Many2one("account_src_id", "account.account", orm.FieldOpts{String: "Account on Product", Required: true}),
|
||||
orm.Many2one("account_dest_id", "account.account", orm.FieldOpts{String: "Account to Use", Required: true}),
|
||||
)
|
||||
|
||||
// res.country.group — for fiscal position country grouping
|
||||
orm.NewModel("res.country.group", orm.ModelOpts{
|
||||
Description: "Country Group",
|
||||
Order: "name",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}),
|
||||
orm.Many2many("country_ids", "res.country", orm.FieldOpts{String: "Countries"}),
|
||||
)
|
||||
}
|
||||
568
addons/account/models/account_move.go
Normal file
568
addons/account/models/account_move.go
Normal file
@@ -0,0 +1,568 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"odoo-go/pkg/orm"
|
||||
)
|
||||
|
||||
// initAccountJournal registers account.journal — where entries are posted.
|
||||
// Mirrors: odoo/addons/account/models/account_journal.py
|
||||
func initAccountJournal() {
|
||||
m := orm.NewModel("account.journal", orm.ModelOpts{
|
||||
Description: "Journal",
|
||||
Order: "sequence, type, code",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Journal Name", Required: true, Translate: true}),
|
||||
orm.Char("code", orm.FieldOpts{String: "Short Code", Required: true, Size: 5}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Selection("type", []orm.SelectionItem{
|
||||
{Value: "sale", Label: "Sales"},
|
||||
{Value: "purchase", Label: "Purchase"},
|
||||
{Value: "cash", Label: "Cash"},
|
||||
{Value: "bank", Label: "Bank"},
|
||||
{Value: "general", Label: "Miscellaneous"},
|
||||
{Value: "credit", Label: "Credit Card"},
|
||||
}, orm.FieldOpts{String: "Type", Required: true}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company", Required: true}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency"}),
|
||||
orm.Many2one("default_account_id", "account.account", orm.FieldOpts{String: "Default Account"}),
|
||||
orm.Many2one("suspense_account_id", "account.account", orm.FieldOpts{String: "Suspense Account"}),
|
||||
orm.Many2one("profit_account_id", "account.account", orm.FieldOpts{String: "Profit Account"}),
|
||||
orm.Many2one("loss_account_id", "account.account", orm.FieldOpts{String: "Loss Account"}),
|
||||
orm.Many2one("sequence_id", "ir.sequence", orm.FieldOpts{String: "Entry Sequence"}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
orm.Integer("color", orm.FieldOpts{String: "Color"}),
|
||||
orm.Char("bank_acc_number", orm.FieldOpts{String: "Account Number"}),
|
||||
orm.Many2one("bank_id", "res.bank", orm.FieldOpts{String: "Bank"}),
|
||||
orm.Many2one("bank_account_id", "res.partner.bank", orm.FieldOpts{String: "Bank Account"}),
|
||||
orm.Boolean("restrict_mode_hash_table", orm.FieldOpts{String: "Lock Posted Entries with Hash"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initAccountMove registers account.move — the core journal entry / invoice model.
|
||||
// Mirrors: odoo/addons/account/models/account_move.py
|
||||
//
|
||||
// account.move is THE central model in Odoo accounting. It represents:
|
||||
// - Journal entries (manual bookkeeping)
|
||||
// - Customer invoices / credit notes
|
||||
// - Vendor bills / refunds
|
||||
// - Receipts
|
||||
func initAccountMove() {
|
||||
m := orm.NewModel("account.move", orm.ModelOpts{
|
||||
Description: "Journal Entry",
|
||||
Order: "date desc, name desc, id desc",
|
||||
RecName: "name",
|
||||
})
|
||||
|
||||
// -- Identity --
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Number", Index: true, Readonly: true, Default: "/"}),
|
||||
orm.Date("date", orm.FieldOpts{String: "Date", Required: true, Index: true}),
|
||||
orm.Char("ref", orm.FieldOpts{String: "Reference"}),
|
||||
)
|
||||
|
||||
// -- Type & State --
|
||||
m.AddFields(
|
||||
orm.Selection("move_type", []orm.SelectionItem{
|
||||
{Value: "entry", Label: "Journal Entry"},
|
||||
{Value: "out_invoice", Label: "Customer Invoice"},
|
||||
{Value: "out_refund", Label: "Customer Credit Note"},
|
||||
{Value: "in_invoice", Label: "Vendor Bill"},
|
||||
{Value: "in_refund", Label: "Vendor Credit Note"},
|
||||
{Value: "out_receipt", Label: "Sales Receipt"},
|
||||
{Value: "in_receipt", Label: "Purchase Receipt"},
|
||||
}, orm.FieldOpts{String: "Type", Required: true, Default: "entry", Index: true}),
|
||||
orm.Selection("state", []orm.SelectionItem{
|
||||
{Value: "draft", Label: "Draft"},
|
||||
{Value: "posted", Label: "Posted"},
|
||||
{Value: "cancel", Label: "Cancelled"},
|
||||
}, orm.FieldOpts{String: "Status", Default: "draft", Required: true, Readonly: true, Index: true}),
|
||||
orm.Selection("payment_state", []orm.SelectionItem{
|
||||
{Value: "not_paid", Label: "Not Paid"},
|
||||
{Value: "in_payment", Label: "In Payment"},
|
||||
{Value: "paid", Label: "Paid"},
|
||||
{Value: "partial", Label: "Partially Paid"},
|
||||
{Value: "reversed", Label: "Reversed"},
|
||||
{Value: "blocked", Label: "Blocked"},
|
||||
}, orm.FieldOpts{String: "Payment Status", Compute: "_compute_payment_state", Store: true}),
|
||||
)
|
||||
|
||||
// -- Relationships --
|
||||
m.AddFields(
|
||||
orm.Many2one("journal_id", "account.journal", orm.FieldOpts{
|
||||
String: "Journal", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{
|
||||
String: "Currency", Required: true,
|
||||
}),
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{
|
||||
String: "Partner", Index: true,
|
||||
}),
|
||||
orm.Many2one("commercial_partner_id", "res.partner", orm.FieldOpts{
|
||||
String: "Commercial Entity", Related: "partner_id.commercial_partner_id",
|
||||
}),
|
||||
orm.Many2one("fiscal_position_id", "account.fiscal.position", orm.FieldOpts{
|
||||
String: "Fiscal Position",
|
||||
}),
|
||||
orm.Many2one("partner_bank_id", "res.partner.bank", orm.FieldOpts{
|
||||
String: "Recipient Bank",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Lines --
|
||||
m.AddFields(
|
||||
orm.One2many("line_ids", "account.move.line", "move_id", orm.FieldOpts{
|
||||
String: "Journal Items",
|
||||
}),
|
||||
orm.One2many("invoice_line_ids", "account.move.line", "move_id", orm.FieldOpts{
|
||||
String: "Invoice Lines",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Amounts (Computed) --
|
||||
m.AddFields(
|
||||
orm.Monetary("amount_untaxed", orm.FieldOpts{String: "Untaxed Amount", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id"}),
|
||||
orm.Monetary("amount_tax", orm.FieldOpts{String: "Tax", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id"}),
|
||||
orm.Monetary("amount_total", orm.FieldOpts{String: "Total", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id"}),
|
||||
orm.Monetary("amount_residual", orm.FieldOpts{String: "Amount Due", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id"}),
|
||||
orm.Monetary("amount_total_in_currency_signed", orm.FieldOpts{String: "Total in Currency Signed", Compute: "_compute_amount", Store: true}),
|
||||
)
|
||||
|
||||
// -- Invoice specific --
|
||||
m.AddFields(
|
||||
orm.Date("invoice_date", orm.FieldOpts{String: "Invoice/Bill Date"}),
|
||||
orm.Date("invoice_date_due", orm.FieldOpts{String: "Due Date"}),
|
||||
orm.Char("invoice_origin", orm.FieldOpts{String: "Source Document"}),
|
||||
orm.Many2one("invoice_payment_term_id", "account.payment.term", orm.FieldOpts{
|
||||
String: "Payment Terms",
|
||||
}),
|
||||
orm.Text("narration", orm.FieldOpts{String: "Terms and Conditions"}),
|
||||
)
|
||||
|
||||
// -- Technical --
|
||||
m.AddFields(
|
||||
orm.Boolean("auto_post", orm.FieldOpts{String: "Auto-post"}),
|
||||
orm.Char("sequence_prefix", orm.FieldOpts{String: "Sequence Prefix"}),
|
||||
orm.Integer("sequence_number", orm.FieldOpts{String: "Sequence Number"}),
|
||||
)
|
||||
|
||||
// -- Computed Fields --
|
||||
// _compute_amount: sums invoice lines to produce totals.
|
||||
// Mirrors: odoo/addons/account/models/account_move.py AccountMove._compute_amount()
|
||||
computeAmount := func(rs *orm.Recordset) (orm.Values, error) {
|
||||
env := rs.Env()
|
||||
moveID := rs.IDs()[0]
|
||||
|
||||
var untaxed, tax, total float64
|
||||
rows, err := env.Tx().Query(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(debit), 0), COALESCE(SUM(credit), 0)
|
||||
FROM account_move_line WHERE move_id = $1`, moveID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
if rows.Next() {
|
||||
var debitSum, creditSum float64
|
||||
if err := rows.Scan(&debitSum, &creditSum); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
total = debitSum // For invoices, total = sum of debits (or credits)
|
||||
if debitSum > creditSum {
|
||||
total = debitSum
|
||||
} else {
|
||||
total = creditSum
|
||||
}
|
||||
// Tax lines have display_type='tax', product lines don't
|
||||
untaxed = total // Simplified: full total as untaxed for now
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
// Get actual tax amount from tax lines
|
||||
err = env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(ABS(balance)), 0)
|
||||
FROM account_move_line WHERE move_id = $1 AND display_type = 'tax'`,
|
||||
moveID).Scan(&tax)
|
||||
if err != nil {
|
||||
tax = 0
|
||||
}
|
||||
if tax > 0 {
|
||||
untaxed = total - tax
|
||||
}
|
||||
|
||||
return orm.Values{
|
||||
"amount_untaxed": untaxed,
|
||||
"amount_tax": tax,
|
||||
"amount_total": total,
|
||||
"amount_residual": total, // Simplified: residual = total until payments
|
||||
}, nil
|
||||
}
|
||||
|
||||
m.RegisterCompute("amount_untaxed", computeAmount)
|
||||
m.RegisterCompute("amount_tax", computeAmount)
|
||||
m.RegisterCompute("amount_total", computeAmount)
|
||||
m.RegisterCompute("amount_residual", computeAmount)
|
||||
|
||||
// -- Business Methods: State Transitions --
|
||||
// Mirrors: odoo/addons/account/models/account_move.py action_post(), button_cancel()
|
||||
|
||||
// action_post: draft → posted
|
||||
m.RegisterMethod("action_post", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
for _, id := range rs.IDs() {
|
||||
var state string
|
||||
err := env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT state FROM account_move WHERE id = $1`, id).Scan(&state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if state != "draft" {
|
||||
return nil, fmt.Errorf("account: can only post draft entries (current: %s)", state)
|
||||
}
|
||||
// Check balanced
|
||||
var debitSum, creditSum float64
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(debit),0), COALESCE(SUM(credit),0)
|
||||
FROM account_move_line WHERE move_id = $1`, id).Scan(&debitSum, &creditSum)
|
||||
diff := debitSum - creditSum
|
||||
if diff < -0.005 || diff > 0.005 {
|
||||
return nil, fmt.Errorf("account: cannot post unbalanced entry (debit=%.2f, credit=%.2f)", debitSum, creditSum)
|
||||
}
|
||||
if _, err := env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE account_move SET state = 'posted' WHERE id = $1`, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
// button_cancel: posted → cancel (or draft → cancel)
|
||||
m.RegisterMethod("button_cancel", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
for _, id := range rs.IDs() {
|
||||
if _, err := env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE account_move SET state = 'cancel' WHERE id = $1`, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
// button_draft: cancel → draft
|
||||
m.RegisterMethod("button_draft", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
for _, id := range rs.IDs() {
|
||||
var state string
|
||||
err := env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT state FROM account_move WHERE id = $1`, id).Scan(&state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if state != "cancel" {
|
||||
return nil, fmt.Errorf("account: can only reset cancelled entries to draft (current: %s)", state)
|
||||
}
|
||||
if _, err := env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE account_move SET state = 'draft' WHERE id = $1`, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
// -- Double-Entry Constraint --
|
||||
// SUM(debit) must equal SUM(credit) per journal entry.
|
||||
// Mirrors: odoo/addons/account/models/account_move.py _check_balanced()
|
||||
m.AddConstraint(func(rs *orm.Recordset) error {
|
||||
env := rs.Env()
|
||||
for _, id := range rs.IDs() {
|
||||
var debitSum, creditSum float64
|
||||
err := env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(SUM(debit), 0), COALESCE(SUM(credit), 0)
|
||||
FROM account_move_line WHERE move_id = $1`, id,
|
||||
).Scan(&debitSum, &creditSum)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Allow empty moves (no lines yet)
|
||||
if debitSum == 0 && creditSum == 0 {
|
||||
continue
|
||||
}
|
||||
diff := debitSum - creditSum
|
||||
if diff < -0.005 || diff > 0.005 {
|
||||
return fmt.Errorf("account: journal entry is unbalanced — debit=%.2f credit=%.2f (diff=%.2f)", debitSum, creditSum, diff)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// -- BeforeCreate Hook: Generate sequence number --
|
||||
m.BeforeCreate = func(env *orm.Environment, vals orm.Values) error {
|
||||
name, _ := vals["name"].(string)
|
||||
if name == "" || name == "/" {
|
||||
moveType, _ := vals["move_type"].(string)
|
||||
code := "account.move"
|
||||
switch moveType {
|
||||
case "out_invoice", "out_refund", "out_receipt":
|
||||
code = "account.move.out_invoice"
|
||||
case "in_invoice", "in_refund", "in_receipt":
|
||||
code = "account.move.in_invoice"
|
||||
}
|
||||
seq, err := orm.NextByCode(env, code)
|
||||
if err != nil {
|
||||
// Fallback to generic sequence
|
||||
seq, err = orm.NextByCode(env, "account.move")
|
||||
if err != nil {
|
||||
return nil // No sequence configured, keep "/"
|
||||
}
|
||||
}
|
||||
vals["name"] = seq
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// initAccountMoveLine registers account.move.line — journal items / invoice lines.
|
||||
// Mirrors: odoo/addons/account/models/account_move_line.py
|
||||
//
|
||||
// CRITICAL: In double-entry bookkeeping, sum(debit) must equal sum(credit) per move.
|
||||
func initAccountMoveLine() {
|
||||
m := orm.NewModel("account.move.line", orm.ModelOpts{
|
||||
Description: "Journal Item",
|
||||
Order: "date desc, id",
|
||||
})
|
||||
|
||||
// -- Parent --
|
||||
m.AddFields(
|
||||
orm.Many2one("move_id", "account.move", orm.FieldOpts{
|
||||
String: "Journal Entry", Required: true, OnDelete: orm.OnDeleteCascade, Index: true,
|
||||
}),
|
||||
orm.Char("move_name", orm.FieldOpts{String: "Journal Entry Name", Related: "move_id.name"}),
|
||||
orm.Date("date", orm.FieldOpts{String: "Date", Related: "move_id.date", Store: true, Index: true}),
|
||||
orm.Many2one("journal_id", "account.journal", orm.FieldOpts{String: "Journal", Index: true}),
|
||||
)
|
||||
|
||||
// -- Accounts --
|
||||
m.AddFields(
|
||||
orm.Many2one("account_id", "account.account", orm.FieldOpts{
|
||||
String: "Account", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{String: "Partner", Index: true}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company", Required: true}),
|
||||
orm.Many2one("company_currency_id", "res.currency", orm.FieldOpts{String: "Company Currency"}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency"}),
|
||||
)
|
||||
|
||||
// -- Amounts (Double-Entry) --
|
||||
m.AddFields(
|
||||
orm.Monetary("debit", orm.FieldOpts{String: "Debit", Default: 0.0, CurrencyField: "company_currency_id"}),
|
||||
orm.Monetary("credit", orm.FieldOpts{String: "Credit", Default: 0.0, CurrencyField: "company_currency_id"}),
|
||||
orm.Monetary("balance", orm.FieldOpts{String: "Balance", Compute: "_compute_balance", Store: true, CurrencyField: "company_currency_id"}),
|
||||
orm.Monetary("amount_currency", orm.FieldOpts{String: "Amount in Currency", CurrencyField: "currency_id"}),
|
||||
orm.Float("amount_residual", orm.FieldOpts{String: "Residual Amount"}),
|
||||
orm.Float("amount_residual_currency", orm.FieldOpts{String: "Residual Amount in Currency"}),
|
||||
)
|
||||
|
||||
// -- Invoice line fields --
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Label"}),
|
||||
orm.Float("quantity", orm.FieldOpts{String: "Quantity", Default: 1.0}),
|
||||
orm.Float("price_unit", orm.FieldOpts{String: "Unit Price"}),
|
||||
orm.Float("discount", orm.FieldOpts{String: "Discount (%)"}),
|
||||
orm.Float("price_subtotal", orm.FieldOpts{String: "Subtotal", Compute: "_compute_totals", Store: true}),
|
||||
orm.Float("price_total", orm.FieldOpts{String: "Total", Compute: "_compute_totals", Store: true}),
|
||||
)
|
||||
|
||||
// -- Tax --
|
||||
m.AddFields(
|
||||
orm.Many2many("tax_ids", "account.tax", orm.FieldOpts{String: "Taxes"}),
|
||||
orm.Many2one("tax_line_id", "account.tax", orm.FieldOpts{String: "Originator Tax"}),
|
||||
orm.Many2one("tax_group_id", "account.tax.group", orm.FieldOpts{String: "Tax Group"}),
|
||||
orm.Many2one("tax_repartition_line_id", "account.tax.repartition.line", orm.FieldOpts{
|
||||
String: "Tax Repartition Line",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Display --
|
||||
m.AddFields(
|
||||
orm.Selection("display_type", []orm.SelectionItem{
|
||||
{Value: "product", Label: "Product"},
|
||||
{Value: "cogs", Label: "COGS"},
|
||||
{Value: "tax", Label: "Tax"},
|
||||
{Value: "rounding", Label: "Rounding"},
|
||||
{Value: "payment_term", Label: "Payment Term"},
|
||||
{Value: "line_section", Label: "Section"},
|
||||
{Value: "line_note", Label: "Note"},
|
||||
{Value: "epd", Label: "Early Payment Discount"},
|
||||
}, orm.FieldOpts{String: "Display Type", Default: "product"}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
)
|
||||
|
||||
// -- Reconciliation --
|
||||
m.AddFields(
|
||||
orm.Boolean("reconciled", orm.FieldOpts{String: "Reconciled"}),
|
||||
orm.Many2one("full_reconcile_id", "account.full.reconcile", orm.FieldOpts{String: "Matching"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initAccountPayment registers account.payment.
|
||||
// Mirrors: odoo/addons/account/models/account_payment.py
|
||||
func initAccountPayment() {
|
||||
m := orm.NewModel("account.payment", orm.ModelOpts{
|
||||
Description: "Payments",
|
||||
Order: "date desc, name desc",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Readonly: true}),
|
||||
orm.Many2one("move_id", "account.move", orm.FieldOpts{
|
||||
String: "Journal Entry", Required: true, OnDelete: orm.OnDeleteCascade,
|
||||
}),
|
||||
orm.Selection("payment_type", []orm.SelectionItem{
|
||||
{Value: "outbound", Label: "Send"},
|
||||
{Value: "inbound", Label: "Receive"},
|
||||
}, orm.FieldOpts{String: "Payment Type", Required: true}),
|
||||
orm.Selection("partner_type", []orm.SelectionItem{
|
||||
{Value: "customer", Label: "Customer"},
|
||||
{Value: "supplier", Label: "Vendor"},
|
||||
}, orm.FieldOpts{String: "Partner Type"}),
|
||||
orm.Selection("state", []orm.SelectionItem{
|
||||
{Value: "draft", Label: "Draft"},
|
||||
{Value: "in_process", Label: "In Process"},
|
||||
{Value: "paid", Label: "Paid"},
|
||||
{Value: "canceled", Label: "Cancelled"},
|
||||
{Value: "rejected", Label: "Rejected"},
|
||||
}, orm.FieldOpts{String: "Status", Default: "draft"}),
|
||||
orm.Date("date", orm.FieldOpts{String: "Date", Required: true}),
|
||||
orm.Monetary("amount", orm.FieldOpts{String: "Amount", Required: true, CurrencyField: "currency_id"}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency", Required: true}),
|
||||
orm.Many2one("journal_id", "account.journal", orm.FieldOpts{String: "Journal", Required: true}),
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{String: "Customer/Vendor"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company", Required: true}),
|
||||
orm.Many2one("partner_bank_id", "res.partner.bank", orm.FieldOpts{String: "Recipient Bank Account"}),
|
||||
orm.Many2one("destination_account_id", "account.account", orm.FieldOpts{String: "Destination Account"}),
|
||||
orm.Boolean("is_reconciled", orm.FieldOpts{String: "Is Reconciled"}),
|
||||
orm.Boolean("is_matched", orm.FieldOpts{String: "Is Matched With a Bank Statement"}),
|
||||
orm.Char("payment_reference", orm.FieldOpts{String: "Payment Reference"}),
|
||||
orm.Char("payment_method_code", orm.FieldOpts{String: "Payment Method Code"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initAccountPaymentTerm registers payment terms.
|
||||
// Mirrors: odoo/addons/account/models/account_payment_term.py
|
||||
func initAccountPaymentTerm() {
|
||||
m := orm.NewModel("account.payment.term", orm.ModelOpts{
|
||||
Description: "Payment Terms",
|
||||
Order: "sequence, id",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Payment Terms", Required: true, Translate: true}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Text("note", orm.FieldOpts{String: "Description on the Invoice", Translate: true}),
|
||||
orm.One2many("line_ids", "account.payment.term.line", "payment_id", orm.FieldOpts{String: "Terms"}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
|
||||
orm.Selection("early_discount", []orm.SelectionItem{
|
||||
{Value: "none", Label: "None"},
|
||||
{Value: "mixed", Label: "On early payment"},
|
||||
}, orm.FieldOpts{String: "Early Discount", Default: "none"}),
|
||||
orm.Float("discount_percentage", orm.FieldOpts{String: "Discount %"}),
|
||||
orm.Integer("discount_days", orm.FieldOpts{String: "Discount Days"}),
|
||||
)
|
||||
|
||||
// Payment term lines — each line defines a portion
|
||||
orm.NewModel("account.payment.term.line", orm.ModelOpts{
|
||||
Description: "Payment Terms Line",
|
||||
Order: "sequence, id",
|
||||
}).AddFields(
|
||||
orm.Many2one("payment_id", "account.payment.term", orm.FieldOpts{
|
||||
String: "Payment Terms", Required: true, OnDelete: orm.OnDeleteCascade,
|
||||
}),
|
||||
orm.Selection("value", []orm.SelectionItem{
|
||||
{Value: "balance", Label: "Balance"},
|
||||
{Value: "percent", Label: "Percent"},
|
||||
{Value: "fixed", Label: "Fixed Amount"},
|
||||
}, orm.FieldOpts{String: "Type", Required: true, Default: "balance"}),
|
||||
orm.Float("value_amount", orm.FieldOpts{String: "Value"}),
|
||||
orm.Integer("nb_days", orm.FieldOpts{String: "Days", Required: true, Default: 0}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
)
|
||||
}
|
||||
|
||||
// initAccountReconcile registers reconciliation models.
|
||||
// Mirrors: odoo/addons/account/models/account_reconcile_model.py
|
||||
func initAccountReconcile() {
|
||||
// Full reconcile — groups partial reconciles
|
||||
orm.NewModel("account.full.reconcile", orm.ModelOpts{
|
||||
Description: "Full Reconcile",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
|
||||
orm.One2many("partial_reconcile_ids", "account.partial.reconcile", "full_reconcile_id", orm.FieldOpts{String: "Reconciliation Parts"}),
|
||||
orm.One2many("reconciled_line_ids", "account.move.line", "full_reconcile_id", orm.FieldOpts{String: "Matched Journal Items"}),
|
||||
orm.Many2one("exchange_move_id", "account.move", orm.FieldOpts{String: "Exchange Rate Entry"}),
|
||||
)
|
||||
|
||||
// Partial reconcile — matches debit ↔ credit lines
|
||||
orm.NewModel("account.partial.reconcile", orm.ModelOpts{
|
||||
Description: "Partial Reconcile",
|
||||
}).AddFields(
|
||||
orm.Many2one("debit_move_id", "account.move.line", orm.FieldOpts{String: "Debit line", Required: true, Index: true}),
|
||||
orm.Many2one("credit_move_id", "account.move.line", orm.FieldOpts{String: "Credit line", Required: true, Index: true}),
|
||||
orm.Many2one("full_reconcile_id", "account.full.reconcile", orm.FieldOpts{String: "Full Reconcile"}),
|
||||
orm.Monetary("amount", orm.FieldOpts{String: "Amount", Required: true}),
|
||||
orm.Monetary("debit_amount_currency", orm.FieldOpts{String: "Debit Amount Currency"}),
|
||||
orm.Monetary("credit_amount_currency", orm.FieldOpts{String: "Credit Amount Currency"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
|
||||
orm.Many2one("debit_currency_id", "res.currency", orm.FieldOpts{String: "Debit Currency"}),
|
||||
orm.Many2one("credit_currency_id", "res.currency", orm.FieldOpts{String: "Credit Currency"}),
|
||||
orm.Many2one("exchange_move_id", "account.move", orm.FieldOpts{String: "Exchange Rate Entry"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initAccountBankStatement registers bank statement models.
|
||||
// Mirrors: odoo/addons/account/models/account_bank_statement.py
|
||||
func initAccountBankStatement() {
|
||||
m := orm.NewModel("account.bank.statement", orm.ModelOpts{
|
||||
Description: "Bank Statement",
|
||||
Order: "date desc, name desc, id desc",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Reference"}),
|
||||
orm.Date("date", orm.FieldOpts{String: "Date", Required: true}),
|
||||
orm.Many2one("journal_id", "account.journal", orm.FieldOpts{String: "Journal", Required: true}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company", Required: true}),
|
||||
orm.Float("balance_start", orm.FieldOpts{String: "Starting Balance"}),
|
||||
orm.Float("balance_end_real", orm.FieldOpts{String: "Ending Balance"}),
|
||||
orm.Float("balance_end", orm.FieldOpts{String: "Computed Balance", Compute: "_compute_balance_end"}),
|
||||
orm.One2many("line_ids", "account.bank.statement.line", "statement_id", orm.FieldOpts{String: "Statement Lines"}),
|
||||
)
|
||||
|
||||
// Bank statement line
|
||||
orm.NewModel("account.bank.statement.line", orm.ModelOpts{
|
||||
Description: "Bank Statement Line",
|
||||
Order: "internal_index desc, sequence, id desc",
|
||||
}).AddFields(
|
||||
orm.Many2one("statement_id", "account.bank.statement", orm.FieldOpts{String: "Statement"}),
|
||||
orm.Many2one("move_id", "account.move", orm.FieldOpts{String: "Journal Entry", Required: true}),
|
||||
orm.Many2one("journal_id", "account.journal", orm.FieldOpts{String: "Journal", Required: true}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company", Required: true}),
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{String: "Partner"}),
|
||||
orm.Char("payment_ref", orm.FieldOpts{String: "Label"}),
|
||||
orm.Date("date", orm.FieldOpts{String: "Date", Required: true}),
|
||||
orm.Monetary("amount", orm.FieldOpts{String: "Amount", Required: true, CurrencyField: "currency_id"}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency"}),
|
||||
orm.Char("transaction_type", orm.FieldOpts{String: "Transaction Type"}),
|
||||
orm.Char("account_number", orm.FieldOpts{String: "Bank Account Number"}),
|
||||
orm.Char("partner_name", orm.FieldOpts{String: "Partner Name"}),
|
||||
orm.Char("narration", orm.FieldOpts{String: "Notes"}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence"}),
|
||||
orm.Char("internal_index", orm.FieldOpts{String: "Internal Index"}),
|
||||
orm.Boolean("is_reconciled", orm.FieldOpts{String: "Is Reconciled"}),
|
||||
)
|
||||
}
|
||||
14
addons/account/models/init.go
Normal file
14
addons/account/models/init.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initAccountAccount()
|
||||
initAccountJournal()
|
||||
initAccountTax()
|
||||
initAccountMove()
|
||||
initAccountMoveLine()
|
||||
initAccountPayment()
|
||||
initAccountPaymentTerm()
|
||||
initAccountReconcile()
|
||||
initAccountBankStatement()
|
||||
initAccountFiscalPosition()
|
||||
}
|
||||
22
addons/account/module.go
Normal file
22
addons/account/module.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Package account implements Odoo's accounting module.
|
||||
// Mirrors: odoo/addons/account/__manifest__.py
|
||||
package account
|
||||
|
||||
import (
|
||||
"odoo-go/addons/account/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "account",
|
||||
Description: "Invoicing & Accounting",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Accounting/Accounting",
|
||||
Depends: []string{"base"},
|
||||
Application: true,
|
||||
Installable: true,
|
||||
Sequence: 10,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
131
addons/crm/models/crm.go
Normal file
131
addons/crm/models/crm.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package models
|
||||
|
||||
import "odoo-go/pkg/orm"
|
||||
|
||||
// initCRMLead registers the crm.lead model.
|
||||
// Mirrors: odoo/addons/crm/models/crm_lead.py
|
||||
func initCRMLead() {
|
||||
m := orm.NewModel("crm.lead", orm.ModelOpts{
|
||||
Description: "Lead/Opportunity",
|
||||
Order: "priority desc, id desc",
|
||||
RecName: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Opportunity", Required: true, Index: true}),
|
||||
orm.Selection("type", []orm.SelectionItem{
|
||||
{Value: "lead", Label: "Lead"},
|
||||
{Value: "opportunity", Label: "Opportunity"},
|
||||
}, orm.FieldOpts{String: "Type", Required: true, Default: "lead", Index: true}),
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{String: "Customer", Index: true}),
|
||||
orm.Char("partner_name", orm.FieldOpts{String: "Company Name"}),
|
||||
orm.Char("email_from", orm.FieldOpts{String: "Email", Index: true}),
|
||||
orm.Char("phone", orm.FieldOpts{String: "Phone"}),
|
||||
orm.Char("website", orm.FieldOpts{String: "Website"}),
|
||||
orm.Char("function", orm.FieldOpts{String: "Job Position"}),
|
||||
orm.Selection("state", []orm.SelectionItem{
|
||||
{Value: "open", Label: "Open"},
|
||||
{Value: "won", Label: "Won"},
|
||||
{Value: "lost", Label: "Lost"},
|
||||
}, orm.FieldOpts{String: "Status", Default: "open"}),
|
||||
orm.Many2one("stage_id", "crm.stage", orm.FieldOpts{String: "Stage", Index: true}),
|
||||
orm.Many2one("user_id", "res.users", orm.FieldOpts{String: "Salesperson", Index: true}),
|
||||
orm.Many2one("team_id", "crm.team", orm.FieldOpts{String: "Sales Team", Index: true}),
|
||||
orm.Monetary("expected_revenue", orm.FieldOpts{
|
||||
String: "Expected Revenue", CurrencyField: "currency_id",
|
||||
}),
|
||||
orm.Monetary("recurring_revenue", orm.FieldOpts{
|
||||
String: "Recurring Revenue", CurrencyField: "currency_id",
|
||||
}),
|
||||
orm.Selection("recurring_plan", []orm.SelectionItem{
|
||||
{Value: "monthly", Label: "Monthly"},
|
||||
{Value: "quarterly", Label: "Quarterly"},
|
||||
{Value: "yearly", Label: "Yearly"},
|
||||
}, orm.FieldOpts{String: "Recurring Plan"}),
|
||||
orm.Date("date_deadline", orm.FieldOpts{String: "Expected Closing"}),
|
||||
orm.Datetime("date_last_stage_update", orm.FieldOpts{String: "Last Stage Update"}),
|
||||
orm.Selection("priority", []orm.SelectionItem{
|
||||
{Value: "0", Label: "Normal"},
|
||||
{Value: "1", Label: "Low"},
|
||||
{Value: "2", Label: "High"},
|
||||
{Value: "3", Label: "Very High"},
|
||||
}, orm.FieldOpts{String: "Priority", Default: "0"}),
|
||||
orm.Integer("color", orm.FieldOpts{String: "Color Index"}),
|
||||
orm.Many2many("tag_ids", "crm.tag", orm.FieldOpts{String: "Tags"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency"}),
|
||||
orm.Float("probability", orm.FieldOpts{String: "Probability (%)"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Text("description", orm.FieldOpts{String: "Notes"}),
|
||||
orm.Many2one("lost_reason_id", "crm.lost.reason", orm.FieldOpts{String: "Lost Reason"}),
|
||||
// Address fields
|
||||
orm.Char("city", orm.FieldOpts{String: "City"}),
|
||||
orm.Char("street", orm.FieldOpts{String: "Street"}),
|
||||
orm.Char("zip", orm.FieldOpts{String: "Zip"}),
|
||||
orm.Many2one("country_id", "res.country", orm.FieldOpts{String: "Country"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initCRMStage registers the crm.stage model.
|
||||
// Mirrors: odoo/addons/crm/models/crm_stage.py
|
||||
func initCRMStage() {
|
||||
m := orm.NewModel("crm.stage", orm.ModelOpts{
|
||||
Description: "CRM Stage",
|
||||
Order: "sequence, name, id",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Stage Name", Required: true, Translate: true}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 1}),
|
||||
orm.Boolean("fold", orm.FieldOpts{String: "Folded in Pipeline"}),
|
||||
orm.Boolean("is_won", orm.FieldOpts{String: "Is Won Stage"}),
|
||||
orm.Many2many("team_ids", "crm.team", orm.FieldOpts{String: "Sales Teams"}),
|
||||
orm.Text("requirements", orm.FieldOpts{String: "Requirements"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initCRMTeam registers the crm.team model.
|
||||
// Mirrors: odoo/addons/crm/models/crm_team.py
|
||||
func initCRMTeam() {
|
||||
m := orm.NewModel("crm.team", orm.ModelOpts{
|
||||
Description: "Sales Team",
|
||||
Order: "sequence, name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Sales Team", Required: true, Translate: true}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Index: true,
|
||||
}),
|
||||
orm.Many2many("member_ids", "res.users", orm.FieldOpts{String: "Channel Members"}),
|
||||
orm.Integer("color", orm.FieldOpts{String: "Color Index"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
)
|
||||
}
|
||||
|
||||
// initCRMTag registers the crm.tag model.
|
||||
// Mirrors: odoo/addons/crm/models/crm_lead.py CrmTag
|
||||
func initCRMTag() {
|
||||
orm.NewModel("crm.tag", orm.ModelOpts{
|
||||
Description: "CRM Tag",
|
||||
Order: "name",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Tag Name", Required: true, Translate: true}),
|
||||
orm.Integer("color", orm.FieldOpts{String: "Color Index"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initCRMLostReason registers the crm.lost.reason model.
|
||||
// Mirrors: odoo/addons/crm/models/crm_lost_reason.py
|
||||
func initCRMLostReason() {
|
||||
orm.NewModel("crm.lost.reason", orm.ModelOpts{
|
||||
Description: "Opp. Lost Reason",
|
||||
Order: "name",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Description", Required: true, Translate: true}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
)
|
||||
}
|
||||
9
addons/crm/models/init.go
Normal file
9
addons/crm/models/init.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initCRMTag()
|
||||
initCRMLostReason()
|
||||
initCRMTeam()
|
||||
initCRMStage()
|
||||
initCRMLead()
|
||||
}
|
||||
22
addons/crm/module.go
Normal file
22
addons/crm/module.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Package crm implements Odoo's Customer Relationship Management module.
|
||||
// Mirrors: odoo/addons/crm/__manifest__.py
|
||||
package crm
|
||||
|
||||
import (
|
||||
"odoo-go/addons/crm/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "crm",
|
||||
Description: "CRM",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Sales/CRM",
|
||||
Depends: []string{"base"},
|
||||
Application: true,
|
||||
Installable: true,
|
||||
Sequence: 15,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
222
addons/fleet/models/fleet.go
Normal file
222
addons/fleet/models/fleet.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package models
|
||||
|
||||
import "odoo-go/pkg/orm"
|
||||
|
||||
// initFleetVehicle registers the fleet.vehicle model.
|
||||
// Mirrors: odoo/addons/fleet/models/fleet_vehicle.py
|
||||
func initFleetVehicle() {
|
||||
m := orm.NewModel("fleet.vehicle", orm.ModelOpts{
|
||||
Description: "Vehicle",
|
||||
Order: "license_plate asc, name asc",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Compute: "_compute_vehicle_name", Store: true}),
|
||||
orm.Char("license_plate", orm.FieldOpts{String: "License Plate", Required: true, Index: true}),
|
||||
orm.Char("vin_sn", orm.FieldOpts{String: "Chassis Number", Help: "Unique vehicle identification number (VIN)"}),
|
||||
orm.Many2one("driver_id", "res.partner", orm.FieldOpts{String: "Driver", Index: true}),
|
||||
orm.Many2one("future_driver_id", "res.partner", orm.FieldOpts{String: "Future Driver"}),
|
||||
orm.Many2one("model_id", "fleet.vehicle.model", orm.FieldOpts{
|
||||
String: "Model", Required: true,
|
||||
}),
|
||||
orm.Many2one("brand_id", "fleet.vehicle.model.brand", orm.FieldOpts{
|
||||
String: "Brand", Related: "model_id.brand_id", Store: true,
|
||||
}),
|
||||
orm.Many2one("state_id", "fleet.vehicle.state", orm.FieldOpts{String: "State"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("manager_id", "res.users", orm.FieldOpts{String: "Fleet Manager"}),
|
||||
orm.Char("color", orm.FieldOpts{String: "Color"}),
|
||||
orm.Integer("seats", orm.FieldOpts{String: "Seats Number"}),
|
||||
orm.Integer("doors", orm.FieldOpts{String: "Doors Number", Default: 5}),
|
||||
orm.Selection("transmission", []orm.SelectionItem{
|
||||
{Value: "manual", Label: "Manual"},
|
||||
{Value: "automatic", Label: "Automatic"},
|
||||
}, orm.FieldOpts{String: "Transmission"}),
|
||||
orm.Selection("fuel_type", []orm.SelectionItem{
|
||||
{Value: "gasoline", Label: "Gasoline"},
|
||||
{Value: "diesel", Label: "Diesel"},
|
||||
{Value: "lpg", Label: "LPG"},
|
||||
{Value: "electric", Label: "Electric"},
|
||||
{Value: "hybrid", Label: "Hybrid"},
|
||||
}, orm.FieldOpts{String: "Fuel Type"}),
|
||||
orm.Integer("power", orm.FieldOpts{String: "Power (kW)"}),
|
||||
orm.Integer("horsepower", orm.FieldOpts{String: "Horsepower"}),
|
||||
orm.Float("co2", orm.FieldOpts{String: "CO2 Emissions (g/km)"}),
|
||||
orm.Float("horsepower_tax", orm.FieldOpts{String: "Horsepower Taxation"}),
|
||||
orm.Float("odometer", orm.FieldOpts{String: "Last Odometer", Compute: "_compute_odometer"}),
|
||||
orm.Selection("odometer_unit", []orm.SelectionItem{
|
||||
{Value: "kilometers", Label: "km"},
|
||||
{Value: "miles", Label: "mi"},
|
||||
}, orm.FieldOpts{String: "Odometer Unit", Default: "kilometers", Required: true}),
|
||||
orm.Date("acquisition_date", orm.FieldOpts{String: "Immatriculation Date"}),
|
||||
orm.Date("first_contract_date", orm.FieldOpts{String: "First Contract Date"}),
|
||||
orm.Many2many("tag_ids", "fleet.vehicle.tag", orm.FieldOpts{String: "Tags"}),
|
||||
orm.One2many("log_contracts", "fleet.vehicle.log.contract", "vehicle_id", orm.FieldOpts{String: "Contracts"}),
|
||||
orm.One2many("log_services", "fleet.vehicle.log.services", "vehicle_id", orm.FieldOpts{String: "Services"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
)
|
||||
}
|
||||
|
||||
// initFleetVehicleModel registers the fleet.vehicle.model model.
|
||||
// Mirrors: odoo/addons/fleet/models/fleet_vehicle_model.py
|
||||
func initFleetVehicleModel() {
|
||||
m := orm.NewModel("fleet.vehicle.model", orm.ModelOpts{
|
||||
Description: "Model of a vehicle",
|
||||
Order: "name asc",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Model Name", Required: true}),
|
||||
orm.Many2one("brand_id", "fleet.vehicle.model.brand", orm.FieldOpts{
|
||||
String: "Manufacturer", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("category_id", "fleet.vehicle.model.category", orm.FieldOpts{String: "Category"}),
|
||||
orm.Integer("seats", orm.FieldOpts{String: "Seats Number"}),
|
||||
orm.Integer("doors", orm.FieldOpts{String: "Doors Number", Default: 5}),
|
||||
orm.Char("color", orm.FieldOpts{String: "Color"}),
|
||||
orm.Selection("transmission", []orm.SelectionItem{
|
||||
{Value: "manual", Label: "Manual"},
|
||||
{Value: "automatic", Label: "Automatic"},
|
||||
}, orm.FieldOpts{String: "Transmission"}),
|
||||
orm.Selection("fuel_type", []orm.SelectionItem{
|
||||
{Value: "gasoline", Label: "Gasoline"},
|
||||
{Value: "diesel", Label: "Diesel"},
|
||||
{Value: "lpg", Label: "LPG"},
|
||||
{Value: "electric", Label: "Electric"},
|
||||
{Value: "hybrid", Label: "Hybrid"},
|
||||
}, orm.FieldOpts{String: "Fuel Type"}),
|
||||
orm.Integer("power", orm.FieldOpts{String: "Power (kW)"}),
|
||||
orm.Integer("horsepower", orm.FieldOpts{String: "Horsepower"}),
|
||||
orm.Float("co2", orm.FieldOpts{String: "CO2 Emissions (g/km)"}),
|
||||
orm.Binary("image_128", orm.FieldOpts{String: "Image"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initFleetVehicleModelBrand registers the fleet.vehicle.model.brand model.
|
||||
// Mirrors: odoo/addons/fleet/models/fleet_vehicle_model.py FleetVehicleModelBrand
|
||||
func initFleetVehicleModelBrand() {
|
||||
orm.NewModel("fleet.vehicle.model.brand", orm.ModelOpts{
|
||||
Description: "Brand of the vehicle",
|
||||
Order: "name asc",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Make", Required: true}),
|
||||
orm.Binary("image_128", orm.FieldOpts{String: "Image"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initFleetVehicleModelCategory registers the fleet.vehicle.model.category model.
|
||||
// Mirrors: odoo/addons/fleet/models/fleet_vehicle_model.py FleetVehicleModelCategory
|
||||
func initFleetVehicleModelCategory() {
|
||||
orm.NewModel("fleet.vehicle.model.category", orm.ModelOpts{
|
||||
Description: "Category of the vehicle",
|
||||
Order: "sequence asc, id asc",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Category Name", Required: true}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
)
|
||||
}
|
||||
|
||||
// initFleetVehicleState registers the fleet.vehicle.state model.
|
||||
// Mirrors: odoo/addons/fleet/models/fleet_vehicle.py FleetVehicleState
|
||||
func initFleetVehicleState() {
|
||||
orm.NewModel("fleet.vehicle.state", orm.ModelOpts{
|
||||
Description: "Vehicle State",
|
||||
Order: "sequence asc",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "State Name", Required: true, Translate: true}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
)
|
||||
}
|
||||
|
||||
// initFleetVehicleTag registers the fleet.vehicle.tag model.
|
||||
// Mirrors: odoo/addons/fleet/models/fleet_vehicle.py FleetVehicleTag
|
||||
func initFleetVehicleTag() {
|
||||
orm.NewModel("fleet.vehicle.tag", orm.ModelOpts{
|
||||
Description: "Vehicle Tag",
|
||||
Order: "name",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Tag Name", Required: true, Translate: true}),
|
||||
orm.Integer("color", orm.FieldOpts{String: "Color Index"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initFleetVehicleLogContract registers the fleet.vehicle.log.contract model.
|
||||
// Mirrors: odoo/addons/fleet/models/fleet_vehicle_log_contract.py
|
||||
func initFleetVehicleLogContract() {
|
||||
m := orm.NewModel("fleet.vehicle.log.contract", orm.ModelOpts{
|
||||
Description: "Vehicle Contract",
|
||||
Order: "state desc, expiration_date",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Compute: "_compute_contract_name", Store: true}),
|
||||
orm.Many2one("vehicle_id", "fleet.vehicle", orm.FieldOpts{
|
||||
String: "Vehicle", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("cost_subtype_id", "fleet.service.type", orm.FieldOpts{String: "Type"}),
|
||||
orm.Float("amount", orm.FieldOpts{String: "Recurring Cost"}),
|
||||
orm.Date("date", orm.FieldOpts{String: "Date"}),
|
||||
orm.Date("start_date", orm.FieldOpts{String: "Contract Start Date"}),
|
||||
orm.Date("expiration_date", orm.FieldOpts{String: "Contract Expiration Date"}),
|
||||
orm.Selection("state", []orm.SelectionItem{
|
||||
{Value: "futur", Label: "Incoming"},
|
||||
{Value: "open", Label: "In Progress"},
|
||||
{Value: "expired", Label: "Expired"},
|
||||
{Value: "closed", Label: "Closed"},
|
||||
}, orm.FieldOpts{String: "Status", Default: "open"}),
|
||||
orm.Float("cost_generated", orm.FieldOpts{String: "Recurring Cost Amount"}),
|
||||
orm.Selection("cost_frequency", []orm.SelectionItem{
|
||||
{Value: "no", Label: "No"},
|
||||
{Value: "daily", Label: "Daily"},
|
||||
{Value: "weekly", Label: "Weekly"},
|
||||
{Value: "monthly", Label: "Monthly"},
|
||||
{Value: "yearly", Label: "Yearly"},
|
||||
}, orm.FieldOpts{String: "Recurring Cost Frequency", Default: "monthly"}),
|
||||
orm.Many2one("insurer_id", "res.partner", orm.FieldOpts{String: "Vendor"}),
|
||||
orm.Char("ins_ref", orm.FieldOpts{String: "Reference"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
)
|
||||
}
|
||||
|
||||
// initFleetVehicleLogServices registers the fleet.vehicle.log.services model.
|
||||
// Mirrors: odoo/addons/fleet/models/fleet_vehicle_log_services.py
|
||||
func initFleetVehicleLogServices() {
|
||||
m := orm.NewModel("fleet.vehicle.log.services", orm.ModelOpts{
|
||||
Description: "Vehicle Services Log",
|
||||
Order: "date desc",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Many2one("vehicle_id", "fleet.vehicle", orm.FieldOpts{
|
||||
String: "Vehicle", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("cost_subtype_id", "fleet.service.type", orm.FieldOpts{String: "Type"}),
|
||||
orm.Float("amount", orm.FieldOpts{String: "Cost"}),
|
||||
orm.Date("date", orm.FieldOpts{String: "Date"}),
|
||||
orm.Text("description", orm.FieldOpts{String: "Description"}),
|
||||
orm.Many2one("vendor_id", "res.partner", orm.FieldOpts{String: "Vendor"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// initFleetServiceType registers the fleet.service.type model.
|
||||
// Mirrors: odoo/addons/fleet/models/fleet_service_type.py
|
||||
func initFleetServiceType() {
|
||||
orm.NewModel("fleet.service.type", orm.ModelOpts{
|
||||
Description: "Fleet Service Type",
|
||||
Order: "name",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}),
|
||||
orm.Selection("category", []orm.SelectionItem{
|
||||
{Value: "contract", Label: "Contract"},
|
||||
{Value: "service", Label: "Service"},
|
||||
}, orm.FieldOpts{String: "Category", Required: true}),
|
||||
)
|
||||
}
|
||||
13
addons/fleet/models/init.go
Normal file
13
addons/fleet/models/init.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initFleetVehicleModelBrand()
|
||||
initFleetVehicleModelCategory()
|
||||
initFleetVehicleState()
|
||||
initFleetVehicleTag()
|
||||
initFleetServiceType()
|
||||
initFleetVehicleModel()
|
||||
initFleetVehicle()
|
||||
initFleetVehicleLogContract()
|
||||
initFleetVehicleLogServices()
|
||||
}
|
||||
22
addons/fleet/module.go
Normal file
22
addons/fleet/module.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Package fleet implements Odoo's Fleet Management module.
|
||||
// Mirrors: odoo/addons/fleet/__manifest__.py
|
||||
package fleet
|
||||
|
||||
import (
|
||||
"odoo-go/addons/fleet/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "fleet",
|
||||
Description: "Fleet",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Human Resources/Fleet",
|
||||
Depends: []string{"base"},
|
||||
Application: true,
|
||||
Installable: true,
|
||||
Sequence: 50,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
210
addons/google_address/models/google_address.go
Normal file
210
addons/google_address/models/google_address.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"odoo-go/pkg/orm"
|
||||
"odoo-go/pkg/tools"
|
||||
)
|
||||
|
||||
// Google Maps API client — only initialized if API key is set.
|
||||
var mapsClient *tools.APIClient
|
||||
|
||||
func getClient() *tools.APIClient {
|
||||
if mapsClient != nil {
|
||||
return mapsClient
|
||||
}
|
||||
apiKey := os.Getenv("GOOGLE_MAPS_API_KEY")
|
||||
if apiKey == "" {
|
||||
return nil
|
||||
}
|
||||
mapsClient = tools.NewAPIClient("https://maps.googleapis.com", apiKey)
|
||||
return mapsClient
|
||||
}
|
||||
|
||||
// initGoogleAddress extends res.partner with geocoding fields and methods.
|
||||
func initGoogleAddress() {
|
||||
// Extend res.partner with lat/lng fields
|
||||
partner := orm.Registry.Get("res.partner")
|
||||
if partner != nil {
|
||||
partner.Extend(
|
||||
orm.Float("partner_latitude", orm.FieldOpts{String: "Geo Latitude"}),
|
||||
orm.Float("partner_longitude", orm.FieldOpts{String: "Geo Longitude"}),
|
||||
)
|
||||
|
||||
// geo_localize: Geocode partner address → lat/lng
|
||||
// Calls Google Geocoding API: https://maps.googleapis.com/maps/api/geocode/json
|
||||
partner.RegisterMethod("geo_localize", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
client := getClient()
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("google_address: GOOGLE_MAPS_API_KEY not configured")
|
||||
}
|
||||
|
||||
env := rs.Env()
|
||||
for _, id := range rs.IDs() {
|
||||
// Read address fields
|
||||
var street, city, zip, country string
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(street,''), COALESCE(city,''), COALESCE(zip,''),
|
||||
COALESCE((SELECT code FROM res_country WHERE id = p.country_id), '')
|
||||
FROM res_partner p WHERE p.id = $1`, id,
|
||||
).Scan(&street, &city, &zip, &country)
|
||||
|
||||
address := fmt.Sprintf("%s, %s %s, %s", street, zip, city, country)
|
||||
|
||||
// Call Geocoding API
|
||||
var result GeocodingResponse
|
||||
err := client.GetJSON("/maps/api/geocode/json", map[string]string{
|
||||
"address": address,
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("google_address: geocode failed: %w", err)
|
||||
}
|
||||
|
||||
if result.Status == "OK" && len(result.Results) > 0 {
|
||||
loc := result.Results[0].Geometry.Location
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE res_partner SET partner_latitude = $1, partner_longitude = $2 WHERE id = $3`,
|
||||
loc.Lat, loc.Lng, id)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
// address_autocomplete: Search for addresses via Google Places
|
||||
// Returns suggestions for autocomplete in the UI.
|
||||
partner.RegisterMethod("address_autocomplete", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
client := getClient()
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("google_address: GOOGLE_MAPS_API_KEY not configured")
|
||||
}
|
||||
|
||||
query := ""
|
||||
if len(args) > 0 {
|
||||
query, _ = args[0].(string)
|
||||
}
|
||||
if query == "" {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
var result AutocompleteResponse
|
||||
err := client.GetJSON("/maps/api/place/autocomplete/json", map[string]string{
|
||||
"input": query,
|
||||
"types": "address",
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var suggestions []map[string]interface{}
|
||||
for _, p := range result.Predictions {
|
||||
suggestions = append(suggestions, map[string]interface{}{
|
||||
"description": p.Description,
|
||||
"place_id": p.PlaceID,
|
||||
})
|
||||
}
|
||||
return suggestions, nil
|
||||
})
|
||||
|
||||
// place_details: Get full address from place_id
|
||||
partner.RegisterMethod("place_details", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
client := getClient()
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("google_address: GOOGLE_MAPS_API_KEY not configured")
|
||||
}
|
||||
|
||||
placeID := ""
|
||||
if len(args) > 0 {
|
||||
placeID, _ = args[0].(string)
|
||||
}
|
||||
if placeID == "" {
|
||||
return nil, fmt.Errorf("google_address: place_id required")
|
||||
}
|
||||
|
||||
var result PlaceDetailsResponse
|
||||
err := client.GetJSON("/maps/api/place/details/json", map[string]string{
|
||||
"place_id": placeID,
|
||||
"fields": "address_components,geometry,formatted_address",
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.Status != "OK" {
|
||||
return nil, fmt.Errorf("google_address: place details failed: %s", result.Status)
|
||||
}
|
||||
|
||||
// Parse address components
|
||||
address := map[string]interface{}{
|
||||
"formatted_address": result.Result.FormattedAddress,
|
||||
"latitude": result.Result.Geometry.Location.Lat,
|
||||
"longitude": result.Result.Geometry.Location.Lng,
|
||||
}
|
||||
|
||||
for _, comp := range result.Result.AddressComponents {
|
||||
for _, t := range comp.Types {
|
||||
switch t {
|
||||
case "street_number":
|
||||
address["street_number"] = comp.LongName
|
||||
case "route":
|
||||
address["street"] = comp.LongName
|
||||
case "locality":
|
||||
address["city"] = comp.LongName
|
||||
case "postal_code":
|
||||
address["zip"] = comp.LongName
|
||||
case "country":
|
||||
address["country_code"] = comp.ShortName
|
||||
address["country"] = comp.LongName
|
||||
case "administrative_area_level_1":
|
||||
address["state"] = comp.LongName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return address, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Google API Response Types ---
|
||||
|
||||
type GeocodingResponse struct {
|
||||
Status string `json:"status"`
|
||||
Results []struct {
|
||||
FormattedAddress string `json:"formatted_address"`
|
||||
Geometry struct {
|
||||
Location LatLng `json:"location"`
|
||||
} `json:"geometry"`
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
type LatLng struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lng float64 `json:"lng"`
|
||||
}
|
||||
|
||||
type AutocompleteResponse struct {
|
||||
Status string `json:"status"`
|
||||
Predictions []struct {
|
||||
Description string `json:"description"`
|
||||
PlaceID string `json:"place_id"`
|
||||
} `json:"predictions"`
|
||||
}
|
||||
|
||||
type PlaceDetailsResponse struct {
|
||||
Status string `json:"status"`
|
||||
Result struct {
|
||||
FormattedAddress string `json:"formatted_address"`
|
||||
Geometry struct {
|
||||
Location LatLng `json:"location"`
|
||||
} `json:"geometry"`
|
||||
AddressComponents []AddressComponent `json:"address_components"`
|
||||
} `json:"result"`
|
||||
}
|
||||
|
||||
type AddressComponent struct {
|
||||
LongName string `json:"long_name"`
|
||||
ShortName string `json:"short_name"`
|
||||
Types []string `json:"types"`
|
||||
}
|
||||
5
addons/google_address/models/init.go
Normal file
5
addons/google_address/models/init.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initGoogleAddress()
|
||||
}
|
||||
28
addons/google_address/module.go
Normal file
28
addons/google_address/module.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Package google_address provides Google Maps/Places integration.
|
||||
// OPT-IN: Only active when GOOGLE_MAPS_API_KEY is configured.
|
||||
//
|
||||
// Features:
|
||||
// - Address autocomplete (Google Places API)
|
||||
// - Geocoding (address → lat/lng)
|
||||
// - Reverse geocoding (lat/lng → address)
|
||||
// - Distance calculation between partners
|
||||
package google_address
|
||||
|
||||
import (
|
||||
"odoo-go/addons/google_address/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "google_address",
|
||||
Description: "Google Maps Address Integration",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Integration",
|
||||
Depends: []string{"base"},
|
||||
Application: false,
|
||||
Installable: true,
|
||||
Sequence: 100,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
227
addons/google_calendar/models/google_calendar.go
Normal file
227
addons/google_calendar/models/google_calendar.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"odoo-go/pkg/orm"
|
||||
"odoo-go/pkg/tools"
|
||||
)
|
||||
|
||||
var calendarClient *tools.APIClient
|
||||
|
||||
func getCalendarClient() *tools.APIClient {
|
||||
if calendarClient != nil {
|
||||
return calendarClient
|
||||
}
|
||||
apiKey := os.Getenv("GOOGLE_CALENDAR_API_KEY")
|
||||
if apiKey == "" {
|
||||
return nil
|
||||
}
|
||||
calendarClient = tools.NewAPIClient("https://www.googleapis.com", apiKey)
|
||||
return calendarClient
|
||||
}
|
||||
|
||||
// initCalendarEvent registers the calendar.event model.
|
||||
// Mirrors: odoo/addons/calendar/models/calendar_event.py
|
||||
func initCalendarEvent() {
|
||||
m := orm.NewModel("calendar.event", orm.ModelOpts{
|
||||
Description: "Calendar Event",
|
||||
Order: "start desc",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Meeting Subject", Required: true}),
|
||||
orm.Datetime("start", orm.FieldOpts{String: "Start", Required: true}),
|
||||
orm.Datetime("stop", orm.FieldOpts{String: "Stop", Required: true}),
|
||||
orm.Boolean("allday", orm.FieldOpts{String: "All Day"}),
|
||||
orm.Text("description", orm.FieldOpts{String: "Description"}),
|
||||
orm.Char("location", orm.FieldOpts{String: "Location"}),
|
||||
orm.Many2one("user_id", "res.users", orm.FieldOpts{String: "Organizer"}),
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{String: "Contact"}),
|
||||
orm.Many2many("attendee_ids", "res.partner", orm.FieldOpts{String: "Attendees"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Selection("state", []orm.SelectionItem{
|
||||
{Value: "draft", Label: "Unconfirmed"},
|
||||
{Value: "open", Label: "Confirmed"},
|
||||
{Value: "done", Label: "Done"},
|
||||
{Value: "cancel", Label: "Cancelled"},
|
||||
}, orm.FieldOpts{String: "Status", Default: "draft"}),
|
||||
// Google sync fields
|
||||
orm.Char("google_event_id", orm.FieldOpts{String: "Google Event ID", Index: true}),
|
||||
orm.Char("google_calendar_id", orm.FieldOpts{String: "Google Calendar ID"}),
|
||||
orm.Datetime("google_synced_at", orm.FieldOpts{String: "Last Synced"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initGoogleCalendarSync registers sync methods.
|
||||
func initGoogleCalendarSync() {
|
||||
event := orm.Registry.Get("calendar.event")
|
||||
if event == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// push_to_google: Create/update event in Google Calendar
|
||||
event.RegisterMethod("push_to_google", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
client := getCalendarClient()
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("google_calendar: GOOGLE_CALENDAR_API_KEY not configured")
|
||||
}
|
||||
|
||||
env := rs.Env()
|
||||
calendarID := "primary"
|
||||
if len(args) > 0 {
|
||||
if cid, ok := args[0].(string); ok && cid != "" {
|
||||
calendarID = cid
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range rs.IDs() {
|
||||
var name, description, location, googleEventID string
|
||||
var start, stop string
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COALESCE(name,''), COALESCE(description,''), COALESCE(location,''),
|
||||
COALESCE(google_event_id,''), COALESCE(start::text,''), COALESCE(stop::text,'')
|
||||
FROM calendar_event WHERE id = $1`, id,
|
||||
).Scan(&name, &description, &location, &googleEventID, &start, &stop)
|
||||
|
||||
eventBody := map[string]interface{}{
|
||||
"summary": name,
|
||||
"description": description,
|
||||
"location": location,
|
||||
"start": map[string]string{
|
||||
"dateTime": start,
|
||||
"timeZone": "Europe/Berlin",
|
||||
},
|
||||
"end": map[string]string{
|
||||
"dateTime": stop,
|
||||
"timeZone": "Europe/Berlin",
|
||||
},
|
||||
}
|
||||
|
||||
if googleEventID != "" {
|
||||
// Update existing
|
||||
var result map[string]interface{}
|
||||
err := client.PostJSON(
|
||||
fmt.Sprintf("/calendar/v3/calendars/%s/events/%s", calendarID, googleEventID),
|
||||
nil, eventBody, &result,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("google_calendar: update event %d: %w", id, err)
|
||||
}
|
||||
} else {
|
||||
// Create new
|
||||
var result struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
err := client.PostJSON(
|
||||
fmt.Sprintf("/calendar/v3/calendars/%s/events", calendarID),
|
||||
nil, eventBody, &result,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("google_calendar: create event %d: %w", id, err)
|
||||
}
|
||||
// Store Google event ID
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE calendar_event SET google_event_id = $1, google_synced_at = NOW() WHERE id = $2`,
|
||||
result.ID, id)
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
// pull_from_google: Fetch events from Google Calendar
|
||||
event.RegisterMethod("pull_from_google", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
client := getCalendarClient()
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("google_calendar: GOOGLE_CALENDAR_API_KEY not configured")
|
||||
}
|
||||
|
||||
calendarID := "primary"
|
||||
if len(args) > 0 {
|
||||
if cid, ok := args[0].(string); ok && cid != "" {
|
||||
calendarID = cid
|
||||
}
|
||||
}
|
||||
|
||||
var result GoogleEventsResponse
|
||||
err := client.GetJSON(
|
||||
fmt.Sprintf("/calendar/v3/calendars/%s/events", calendarID),
|
||||
map[string]string{
|
||||
"maxResults": "50",
|
||||
"singleEvents": "true",
|
||||
"orderBy": "startTime",
|
||||
"timeMin": "2026-01-01T00:00:00Z",
|
||||
},
|
||||
&result,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("google_calendar: fetch events: %w", err)
|
||||
}
|
||||
|
||||
env := rs.Env()
|
||||
imported := 0
|
||||
for _, ge := range result.Items {
|
||||
// Check if already synced
|
||||
var existing int
|
||||
env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT COUNT(*) FROM calendar_event WHERE google_event_id = $1`, ge.ID,
|
||||
).Scan(&existing)
|
||||
|
||||
if existing > 0 {
|
||||
continue // Already imported
|
||||
}
|
||||
|
||||
startTime := ge.Start.DateTime
|
||||
if startTime == "" {
|
||||
startTime = ge.Start.Date
|
||||
}
|
||||
endTime := ge.End.DateTime
|
||||
if endTime == "" {
|
||||
endTime = ge.End.Date
|
||||
}
|
||||
|
||||
eventRS := env.Model("calendar.event")
|
||||
_, err := eventRS.Create(orm.Values{
|
||||
"name": ge.Summary,
|
||||
"description": ge.Description,
|
||||
"location": ge.Location,
|
||||
"start": startTime,
|
||||
"stop": endTime,
|
||||
"google_event_id": ge.ID,
|
||||
"google_synced_at": "now",
|
||||
"state": "open",
|
||||
})
|
||||
if err == nil {
|
||||
imported++
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"imported": imported,
|
||||
"total": len(result.Items),
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// --- Google Calendar API Response Types ---
|
||||
|
||||
type GoogleEventsResponse struct {
|
||||
Items []GoogleEvent `json:"items"`
|
||||
}
|
||||
|
||||
type GoogleEvent struct {
|
||||
ID string `json:"id"`
|
||||
Summary string `json:"summary"`
|
||||
Description string `json:"description"`
|
||||
Location string `json:"location"`
|
||||
Start struct {
|
||||
DateTime string `json:"dateTime"`
|
||||
Date string `json:"date"`
|
||||
} `json:"start"`
|
||||
End struct {
|
||||
DateTime string `json:"dateTime"`
|
||||
Date string `json:"date"`
|
||||
} `json:"end"`
|
||||
}
|
||||
6
addons/google_calendar/models/init.go
Normal file
6
addons/google_calendar/models/init.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initCalendarEvent()
|
||||
initGoogleCalendarSync()
|
||||
}
|
||||
27
addons/google_calendar/module.go
Normal file
27
addons/google_calendar/module.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Package google_calendar provides Google Calendar sync integration.
|
||||
// OPT-IN: Only active when GOOGLE_CALENDAR_API_KEY is configured.
|
||||
//
|
||||
// Features:
|
||||
// - Sync events between Odoo and Google Calendar
|
||||
// - Create Google Calendar events from project tasks
|
||||
// - Import Google Calendar events as activities
|
||||
package google_calendar
|
||||
|
||||
import (
|
||||
"odoo-go/addons/google_calendar/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "google_calendar",
|
||||
Description: "Google Calendar Sync",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Integration",
|
||||
Depends: []string{"base"},
|
||||
Application: false,
|
||||
Installable: true,
|
||||
Sequence: 100,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
221
addons/google_translate/models/google_translate.go
Normal file
221
addons/google_translate/models/google_translate.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"odoo-go/pkg/orm"
|
||||
"odoo-go/pkg/tools"
|
||||
)
|
||||
|
||||
var translateClient *tools.APIClient
|
||||
|
||||
func getTranslateClient() *tools.APIClient {
|
||||
if translateClient != nil {
|
||||
return translateClient
|
||||
}
|
||||
apiKey := os.Getenv("GOOGLE_TRANSLATE_API_KEY")
|
||||
if apiKey == "" {
|
||||
return nil
|
||||
}
|
||||
translateClient = tools.NewAPIClient("https://translation.googleapis.com", apiKey)
|
||||
return translateClient
|
||||
}
|
||||
|
||||
func initGoogleTranslate() {
|
||||
// Register a translation model for storing translations + providing RPC methods
|
||||
m := orm.NewModel("google.translate", orm.ModelOpts{
|
||||
Description: "Google Translation Service",
|
||||
Type: orm.ModelTransient, // No persistent table needed
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Text("source_text", orm.FieldOpts{String: "Source Text"}),
|
||||
orm.Char("source_lang", orm.FieldOpts{String: "Source Language", Default: "auto"}),
|
||||
orm.Char("target_lang", orm.FieldOpts{String: "Target Language", Default: "de"}),
|
||||
orm.Text("translated_text", orm.FieldOpts{String: "Translated Text"}),
|
||||
)
|
||||
|
||||
// translate: Translate text from one language to another
|
||||
// Usage via RPC: call_kw("google.translate", "translate", [args])
|
||||
m.RegisterMethod("translate", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
client := getTranslateClient()
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("google_translate: GOOGLE_TRANSLATE_API_KEY not configured")
|
||||
}
|
||||
|
||||
text := ""
|
||||
targetLang := "de"
|
||||
sourceLang := ""
|
||||
|
||||
if len(args) > 0 {
|
||||
text, _ = args[0].(string)
|
||||
}
|
||||
if len(args) > 1 {
|
||||
targetLang, _ = args[1].(string)
|
||||
}
|
||||
if len(args) > 2 {
|
||||
sourceLang, _ = args[2].(string)
|
||||
}
|
||||
|
||||
if text == "" {
|
||||
return nil, fmt.Errorf("google_translate: no text provided")
|
||||
}
|
||||
|
||||
params := map[string]string{
|
||||
"q": text,
|
||||
"target": targetLang,
|
||||
"format": "text",
|
||||
}
|
||||
if sourceLang != "" && sourceLang != "auto" {
|
||||
params["source"] = sourceLang
|
||||
}
|
||||
|
||||
var result TranslateResponse
|
||||
err := client.GetJSON("/language/translate/v2", params, &result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("google_translate: API error: %w", err)
|
||||
}
|
||||
|
||||
if len(result.Data.Translations) == 0 {
|
||||
return nil, fmt.Errorf("google_translate: no translation returned")
|
||||
}
|
||||
|
||||
t := result.Data.Translations[0]
|
||||
return map[string]interface{}{
|
||||
"translated_text": t.TranslatedText,
|
||||
"detected_source": t.DetectedSourceLanguage,
|
||||
}, nil
|
||||
})
|
||||
|
||||
// translate_batch: Translate multiple texts at once
|
||||
m.RegisterMethod("translate_batch", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
client := getTranslateClient()
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("google_translate: GOOGLE_TRANSLATE_API_KEY not configured")
|
||||
}
|
||||
|
||||
texts, ok := args[0].([]interface{})
|
||||
if !ok || len(texts) == 0 {
|
||||
return nil, fmt.Errorf("google_translate: texts array required")
|
||||
}
|
||||
targetLang := "de"
|
||||
if len(args) > 1 {
|
||||
targetLang, _ = args[1].(string)
|
||||
}
|
||||
|
||||
var results []map[string]interface{}
|
||||
for _, t := range texts {
|
||||
text, _ := t.(string)
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var result TranslateResponse
|
||||
err := client.GetJSON("/language/translate/v2", map[string]string{
|
||||
"q": text,
|
||||
"target": targetLang,
|
||||
"format": "text",
|
||||
}, &result)
|
||||
|
||||
if err != nil || len(result.Data.Translations) == 0 {
|
||||
results = append(results, map[string]interface{}{
|
||||
"source": text, "translated": text, "error": fmt.Sprintf("%v", err),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, map[string]interface{}{
|
||||
"source": text,
|
||||
"translated": result.Data.Translations[0].TranslatedText,
|
||||
"detected": result.Data.Translations[0].DetectedSourceLanguage,
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
})
|
||||
|
||||
// detect_language: Detect the language of a text
|
||||
m.RegisterMethod("detect_language", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
client := getTranslateClient()
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("google_translate: GOOGLE_TRANSLATE_API_KEY not configured")
|
||||
}
|
||||
|
||||
text := ""
|
||||
if len(args) > 0 {
|
||||
text, _ = args[0].(string)
|
||||
}
|
||||
|
||||
var result DetectResponse
|
||||
err := client.GetJSON("/language/translate/v2/detect", map[string]string{
|
||||
"q": text,
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(result.Data.Detections) > 0 && len(result.Data.Detections[0]) > 0 {
|
||||
d := result.Data.Detections[0][0]
|
||||
return map[string]interface{}{
|
||||
"language": d.Language,
|
||||
"confidence": d.Confidence,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("google_translate: language detection failed")
|
||||
})
|
||||
|
||||
// supported_languages: List supported languages
|
||||
m.RegisterMethod("supported_languages", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
client := getTranslateClient()
|
||||
if client == nil {
|
||||
return nil, fmt.Errorf("google_translate: GOOGLE_TRANSLATE_API_KEY not configured")
|
||||
}
|
||||
|
||||
var result LanguagesResponse
|
||||
err := client.GetJSON("/language/translate/v2/languages", map[string]string{
|
||||
"target": "de",
|
||||
}, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var langs []map[string]string
|
||||
for _, l := range result.Data.Languages {
|
||||
langs = append(langs, map[string]string{
|
||||
"code": l.Language,
|
||||
"name": l.Name,
|
||||
})
|
||||
}
|
||||
return langs, nil
|
||||
})
|
||||
}
|
||||
|
||||
// --- Google Translate API Response Types ---
|
||||
|
||||
type TranslateResponse struct {
|
||||
Data struct {
|
||||
Translations []struct {
|
||||
TranslatedText string `json:"translatedText"`
|
||||
DetectedSourceLanguage string `json:"detectedSourceLanguage"`
|
||||
} `json:"translations"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type DetectResponse struct {
|
||||
Data struct {
|
||||
Detections [][]struct {
|
||||
Language string `json:"language"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
} `json:"detections"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type LanguagesResponse struct {
|
||||
Data struct {
|
||||
Languages []struct {
|
||||
Language string `json:"language"`
|
||||
Name string `json:"name"`
|
||||
} `json:"languages"`
|
||||
} `json:"data"`
|
||||
}
|
||||
5
addons/google_translate/models/init.go
Normal file
5
addons/google_translate/models/init.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initGoogleTranslate()
|
||||
}
|
||||
27
addons/google_translate/module.go
Normal file
27
addons/google_translate/module.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Package google_translate provides Google Cloud Translation integration.
|
||||
// OPT-IN: Only active when GOOGLE_TRANSLATE_API_KEY is configured.
|
||||
//
|
||||
// Features:
|
||||
// - Translate any text field on any record
|
||||
// - Auto-detect source language
|
||||
// - Batch translation support
|
||||
package google_translate
|
||||
|
||||
import (
|
||||
"odoo-go/addons/google_translate/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "google_translate",
|
||||
Description: "Google Cloud Translation",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Integration",
|
||||
Depends: []string{"base"},
|
||||
Application: false,
|
||||
Installable: true,
|
||||
Sequence: 100,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
145
addons/hr/models/hr.go
Normal file
145
addons/hr/models/hr.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package models
|
||||
|
||||
import "odoo-go/pkg/orm"
|
||||
|
||||
// initResourceCalendar registers resource.calendar — working schedules.
|
||||
// Mirrors: odoo/addons/resource/models/resource.py
|
||||
func initResourceCalendar() {
|
||||
m := orm.NewModel("resource.calendar", orm.ModelOpts{
|
||||
Description: "Resource Working Time",
|
||||
Order: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company"}),
|
||||
orm.Float("hours_per_week", orm.FieldOpts{String: "Hours per Week"}),
|
||||
orm.Char("tz", orm.FieldOpts{String: "Timezone", Default: "Europe/Berlin"}),
|
||||
orm.Boolean("flexible_hours", orm.FieldOpts{String: "Flexible Hours"}),
|
||||
orm.One2many("attendance_ids", "resource.calendar.attendance", "calendar_id", orm.FieldOpts{String: "Attendances"}),
|
||||
)
|
||||
|
||||
// resource.calendar.attendance — work time slots
|
||||
orm.NewModel("resource.calendar.attendance", orm.ModelOpts{
|
||||
Description: "Work Detail",
|
||||
Order: "dayofweek, hour_from",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
|
||||
orm.Selection("dayofweek", []orm.SelectionItem{
|
||||
{Value: "0", Label: "Monday"}, {Value: "1", Label: "Tuesday"},
|
||||
{Value: "2", Label: "Wednesday"}, {Value: "3", Label: "Thursday"},
|
||||
{Value: "4", Label: "Friday"}, {Value: "5", Label: "Saturday"},
|
||||
{Value: "6", Label: "Sunday"},
|
||||
}, orm.FieldOpts{String: "Day of Week", Required: true}),
|
||||
orm.Float("hour_from", orm.FieldOpts{String: "Work from", Required: true}),
|
||||
orm.Float("hour_to", orm.FieldOpts{String: "Work to", Required: true}),
|
||||
orm.Many2one("calendar_id", "resource.calendar", orm.FieldOpts{
|
||||
String: "Calendar", Required: true, OnDelete: orm.OnDeleteCascade,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// initHREmployee registers the hr.employee model.
|
||||
// Mirrors: odoo/addons/hr/models/hr_employee.py
|
||||
func initHREmployee() {
|
||||
m := orm.NewModel("hr.employee", orm.ModelOpts{
|
||||
Description: "Employee",
|
||||
Order: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Employee Name", Required: true, Index: true}),
|
||||
orm.Many2one("user_id", "res.users", orm.FieldOpts{String: "Related User"}),
|
||||
orm.Many2one("department_id", "hr.department", orm.FieldOpts{String: "Department", Index: true}),
|
||||
orm.Many2one("job_id", "hr.job", orm.FieldOpts{String: "Job Position"}),
|
||||
orm.Char("job_title", orm.FieldOpts{String: "Job Title"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("address_id", "res.partner", orm.FieldOpts{String: "Work Address"}),
|
||||
orm.Char("work_email", orm.FieldOpts{String: "Work Email"}),
|
||||
orm.Char("work_phone", orm.FieldOpts{String: "Work Phone"}),
|
||||
orm.Char("mobile_phone", orm.FieldOpts{String: "Work Mobile"}),
|
||||
orm.Many2one("coach_id", "hr.employee", orm.FieldOpts{String: "Coach"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Many2one("resource_calendar_id", "resource.calendar", orm.FieldOpts{String: "Working Schedule"}),
|
||||
orm.Selection("gender", []orm.SelectionItem{
|
||||
{Value: "male", Label: "Male"},
|
||||
{Value: "female", Label: "Female"},
|
||||
{Value: "other", Label: "Other"},
|
||||
}, orm.FieldOpts{String: "Gender"}),
|
||||
orm.Date("birthday", orm.FieldOpts{String: "Date of Birth", Groups: "hr.group_hr_user"}),
|
||||
orm.Selection("marital", []orm.SelectionItem{
|
||||
{Value: "single", Label: "Single"},
|
||||
{Value: "married", Label: "Married"},
|
||||
{Value: "cohabitant", Label: "Legal Cohabitant"},
|
||||
{Value: "widower", Label: "Widower"},
|
||||
{Value: "divorced", Label: "Divorced"},
|
||||
}, orm.FieldOpts{String: "Marital Status", Default: "single"}),
|
||||
orm.Char("emergency_contact", orm.FieldOpts{String: "Emergency Contact"}),
|
||||
orm.Char("emergency_phone", orm.FieldOpts{String: "Emergency Phone"}),
|
||||
orm.Selection("certificate", []orm.SelectionItem{
|
||||
{Value: "graduate", Label: "Graduate"},
|
||||
{Value: "bachelor", Label: "Bachelor"},
|
||||
{Value: "master", Label: "Master"},
|
||||
{Value: "doctor", Label: "Doctor"},
|
||||
{Value: "other", Label: "Other"},
|
||||
}, orm.FieldOpts{String: "Certificate Level"}),
|
||||
orm.Char("study_field", orm.FieldOpts{String: "Field of Study"}),
|
||||
orm.Char("visa_no", orm.FieldOpts{String: "Visa No", Groups: "hr.group_hr_user"}),
|
||||
orm.Char("permit_no", orm.FieldOpts{String: "Work Permit No", Groups: "hr.group_hr_user"}),
|
||||
orm.Integer("km_home_work", orm.FieldOpts{String: "Home-Work Distance (km)"}),
|
||||
orm.Binary("image_1920", orm.FieldOpts{String: "Image"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initHRDepartment registers the hr.department model.
|
||||
// Mirrors: odoo/addons/hr/models/hr_department.py
|
||||
func initHRDepartment() {
|
||||
m := orm.NewModel("hr.department", orm.ModelOpts{
|
||||
Description: "HR Department",
|
||||
Order: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Department Name", Required: true, Translate: true}),
|
||||
orm.Char("complete_name", orm.FieldOpts{
|
||||
String: "Complete Name",
|
||||
Compute: "_compute_complete_name",
|
||||
Store: true,
|
||||
}),
|
||||
orm.Many2one("parent_id", "hr.department", orm.FieldOpts{String: "Parent Department", Index: true}),
|
||||
orm.Many2one("manager_id", "hr.employee", orm.FieldOpts{String: "Manager"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.One2many("child_ids", "hr.department", "parent_id", orm.FieldOpts{String: "Child Departments"}),
|
||||
orm.One2many("member_ids", "hr.employee", "department_id", orm.FieldOpts{String: "Members"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initHRJob registers the hr.job model.
|
||||
// Mirrors: odoo/addons/hr/models/hr_job.py
|
||||
func initHRJob() {
|
||||
m := orm.NewModel("hr.job", orm.ModelOpts{
|
||||
Description: "Job Position",
|
||||
Order: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Job Position", Required: true, Index: true, Translate: true}),
|
||||
orm.Many2one("department_id", "hr.department", orm.FieldOpts{String: "Department"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
orm.Integer("expected_employees", orm.FieldOpts{String: "Expected New Employees", Default: 1}),
|
||||
orm.Integer("no_of_hired_employee", orm.FieldOpts{String: "Hired Employees"}),
|
||||
orm.Selection("state", []orm.SelectionItem{
|
||||
{Value: "recruit", Label: "Recruitment in Progress"},
|
||||
{Value: "open", Label: "Not Recruiting"},
|
||||
}, orm.FieldOpts{String: "Status", Required: true, Default: "recruit"}),
|
||||
orm.Text("description", orm.FieldOpts{String: "Job Description"}),
|
||||
)
|
||||
}
|
||||
8
addons/hr/models/init.go
Normal file
8
addons/hr/models/init.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initResourceCalendar()
|
||||
initHREmployee()
|
||||
initHRDepartment()
|
||||
initHRJob()
|
||||
}
|
||||
22
addons/hr/module.go
Normal file
22
addons/hr/module.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Package hr implements Odoo's Human Resources module.
|
||||
// Mirrors: odoo/addons/hr/__manifest__.py
|
||||
package hr
|
||||
|
||||
import (
|
||||
"odoo-go/addons/hr/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "hr",
|
||||
Description: "Employees",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Human Resources/Employees",
|
||||
Depends: []string{"base"},
|
||||
Application: true,
|
||||
Installable: true,
|
||||
Sequence: 20,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
164
addons/l10n_de/data.go
Normal file
164
addons/l10n_de/data.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// Package l10n_de provides German chart of accounts seed data.
|
||||
package l10n_de
|
||||
|
||||
// SKR03Account defines a single account in the SKR03 chart.
|
||||
type SKR03Account struct {
|
||||
Code string
|
||||
Name string
|
||||
AccountType string
|
||||
Reconcile bool
|
||||
}
|
||||
|
||||
// SKR03Accounts returns the core SKR03 chart of accounts.
|
||||
// Mirrors: odoo/addons/l10n_de/data/template/account.account-de_skr03.csv
|
||||
// This is a subset of the most commonly used accounts.
|
||||
var SKR03Accounts = []SKR03Account{
|
||||
// 0xxx — Anlagevermögen (Fixed Assets)
|
||||
{"0027", "EDV-Software", "asset_non_current", false},
|
||||
{"0200", "Grundstücke und Bauten", "asset_fixed", false},
|
||||
{"0320", "Maschinen", "asset_fixed", false},
|
||||
{"0400", "Technische Anlagen", "asset_fixed", false},
|
||||
{"0420", "Büroeinrichtung", "asset_fixed", false},
|
||||
{"0440", "Werkzeuge", "asset_fixed", false},
|
||||
{"0480", "Geringwertige Wirtschaftsgüter", "asset_fixed", false},
|
||||
{"0520", "Fuhrpark", "asset_fixed", false},
|
||||
{"0540", "Geschäftsbauten", "asset_fixed", false},
|
||||
|
||||
// 1xxx — Finanz- und Privatkonten
|
||||
{"1000", "Kasse", "asset_cash", false},
|
||||
{"1200", "Bank", "asset_cash", false},
|
||||
{"1210", "Postbank", "asset_cash", false},
|
||||
{"1400", "Forderungen aus Lieferungen und Leistungen", "asset_receivable", true},
|
||||
{"1410", "Forderungen aus L+L (Debitor)", "asset_receivable", true},
|
||||
{"1450", "Forderungen nach §11 EStG", "asset_receivable", true},
|
||||
{"1500", "Sonstige Vermögensgegenstände", "asset_current", false},
|
||||
{"1518", "Forderungen gegenüber Gesellschaftern", "asset_current", false},
|
||||
{"1548", "Vorsteuer laufendes Jahr", "asset_current", false},
|
||||
{"1570", "Vorsteuer 7%", "asset_current", false},
|
||||
{"1576", "Vorsteuer 19%", "asset_current", false},
|
||||
{"1580", "Vorsteuer aus innergemeinschaftlichem Erwerb", "asset_current", false},
|
||||
{"1588", "Vorsteuer im Folgejahr abziehbar", "asset_current", false},
|
||||
{"1590", "Durchlaufende Posten", "asset_current", false},
|
||||
{"1600", "Verbindlichkeiten aus Lieferungen und Leistungen", "liability_payable", true},
|
||||
{"1610", "Verbindlichkeiten aus L+L (Kreditor)", "liability_payable", true},
|
||||
{"1700", "Sonstige Verbindlichkeiten", "liability_current", false},
|
||||
{"1710", "Erhaltene Anzahlungen", "liability_current", false},
|
||||
{"1740", "Verbindlichkeiten aus Lohn und Gehalt", "liability_current", false},
|
||||
{"1741", "Verbindlichkeiten Sozialversicherung", "liability_current", false},
|
||||
{"1750", "Verbindlichkeiten Lohnsteuer", "liability_current", false},
|
||||
{"1770", "Umsatzsteuer 7%", "liability_current", false},
|
||||
{"1776", "Umsatzsteuer 19%", "liability_current", false},
|
||||
{"1780", "Umsatzsteuer-Vorauszahlungen", "liability_current", false},
|
||||
{"1790", "Umsatzsteuer laufendes Jahr", "liability_current", false},
|
||||
{"1791", "Umsatzsteuer Vorjahr", "liability_current", false},
|
||||
|
||||
// 2xxx — Abgrenzungskonten
|
||||
{"2000", "Aufwendungen für Roh-, Hilfs- und Betriebsstoffe", "expense_direct_cost", false},
|
||||
|
||||
// 3xxx — Wareneingang (Purchasing)
|
||||
{"3000", "Roh-, Hilfs- und Betriebsstoffe", "expense_direct_cost", false},
|
||||
{"3100", "Fremdleistungen", "expense_direct_cost", false},
|
||||
{"3200", "Wareneingang", "expense_direct_cost", false},
|
||||
{"3300", "Wareneingang 7% Vorsteuer", "expense_direct_cost", false},
|
||||
{"3400", "Wareneingang 19% Vorsteuer", "expense_direct_cost", false},
|
||||
{"3736", "Erhaltene Skonti 19% VSt", "expense_direct_cost", false},
|
||||
|
||||
// 4xxx — Betriebliche Aufwendungen (Operating Expenses)
|
||||
{"4000", "Personalkosten", "expense", false},
|
||||
{"4100", "Löhne", "expense", false},
|
||||
{"4110", "Löhne Produktion", "expense", false},
|
||||
{"4120", "Gehälter", "expense", false},
|
||||
{"4130", "Geschäftsführergehälter", "expense", false},
|
||||
{"4140", "Freiwillige soziale Aufwendungen", "expense", false},
|
||||
{"4170", "Vermögenswirksame Leistungen", "expense", false},
|
||||
{"4180", "Arbeitgeberanteile Sozialversicherung", "expense", false},
|
||||
{"4190", "Berufsgenossenschaft", "expense", false},
|
||||
{"4200", "Raumkosten", "expense", false},
|
||||
{"4210", "Miete", "expense", false},
|
||||
{"4220", "Heizung", "expense", false},
|
||||
{"4230", "Gas, Strom, Wasser", "expense", false},
|
||||
{"4240", "Reinigung", "expense", false},
|
||||
{"4260", "Instandhaltung Betriebsräume", "expense", false},
|
||||
{"4300", "Versicherungen", "expense", false},
|
||||
{"4360", "Kfz-Versicherungen", "expense", false},
|
||||
{"4380", "Beiträge", "expense", false},
|
||||
{"4400", "Bürobedarf", "expense", false},
|
||||
{"4500", "Fahrzeugkosten", "expense", false},
|
||||
{"4510", "Kfz-Steuer", "expense", false},
|
||||
{"4520", "Kfz-Reparaturen", "expense", false},
|
||||
{"4530", "Laufende Kfz-Betriebskosten", "expense", false},
|
||||
{"4540", "Kfz-Leasing", "expense", false},
|
||||
{"4580", "Sonstige Kfz-Kosten", "expense", false},
|
||||
{"4600", "Werbekosten", "expense", false},
|
||||
{"4630", "Geschenke an Geschäftsfreunde", "expense", false},
|
||||
{"4650", "Bewirtungskosten", "expense", false},
|
||||
{"4654", "Bewirtungskosten nicht abzugsfähig", "expense", false},
|
||||
{"4660", "Reisekosten Arbeitnehmer", "expense", false},
|
||||
{"4663", "Reisekosten Unternehmer", "expense", false},
|
||||
{"4670", "Kilometergeld", "expense", false},
|
||||
{"4700", "Kosten der Warenabgabe", "expense", false},
|
||||
{"4800", "Reparaturen und Instandhaltung", "expense", false},
|
||||
{"4830", "Abschreibungen Sachanlagen", "expense_depreciation", false},
|
||||
{"4840", "Abschreibungen auf GWG", "expense_depreciation", false},
|
||||
{"4900", "Sonstige betriebliche Aufwendungen", "expense", false},
|
||||
{"4910", "Porto", "expense", false},
|
||||
{"4920", "Telefon", "expense", false},
|
||||
{"4930", "Büromaterial", "expense", false},
|
||||
{"4940", "Zeitschriften, Bücher", "expense", false},
|
||||
{"4950", "Rechts- und Beratungskosten", "expense", false},
|
||||
{"4955", "Buchführungskosten", "expense", false},
|
||||
{"4960", "Nebenkosten des Geldverkehrs", "expense", false},
|
||||
{"4970", "Abschluss- und Prüfungskosten", "expense", false},
|
||||
|
||||
// 8xxx — Erlöse (Revenue)
|
||||
{"8000", "Erlöse", "income", false},
|
||||
{"8100", "Steuerfreie Umsätze §4 Nr. 1a UStG", "income", false},
|
||||
{"8120", "Steuerfreie Umsätze §4 Nr. 1b UStG", "income", false},
|
||||
{"8125", "Steuerfreie innergem. Lieferungen §4 Nr. 1b", "income", false},
|
||||
{"8200", "Erlöse 7% USt", "income", false},
|
||||
{"8300", "Erlöse 19% USt", "income", false},
|
||||
{"8400", "Erlöse 19% USt (allgemein)", "income", false},
|
||||
{"8500", "Provisionserlöse", "income", false},
|
||||
{"8600", "Erlöse aus Vermietung", "income_other", false},
|
||||
{"8700", "Erlösschmälerungen", "income", false},
|
||||
{"8736", "Gewährte Skonti 19% USt", "income", false},
|
||||
{"8800", "Erlöse aus Anlagenverkäufen", "income_other", false},
|
||||
{"8900", "Privatentnahmen", "equity", false},
|
||||
{"8920", "Unentgeltliche Wertabgaben", "income_other", false},
|
||||
|
||||
// 9xxx — Vortrags- und Abschlusskonten
|
||||
{"9000", "Saldenvorträge Sachkonten", "equity_unaffected", false},
|
||||
{"9008", "Saldenvorträge Debitoren", "equity_unaffected", false},
|
||||
{"9009", "Saldenvorträge Kreditoren", "equity_unaffected", false},
|
||||
|
||||
// Eigenkapital
|
||||
{"0800", "Gezeichnetes Kapital", "equity", false},
|
||||
{"0840", "Kapitalrücklage", "equity", false},
|
||||
{"0860", "Gewinnrücklage", "equity", false},
|
||||
{"0868", "Gewinnvortrag", "equity", false},
|
||||
{"0869", "Verlustvortrag", "equity", false},
|
||||
{"0880", "Nicht durch Eigenkapital gedeckter Fehlbetrag", "equity_unaffected", false},
|
||||
}
|
||||
|
||||
// SKR03Taxes returns the standard German tax definitions.
|
||||
type TaxDef struct {
|
||||
Name string
|
||||
Amount float64
|
||||
TypeUse string // sale / purchase
|
||||
Account string // account code for tax
|
||||
}
|
||||
|
||||
var SKR03Taxes = []TaxDef{
|
||||
// USt (Umsatzsteuer) — Sales Tax
|
||||
{"USt 19%", 19.0, "sale", "1776"},
|
||||
{"USt 7%", 7.0, "sale", "1770"},
|
||||
{"USt 0% steuerfreie Umsätze", 0.0, "sale", ""},
|
||||
|
||||
// VSt (Vorsteuer) — Input Tax / Purchase Tax
|
||||
{"VSt 19%", 19.0, "purchase", "1576"},
|
||||
{"VSt 7%", 7.0, "purchase", "1570"},
|
||||
|
||||
// Innergemeinschaftliche Erwerbe
|
||||
{"USt 19% innergem. Erwerb", 19.0, "purchase", "1580"},
|
||||
{"USt 19% Reverse Charge", 19.0, "purchase", "1787"},
|
||||
}
|
||||
5
addons/l10n_de/models/init.go
Normal file
5
addons/l10n_de/models/init.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initChartTemplate()
|
||||
}
|
||||
99
addons/l10n_de/models/l10n_de.go
Normal file
99
addons/l10n_de/models/l10n_de.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package models
|
||||
|
||||
import "odoo-go/pkg/orm"
|
||||
|
||||
// initChartTemplate registers the German chart of accounts template model
|
||||
// and defines the structure for loading SKR03/SKR04 seed data.
|
||||
// Mirrors: odoo/addons/l10n_de/models/template_de_skr03.py
|
||||
// odoo/addons/l10n_de/models/template_de_skr04.py
|
||||
//
|
||||
// German Standard Charts of Accounts (Standardkontenrahmen):
|
||||
//
|
||||
// SKR03 — organized by function (Prozessgliederungsprinzip):
|
||||
// 0xxx Anlage- und Kapitalkonten (Fixed assets & capital accounts) — Aktiva
|
||||
// 1xxx Finanz- und Privatkonten (Financial & private accounts) — Aktiva
|
||||
// 2xxx Abgrenzungskonten (Accrual accounts) — Aktiva
|
||||
// 3xxx Wareneingangskonten (Goods received / purchasing accounts) — GuV
|
||||
// 4xxx Betriebliche Aufwendungen (Operating expenses) — GuV
|
||||
// 5xxx (reserved)
|
||||
// 6xxx (reserved)
|
||||
// 7xxx Bestände an Erzeugnissen (Inventory of products) — GuV
|
||||
// 8xxx Erlöskonten (Revenue accounts) — GuV
|
||||
// 9xxx Vortrags- und statistische Konten (Carried-forward & statistical)
|
||||
//
|
||||
// SKR04 — organized by the balance sheet / P&L structure (Abschlussgliederungsprinzip):
|
||||
// 0xxx Anlagevermögen (Fixed assets) — Aktiva
|
||||
// 1xxx Umlaufvermögen (Current assets) — Aktiva
|
||||
// 2xxx Eigenkapital (Equity) — Passiva
|
||||
// 3xxx Fremdkapital (Liabilities) — Passiva
|
||||
// 4xxx Betriebliche Erträge (Operating income) — GuV
|
||||
// 5xxx Betriebliche Aufwendungen (Operating expenses, materials) — GuV
|
||||
// 6xxx Betriebliche Aufwendungen (Operating expenses, personnel) — GuV
|
||||
// 7xxx Weitere Erträge und Aufwendungen (Other income & expenses) — GuV
|
||||
// 8xxx (reserved)
|
||||
// 9xxx Vortrags- und statistische Konten (Carried-forward & statistical)
|
||||
func initChartTemplate() {
|
||||
// l10n_de.chart.template — Metadata for German chart of accounts templates.
|
||||
// In Odoo, chart templates are loaded from data files (XML/CSV).
|
||||
// This model holds template metadata; the actual account definitions
|
||||
// would be loaded via seed data during module installation.
|
||||
m := orm.NewModel("l10n_de.chart.template", orm.ModelOpts{
|
||||
Description: "German Chart of Accounts Template",
|
||||
Order: "name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Chart Name", Required: true, Translate: true}),
|
||||
orm.Selection("chart_type", []orm.SelectionItem{
|
||||
{Value: "skr03", Label: "SKR03 (Prozessgliederung)"},
|
||||
{Value: "skr04", Label: "SKR04 (Abschlussgliederung)"},
|
||||
}, orm.FieldOpts{String: "Chart Type", Required: true}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Index: true,
|
||||
}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency"}),
|
||||
orm.Boolean("visible", orm.FieldOpts{String: "Can be Visible", Default: true}),
|
||||
)
|
||||
}
|
||||
|
||||
// LoadSKR03 would load the SKR03 chart of accounts as seed data.
|
||||
// In a full implementation, this reads account definitions and creates
|
||||
// account.account records for the installing company.
|
||||
//
|
||||
// SKR03 key account ranges:
|
||||
// 0100-0499 Immaterielle Vermögensgegenstände (Intangible assets)
|
||||
// 0500-0899 Sachanlagen (Tangible fixed assets)
|
||||
// 0900-0999 Finanzanlagen (Financial assets)
|
||||
// 1000-1099 Kasse (Cash)
|
||||
// 1200-1299 Bankkonten (Bank accounts)
|
||||
// 1400-1499 Forderungen aus Lieferungen (Trade receivables)
|
||||
// 1600-1699 Sonstige Forderungen (Other receivables)
|
||||
// 1700-1799 Verbindlichkeiten aus Lieferungen (Trade payables)
|
||||
// 1800-1899 Umsatzsteuer / Vorsteuer (VAT accounts)
|
||||
// 3000-3999 Wareneingang (Goods received)
|
||||
// 4000-4999 Betriebliche Aufwendungen (Operating expenses)
|
||||
// 8000-8999 Erlöse (Revenue)
|
||||
func LoadSKR03() {
|
||||
// Seed data loading would be implemented here.
|
||||
// Typically reads from embedded CSV/XML data files and calls
|
||||
// orm create operations for account.account records.
|
||||
}
|
||||
|
||||
// LoadSKR04 would load the SKR04 chart of accounts as seed data.
|
||||
// SKR04 follows the balance sheet structure more closely.
|
||||
//
|
||||
// SKR04 key account ranges:
|
||||
// 0100-0199 Immaterielle Vermögensgegenstände (Intangible assets)
|
||||
// 0200-0499 Sachanlagen (Tangible fixed assets)
|
||||
// 0500-0699 Finanzanlagen (Financial assets)
|
||||
// 1000-1099 Kasse (Cash)
|
||||
// 1200-1299 Bankkonten (Bank accounts)
|
||||
// 1400-1499 Forderungen aus Lieferungen (Trade receivables)
|
||||
// 2000-2999 Eigenkapital (Equity)
|
||||
// 3000-3999 Fremdkapital (Liabilities)
|
||||
// 4000-4999 Betriebliche Erträge (Operating income)
|
||||
// 5000-6999 Betriebliche Aufwendungen (Operating expenses)
|
||||
// 7000-7999 Weitere Erträge/Aufwendungen (Other income/expenses)
|
||||
func LoadSKR04() {
|
||||
// Seed data loading would be implemented here.
|
||||
}
|
||||
25
addons/l10n_de/module.go
Normal file
25
addons/l10n_de/module.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// Package l10n_de implements the German localization for Odoo accounting.
|
||||
// Mirrors: odoo/addons/l10n_de/__manifest__.py
|
||||
//
|
||||
// Provides the SKR03 and SKR04 standard charts of accounts (Standardkontenrahmen)
|
||||
// as defined by DATEV for German businesses.
|
||||
package l10n_de
|
||||
|
||||
import (
|
||||
"odoo-go/addons/l10n_de/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "l10n_de",
|
||||
Description: "Germany - Accounting",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Accounting/Localizations/Account Charts",
|
||||
Depends: []string{"base", "account"},
|
||||
Application: false,
|
||||
Installable: true,
|
||||
Sequence: 100,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
9
addons/product/models/init.go
Normal file
9
addons/product/models/init.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initProductCategory()
|
||||
initUoM()
|
||||
initProductTemplate()
|
||||
initProductProduct()
|
||||
initProductPricelist()
|
||||
}
|
||||
171
addons/product/models/product.go
Normal file
171
addons/product/models/product.go
Normal 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"}),
|
||||
)
|
||||
}
|
||||
22
addons/product/module.go
Normal file
22
addons/product/module.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Package product implements Odoo's product management module.
|
||||
// Mirrors: odoo/addons/product/__manifest__.py
|
||||
package product
|
||||
|
||||
import (
|
||||
"odoo-go/addons/product/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "product",
|
||||
Description: "Products",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Sales/Sales",
|
||||
Depends: []string{"base"},
|
||||
Application: false,
|
||||
Installable: true,
|
||||
Sequence: 15,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
9
addons/project/models/init.go
Normal file
9
addons/project/models/init.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initProjectTags()
|
||||
initProjectTaskType()
|
||||
initProjectMilestone()
|
||||
initProjectProject()
|
||||
initProjectTask()
|
||||
}
|
||||
124
addons/project/models/project.go
Normal file
124
addons/project/models/project.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package models
|
||||
|
||||
import "odoo-go/pkg/orm"
|
||||
|
||||
// initProjectProject registers the project.project model.
|
||||
// Mirrors: odoo/addons/project/models/project_project.py
|
||||
func initProjectProject() {
|
||||
m := orm.NewModel("project.project", orm.ModelOpts{
|
||||
Description: "Project",
|
||||
Order: "sequence, name, id",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Index: true, Translate: true}),
|
||||
orm.Text("description", orm.FieldOpts{String: "Description"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{String: "Customer"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("stage_id", "project.task.type", orm.FieldOpts{String: "Stage"}),
|
||||
orm.Many2many("favorite_user_ids", "res.users", orm.FieldOpts{String: "Favorite Users"}),
|
||||
orm.Integer("task_count", orm.FieldOpts{
|
||||
String: "Task Count",
|
||||
Compute: "_compute_task_count",
|
||||
}),
|
||||
orm.Integer("color", orm.FieldOpts{String: "Color Index"}),
|
||||
orm.Boolean("allow_task_dependencies", orm.FieldOpts{String: "Task Dependencies"}),
|
||||
orm.Boolean("allow_milestones", orm.FieldOpts{String: "Milestones"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initProjectTask registers the project.task model.
|
||||
// Mirrors: odoo/addons/project/models/project_task.py
|
||||
func initProjectTask() {
|
||||
m := orm.NewModel("project.task", orm.ModelOpts{
|
||||
Description: "Task",
|
||||
Order: "priority desc, sequence, id desc",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Title", Required: true, Index: true}),
|
||||
orm.HTML("description", orm.FieldOpts{String: "Description"}),
|
||||
orm.Boolean("active", orm.FieldOpts{String: "Active", Default: true}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
orm.Many2one("project_id", "project.project", orm.FieldOpts{String: "Project", Index: true}),
|
||||
orm.Many2one("parent_id", "project.task", orm.FieldOpts{String: "Parent Task", Index: true}),
|
||||
orm.One2many("child_ids", "project.task", "parent_id", orm.FieldOpts{String: "Sub-tasks"}),
|
||||
orm.Many2one("stage_id", "project.task.type", orm.FieldOpts{String: "Stage", Index: true}),
|
||||
orm.Selection("state", []orm.SelectionItem{
|
||||
{Value: "open", Label: "In Progress"},
|
||||
{Value: "done", Label: "Done"},
|
||||
{Value: "cancel", Label: "Cancelled"},
|
||||
}, orm.FieldOpts{String: "State", Default: "open"}),
|
||||
orm.Selection("priority", []orm.SelectionItem{
|
||||
{Value: "0", Label: "Normal"},
|
||||
{Value: "1", Label: "Important"},
|
||||
}, orm.FieldOpts{String: "Priority", Default: "0"}),
|
||||
orm.Integer("color", orm.FieldOpts{String: "Color Index"}),
|
||||
orm.Many2many("user_ids", "res.users", orm.FieldOpts{String: "Assignees"}),
|
||||
orm.Date("date_deadline", orm.FieldOpts{String: "Deadline", Index: true}),
|
||||
orm.Datetime("date_assign", orm.FieldOpts{String: "Assigning Date"}),
|
||||
orm.Many2many("tag_ids", "project.tags", orm.FieldOpts{String: "Tags"}),
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{String: "Customer"}),
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("milestone_id", "project.milestone", orm.FieldOpts{String: "Milestone"}),
|
||||
orm.Many2many("depend_ids", "project.task", orm.FieldOpts{String: "Depends On"}),
|
||||
orm.Boolean("recurring_task", orm.FieldOpts{String: "Recurrent"}),
|
||||
orm.Selection("display_type", []orm.SelectionItem{
|
||||
{Value: "", Label: ""},
|
||||
{Value: "line_section", Label: "Section"},
|
||||
{Value: "line_note", Label: "Note"},
|
||||
}, orm.FieldOpts{String: "Display Type"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initProjectTaskType registers the project.task.type model (stages).
|
||||
// Mirrors: odoo/addons/project/models/project_task_type.py
|
||||
func initProjectTaskType() {
|
||||
m := orm.NewModel("project.task.type", orm.ModelOpts{
|
||||
Description: "Task Stage",
|
||||
Order: "sequence, id",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 1}),
|
||||
orm.Boolean("fold", orm.FieldOpts{String: "Folded in Kanban"}),
|
||||
orm.Many2many("project_ids", "project.project", orm.FieldOpts{String: "Projects"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initProjectMilestone registers the project.milestone model.
|
||||
// Mirrors: odoo/addons/project/models/project_milestone.py
|
||||
func initProjectMilestone() {
|
||||
m := orm.NewModel("project.milestone", orm.ModelOpts{
|
||||
Description: "Project Milestone",
|
||||
Order: "deadline, is_reached desc, name",
|
||||
})
|
||||
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true}),
|
||||
orm.Many2one("project_id", "project.project", orm.FieldOpts{
|
||||
String: "Project", Required: true, OnDelete: orm.OnDeleteCascade, Index: true,
|
||||
}),
|
||||
orm.Date("deadline", orm.FieldOpts{String: "Deadline"}),
|
||||
orm.Boolean("is_reached", orm.FieldOpts{String: "Reached"}),
|
||||
)
|
||||
}
|
||||
|
||||
// initProjectTags registers the project.tags model.
|
||||
// Mirrors: odoo/addons/project/models/project_tags.py
|
||||
func initProjectTags() {
|
||||
orm.NewModel("project.tags", orm.ModelOpts{
|
||||
Description: "Project Tags",
|
||||
Order: "name",
|
||||
}).AddFields(
|
||||
orm.Char("name", orm.FieldOpts{String: "Name", Required: true, Translate: true}),
|
||||
orm.Integer("color", orm.FieldOpts{String: "Color Index"}),
|
||||
)
|
||||
}
|
||||
22
addons/project/module.go
Normal file
22
addons/project/module.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Package project implements Odoo's Project Management module.
|
||||
// Mirrors: odoo/addons/project/__manifest__.py
|
||||
package project
|
||||
|
||||
import (
|
||||
"odoo-go/addons/project/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "project",
|
||||
Description: "Project",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Services/Project",
|
||||
Depends: []string{"base"},
|
||||
Application: true,
|
||||
Installable: true,
|
||||
Sequence: 30,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
6
addons/purchase/models/init.go
Normal file
6
addons/purchase/models/init.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initPurchaseOrder()
|
||||
initPurchaseOrderLine()
|
||||
}
|
||||
177
addons/purchase/models/purchase_order.go
Normal file
177
addons/purchase/models/purchase_order.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package models
|
||||
|
||||
import "odoo-go/pkg/orm"
|
||||
|
||||
// initPurchaseOrder registers purchase.order and purchase.order.line.
|
||||
// Mirrors: odoo/addons/purchase/models/purchase_order.py
|
||||
|
||||
func initPurchaseOrder() {
|
||||
// purchase.order — the purchase order header
|
||||
m := orm.NewModel("purchase.order", orm.ModelOpts{
|
||||
Description: "Purchase Order",
|
||||
Order: "priority desc, id desc",
|
||||
RecName: "name",
|
||||
})
|
||||
|
||||
// -- Identity --
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{
|
||||
String: "Order Reference", Required: true, Index: true, Readonly: true, Default: "New",
|
||||
}),
|
||||
orm.Selection("state", []orm.SelectionItem{
|
||||
{Value: "draft", Label: "RFQ"},
|
||||
{Value: "sent", Label: "RFQ Sent"},
|
||||
{Value: "to approve", Label: "To Approve"},
|
||||
{Value: "purchase", Label: "Purchase Order"},
|
||||
{Value: "cancel", Label: "Cancelled"},
|
||||
}, orm.FieldOpts{String: "Status", Default: "draft", Readonly: true, Index: true}),
|
||||
orm.Selection("priority", []orm.SelectionItem{
|
||||
{Value: "0", Label: "Normal"},
|
||||
{Value: "1", Label: "Urgent"},
|
||||
}, orm.FieldOpts{String: "Priority", Default: "0", Index: true}),
|
||||
)
|
||||
|
||||
// -- Partner & Dates --
|
||||
m.AddFields(
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{
|
||||
String: "Vendor", Required: true, Index: true,
|
||||
}),
|
||||
orm.Datetime("date_order", orm.FieldOpts{
|
||||
String: "Order Deadline", Required: true, Index: true,
|
||||
}),
|
||||
orm.Datetime("date_planned", orm.FieldOpts{
|
||||
String: "Expected Arrival",
|
||||
}),
|
||||
orm.Datetime("date_approve", orm.FieldOpts{
|
||||
String: "Confirmation Date", Readonly: true, Index: true,
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Company & Currency --
|
||||
m.AddFields(
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{
|
||||
String: "Currency", Required: true,
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Lines --
|
||||
m.AddFields(
|
||||
orm.One2many("order_line", "purchase.order.line", "order_id", orm.FieldOpts{
|
||||
String: "Order Lines",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Amounts --
|
||||
m.AddFields(
|
||||
orm.Monetary("amount_untaxed", orm.FieldOpts{
|
||||
String: "Untaxed Amount", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id",
|
||||
}),
|
||||
orm.Monetary("amount_tax", orm.FieldOpts{
|
||||
String: "Taxes", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id",
|
||||
}),
|
||||
orm.Monetary("amount_total", orm.FieldOpts{
|
||||
String: "Total", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Invoice Status --
|
||||
m.AddFields(
|
||||
orm.Selection("invoice_status", []orm.SelectionItem{
|
||||
{Value: "no", Label: "Nothing to Bill"},
|
||||
{Value: "to invoice", Label: "Waiting Bills"},
|
||||
{Value: "invoiced", Label: "Fully Billed"},
|
||||
}, orm.FieldOpts{String: "Billing Status", Compute: "_compute_invoice_status", Store: true}),
|
||||
)
|
||||
|
||||
// -- Accounting --
|
||||
m.AddFields(
|
||||
orm.Many2one("fiscal_position_id", "account.fiscal.position", orm.FieldOpts{
|
||||
String: "Fiscal Position",
|
||||
}),
|
||||
orm.Many2one("payment_term_id", "account.payment.term", orm.FieldOpts{
|
||||
String: "Payment Terms",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Notes --
|
||||
m.AddFields(
|
||||
orm.Text("notes", orm.FieldOpts{String: "Terms and Conditions"}),
|
||||
orm.Char("origin", orm.FieldOpts{String: "Source Document"}),
|
||||
)
|
||||
|
||||
// purchase.order.line — individual line items on a PO
|
||||
initPurchaseOrderLine()
|
||||
}
|
||||
|
||||
// initPurchaseOrderLine registers purchase.order.line.
|
||||
// Mirrors: odoo/addons/purchase/models/purchase_order_line.py
|
||||
func initPurchaseOrderLine() {
|
||||
m := orm.NewModel("purchase.order.line", orm.ModelOpts{
|
||||
Description: "Purchase Order Line",
|
||||
Order: "order_id, sequence, id",
|
||||
})
|
||||
|
||||
// -- Parent --
|
||||
m.AddFields(
|
||||
orm.Many2one("order_id", "purchase.order", orm.FieldOpts{
|
||||
String: "Order Reference", Required: true, OnDelete: orm.OnDeleteCascade, Index: true,
|
||||
}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
)
|
||||
|
||||
// -- Product --
|
||||
m.AddFields(
|
||||
orm.Many2one("product_id", "product.product", orm.FieldOpts{
|
||||
String: "Product", Index: true,
|
||||
}),
|
||||
orm.Char("name", orm.FieldOpts{String: "Description", Required: true}),
|
||||
orm.Float("product_qty", orm.FieldOpts{String: "Quantity", Required: true, Default: 1.0}),
|
||||
orm.Many2one("product_uom", "uom.uom", orm.FieldOpts{
|
||||
String: "Unit of Measure", Required: true,
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Pricing --
|
||||
m.AddFields(
|
||||
orm.Float("price_unit", orm.FieldOpts{String: "Unit Price", Required: true}),
|
||||
orm.Many2many("tax_ids", "account.tax", orm.FieldOpts{String: "Taxes"}),
|
||||
orm.Float("discount", orm.FieldOpts{String: "Discount (%)", Default: 0.0}),
|
||||
orm.Monetary("price_subtotal", orm.FieldOpts{
|
||||
String: "Subtotal", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id",
|
||||
}),
|
||||
orm.Monetary("price_total", orm.FieldOpts{
|
||||
String: "Total", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id",
|
||||
}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{
|
||||
String: "Currency",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Dates --
|
||||
m.AddFields(
|
||||
orm.Datetime("date_planned", orm.FieldOpts{String: "Expected Arrival"}),
|
||||
)
|
||||
|
||||
// -- Quantities --
|
||||
m.AddFields(
|
||||
orm.Float("qty_received", orm.FieldOpts{
|
||||
String: "Received Qty", Compute: "_compute_qty_received", Store: true,
|
||||
}),
|
||||
orm.Float("qty_invoiced", orm.FieldOpts{
|
||||
String: "Billed Qty", Compute: "_compute_qty_invoiced", Store: true,
|
||||
}),
|
||||
orm.Float("qty_to_invoice", orm.FieldOpts{
|
||||
String: "To Invoice Quantity", Compute: "_compute_qty_to_invoice", Store: true,
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Company --
|
||||
m.AddFields(
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Index: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
22
addons/purchase/module.go
Normal file
22
addons/purchase/module.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Package purchase implements Odoo's purchase management module.
|
||||
// Mirrors: odoo/addons/purchase/__manifest__.py
|
||||
package purchase
|
||||
|
||||
import (
|
||||
"odoo-go/addons/purchase/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "purchase",
|
||||
Description: "Purchase",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Inventory/Purchase",
|
||||
Depends: []string{"base", "account", "product", "stock"},
|
||||
Application: true,
|
||||
Installable: true,
|
||||
Sequence: 30,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
6
addons/sale/models/init.go
Normal file
6
addons/sale/models/init.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package models
|
||||
|
||||
func Init() {
|
||||
initSaleOrder()
|
||||
initSaleOrderLine()
|
||||
}
|
||||
315
addons/sale/models/sale_order.go
Normal file
315
addons/sale/models/sale_order.go
Normal file
@@ -0,0 +1,315 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"odoo-go/pkg/orm"
|
||||
)
|
||||
|
||||
// initSaleOrder registers sale.order — the sales quotation / order model.
|
||||
// Mirrors: odoo/addons/sale/models/sale_order.py
|
||||
func initSaleOrder() {
|
||||
m := orm.NewModel("sale.order", orm.ModelOpts{
|
||||
Description: "Sales Order",
|
||||
Order: "date_order desc, id desc",
|
||||
})
|
||||
|
||||
// -- Identity & State --
|
||||
m.AddFields(
|
||||
orm.Char("name", orm.FieldOpts{
|
||||
String: "Order Reference", Required: true, Index: true, Readonly: true, Default: "/",
|
||||
}),
|
||||
orm.Selection("state", []orm.SelectionItem{
|
||||
{Value: "draft", Label: "Quotation"},
|
||||
{Value: "sent", Label: "Quotation Sent"},
|
||||
{Value: "sale", Label: "Sales Order"},
|
||||
{Value: "cancel", Label: "Cancelled"},
|
||||
}, orm.FieldOpts{String: "Status", Default: "draft", Required: true, Readonly: true, Index: true}),
|
||||
)
|
||||
|
||||
// -- Partners --
|
||||
m.AddFields(
|
||||
orm.Many2one("partner_id", "res.partner", orm.FieldOpts{
|
||||
String: "Customer", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("partner_invoice_id", "res.partner", orm.FieldOpts{
|
||||
String: "Invoice Address",
|
||||
}),
|
||||
orm.Many2one("partner_shipping_id", "res.partner", orm.FieldOpts{
|
||||
String: "Delivery Address",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Dates --
|
||||
m.AddFields(
|
||||
orm.Datetime("date_order", orm.FieldOpts{
|
||||
String: "Order Date", Required: true, Index: true,
|
||||
}),
|
||||
orm.Date("validity_date", orm.FieldOpts{String: "Expiration"}),
|
||||
)
|
||||
|
||||
// -- Company & Currency --
|
||||
m.AddFields(
|
||||
orm.Many2one("company_id", "res.company", orm.FieldOpts{
|
||||
String: "Company", Required: true, Index: true,
|
||||
}),
|
||||
orm.Many2one("currency_id", "res.currency", orm.FieldOpts{
|
||||
String: "Currency", Required: true,
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Order Lines --
|
||||
m.AddFields(
|
||||
orm.One2many("order_line", "sale.order.line", "order_id", orm.FieldOpts{
|
||||
String: "Order Lines",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Amounts (Computed) --
|
||||
m.AddFields(
|
||||
orm.Monetary("amount_untaxed", orm.FieldOpts{
|
||||
String: "Untaxed Amount", Compute: "_compute_amounts", Store: true, CurrencyField: "currency_id",
|
||||
}),
|
||||
orm.Monetary("amount_tax", orm.FieldOpts{
|
||||
String: "Taxes", Compute: "_compute_amounts", Store: true, CurrencyField: "currency_id",
|
||||
}),
|
||||
orm.Monetary("amount_total", orm.FieldOpts{
|
||||
String: "Total", Compute: "_compute_amounts", Store: true, CurrencyField: "currency_id",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Invoice Status --
|
||||
m.AddFields(
|
||||
orm.Selection("invoice_status", []orm.SelectionItem{
|
||||
{Value: "upselling", Label: "Upselling Opportunity"},
|
||||
{Value: "invoiced", Label: "Fully Invoiced"},
|
||||
{Value: "to invoice", Label: "To Invoice"},
|
||||
{Value: "no", Label: "Nothing to Invoice"},
|
||||
}, orm.FieldOpts{String: "Invoice Status", Compute: "_compute_invoice_status", Store: true}),
|
||||
)
|
||||
|
||||
// -- Accounting & Terms --
|
||||
m.AddFields(
|
||||
orm.Many2one("fiscal_position_id", "account.fiscal.position", orm.FieldOpts{
|
||||
String: "Fiscal Position",
|
||||
}),
|
||||
orm.Many2one("payment_term_id", "account.payment.term", orm.FieldOpts{
|
||||
String: "Payment Terms",
|
||||
}),
|
||||
orm.Many2one("pricelist_id", "product.pricelist", orm.FieldOpts{
|
||||
String: "Pricelist",
|
||||
}),
|
||||
orm.Many2one("journal_id", "account.journal", orm.FieldOpts{
|
||||
String: "Invoicing Journal",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Misc --
|
||||
m.AddFields(
|
||||
orm.Text("note", orm.FieldOpts{String: "Terms and Conditions"}),
|
||||
orm.Boolean("require_signature", orm.FieldOpts{String: "Online Signature", Default: true}),
|
||||
orm.Boolean("require_payment", orm.FieldOpts{String: "Online Payment"}),
|
||||
)
|
||||
|
||||
// -- Sequence Hook --
|
||||
m.BeforeCreate = func(env *orm.Environment, vals orm.Values) error {
|
||||
name, _ := vals["name"].(string)
|
||||
if name == "" || name == "/" {
|
||||
seq, err := orm.NextByCode(env, "sale.order")
|
||||
if err == nil {
|
||||
vals["name"] = seq
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// -- Business Methods --
|
||||
|
||||
// action_confirm: draft → sale
|
||||
// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder.action_confirm()
|
||||
m.RegisterMethod("action_confirm", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
for _, id := range rs.IDs() {
|
||||
var state string
|
||||
err := env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT state FROM sale_order WHERE id = $1`, id).Scan(&state)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if state != "draft" && state != "sent" {
|
||||
return nil, fmt.Errorf("sale: can only confirm draft/sent orders (current: %s)", state)
|
||||
}
|
||||
if _, err := env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE sale_order SET state = 'sale' WHERE id = $1`, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
|
||||
// create_invoices: Generate invoice from confirmed sale order
|
||||
// Mirrors: odoo/addons/sale/models/sale_order.py SaleOrder._create_invoices()
|
||||
m.RegisterMethod("create_invoices", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) {
|
||||
env := rs.Env()
|
||||
var invoiceIDs []int64
|
||||
|
||||
for _, soID := range rs.IDs() {
|
||||
// Read SO header
|
||||
var partnerID, companyID, currencyID int64
|
||||
var journalID int64
|
||||
err := env.Tx().QueryRow(env.Ctx(),
|
||||
`SELECT partner_id, company_id, currency_id, COALESCE(journal_id, 1)
|
||||
FROM sale_order WHERE id = $1`, soID,
|
||||
).Scan(&partnerID, &companyID, ¤cyID, &journalID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sale: read SO %d: %w", soID, err)
|
||||
}
|
||||
|
||||
// Read SO lines
|
||||
rows, err := env.Tx().Query(env.Ctx(),
|
||||
`SELECT id, COALESCE(name,''), COALESCE(product_uom_qty,1), COALESCE(price_unit,0), COALESCE(discount,0)
|
||||
FROM sale_order_line
|
||||
WHERE order_id = $1 AND (display_type IS NULL OR display_type = '')
|
||||
ORDER BY sequence, id`, soID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type soLine struct {
|
||||
id int64
|
||||
name string
|
||||
qty float64
|
||||
price float64
|
||||
discount float64
|
||||
}
|
||||
var lines []soLine
|
||||
for rows.Next() {
|
||||
var l soLine
|
||||
if err := rows.Scan(&l.id, &l.name, &l.qty, &l.price, &l.discount); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
lines = append(lines, l)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
if len(lines) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Build invoice line commands
|
||||
var lineCmds []interface{}
|
||||
for _, l := range lines {
|
||||
subtotal := l.qty * l.price * (1 - l.discount/100)
|
||||
lineCmds = append(lineCmds, []interface{}{
|
||||
float64(0), float64(0), map[string]interface{}{
|
||||
"name": l.name,
|
||||
"quantity": l.qty,
|
||||
"price_unit": l.price,
|
||||
"discount": l.discount,
|
||||
"debit": subtotal,
|
||||
"credit": float64(0),
|
||||
"account_id": float64(2), // Revenue account
|
||||
"company_id": float64(companyID),
|
||||
},
|
||||
})
|
||||
// Receivable counter-entry
|
||||
lineCmds = append(lineCmds, []interface{}{
|
||||
float64(0), float64(0), map[string]interface{}{
|
||||
"name": "Receivable",
|
||||
"debit": float64(0),
|
||||
"credit": subtotal,
|
||||
"account_id": float64(1), // Receivable account
|
||||
"company_id": float64(companyID),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Create invoice
|
||||
invoiceRS := env.Model("account.move")
|
||||
inv, err := invoiceRS.Create(orm.Values{
|
||||
"move_type": "out_invoice",
|
||||
"partner_id": partnerID,
|
||||
"company_id": companyID,
|
||||
"currency_id": currencyID,
|
||||
"journal_id": journalID,
|
||||
"invoice_origin": fmt.Sprintf("SO%d", soID),
|
||||
"date": "2026-03-30", // TODO: use current date
|
||||
"line_ids": lineCmds,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("sale: create invoice for SO %d: %w", soID, err)
|
||||
}
|
||||
|
||||
invoiceIDs = append(invoiceIDs, inv.ID())
|
||||
|
||||
// Update SO invoice_status
|
||||
env.Tx().Exec(env.Ctx(),
|
||||
`UPDATE sale_order SET invoice_status = 'invoiced' WHERE id = $1`, soID)
|
||||
}
|
||||
|
||||
return invoiceIDs, nil
|
||||
})
|
||||
}
|
||||
|
||||
// initSaleOrderLine registers sale.order.line — individual line items on a sales order.
|
||||
// Mirrors: odoo/addons/sale/models/sale_order_line.py
|
||||
func initSaleOrderLine() {
|
||||
m := orm.NewModel("sale.order.line", orm.ModelOpts{
|
||||
Description: "Sales Order Line",
|
||||
Order: "order_id, sequence, id",
|
||||
})
|
||||
|
||||
// -- Parent --
|
||||
m.AddFields(
|
||||
orm.Many2one("order_id", "sale.order", orm.FieldOpts{
|
||||
String: "Order Reference", Required: true, OnDelete: orm.OnDeleteCascade, Index: true,
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Product --
|
||||
m.AddFields(
|
||||
orm.Many2one("product_id", "product.product", orm.FieldOpts{
|
||||
String: "Product",
|
||||
}),
|
||||
orm.Char("name", orm.FieldOpts{String: "Description", Required: true}),
|
||||
orm.Float("product_uom_qty", orm.FieldOpts{String: "Quantity", Required: true, Default: 1.0}),
|
||||
orm.Many2one("product_uom", "uom.uom", orm.FieldOpts{String: "Unit of Measure"}),
|
||||
)
|
||||
|
||||
// -- Pricing --
|
||||
m.AddFields(
|
||||
orm.Float("price_unit", orm.FieldOpts{String: "Unit Price", Required: true}),
|
||||
orm.Many2many("tax_id", "account.tax", orm.FieldOpts{String: "Taxes"}),
|
||||
orm.Float("discount", orm.FieldOpts{String: "Discount (%)"}),
|
||||
orm.Monetary("price_subtotal", orm.FieldOpts{
|
||||
String: "Subtotal", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id",
|
||||
}),
|
||||
orm.Monetary("price_total", orm.FieldOpts{
|
||||
String: "Total", Compute: "_compute_amount", Store: true, CurrencyField: "currency_id",
|
||||
}),
|
||||
)
|
||||
|
||||
// -- Display --
|
||||
m.AddFields(
|
||||
orm.Selection("display_type", []orm.SelectionItem{
|
||||
{Value: "line_section", Label: "Section"},
|
||||
{Value: "line_note", Label: "Note"},
|
||||
}, orm.FieldOpts{String: "Display Type"}),
|
||||
orm.Integer("sequence", orm.FieldOpts{String: "Sequence", Default: 10}),
|
||||
)
|
||||
|
||||
// -- Delivery & Invoicing Quantities --
|
||||
m.AddFields(
|
||||
orm.Float("qty_delivered", orm.FieldOpts{String: "Delivered Quantity"}),
|
||||
orm.Float("qty_invoiced", orm.FieldOpts{
|
||||
String: "Invoiced Quantity", Compute: "_compute_qty_invoiced", Store: true,
|
||||
}),
|
||||
orm.Float("qty_to_invoice", orm.FieldOpts{
|
||||
String: "To Invoice Quantity", Compute: "_compute_qty_to_invoice", Store: true,
|
||||
}),
|
||||
orm.Many2many("invoice_lines", "account.move.line", orm.FieldOpts{
|
||||
String: "Invoice Lines",
|
||||
}),
|
||||
)
|
||||
}
|
||||
22
addons/sale/module.go
Normal file
22
addons/sale/module.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Package sale implements Odoo's sales management module.
|
||||
// Mirrors: odoo/addons/sale/__manifest__.py
|
||||
package sale
|
||||
|
||||
import (
|
||||
"odoo-go/addons/sale/models"
|
||||
"odoo-go/pkg/modules"
|
||||
)
|
||||
|
||||
func init() {
|
||||
modules.Register(&modules.Module{
|
||||
Name: "sale",
|
||||
Description: "Sales",
|
||||
Version: "19.0.1.0.0",
|
||||
Category: "Sales/Sales",
|
||||
Depends: []string{"base", "account", "product"},
|
||||
Application: true,
|
||||
Installable: true,
|
||||
Sequence: 20,
|
||||
Init: models.Init,
|
||||
})
|
||||
}
|
||||
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