154 lines
7.2 KiB
Python
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
|