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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user