19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -2,8 +2,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
import itertools
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.exceptions import UserError, RedirectWarning
from odoo.tools import groupby, SQL
class AccountAnalyticAccount(models.Model):
@ -12,6 +14,7 @@ class AccountAnalyticAccount(models.Model):
_description = 'Analytic Account'
_order = 'plan_id, name asc'
_check_company_auto = True
_check_company_domain = models.check_company_domain_parent_of
_rec_names_search = ['name', 'code']
name = fields.Char(
@ -19,6 +22,7 @@ class AccountAnalyticAccount(models.Model):
index='trigram',
required=True,
tracking=True,
translate=True,
)
code = fields.Char(
string='Reference',
@ -31,18 +35,16 @@ class AccountAnalyticAccount(models.Model):
default=True,
tracking=True,
)
plan_id = fields.Many2one(
'account.analytic.plan',
string='Plan',
check_company=True,
required=True,
index=True,
)
root_plan_id = fields.Many2one(
'account.analytic.plan',
string='Root Plan',
check_company=True,
compute="_compute_root_plan",
related="plan_id.root_id",
store=True,
)
color = fields.Integer(
@ -52,7 +54,7 @@ class AccountAnalyticAccount(models.Model):
line_ids = fields.One2many(
'account.analytic.line',
'account_id',
'auto_account_id', # magic link to the right column (plan) by using the context in the view
string="Analytic Lines",
)
@ -62,13 +64,14 @@ class AccountAnalyticAccount(models.Model):
default=lambda self: self.env.company,
)
# use auto_join to speed up name_search call
partner_id = fields.Many2one(
'res.partner',
string='Customer',
auto_join=True,
# use bypass_access to speed up name_search call
bypass_search_access=True,
tracking=True,
check_company=True,
index='btree_not_null',
)
balance = fields.Monetary(
@ -91,101 +94,150 @@ class AccountAnalyticAccount(models.Model):
@api.constrains('company_id')
def _check_company_consistency(self):
analytic_accounts = self.filtered('company_id')
for company, accounts in groupby(self, lambda account: account.company_id):
if company and self.env['account.analytic.line'].sudo().search_count([
('auto_account_id', 'in', [account.id for account in accounts]),
'!', ('company_id', 'child_of', company.id),
], limit=1):
raise UserError(_("You can't change the company of an analytic account that already has analytic items! It's a recipe for an analytical disaster!"))
if not analytic_accounts:
return
self.flush_recordset(['company_id'])
self.env['account.analytic.line'].flush_model(['account_id', 'company_id'])
self._cr.execute('''
SELECT line.account_id
FROM account_analytic_line line
JOIN account_analytic_account account ON line.account_id = account.id
WHERE line.company_id != account.company_id and account.company_id IS NOT NULL
AND account.id IN %s
''', [tuple(self.ids)])
if self._cr.fetchone():
raise UserError(_("You can't set a different company on your analytic account since there are some analytic items linked to it."))
def name_get(self):
res = []
@api.depends('code', 'partner_id')
def _compute_display_name(self):
for analytic in self:
name = analytic.name
if analytic.code:
name = f'[{analytic.code}] {name}'
if analytic.partner_id.commercial_partner_id.name:
name = f'{name} - {analytic.partner_id.commercial_partner_id.name}'
res.append((analytic.id, name))
return res
analytic.display_name = name
def copy_data(self, default=None):
default = dict(default or {})
default.setdefault('name', _("%s (copy)", self.name))
return super().copy_data(default)
vals_list = super().copy_data(default=default)
if 'name' not in default:
for account, vals in zip(self, vals_list):
vals['name'] = _("%s (copy)", account.name)
return vals_list
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
"""
Override read_group to calculate the sum of the non-stored fields that depend on the user context
"""
res = super(AccountAnalyticAccount, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
accounts = self.env['account.analytic.account']
for line in res:
if '__domain' in line:
accounts = self.search(line['__domain'])
if 'balance' in fields:
line['balance'] = sum(accounts.mapped('balance'))
if 'debit' in fields:
line['debit'] = sum(accounts.mapped('debit'))
if 'credit' in fields:
line['credit'] = sum(accounts.mapped('credit'))
return res
def web_read(self, specification: dict[str, dict]) -> list[dict]:
self_context = self
if len(self) == 1:
self_context = self.with_context(analytic_plan_id=self.plan_id.id)
return super(AccountAnalyticAccount, self_context).web_read(specification)
def _read_group_select(self, aggregate_spec, query):
# flag balance/debit/credit as aggregatable, and manually sum the values
# from the records in the group
if aggregate_spec in (
'balance:sum',
'balance:sum_currency',
'debit:sum',
'debit:sum_currency',
'credit:sum',
'credit:sum_currency',
):
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 (
'balance:sum',
'balance:sum_currency',
'debit:sum',
'debit:sum_currency',
'credit:sum',
'credit:sum_currency',
):
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)
return super()._read_group_postprocess_aggregate(aggregate_spec, raw_values)
@api.depends('line_ids.amount')
def _compute_debit_credit_balance(self):
Curr = self.env['res.currency']
analytic_line_obj = self.env['account.analytic.line']
domain = [
('account_id', 'in', self.ids),
('company_id', 'in', [False] + self.env.companies.ids)
]
if self._context.get('from_date', False):
domain.append(('date', '>=', self._context['from_date']))
if self._context.get('to_date', False):
domain.append(('date', '<=', self._context['to_date']))
def convert(amount, from_currency):
return from_currency._convert(
from_amount=amount,
to_currency=self.env.company.currency_id,
company=self.env.company,
date=fields.Date.today(),
)
user_currency = self.env.company.currency_id
credit_groups = analytic_line_obj.read_group(
domain=domain + [('amount', '>=', 0.0)],
fields=['account_id', 'currency_id', 'amount'],
groupby=['account_id', 'currency_id'],
lazy=False,
)
data_credit = defaultdict(float)
for l in credit_groups:
data_credit[l['account_id'][0]] += Curr.browse(l['currency_id'][0])._convert(
l['amount'], user_currency, self.env.company, fields.Date.today())
domain = [('company_id', 'in', [False] + self.env.companies.ids)]
if self.env.context.get('from_date', False):
domain.append(('date', '>=', self.env.context['from_date']))
if self.env.context.get('to_date', False):
domain.append(('date', '<=', self.env.context['to_date']))
debit_groups = analytic_line_obj.read_group(
domain=domain + [('amount', '<', 0.0)],
fields=['account_id', 'currency_id', 'amount'],
groupby=['account_id', 'currency_id'],
lazy=False,
)
data_debit = defaultdict(float)
for l in debit_groups:
data_debit[l['account_id'][0]] += Curr.browse(l['currency_id'][0])._convert(
l['amount'], user_currency, self.env.company, fields.Date.today())
for plan, accounts in self.grouped('plan_id').items():
if not plan:
accounts.debit = accounts.credit = accounts.balance = 0
continue
credit_groups = self.env['account.analytic.line']._read_group(
domain=domain + [(plan._column_name(), 'in', self.ids), ('amount', '>=', 0.0)],
groupby=[plan._column_name(), 'currency_id'],
aggregates=['amount:sum'],
)
data_credit = defaultdict(float)
for account, currency, amount_sum in credit_groups:
data_credit[account.id] += convert(amount_sum, currency)
for account in self:
account.debit = abs(data_debit.get(account.id, 0.0))
account.credit = data_credit.get(account.id, 0.0)
account.balance = account.credit - account.debit
debit_groups = self.env['account.analytic.line']._read_group(
domain=domain + [(plan._column_name(), 'in', self.ids), ('amount', '<', 0.0)],
groupby=[plan._column_name(), 'currency_id'],
aggregates=['amount:sum'],
)
data_debit = defaultdict(float)
for account, currency, amount_sum in debit_groups:
data_debit[account.id] += convert(amount_sum, currency)
@api.depends('plan_id', 'plan_id.parent_path')
def _compute_root_plan(self):
for account in self:
account.root_plan_id = int(account.plan_id.parent_path[:-1].split('/')[0]) if account.plan_id.parent_path else None
for account in accounts:
account.debit = -data_debit.get(account.id, 0.0)
account.credit = data_credit.get(account.id, 0.0)
account.balance = account.credit - account.debit
def _update_accounts_in_analytic_lines(self, new_fname, current_fname, accounts):
if current_fname != new_fname:
domain = [
(new_fname, 'not in', accounts.ids + [False]),
(current_fname, 'in', accounts.ids),
]
if self.env['account.analytic.line'].sudo().search_count(domain, limit=1):
list_view = self.env.ref('analytic.view_account_analytic_line_tree', raise_if_not_found=False)
raise RedirectWarning(
message=_("Whoa there! Making this change would wipe out your current data. Let's avoid that, shall we?"),
action={
'res_model': 'account.analytic.line',
'type': 'ir.actions.act_window',
'domain': domain,
'target': 'new',
'views': [(list_view and list_view.id, 'list')]
},
button_text=_("See them"),
)
self.env.cr.execute(SQL(
"""
UPDATE account_analytic_line
SET %(new_fname)s = %(current_fname)s,
%(current_fname)s = NULL
WHERE %(current_fname)s = ANY(%(account_ids)s)
""",
new_fname=SQL.identifier(new_fname),
current_fname=SQL.identifier(current_fname),
account_ids=accounts.ids,
))
self.env['account.analytic.line'].invalidate_model()
def write(self, vals):
if vals.get('plan_id'):
new_fname = self.env['account.analytic.plan'].browse(vals['plan_id'])._column_name()
for plan, accounts in self.grouped('plan_id').items():
current_fname = plan._column_name()
self._update_accounts_in_analytic_lines(new_fname, current_fname, accounts)
return super().write(vals)