Files
enterprise-addons/account_budget/models/budget_line.py
T

154 lines
7.2 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.tools import SQL, Query
class BudgetLine(models.Model):
_name = 'budget.line'
_inherit = ['analytic.plan.fields.mixin']
_description = "Budget Line"
_order = 'sequence, id'
name = fields.Char(related='budget_analytic_id.name', string='Budget Name')
sequence = fields.Integer('Sequence', default=10)
budget_analytic_id = fields.Many2one('budget.analytic', 'Budget Analytic', ondelete='cascade', index=True, required=True)
date_from = fields.Date('Start Date', related='budget_analytic_id.date_from', store=True)
date_to = fields.Date('End Date', related='budget_analytic_id.date_to', store=True)
currency_id = fields.Many2one('res.currency', compute='_compute_currency_id', readonly=True)
budget_amount = fields.Monetary(
string='Budgeted')
achieved_amount = fields.Monetary(
compute='_compute_all',
string='Achieved',
help="Amount Billed/Invoiced.")
achieved_percentage = fields.Float(
compute='_compute_all',
string='Achieved (%)')
theoritical_amount = fields.Monetary(
compute='_compute_theoritical_amount',
string='Theoretical',
help="Amount supposed to be Billed/Invoiced, formula = (Budget Amount / Budget Days) x Budget Days Completed")
theoritical_percentage = fields.Float(
compute='_compute_theoritical_amount',
string='Theoretical (%)',
aggregator='avg')
company_id = fields.Many2one(related='budget_analytic_id.company_id', comodel_name='res.company', string='Company', store=True, readonly=True)
is_above_budget = fields.Boolean(compute='_compute_above_budget')
budget_analytic_state = fields.Selection(related='budget_analytic_id.state', string='Budget State', store=True, readonly=True)
@api.constrains('date_from', 'date_to')
def _check_date_range(self):
for line in self:
if line.date_from and line.date_to and line.date_to < line.date_from:
raise ValidationError(_("The 'End Date' must be greater than or equal to 'Start Date'."))
@api.depends('company_id')
def _compute_currency_id(self):
for budget in self:
budget.currency_id = (budget.company_id or self.env.company).currency_id
@api.depends('achieved_amount', 'budget_amount')
def _compute_above_budget(self):
for line in self:
line.is_above_budget = line.achieved_amount > line.budget_amount
def _compute_display_name(self):
for line in self:
analytic_accounts = [
line[fname].display_name
for fname in line._get_plan_fnames()
if line[fname]
]
line.display_name = line.budget_analytic_id.name
if analytic_accounts:
line.display_name = f"{line.display_name}: {'; '.join(analytic_accounts)}"
def _compute_all(self):
grouped = dict(self.env['budget.report']._read_group(
domain=[('budget_line_id', 'in', self.ids)],
groupby=['budget_line_id'],
aggregates=['achieved:sum'],
))
for line in self:
achieved = grouped.get(line, 0)
line.achieved_amount = achieved
line.achieved_percentage = line.budget_amount and (line.achieved_amount / line.budget_amount)
@api.depends('date_from', 'date_to')
def _compute_theoritical_amount(self):
today = fields.Date.context_today(self)
for line in self:
if not line.date_from or not line.date_to:
line.theoritical_amount = 0
line.theoritical_percentage = 0
continue
# One day is added since we need to include the start and end date in the computation.
# For example, between April 1st and April 30th, the timedelta must be 30 days.
line_timedelta = line.date_to - line.date_from + timedelta(days=1)
elapsed_timedelta = min(max(today, line.date_from), line.date_to) - line.date_from + timedelta(days=1)
line.theoritical_amount = line_timedelta and (elapsed_timedelta.total_seconds() / line_timedelta.total_seconds()) * line.budget_amount
line.theoritical_percentage = line.budget_amount and (line.theoritical_amount / line.budget_amount)
def _field_to_sql(self, alias: str, field_expr: str, query: (Query | None) = None) -> SQL:
if field_expr == 'currency_id':
company_alias = query.left_join(alias, 'company_id', 'res_company', 'id', 'company_id')
return SQL(
"COALESCE(%(company_alias)s.currency_id, %(env_currency_id)s)",
company_alias=SQL.identifier(company_alias),
env_currency_id=self.env.company.currency_id.id,
)
return super()._field_to_sql(alias, field_expr, query)
def _read_group_select(self, aggregate_spec, query):
# flag achieved_amount/theoritical_amount as aggregatable
# and manually sum the values from the records in the group
if aggregate_spec in (
'achieved_amount:sum',
'achieved_amount:sum_currency',
'theoritical_amount:sum',
'theoritical_amount:sum_currency',
'theoritical_percentage:avg'
):
return super()._read_group_select('id:recordset', query)
return super()._read_group_select(aggregate_spec, query)
def _read_group_postprocess_aggregate(self, aggregate_spec, raw_values):
if aggregate_spec in (
'achieved_amount:sum',
'achieved_amount:sum_currency',
'theoritical_amount:sum',
'theoritical_amount:sum_currency',
'theoritical_percentage:avg'
):
field_name, op = aggregate_spec.split(':')
column = super()._read_group_postprocess_aggregate('id:recordset', raw_values)
if op == 'sum':
return (sum(records.mapped(field_name)) for records in column)
if op == 'sum_currency':
return (sum(record.currency_id._convert(
from_amount=record[field_name],
to_currency=self.env.company.currency_id,
) for record in records) for records in column)
if op == 'avg':
return (sum(records.mapped(field_name)) / len(records) for records in column)
return super()._read_group_postprocess_aggregate(aggregate_spec, raw_values)
def action_open_budget_entries(self):
project_plan, other_plans = self.env['account.analytic.plan']._get_all_plans()
all_plan = project_plan + other_plans
domain = [('budget_analytic_id', '=', self.budget_analytic_id.id), ('budget_line_id', '=', self.id)]
for plan in all_plan:
fname = plan._column_name()
if self[fname]:
domain += [(fname, 'in', self[fname].ids)]
action = self.env['ir.actions.act_window']._for_xml_id('account_budget.budget_report_action')
action.update({
'domain': domain,
'views': [(False, 'list'), (False, 'pivot'), (False, 'graph')]
})
return action