311 lines
11 KiB
Python
311 lines
11 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from random import randint
|
||
|
|
||
|
from odoo import api, fields, models, _
|
||
|
from odoo.exceptions import UserError
|
||
|
from odoo.tools import ormcache, make_index_name, create_index
|
||
|
|
||
|
|
||
|
class AccountAnalyticPlan(models.Model):
|
||
|
_name = 'account.analytic.plan'
|
||
|
_description = 'Analytic Plans'
|
||
|
_parent_store = True
|
||
|
_rec_name = 'complete_name'
|
||
|
_order = 'sequence asc, id'
|
||
|
|
||
|
def _default_color(self):
|
||
|
return randint(1, 11)
|
||
|
|
||
|
name = fields.Char(
|
||
|
required=True,
|
||
|
translate=True,
|
||
|
inverse='_inverse_name',
|
||
|
)
|
||
|
description = fields.Text(string='Description')
|
||
|
parent_id = fields.Many2one(
|
||
|
'account.analytic.plan',
|
||
|
string="Parent",
|
||
|
inverse='_inverse_parent_id',
|
||
|
ondelete='cascade',
|
||
|
domain="['!', ('id', 'child_of', id)]",
|
||
|
)
|
||
|
parent_path = fields.Char(
|
||
|
index='btree',
|
||
|
unaccent=False,
|
||
|
)
|
||
|
root_id = fields.Many2one(
|
||
|
'account.analytic.plan',
|
||
|
compute='_compute_root_id',
|
||
|
)
|
||
|
children_ids = fields.One2many(
|
||
|
'account.analytic.plan',
|
||
|
'parent_id',
|
||
|
string="Childrens",
|
||
|
)
|
||
|
children_count = fields.Integer(
|
||
|
'Children Plans Count',
|
||
|
compute='_compute_children_count',
|
||
|
)
|
||
|
complete_name = fields.Char(
|
||
|
'Complete Name',
|
||
|
compute='_compute_complete_name',
|
||
|
recursive=True,
|
||
|
store=True,
|
||
|
)
|
||
|
account_ids = fields.One2many(
|
||
|
'account.analytic.account',
|
||
|
'plan_id',
|
||
|
string="Accounts",
|
||
|
)
|
||
|
account_count = fields.Integer(
|
||
|
'Analytic Accounts Count',
|
||
|
compute='_compute_analytic_account_count',
|
||
|
)
|
||
|
all_account_count = fields.Integer(
|
||
|
'All Analytic Accounts Count',
|
||
|
compute='_compute_all_analytic_account_count',
|
||
|
)
|
||
|
color = fields.Integer(
|
||
|
'Color',
|
||
|
default=_default_color,
|
||
|
)
|
||
|
sequence = fields.Integer(default=10)
|
||
|
|
||
|
default_applicability = fields.Selection(
|
||
|
selection=[
|
||
|
('optional', 'Optional'),
|
||
|
('mandatory', 'Mandatory'),
|
||
|
('unavailable', 'Unavailable'),
|
||
|
],
|
||
|
string="Default Applicability",
|
||
|
required=True,
|
||
|
default='optional', # actually set in _auto_init because company dependent
|
||
|
readonly=False,
|
||
|
company_dependent=True,
|
||
|
)
|
||
|
applicability_ids = fields.One2many(
|
||
|
'account.analytic.applicability',
|
||
|
'analytic_plan_id',
|
||
|
string='Applicability',
|
||
|
domain="[('company_id', '=', current_company_id)]",
|
||
|
)
|
||
|
|
||
|
def _auto_init(self):
|
||
|
super()._auto_init()
|
||
|
def precommit():
|
||
|
self.env['ir.property']._set_default(
|
||
|
name='default_applicability',
|
||
|
model=self._name,
|
||
|
value='optional',
|
||
|
)
|
||
|
self.env.cr.precommit.add(precommit)
|
||
|
|
||
|
@ormcache()
|
||
|
def __get_all_plans(self):
|
||
|
project_plan = self.browse(int(self.env['ir.config_parameter'].sudo().get_param('analytic.project_plan', 0)))
|
||
|
if not project_plan:
|
||
|
raise UserError(_("A 'Project' plan needs to exist and its id needs to be set as `analytic.project_plan` in the system variables"))
|
||
|
other_plans = self.sudo().search([('parent_id', '=', False)]) - project_plan
|
||
|
return project_plan.id, other_plans.ids
|
||
|
|
||
|
def _get_all_plans(self):
|
||
|
return map(self.browse, self.__get_all_plans())
|
||
|
|
||
|
def _strict_column_name(self):
|
||
|
self.ensure_one()
|
||
|
project_plan, _other_plans = self._get_all_plans()
|
||
|
return 'account_id' if self == project_plan else f"x_plan{self.id}_id"
|
||
|
|
||
|
def _column_name(self):
|
||
|
return self.root_id._strict_column_name()
|
||
|
|
||
|
def _inverse_name(self):
|
||
|
self._sync_plan_column()
|
||
|
|
||
|
def _inverse_parent_id(self):
|
||
|
self._sync_plan_column()
|
||
|
|
||
|
@api.depends('parent_id', 'parent_path')
|
||
|
def _compute_root_id(self):
|
||
|
for plan in self.sudo():
|
||
|
plan.root_id = int(plan.parent_path[:-1].split('/')[0]) if plan.parent_path else plan
|
||
|
|
||
|
@api.depends('name', 'parent_id.complete_name')
|
||
|
def _compute_complete_name(self):
|
||
|
for plan in self:
|
||
|
if plan.parent_id:
|
||
|
plan.complete_name = '%s / %s' % (plan.parent_id.complete_name, plan.name)
|
||
|
else:
|
||
|
plan.complete_name = plan.name
|
||
|
|
||
|
@api.depends('account_ids')
|
||
|
def _compute_analytic_account_count(self):
|
||
|
for plan in self:
|
||
|
plan.account_count = len(plan.account_ids)
|
||
|
|
||
|
@api.depends('account_ids', 'children_ids')
|
||
|
def _compute_all_analytic_account_count(self):
|
||
|
# Get all children_ids from each plan
|
||
|
self.env.cr.execute("""
|
||
|
SELECT parent.id,
|
||
|
array_agg(child.id) as children_ids
|
||
|
FROM account_analytic_plan parent
|
||
|
JOIN account_analytic_plan child ON child.parent_path LIKE parent.parent_path || '%%'
|
||
|
WHERE parent.id IN %s
|
||
|
GROUP BY parent.id
|
||
|
""", [tuple(self.ids)])
|
||
|
all_children_ids = dict(self.env.cr.fetchall())
|
||
|
|
||
|
plans_count = dict(
|
||
|
self.env['account.analytic.account']._read_group(
|
||
|
domain=[('plan_id', 'child_of', self.ids)],
|
||
|
aggregates=['id:count'],
|
||
|
groupby=['plan_id']
|
||
|
)
|
||
|
)
|
||
|
plans_count = {k.id: v for k, v in plans_count.items()}
|
||
|
for plan in self:
|
||
|
plan.all_account_count = sum(plans_count.get(child_id, 0) for child_id in all_children_ids.get(plan.id, []))
|
||
|
|
||
|
@api.depends('children_ids')
|
||
|
def _compute_children_count(self):
|
||
|
for plan in self:
|
||
|
plan.children_count = len(plan.children_ids)
|
||
|
|
||
|
def action_view_analytical_accounts(self):
|
||
|
result = {
|
||
|
"type": "ir.actions.act_window",
|
||
|
"res_model": "account.analytic.account",
|
||
|
"domain": [('plan_id', "child_of", self.id)],
|
||
|
"context": {'default_plan_id': self.id},
|
||
|
"name": _("Analytical Accounts"),
|
||
|
'view_mode': 'list,form',
|
||
|
}
|
||
|
return result
|
||
|
|
||
|
def action_view_children_plans(self):
|
||
|
result = {
|
||
|
"type": "ir.actions.act_window",
|
||
|
"res_model": "account.analytic.plan",
|
||
|
"domain": [('parent_id', '=', self.id)],
|
||
|
"context": {'default_parent_id': self.id,
|
||
|
'default_color': self.color},
|
||
|
"name": _("Analytical Plans"),
|
||
|
'view_mode': 'list,form',
|
||
|
}
|
||
|
return result
|
||
|
|
||
|
@api.model
|
||
|
def get_relevant_plans(self, **kwargs):
|
||
|
""" Returns the list of plans that should be available.
|
||
|
This list is computed based on the applicabilities of root plans. """
|
||
|
record_account_ids = kwargs.get('existing_account_ids', [])
|
||
|
project_plan, other_plans = self.env['account.analytic.plan']._get_all_plans()
|
||
|
root_plans = (project_plan + other_plans).filtered(lambda p: (
|
||
|
p.all_account_count > 0
|
||
|
and not p.parent_id
|
||
|
and p._get_applicability(**kwargs) != 'unavailable'
|
||
|
))
|
||
|
# If we have accounts that are already selected (before the applicability rules changed or from a model),
|
||
|
# we want the plans that were unavailable to be shown in the list (and in optional, because the previous
|
||
|
# percentage could be different from 0)
|
||
|
forced_plans = self.env['account.analytic.account'].browse(record_account_ids).exists().mapped(
|
||
|
'root_plan_id') - root_plans
|
||
|
return [
|
||
|
{
|
||
|
"id": plan.id,
|
||
|
"name": plan.name,
|
||
|
"color": plan.color,
|
||
|
"applicability": plan._get_applicability(**kwargs) if plan in root_plans else 'optional',
|
||
|
"all_account_count": plan.all_account_count
|
||
|
}
|
||
|
for plan in (root_plans + forced_plans).sorted('sequence')
|
||
|
]
|
||
|
|
||
|
def _get_applicability(self, **kwargs):
|
||
|
""" Returns the applicability of the best applicability line or the default applicability """
|
||
|
self.ensure_one()
|
||
|
if 'applicability' in kwargs:
|
||
|
# For models for example, we want all plans to be visible, so we force the applicability
|
||
|
return kwargs['applicability']
|
||
|
else:
|
||
|
score = 0
|
||
|
applicability = self.default_applicability
|
||
|
for applicability_rule in self.applicability_ids.filtered(lambda rule: rule.company_id == self.env.company):
|
||
|
score_rule = applicability_rule._get_score(**kwargs)
|
||
|
if score_rule > score:
|
||
|
applicability = applicability_rule.applicability
|
||
|
score = score_rule
|
||
|
return applicability
|
||
|
|
||
|
def unlink(self):
|
||
|
# Remove the dynamic field created with the plan (see `_inverse_name`)
|
||
|
self._find_plan_column().unlink()
|
||
|
return super().unlink()
|
||
|
|
||
|
def _find_plan_column(self):
|
||
|
return self.env['ir.model.fields'].sudo().search([
|
||
|
('name', 'in', [plan._strict_column_name() for plan in self]),
|
||
|
('model', '=', 'account.analytic.line'),
|
||
|
])
|
||
|
|
||
|
def _sync_plan_column(self):
|
||
|
# Create/delete a new field/column on analytic lines for this plan, and keep the name in sync.
|
||
|
for plan in self:
|
||
|
prev = plan._find_plan_column()
|
||
|
if plan.parent_id and prev:
|
||
|
prev.unlink()
|
||
|
elif prev:
|
||
|
prev.field_description = plan.name
|
||
|
elif not plan.parent_id:
|
||
|
column = plan._strict_column_name()
|
||
|
self.env['ir.model.fields'].with_context(update_custom_fields=True).sudo().create({
|
||
|
'name': column,
|
||
|
'field_description': plan.name,
|
||
|
'state': 'manual',
|
||
|
'model': 'account.analytic.line',
|
||
|
'model_id': self.env['ir.model']._get_id('account.analytic.line'),
|
||
|
'ttype': 'many2one',
|
||
|
'relation': 'account.analytic.account',
|
||
|
})
|
||
|
tablename = self.env['account.analytic.line']._table
|
||
|
indexname = make_index_name(tablename, column)
|
||
|
create_index(self.env.cr, indexname, tablename, [column], 'btree', f'{column} IS NOT NULL')
|
||
|
|
||
|
|
||
|
class AccountAnalyticApplicability(models.Model):
|
||
|
_name = 'account.analytic.applicability'
|
||
|
_description = "Analytic Plan's Applicabilities"
|
||
|
|
||
|
analytic_plan_id = fields.Many2one('account.analytic.plan')
|
||
|
business_domain = fields.Selection(
|
||
|
selection=[
|
||
|
('general', 'Miscellaneous'),
|
||
|
],
|
||
|
required=True,
|
||
|
string='Domain',
|
||
|
)
|
||
|
applicability = fields.Selection([
|
||
|
('optional', 'Optional'),
|
||
|
('mandatory', 'Mandatory'),
|
||
|
('unavailable', 'Unavailable'),
|
||
|
],
|
||
|
required=True,
|
||
|
string="Applicability",
|
||
|
)
|
||
|
company_id = fields.Many2one(
|
||
|
'res.company',
|
||
|
string='Company',
|
||
|
default=lambda self: self.env.company,
|
||
|
)
|
||
|
|
||
|
def _get_score(self, **kwargs):
|
||
|
""" Gives the score of an applicability with the parameters of kwargs """
|
||
|
self.ensure_one()
|
||
|
if not kwargs.get('business_domain'):
|
||
|
return 0
|
||
|
else:
|
||
|
return 1 if kwargs.get('business_domain') == self.business_domain else -1
|