From 73fa88c44ae64b2fc93ef1233f1ab8f8dbe2b0cc Mon Sep 17 00:00:00 2001 From: Paul Stroobant Date: Fri, 8 Mar 2024 15:14:46 +0100 Subject: [PATCH] [FIX] account, sale, project, hr, purchase: adding missing updates to analytic accounting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Steps to reproduce issue: 1. Have Sales installed 2. Enable Analytic Accounting and create two Projects 3. Go into both Project Settings and add distinct Analytic Accounts 4. Create a Vendor Bill 5. Add a Product Line and put both accounts in the Analytic Distribution 6. Add an account in another column to one of the existing Analytic Distribution Lines 7. Go to Projects and open the Project Updates of the Project whose account was alone in the Analytic Distribution Line 8. You get the error: > invalid literal for int() with base 10: '[int],[int]' Explanation: With commit odoo@dc696c8ed4850a83b6af721002a90f2944891923, the way IDs are stored changed. They are now computed in a csv string. A part of the Analytic Accounting pipeline didn't get updated after that change, making a lot of the features not working properly. Suggested fix: - We must take into account the multiple analytic plans in any order possible in the ID search. - Because the amount of `account.move.line` can increase rapidly, the new ID search must be indexable. - We reworked the counting query to avoid `account.move` being counted multiple times. opw-3626619 closes odoo/odoo#147664 Related: odoo/enterprise#54311 Signed-off-by: William André (wan) --- .../models/account_analytic_account.py | 53 +++++++++++++----- addons/account/tests/test_account_analytic.py | 54 +++++++++++++++++++ .../models/analytic_distribution_model.py | 14 +++-- addons/analytic/models/analytic_mixin.py | 40 +++++++++----- addons/hr_expense/models/analytic.py | 17 ++++-- .../project_account/models/project_project.py | 15 +++++- .../models/project_project.py | 28 +++++++--- .../models/project_project.py | 36 ++++++++++--- .../models/project_project.py | 9 +++- addons/sale_project/models/project.py | 49 +++++++++++++---- 10 files changed, 255 insertions(+), 60 deletions(-) diff --git a/addons/account/models/account_analytic_account.py b/addons/account/models/account_analytic_account.py index 4f797e5751c..e3021c85505 100644 --- a/addons/account/models/account_analytic_account.py +++ b/addons/account/models/account_analytic_account.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from odoo import api, fields, models, _ +from odoo.tools import SQL class AccountAnalyticAccount(models.Model): @@ -24,18 +25,24 @@ class AccountAnalyticAccount(models.Model): ('move_id.move_type', 'in', sale_types), ]) query.add_where( - 'account_move_line.analytic_distribution ?| %s', - [[str(account_id) for account_id in self.ids]], + SQL( + "%s && %s", + [str(account_id) for account_id in self.ids], + self.env['account.move.line']._query_analytic_accounts(), + ) ) query_string, query_param = query.select( - 'jsonb_object_keys(account_move_line.analytic_distribution) as account_id', - 'COUNT(DISTINCT(account_move_line.move_id)) as move_count', + r"""DISTINCT move_id, (regexp_matches(jsonb_object_keys(account_move_line.analytic_distribution), '\d+', 'g'))[1]::int as account_id""" ) - query_string = f"{query_string} GROUP BY jsonb_object_keys(account_move_line.analytic_distribution)" + query_string = f""" + SELECT account_id, count(move_id) FROM + ({query_string}) distribution + GROUP BY account_id + """ self._cr.execute(query_string, query_param) - data = {int(record.get('account_id')): record.get('move_count') for record in self._cr.dictfetchall()} + data = {res['account_id']: res['count'] for res in self._cr.dictfetchall()} for account in self: account.invoice_count = data.get(account.id, 0) @@ -48,25 +55,37 @@ class AccountAnalyticAccount(models.Model): ('move_id.move_type', 'in', purchase_types), ]) query.add_where( - 'account_move_line.analytic_distribution ?| %s', - [[str(account_id) for account_id in self.ids]], + SQL( + "%s && %s", + [str(account_id) for account_id in self.ids], + self.env['account.move.line']._query_analytic_accounts(), + ) ) query_string, query_param = query.select( - 'jsonb_object_keys(account_move_line.analytic_distribution) as account_id', - 'COUNT(DISTINCT(account_move_line.move_id)) as move_count', + r"""DISTINCT move_id, (regexp_matches(jsonb_object_keys(account_move_line.analytic_distribution), '\d+', 'g'))[1]::int as account_id""" ) - query_string = f"{query_string} GROUP BY jsonb_object_keys(account_move_line.analytic_distribution)" + query_string = f""" + SELECT account_id, count(move_id) FROM + ({query_string}) distribution + GROUP BY account_id + """ self._cr.execute(query_string, query_param) - data = {int(record.get('account_id')): record.get('move_count') for record in self._cr.dictfetchall()} + data = {res['account_id']: res['count'] for res in self._cr.dictfetchall()} for account in self: account.vendor_bill_count = data.get(account.id, 0) def action_view_invoice(self): self.ensure_one() query = self.env['account.move.line']._search([('move_id.move_type', 'in', self.env['account.move'].get_sale_types())]) - query.add_where('analytic_distribution ? %s', [str(self.id)]) + query.add_where( + SQL( + "%s && %s", + [str(self.id)], + self.env['account.move.line']._query_analytic_accounts(), + ) + ) query_string, query_param = query.select('DISTINCT account_move_line.move_id') self._cr.execute(query_string, query_param) move_ids = [line.get('move_id') for line in self._cr.dictfetchall()] @@ -83,7 +102,13 @@ class AccountAnalyticAccount(models.Model): def action_view_vendor_bill(self): self.ensure_one() query = self.env['account.move.line']._search([('move_id.move_type', 'in', self.env['account.move'].get_purchase_types())]) - query.add_where('analytic_distribution ? %s', [str(self.id)]) + query.add_where( + SQL( + "%s && %s", + [str(self.id)], + self.env['account.move.line']._query_analytic_accounts(), + ) + ) query_string, query_param = query.select('DISTINCT account_move_line.move_id') self._cr.execute(query_string, query_param) move_ids = [line.get('move_id') for line in self._cr.dictfetchall()] diff --git a/addons/account/tests/test_account_analytic.py b/addons/account/tests/test_account_analytic.py index 2b46055dc2e..0ae4a2f07a4 100644 --- a/addons/account/tests/test_account_analytic.py +++ b/addons/account/tests/test_account_analytic.py @@ -29,6 +29,13 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon): 'company_id': False, }) + cls.cross_plan = cls.env['account.analytic.plan'].create({'name': 'Cross'}) + cls.analytic_account_c = cls.env['account.analytic.account'].create({ + 'name': 'analytic_account_c', + 'plan_id': cls.cross_plan.id, + 'company_id': False, + }) + def create_invoice(self, partner, product): return self.env['account.move'].create([{ 'move_type': 'out_invoice', @@ -210,3 +217,50 @@ class TestAccountAnalyticAccount(AccountTestInvoicingCommon): invoice.invoice_line_ids.analytic_distribution = {self.analytic_account_b.id: 0.9} invoice.action_post() self.assertEqual(invoice.state, 'posted') + + def test_cross_analytics_computing(self): + + out_invoice = self.env['account.move'].create([{ + 'move_type': 'out_invoice', + 'partner_id': self.partner_a.id, + 'date': '2017-01-01', + 'invoice_date': '2017-01-01', + 'invoice_line_ids': [Command.create({ + 'product_id': self.product_b.id, + 'price_unit': 200.0, + 'analytic_distribution': { + f'{self.analytic_account_a.id},{self.analytic_account_c.id}': 20, + f'{self.analytic_account_a.id},{self.analytic_account_b.id}': 80, + }, + })] + }]) + out_invoice.action_post() + in_invoice = self.env['account.move'].create([{ + 'move_type': 'in_invoice', + 'partner_id': self.partner_b.id, + 'date': '2017-01-01', + 'invoice_date': '2017-01-01', + 'invoice_line_ids': [ + Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 200.0, + 'analytic_distribution': { + f'{self.analytic_account_a.id},{self.analytic_account_b.id}': 100, + }, + }), + Command.create({ + 'product_id': self.product_a.id, + 'price_unit': 200.0, + 'analytic_distribution': { + f'{self.analytic_account_a.id},{self.analytic_account_c.id}': 50, + self.analytic_account_b.id: 50, + }, + }) + ] + }]) + in_invoice.action_post() + + self.analytic_account_a._compute_invoice_count() + self.assertEqual(self.analytic_account_a.invoice_count, 1) + self.analytic_account_a._compute_vendor_bill_count() + self.assertEqual(self.analytic_account_a.vendor_bill_count, 1) diff --git a/addons/analytic/models/analytic_distribution_model.py b/addons/analytic/models/analytic_distribution_model.py index 123776295ae..9be68e0d4cf 100644 --- a/addons/analytic/models/analytic_distribution_model.py +++ b/addons/analytic/models/analytic_distribution_model.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ +from odoo.tools import SQL from odoo.exceptions import UserError @@ -38,15 +39,20 @@ class AccountAnalyticDistributionModel(models.Model): @api.constrains('company_id') def _check_company_accounts(self): - query = """ + """Ensure accounts specific to a company isn't used in any distribution model that wouldn't be specific to the company""" + query = SQL( + """ SELECT model.id FROM account_analytic_distribution_model model JOIN account_analytic_account account - ON model.analytic_distribution ? CAST(account.id AS VARCHAR) - WHERE account.company_id IS NOT NULL + ON ARRAY[account.id::text] && %s + WHERE account.company_id IS NOT NULL AND model.id = ANY(%s) AND (model.company_id IS NULL OR model.company_id != account.company_id) - """ + """, + self._query_analytic_accounts('model'), + self.ids, + ) self.flush_model(['company_id', 'analytic_distribution']) self.env.cr.execute(query) if self.env.cr.dictfetchone(): diff --git a/addons/analytic/models/analytic_mixin.py b/addons/analytic/models/analytic_mixin.py index 4f56de03a81..13c63e286c6 100644 --- a/addons/analytic/models/analytic_mixin.py +++ b/addons/analytic/models/analytic_mixin.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import models, fields, api, _ +from odoo.tools import SQL from odoo.tools.float_utils import float_round, float_compare from odoo.exceptions import UserError, ValidationError @@ -29,9 +30,9 @@ class AnalyticMixin(models.AbstractModel): WHERE table_name=%s ''' self.env.cr.execute(query, [self._table]) if self.env.cr.dictfetchone() and self._fields['analytic_distribution'].store: - query = f""" - CREATE INDEX IF NOT EXISTS {self._table}_analytic_distribution_gin_index - ON {self._table} USING gin(analytic_distribution); + query = fr""" + CREATE INDEX IF NOT EXISTS {self._table}_analytic_distribution_accounts_gin_index + ON {self._table} USING gin(regexp_split_to_array(jsonb_path_query_array(analytic_distribution, '$.keyvalue()."key"')::text, '\D+')); """ self.env.cr.execute(query) super().init() @@ -48,18 +49,33 @@ class AnalyticMixin(models.AbstractModel): pass def _search_analytic_distribution(self, operator, value): - if operator not in ['=', '!=', 'ilike', 'not ilike'] or not isinstance(value, (str, bool)): + if operator == 'in' and isinstance(value, (tuple, list)): + account_ids = value + operator_inselect = 'inselect' + elif operator in ('=', '!=', 'ilike', 'not ilike') and isinstance(value, (str, bool)): + operator_name_search = '=' if operator in ('=', '!=') else 'ilike' + account_ids = list(self.env['account.analytic.account']._name_search(name=value, operator=operator_name_search)) + operator_inselect = 'inselect' if operator in ('=', 'ilike') else 'not inselect' + else: raise UserError(_('Operation not supported')) - operator_name_search = '=' if operator in ('=', '!=') else 'ilike' - account_ids = list(self.env['account.analytic.account']._name_search(name=value, operator=operator_name_search)) - query = f""" + query = SQL( + fr""" SELECT id FROM {self._table} - WHERE analytic_distribution ?| array[%s] - """ - operator_inselect = 'inselect' if operator in ('=', 'ilike') else 'not inselect' - return [('id', operator_inselect, (query, [[str(account_id) for account_id in account_ids]]))] + WHERE %s && %s + """, + [str(account_id) for account_id in account_ids], + self._query_analytic_accounts(), + ) + + return [('id', operator_inselect, query)] + + def _query_analytic_accounts(self, table=False): + return SQL( + r"""regexp_split_to_array(jsonb_path_query_array(%s.analytic_distribution, '$.keyvalue()."key"')::text, '\D+')""", + SQL(table or self._table), + ) @api.model def _search(self, domain, offset=0, limit=None, order=None, access_rights_uid=None): @@ -109,6 +125,6 @@ class AnalyticMixin(models.AbstractModel): def _apply_analytic_distribution_domain(self, domain): return [ - ('analytic_distribution_search', leaf[1], leaf[2]) if len(leaf) == 3 and leaf[0] == 'analytic_distribution' and isinstance(leaf[2], str) else leaf + ('analytic_distribution_search', leaf[1], leaf[2]) if len(leaf) == 3 and leaf[0] == 'analytic_distribution' and isinstance(leaf[2], (str, tuple, list)) else leaf for leaf in domain ] diff --git a/addons/hr_expense/models/analytic.py b/addons/hr_expense/models/analytic.py index e3a21b8a98b..1b223bb38c9 100644 --- a/addons/hr_expense/models/analytic.py +++ b/addons/hr_expense/models/analytic.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ +from odoo.tools import SQL from odoo.exceptions import UserError @@ -28,11 +29,17 @@ class AccountAnalyticAccount(models.Model): @api.ondelete(at_uninstall=False) def _unlink_except_account_in_analytic_distribution(self): - self.env.cr.execute(""" - SELECT id FROM hr_expense - WHERE analytic_distribution::jsonb ?| array[%s] - LIMIT 1 - """, ([str(id) for id in self.ids],)) + self.env.cr.execute( + SQL( + r""" + SELECT id FROM hr_expense + WHERE %s && %s + LIMIT 1 + """, + [str(id) for id in self.ids], + self.env['hr.expense']._query_analytic_accounts(), + ) + ) expense_ids = self.env.cr.fetchall() if expense_ids: raise UserError(_("You cannot delete an analytic account that is used in an expense.")) diff --git a/addons/project_account/models/project_project.py b/addons/project_account/models/project_project.py index 906f5fbd57d..44bc4bf43a3 100644 --- a/addons/project_account/models/project_project.py +++ b/addons/project_account/models/project_project.py @@ -5,6 +5,7 @@ import json from collections import defaultdict from odoo import models, _lt +from odoo.tools import SQL from odoo.tools.misc import OrderedSet @@ -29,7 +30,13 @@ class Project(models.Model): """ This method is used in sale_project and project_purchase. Since project_account is the only common module (except project), we create the method here. """ # calculate the cost of bills without a purchase order query = self.env['account.move.line'].sudo()._search(domain) - query.add_where('account_move_line.analytic_distribution ? %s', [str(self.analytic_account_id.id)]) + query.add_where( + SQL( + "%s && %s", + [str(self.analytic_account_id.id)], + self.env['account.move.line'].sudo()._query_analytic_accounts(), + ) + ) # account_move_line__move_id is the alias of the joined table account_move in the query # we can use it, because of the "move_id.move_type" clause in the domain of the query, which generates the join # this is faster than a search_read followed by a browse on the move_id to retrieve the move_type of each account.move.line @@ -45,7 +52,11 @@ class Project(models.Model): price_subtotal = self.env['res.currency'].browse(moves_read['currency_id']).with_prefetch(currency_ids)._convert( from_amount=moves_read['price_subtotal'], to_currency=self.currency_id, ) - analytic_contribution = moves_read['analytic_distribution'][str(self.analytic_account_id.id)] / 100. + # an analytic account can appear several time in an analytic distribution with different repartition percentage + analytic_contribution = sum( + percentage for ids, percentage in moves_read['analytic_distribution'].items() + if str(self.analytic_account_id.id) in ids.split(',') + ) / 100. move_ids.add(moves_read['move_id']) if moves_read['parent_state'] == 'draft': if moves_read['move_type'] == 'in_invoice': diff --git a/addons/project_hr_expense/models/project_project.py b/addons/project_hr_expense/models/project_project.py index 58ca379a94d..8ca06e78375 100644 --- a/addons/project_hr_expense/models/project_project.py +++ b/addons/project_hr_expense/models/project_project.py @@ -4,6 +4,7 @@ import json from odoo import api, fields, models, _, _lt from odoo.osv import expression +from odoo.tools import SQL from collections import defaultdict class Project(models.Model): @@ -17,15 +18,24 @@ class Project(models.Model): self.expenses_count = 0 return query = self.env['hr.expense']._search([]) - query.add_where('hr_expense.analytic_distribution ?| %s', [[str(account_id) for account_id in self.analytic_account_id.ids]]) + query.add_where( + SQL( + "%s && %s", + [str(account_id) for account_id in self.analytic_account_id.ids], + self.env['hr.expense']._query_analytic_accounts(), + ) + ) query_string, query_param = query.select( - 'jsonb_object_keys(analytic_distribution) as account_id', - 'COUNT(DISTINCT(id)) as expense_count', + r"""DISTINCT id, (regexp_matches(jsonb_object_keys(hr_expense.analytic_distribution), '\d+', 'g'))[1]::int as account_id""" ) - query_string = f'{query_string} GROUP BY jsonb_object_keys(analytic_distribution)' + query_string = f""" + SELECT account_id, count(id) FROM + ({query_string}) distribution + GROUP BY account_id + """ self._cr.execute(query_string, query_param) - data = {int(record.get('account_id')): record.get('expense_count') for record in self._cr.dictfetchall()} + data = {res['account_id']: res['count'] for res in self._cr.dictfetchall()} for project in self: project.expenses_count = data.get(project.analytic_account_id.id, 0) @@ -88,7 +98,13 @@ class Project(models.Model): return {} can_see_expense = with_action and self.user_has_groups('hr_expense.group_hr_expense_team_approver') query = self.env['hr.expense']._search([('state', 'in', ['approved', 'done'])]) - query.add_where('hr_expense.analytic_distribution ? %s', [str(self.analytic_account_id.id)]) + query.add_where( + SQL( + "%s && %s", + [str(self.analytic_account_id.id)], + self.env['hr.expense']._query_analytic_accounts(), + ) + ) query_string, query_param = query.select('currency_id', 'array_agg(id) as ids', 'SUM(untaxed_amount_currency) as untaxed_amount') query_string = f"{query_string} GROUP BY currency_id" self._cr.execute(query_string, query_param) diff --git a/addons/project_purchase/models/project_project.py b/addons/project_purchase/models/project_project.py index 93afb3c7b12..11a2b8ce2f4 100644 --- a/addons/project_purchase/models/project_project.py +++ b/addons/project_purchase/models/project_project.py @@ -5,6 +5,7 @@ import json from odoo import api, fields, models, _, _lt from odoo.osv import expression +from odoo.tools import SQL from odoo.tools.misc import OrderedSet @@ -19,16 +20,25 @@ class Project(models.Model): self.purchase_orders_count = 0 return query = self.env['purchase.order.line']._search([]) - query.add_where('purchase_order_line.analytic_distribution ?| %s', [[str(account_id) for account_id in self.analytic_account_id.ids]]) + query.add_where( + SQL( + "%s && %s", + [str(account_id) for account_id in self.analytic_account_id.ids], + self.env['purchase.order.line']._query_analytic_accounts(), + ) + ) query_string, query_param = query.select( - 'jsonb_object_keys(purchase_order_line.analytic_distribution) as account_id', - 'COUNT(DISTINCT(order_id)) as purchase_order_count', + r"""DISTINCT order_id, (regexp_matches(jsonb_object_keys(purchase_order_line.analytic_distribution), '\d+', 'g'))[1]::int as account_id""" ) - query_string = f"{query_string} GROUP BY jsonb_object_keys(purchase_order_line.analytic_distribution)" + query_string = f""" + SELECT account_id, count(order_id) FROM + ({query_string}) distribution + GROUP BY account_id + """ self._cr.execute(query_string, query_param) - data = {int(record.get('account_id')): record.get('purchase_order_count') for record in self._cr.dictfetchall()} + data = {res['account_id']: res['count'] for res in self._cr.dictfetchall()} for project in self: project.purchase_orders_count = data.get(project.analytic_account_id.id, 0) @@ -38,7 +48,13 @@ class Project(models.Model): def action_open_project_purchase_orders(self): query = self.env['purchase.order.line']._search([]) - query.add_where('purchase_order_line.analytic_distribution ? %s', [str(self.analytic_account_id.id)]) + query.add_where( + SQL( + "%s && %s", + [str(self.analytic_account_id.id)], + self.env['purchase.order.line']._query_analytic_accounts(), + ) + ) query_string, query_param = query.select('order_id') self._cr.execute(query_string, query_param) purchase_order_ids = [pol.get('order_id') for pol in self._cr.dictfetchall()] @@ -129,7 +145,13 @@ class Project(models.Model): ('qty_invoiced', '>', 0), '|', ('qty_to_invoice', '>', 0), ('product_uom_qty', '>', 0), ], order=self.env['purchase.order.line']._order) - query.add_where('purchase_order_line.analytic_distribution ? %s', [str(self.analytic_account_id.id)]) + query.add_where( + SQL( + "%s && %s", + [str(self.analytic_account_id.id)], + self.env['purchase.order.line']._query_analytic_accounts(), + ) + ) query_string, query_param = query.select('"purchase_order_line".id', 'qty_invoiced', 'qty_to_invoice', 'product_uom_qty', 'price_unit', 'purchase_order_line.currency_id', '"purchase_order_line".analytic_distribution') self._cr.execute(query_string, query_param) purchase_order_line_read = [{ diff --git a/addons/project_sale_expense/models/project_project.py b/addons/project_sale_expense/models/project_project.py index 5d74a08ecfa..ab66bd1ec29 100644 --- a/addons/project_sale_expense/models/project_project.py +++ b/addons/project_sale_expense/models/project_project.py @@ -4,6 +4,7 @@ import json from odoo import models, fields +from odoo.tools import SQL from collections import defaultdict @@ -15,7 +16,13 @@ class Project(models.Model): return {} can_see_expense = with_action and self.user_has_groups('hr_expense.group_hr_expense_team_approver') query = self.env['hr.expense']._search([('state', 'in', ['approved', 'done'])]) - query.add_where('hr_expense.analytic_distribution ? %s', [str(self.analytic_account_id.id)]) + query.add_where( + SQL( + "%s && %s", + [str(self.analytic_account_id.id)], + self.env['hr.expense']._query_analytic_accounts(), + ) + ) query_string, query_param = query.select('sale_order_id', 'product_id', 'currency_id', 'array_agg(id) as ids', 'SUM(untaxed_amount_currency) as untaxed_amount_currency') query_string = f"{query_string} GROUP BY sale_order_id, product_id, currency_id" self._cr.execute(query_string, query_param) diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index 1acd12911d3..8e3cd0b65c7 100755 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -100,15 +100,24 @@ class Project(models.Model): def _compute_invoice_count(self): query = self.env['account.move.line']._search([('move_id.move_type', 'in', ['out_invoice', 'out_refund'])]) - query.add_where('analytic_distribution ?| %s', [[str(project.analytic_account_id.id) for project in self]]) + query.add_where( + SQL( + "%s && %s", + [str(project.analytic_account_id.id) for project in self], + self.env['account.move.line']._query_analytic_accounts(), + ) + ) query.order = None query_string, query_param = query.select( - 'jsonb_object_keys(account_move_line.analytic_distribution) as account_id', - 'COUNT(DISTINCT move_id) as move_count', + r"""DISTINCT move_id, (regexp_matches(jsonb_object_keys(account_move_line.analytic_distribution), '\d+', 'g'))[1]::int as account_id""" ) - query_string = f"{query_string} GROUP BY jsonb_object_keys(account_move_line.analytic_distribution)" + query_string = f""" + SELECT account_id, count(move_id) FROM + ({query_string}) distribution + GROUP BY account_id + """ self._cr.execute(query_string, query_param) - data = {int(row.get('account_id')): row.get('move_count') for row in self._cr.dictfetchall()} + data = {res['account_id']: res['count'] for res in self._cr.dictfetchall()} for project in self: project.invoice_count = data.get(project.analytic_account_id.id, 0) @@ -226,7 +235,13 @@ class Project(models.Model): def action_open_project_invoices(self): query = self.env['account.move.line']._search([('move_id.move_type', 'in', ['out_invoice', 'out_refund'])]) - query.add_where('analytic_distribution ? %s', [str(self.analytic_account_id.id)]) + query.add_where( + SQL( + "%s && %s", + [str(self.analytic_account_id.id)], + self.env['account.move.line']._query_analytic_accounts(), + ) + ) query_string, query_param = query.select('DISTINCT move_id') self._cr.execute(query_string, query_param) invoice_ids = [line.get('move_id') for line in self._cr.dictfetchall()] @@ -558,7 +573,13 @@ class Project(models.Model): query = self.env['account.move.line'].sudo()._search( self._get_revenues_items_from_invoices_domain([('id', 'not in', excluded_move_line_ids)]), ) - query.add_where('account_move_line.analytic_distribution ? %s', [str(self.analytic_account_id.id)]) + query.add_where( + SQL( + "%s && %s", + [str(self.analytic_account_id.id)], + self.env['account.move.line']._query_analytic_accounts(), + ) + ) # account_move_line__move_id is the alias of the joined table account_move in the query # we can use it, because of the "move_id.move_type" clause in the domain of the query, which generates the join # this is faster than a search_read followed by a browse on the move_id to retrieve the move_type of each account.move.line @@ -572,7 +593,11 @@ class Project(models.Model): for moves_read in invoices_move_line_read: currency = self.env['res.currency'].browse(moves_read['currency_id']).with_prefetch(currency_ids) price_subtotal = currency._convert(moves_read['price_subtotal'], self.currency_id, self.company_id) - analytic_contribution = moves_read['analytic_distribution'][str(self.analytic_account_id.id)] / 100. + # an analytic account can appear several time in an analytic distribution with different repartition percentage + analytic_contribution = sum( + percentage for ids, percentage in moves_read['analytic_distribution'].items() + if str(self.analytic_account_id.id) in ids.split(',') + ) / 100. move_ids.add(moves_read['move_id']) if moves_read['parent_state'] == 'draft': if moves_read['move_type'] == 'out_invoice': @@ -719,7 +744,13 @@ class Project(models.Model): def action_open_project_vendor_bills(self): query = self.env['account.move.line']._search([('move_id.move_type', 'in', ['in_invoice', 'in_refund'])]) - query.add_where('analytic_distribution ? %s', [str(self.analytic_account_id.id)]) + query.add_where( + SQL( + "%s && %s", + [str(self.analytic_account_id.id)], + self.env['account.move.line']._query_analytic_accounts(), + ) + ) query_string, query_param = query.select('DISTINCT move_id') self._cr.execute(query_string, query_param) vendor_bill_ids = [line.get('move_id') for line in self._cr.dictfetchall()]