package models import ( "time" "odoo-go/pkg/orm" ) // initAccountBudget registers budget planning models. // Mirrors: odoo/addons/account_budget/models/account_budget.py // // crossovered.budget defines a budget with a date range and responsibility. // crossovered.budget.lines defines individual budget lines per analytic account. func initAccountBudget() { // -- Budget Header -- m := orm.NewModel("crossovered.budget", orm.ModelOpts{ Description: "Budget", Order: "date_from desc", }) m.AddFields( orm.Char("name", orm.FieldOpts{String: "Budget Name", Required: true}), orm.Many2one("user_id", "res.users", orm.FieldOpts{String: "Responsible"}), orm.Date("date_from", orm.FieldOpts{String: "Start Date", Required: true}), orm.Date("date_to", orm.FieldOpts{String: "End Date", Required: true}), orm.Selection("state", []orm.SelectionItem{ {Value: "draft", Label: "Draft"}, {Value: "cancel", Label: "Cancelled"}, {Value: "confirm", Label: "Confirmed"}, {Value: "validate", Label: "Validated"}, {Value: "done", Label: "Done"}, }, orm.FieldOpts{String: "Status", Default: "draft", Required: true}), orm.One2many("crossovered_budget_line", "crossovered.budget.lines", "crossovered_budget_id", orm.FieldOpts{ String: "Budget Lines", }), orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company", Required: true}), ) // action_budget_confirm: draft -> confirm m.RegisterMethod("action_budget_confirm", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, id := range rs.IDs() { env.Tx().Exec(env.Ctx(), `UPDATE crossovered_budget SET state = 'confirm' WHERE id = $1 AND state = 'draft'`, id) } return true, nil }) // action_budget_validate: confirm -> validate m.RegisterMethod("action_budget_validate", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, id := range rs.IDs() { env.Tx().Exec(env.Ctx(), `UPDATE crossovered_budget SET state = 'validate' WHERE id = $1 AND state = 'confirm'`, id) } return true, nil }) // action_budget_done: validate -> done m.RegisterMethod("action_budget_done", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, id := range rs.IDs() { env.Tx().Exec(env.Ctx(), `UPDATE crossovered_budget SET state = 'done' WHERE id = $1 AND state = 'validate'`, id) } return true, nil }) // action_budget_cancel: any -> cancel m.RegisterMethod("action_budget_cancel", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, id := range rs.IDs() { env.Tx().Exec(env.Ctx(), `UPDATE crossovered_budget SET state = 'cancel' WHERE id = $1`, id) } return true, nil }) // action_budget_draft: cancel -> draft m.RegisterMethod("action_budget_draft", func(rs *orm.Recordset, args ...interface{}) (interface{}, error) { env := rs.Env() for _, id := range rs.IDs() { env.Tx().Exec(env.Ctx(), `UPDATE crossovered_budget SET state = 'draft' WHERE id = $1 AND state = 'cancel'`, id) } return true, nil }) m.DefaultGet = func(env *orm.Environment, fields []string) orm.Values { vals := orm.Values{ "state": "draft", "date_from": time.Now().Format("2006-01-02"), "date_to": time.Now().AddDate(1, 0, 0).Format("2006-01-02"), } companyID := env.CompanyID() if companyID > 0 { vals["company_id"] = companyID } return vals } // -- Budget Lines -- bl := orm.NewModel("crossovered.budget.lines", orm.ModelOpts{ Description: "Budget Line", Order: "date_from", }) bl.AddFields( orm.Many2one("crossovered_budget_id", "crossovered.budget", orm.FieldOpts{ String: "Budget", Required: true, OnDelete: orm.OnDeleteCascade, }), orm.Many2one("analytic_account_id", "account.analytic.account", orm.FieldOpts{ String: "Analytic Account", }), orm.Many2one("general_budget_id", "account.budget.post", orm.FieldOpts{ String: "Budgetary Position", }), orm.Date("date_from", orm.FieldOpts{String: "Start Date", Required: true}), orm.Date("date_to", orm.FieldOpts{String: "End Date", Required: true}), orm.Monetary("planned_amount", orm.FieldOpts{ String: "Planned Amount", Required: true, CurrencyField: "currency_id", }), orm.Monetary("practical_amount", orm.FieldOpts{ String: "Practical Amount", Compute: "_compute_practical_amount", CurrencyField: "currency_id", }), orm.Float("percentage", orm.FieldOpts{ String: "Achievement", Compute: "_compute_percentage", }), orm.Many2one("currency_id", "res.currency", orm.FieldOpts{String: "Currency"}), orm.Many2one("company_id", "res.company", orm.FieldOpts{ String: "Company", Related: "crossovered_budget_id.company_id", }), orm.Many2one("paid_date", "res.company", orm.FieldOpts{String: "Paid Date"}), orm.Boolean("is_above_budget", orm.FieldOpts{ String: "Above Budget", Compute: "_compute_is_above_budget", }), ) // _compute_practical_amount: sum of posted journal entries for the analytic account // in the budget line's date range. // Mirrors: odoo/addons/account_budget/models/account_budget.py _compute_practical_amount() bl.RegisterCompute("practical_amount", func(rs *orm.Recordset) (orm.Values, error) { env := rs.Env() lineID := rs.IDs()[0] var analyticID *int64 var dateFrom, dateTo string env.Tx().QueryRow(env.Ctx(), `SELECT analytic_account_id, date_from::text, date_to::text FROM crossovered_budget_lines WHERE id = $1`, lineID, ).Scan(&analyticID, &dateFrom, &dateTo) if analyticID == nil || *analyticID == 0 { return orm.Values{"practical_amount": 0.0}, nil } var total float64 env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(SUM(al.amount::float8), 0) FROM account_analytic_line al WHERE al.account_id = $1 AND al.date >= $2 AND al.date <= $3`, *analyticID, dateFrom, dateTo, ).Scan(&total) return orm.Values{"practical_amount": total}, nil }) // _compute_percentage: practical / planned * 100 bl.RegisterCompute("percentage", func(rs *orm.Recordset) (orm.Values, error) { env := rs.Env() lineID := rs.IDs()[0] var planned, practical float64 env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(planned_amount::float8, 0), COALESCE(practical_amount::float8, 0) FROM crossovered_budget_lines WHERE id = $1`, lineID, ).Scan(&planned, &practical) pct := 0.0 if planned != 0 { pct = (practical / planned) * 100 } return orm.Values{"percentage": pct}, nil }) // _compute_is_above_budget bl.RegisterCompute("is_above_budget", func(rs *orm.Recordset) (orm.Values, error) { env := rs.Env() lineID := rs.IDs()[0] var planned, practical float64 env.Tx().QueryRow(env.Ctx(), `SELECT COALESCE(planned_amount::float8, 0), COALESCE(practical_amount::float8, 0) FROM crossovered_budget_lines WHERE id = $1`, lineID, ).Scan(&planned, &practical) above := false if planned > 0 { above = practical > planned } else if planned < 0 { above = practical < planned } return orm.Values{"is_above_budget": above}, nil }) // -- Budgetary Position -- // account.budget.post groups accounts for budgeting purposes. // Mirrors: odoo/addons/account_budget/models/account_budget.py AccountBudgetPost bp := orm.NewModel("account.budget.post", orm.ModelOpts{ Description: "Budgetary Position", Order: "name", }) bp.AddFields( orm.Char("name", orm.FieldOpts{String: "Name", Required: true}), orm.Many2many("account_ids", "account.account", orm.FieldOpts{ String: "Accounts", Relation: "account_budget_post_account_rel", Column1: "budget_post_id", Column2: "account_id", }), orm.Many2one("company_id", "res.company", orm.FieldOpts{String: "Company", Required: true}), ) }