From df9e9ec261db91b96ed74fec088298653bcb1f16 Mon Sep 17 00:00:00 2001 From: std-odoo Date: Thu, 12 May 2022 14:17:53 +0000 Subject: [PATCH] [IMP] digest, *: compute the KPI in batch *: account, crm, hr_recruitement, im_livechat, point_of_sale, project, sale_management, website_sale Purpose ======= Now, the KPI are computed based on their `company_id` and not based on the current company. If no company is set on the digest, we compute it based on the current company. The KPIs are computed based on their company. Most of the time, they will all belong to the same company so we can improve the performance by computing them in batch. Task-2827996 See odoo/enterprise/pull/27658 Part-of: odoo/odoo#91945 --- addons/account/models/digest.py | 38 ++++++++---- addons/account/tests/__init__.py | 1 + addons/account/tests/test_digest.py | 66 +++++++++++++++++++++ addons/crm/models/digest.py | 25 +++----- addons/crm/tests/__init__.py | 1 + addons/crm/tests/test_digest.py | 55 +++++++++++++++++ addons/digest/models/digest.py | 74 +++++++++++++++++++---- addons/digest/tests/common.py | 78 +++++++++++++++++++++++++ addons/digest/tests/test_digest.py | 69 +++++++--------------- addons/hr_recruitment/models/digest.py | 13 ++--- addons/im_livechat/models/digest.py | 49 ++++++++-------- addons/im_livechat/tests/__init__.py | 1 + addons/im_livechat/tests/test_digest.py | 63 ++++++++++++++++++++ addons/point_of_sale/models/digest.py | 16 ++--- addons/project/models/digest_digest.py | 15 ++--- addons/rating/models/rating_mixin.py | 2 +- addons/sale_management/models/digest.py | 16 ++--- addons/website_sale/models/digest.py | 18 +++--- 18 files changed, 445 insertions(+), 155 deletions(-) create mode 100644 addons/account/tests/test_digest.py create mode 100644 addons/crm/tests/test_digest.py create mode 100644 addons/digest/tests/common.py create mode 100644 addons/im_livechat/tests/test_digest.py diff --git a/addons/account/models/digest.py b/addons/account/models/digest.py index cb850ed3640..b71cc6979a6 100644 --- a/addons/account/models/digest.py +++ b/addons/account/models/digest.py @@ -14,19 +14,33 @@ class Digest(models.Model): def _compute_kpi_account_total_revenue_value(self): if not self.env.user.has_group('account.group_account_invoice'): raise AccessError(_("Do not have access, skip this data for user's digest email")) + + start, end, companies = self._get_kpi_compute_parameters() + + self._cr.execute(""" + SELECT line.company_id AS company, -SUM(line.balance) AS total + FROM account_move_line line + JOIN account_move move + ON move.id = line.move_id + JOIN account_account account + ON account.id = line.account_id + WHERE line.company_id = ANY(%s) + AND line.date > %s::DATE + AND line.date <= %s::DATE + AND account.internal_group = 'income' + AND move.state = 'posted' + GROUP BY line.company_id + """, [companies.ids, start, end]) + + result = self._cr.dictfetchall() + total_per_companies = { + values['company']: values['total'] + for values in result + } + for record in self: - start, end, company = record._get_kpi_compute_parameters() - self._cr.execute(''' - SELECT -SUM(line.balance) - FROM account_move_line line - JOIN account_move move ON move.id = line.move_id - JOIN account_account account ON account.id = line.account_id - WHERE line.company_id = %s AND line.date > %s::DATE AND line.date <= %s::DATE - AND account.internal_group = 'income' - AND move.state = 'posted' - ''', [company.id, start, end]) - query_res = self._cr.fetchone() - record.kpi_account_total_revenue_value = query_res and query_res[0] or 0.0 + company = record.company_id or self.env.company + record.kpi_account_total_revenue_value = total_per_companies.get(company.id, 0) def _compute_kpis_actions(self, company, user): res = super(Digest, self)._compute_kpis_actions(company, user) diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py index c5e7dfc81eb..7882338da67 100644 --- a/addons/account/tests/__init__.py +++ b/addons/account/tests/__init__.py @@ -22,6 +22,7 @@ from . import test_account_invoice_report from . import test_account_move_line_tax_details from . import test_account_journal_dashboard from . import test_chart_template +from . import test_digest from . import test_fiscal_position from . import test_sequence_mixin from . import test_tax diff --git a/addons/account/tests/test_digest.py b/addons/account/tests/test_digest.py new file mode 100644 index 00000000000..79288673b77 --- /dev/null +++ b/addons/account/tests/test_digest.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.digest.tests.common import TestDigestCommon +from odoo.tools import mute_logger +from odoo.tests import tagged + + +@tagged('post_install') +class TestAccountDigest(TestDigestCommon): + + @classmethod + @mute_logger('odoo.models.unlink') + def setUpClass(cls): + super().setUpClass() + account1 = cls.env['account.account'].search([('internal_group', '=', 'income')], limit=1) + account2 = cls.env['account.account'].search([('internal_group', '!=', 'income')], limit=1) + cls.env['account.journal'].with_company(cls.company_2).create({ + 'name': 'Test Journal', + 'code': 'code', + 'type': 'general', + }) + + comp2_account, comp2_account2 = cls.env['account.account'].create([{ + 'name': 'Account 1 Company 2', + 'internal_group': 'income', + 'company_id': cls.company_2.id, + 'user_type_id': cls.env.ref('account.data_account_type_revenue').id, + 'code': 'code_account_1_company_2', + }, { + 'name': 'Account 2 Company 2', + 'internal_group': 'expense', + 'company_id': cls.company_2.id, + 'user_type_id': cls.env.ref('account.data_account_type_expenses').id, + 'code': 'code_account_2_company_2', + }]) + + moves = cls.env['account.move'].create({ + 'line_ids': [ + (0, 0, {'debit': 5, 'credit': 0, 'account_id': account1.id}), + (0, 0, {'debit': 0, 'credit': 5, 'account_id': account2.id}), + (0, 0, {'debit': 8, 'credit': 0, 'account_id': account1.id}), + (0, 0, {'debit': 0, 'credit': 8, 'account_id': account2.id}), + ], + }) + + moves |= cls.env['account.move'].with_company(cls.company_2).create({ + 'line_ids': [ + (0, 0, {'debit': 0, 'credit': 2, 'account_id': comp2_account.id}), + (0, 0, {'debit': 2, 'credit': 0, 'account_id': comp2_account2.id}), + ], + }) + + moves.state = 'posted' + + def test_kpi_account_total_revenue_value(self): + self.assertEqual(int(self.digest_1.kpi_account_total_revenue_value), -13) + self.assertEqual(int(self.digest_2.kpi_account_total_revenue_value), -2) + self.assertEqual(int(self.digest_3.kpi_account_total_revenue_value), -13) + + self.digest_3.invalidate_cache() + self.assertEqual( + int(self.digest_3.with_company(self.company_2).kpi_account_total_revenue_value), + -2, + msg='When no company is set, the KPI must be computed based on the current company', + ) diff --git a/addons/crm/models/digest.py b/addons/crm/models/digest.py index def92a4205e..25c4299f358 100644 --- a/addons/crm/models/digest.py +++ b/addons/crm/models/digest.py @@ -16,26 +16,19 @@ class Digest(models.Model): def _compute_kpi_crm_lead_created_value(self): if not self.env.user.has_group('sales_team.group_sale_salesman'): raise AccessError(_("Do not have access, skip this data for user's digest email")) - for record in self: - start, end, company = record._get_kpi_compute_parameters() - record.kpi_crm_lead_created_value = self.env['crm.lead'].search_count([ - ('create_date', '>=', start), - ('create_date', '<', end), - ('company_id', '=', company.id) - ]) + + self._calculate_company_based_kpi('crm.lead', 'kpi_crm_lead_created_value') def _compute_kpi_crm_opportunities_won_value(self): if not self.env.user.has_group('sales_team.group_sale_salesman'): raise AccessError(_("Do not have access, skip this data for user's digest email")) - for record in self: - start, end, company = record._get_kpi_compute_parameters() - record.kpi_crm_opportunities_won_value = self.env['crm.lead'].search_count([ - ('type', '=', 'opportunity'), - ('probability', '=', '100'), - ('date_closed', '>=', start), - ('date_closed', '<', end), - ('company_id', '=', company.id) - ]) + + self._calculate_company_based_kpi( + 'crm.lead', + 'kpi_crm_opportunities_won_value', + date_field='date_closed', + additional_domain=[('type', '=', 'opportunity'), ('probability', '=', '100')], + ) def _compute_kpis_actions(self, company, user): res = super(Digest, self)._compute_kpis_actions(company, user) diff --git a/addons/crm/tests/__init__.py b/addons/crm/tests/__init__.py index 7d228158f15..f6a9231caa2 100644 --- a/addons/crm/tests/__init__.py +++ b/addons/crm/tests/__init__.py @@ -14,6 +14,7 @@ from . import test_crm_lead_multicompany from . import test_crm_lead_smart_calendar from . import test_crm_ui from . import test_crm_pls +from . import test_digest from . import test_performances from . import test_res_partner from . import test_sales_team_ui diff --git a/addons/crm/tests/test_digest.py b/addons/crm/tests/test_digest.py new file mode 100644 index 00000000000..06f3443cbd3 --- /dev/null +++ b/addons/crm/tests/test_digest.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, timedelta + +from odoo.addons.digest.tests.common import TestDigestCommon +from odoo.tools import mute_logger + + +class TestCrmDigest(TestDigestCommon): + @classmethod + @mute_logger('odoo.models.unlink') + def setUpClass(cls): + super().setUpClass() + + cls.env['crm.lead'].search([]).unlink() + + cls.env['crm.lead'].create([{ + 'name': 'Lead 1', + 'company_id': cls.company_1.id, + 'probability': 100, + 'type': 'opportunity', + 'date_closed': datetime.now(), + }, { + 'name': 'Lead 2', + 'company_id': cls.company_1.id, + 'probability': 90, + 'type': 'opportunity', + 'date_closed': datetime.now(), + }, { + 'name': 'Lead 3', + 'company_id': False, + 'probability': 100, + 'type': 'opportunity', + 'date_closed': datetime.now(), + }, { + 'name': 'Lead 4', + 'company_id': cls.company_1.id, + 'probability': 100, + 'type': 'opportunity', + 'date_closed': datetime.now() - timedelta(days=700), + }]) + + def test_kpi_crm_lead_created_value(self): + self.assertEqual(self.digest_1.kpi_crm_lead_created_value, 3) + self.assertEqual(self.digest_2.kpi_crm_lead_created_value, 0, + msg='This digest is in a different company') + self.assertEqual(self.digest_3.kpi_crm_lead_created_value, 3, + msg='This digest has no company, should take the current one') + + self.digest_3.invalidate_recordset() + self.assertEqual( + self.digest_3.with_company(self.company_2).kpi_crm_lead_created_value, + 0, + ) diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py index 8717bd5d16c..897cde34878 100644 --- a/addons/digest/models/digest.py +++ b/addons/digest/models/digest.py @@ -12,6 +12,7 @@ from werkzeug.urls import url_join from odoo import api, fields, models, tools, _ from odoo.addons.base.models.ir_mail_server import MailDeliveryException from odoo.exceptions import AccessError +from odoo.osv import expression from odoo.tools.float_utils import float_round _logger = logging.getLogger(__name__) @@ -55,20 +56,33 @@ class Digest(models.Model): digest.available_fields = ', '.join(kpis_values_fields) def _get_kpi_compute_parameters(self): - return fields.Datetime.to_string(self._context.get('start_datetime')), fields.Datetime.to_string(self._context.get('end_datetime')), self.env.company + """Get the parameters used to computed the KPI value.""" + companies = self.company_id + if any(not digest.company_id for digest in self): + # No company: we will use the current company to compute the KPIs + companies |= self.env.company + + return ( + fields.Datetime.to_string(self.env.context.get('start_datetime')), + fields.Datetime.to_string(self.env.context.get('end_datetime')), + companies, + ) def _compute_kpi_res_users_connected_value(self): - for record in self: - start, end, company = record._get_kpi_compute_parameters() - user_connected = self.env['res.users'].search_count([('company_id', '=', company.id), ('login_date', '>=', start), ('login_date', '<', end)]) - record.kpi_res_users_connected_value = user_connected + self._calculate_company_based_kpi( + 'res.users', + 'kpi_res_users_connected_value', + date_field='login_date', + ) def _compute_kpi_mail_message_total_value(self): - discussion_subtype_id = self.env.ref('mail.mt_comment').id - for record in self: - start, end, company = record._get_kpi_compute_parameters() - total_messages = self.env['mail.message'].search_count([('create_date', '>=', start), ('create_date', '<', end), ('subtype_id', '=', discussion_subtype_id), ('message_type', 'in', ['comment', 'email'])]) - record.kpi_mail_message_total_value = total_messages + start, end, __ = self._get_kpi_compute_parameters() + self.kpi_mail_message_total_value = self.env['mail.message'].search_count([ + ('create_date', '>=', start), + ('create_date', '<', end), + ('subtype_id', '=', self.env.ref('mail.mt_comment').id), + ('message_type', 'in', ('comment', 'email')), + ]) @api.onchange('periodicity') def _onchange_periodicity(self): @@ -275,6 +289,9 @@ class Digest(models.Model): if self._fields['%s_value' % field_name].type == 'monetary': converted_amount = tools.format_decimalized_amount(compute_value) compute_value = self._format_currency_amount(converted_amount, company.currency_id) + elif self._fields['%s_value' % field_name].type == 'float': + compute_value = "%.2f" % compute_value + kpi_values['kpi_col%s' % (col_index + 1)].update({ 'value': compute_value, 'margin': margin, @@ -374,6 +391,43 @@ class Digest(models.Model): # FORMATTING / TOOLS # ------------------------------------------------------------ + def _calculate_company_based_kpi(self, model, digest_kpi_field, date_field='create_date', + additional_domain=None, sum_field=None): + """Generic method that computes the KPI on a given model. + + :param model: Model on which we will compute the KPI + This model must have a "company_id" field + :param digest_kpi_field: Field name on which we will write the KPI + :param date_field: Field used for the date range + :param additional_domain: Additional domain + :param sum_field: Field to sum to obtain the KPI, + if None it will count the number of records + """ + start, end, companies = self._get_kpi_compute_parameters() + + base_domain = [ + ('company_id', 'in', companies.ids), + (date_field, '>=', start), + (date_field, '<', end), + ] + + if additional_domain: + base_domain = expression.AND([base_domain, additional_domain]) + + values = self.env[model]._read_group( + domain=base_domain, + fields=[f'{sum_field}:sum'] if sum_field else None, + groupby=['company_id'], + ) + + values_per_company = { + value['company_id'][0]: value[sum_field] if sum_field else value['company_id_count'] + for value in values + } + for digest in self: + company = digest.company_id or self.env.company + digest[digest_kpi_field] = values_per_company.get(company.id, 0) + def _get_kpi_fields(self): return [field_name for field_name, field in self._fields.items() if field.type == 'boolean' and field_name.startswith(('kpi_', 'x_kpi_', 'x_studio_kpi_')) and self[field_name] diff --git a/addons/digest/tests/common.py b/addons/digest/tests/common.py new file mode 100644 index 00000000000..80b3a2542af --- /dev/null +++ b/addons/digest/tests/common.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import itertools +import random + +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from odoo import fields +from odoo.addons.mail.tests import common as mail_test + + +class TestDigestCommon(mail_test.MailCommon): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.company_1 = cls.env.company + cls.company_2 = cls.env['res.company'].create({'name': 'Digest Company 2'}) + + context = { + 'start_datetime': datetime.now() - relativedelta(days=1), + 'end_datetime': datetime.now() + relativedelta(days=1), + } + + cls.all_digests = cls.env['digest.digest'].with_context(context).create([{ + 'name': 'Digest 1', + 'company_id': cls.env.company.id, + 'kpi_mail_message_total': True, + 'kpi_res_users_connected': True, + 'periodicity': 'daily', + }, { + 'name': 'Digest 2', + 'company_id': cls.company_2.id, + }, { + 'name': 'Digest 3', + 'company_id': False, + }]) + + cls.digest_1, cls.digest_2, cls.digest_3 = cls.all_digests + + @classmethod + def _setup_messages(cls): + """ Remove all existing messages, then create a bunch of them on random + partners with the correct types in correct time-bucket: + + - 3 in the previous 24h + - 5 more in the 6 days before that for a total of 8 in the previous week + - 7 more in the 20 days before *that* (because digest doc lies and is + based around weeks and months not days), for a total of 15 in the + previous month + """ + # regular employee can't necessarily access "private" addresses + partners = cls.env['res.partner'].search([('type', '!=', 'private')]) + messages = cls.env['mail.message'] + counter = itertools.count() + + now = fields.Datetime.now() + for count, (low, high) in [ + (3, (0 * 24, 1 * 24)), + (5, (1 * 24, 7 * 24)), + (7, (7 * 24, 27 * 24)), + ]: + for __ in range(count): + create_date = now - relativedelta(hours=random.randint(low + 1, high - 1)) + messages += random.choice(partners).message_post( + author_id=cls.partner_admin.id, + body=f"Awesome Partner! ({next(counter)})", + email_from=cls.partner_admin.email_formatted, + message_type='comment', + subtype_xmlid='mail.mt_comment', + # adjust top and bottom by 1h to avoid overlapping with the + # range limit and dropping out of the digest's selection thing + create_date=create_date, + ) + cls.env.flush_all() diff --git a/addons/digest/tests/test_digest.py b/addons/digest/tests/test_digest.py index 920e03e16f6..c21de08a11d 100644 --- a/addons/digest/tests/test_digest.py +++ b/addons/digest/tests/test_digest.py @@ -1,21 +1,18 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import itertools -import random - from dateutil.relativedelta import relativedelta from lxml import html from werkzeug.urls import url_encode, url_join from odoo import fields, SUPERUSER_ID from odoo.addons.base.tests.common import HttpCaseWithUserDemo -from odoo.addons.mail.tests import common as mail_test +from odoo.addons.digest.tests.common import TestDigestCommon from odoo.tests import tagged from odoo.tests.common import users -class TestDigest(mail_test.MailCommon): +class TestDigest(TestDigestCommon): @classmethod def setUpClass(cls): @@ -34,51 +31,27 @@ class TestDigest(mail_test.MailCommon): # clean logs so that town down is activated cls.env['res.users.log'].search([('create_uid', 'in', (cls.user_admin + cls.user_employee).ids)]).unlink() - cls.test_digest = cls.env['digest.digest'].create({ - 'kpi_mail_message_total': True, - 'kpi_res_users_connected': True, - 'name': "My Digest", - 'periodicity': 'daily', - }) + @users('admin') + def test_digest_kpi_res_users_connected_value(self): + self.env['res.users.log'].search([]).unlink() + # Sanity check + initial_values = self.all_digests.mapped('kpi_res_users_connected_value') + self.assertEqual(initial_values, [0, 0, 0]) - @classmethod - def _setup_messages(cls): - """ Remove all existing messages, then create a bunch of them on random - partners with the correct types in correct time-bucket: + self.env['res.users.log'].with_user(self.user_employee).create({}) + self.env['res.users.log'].with_user(self.user_admin).create({}) - - 3 in the previous 24h - - 5 more in the 6 days before that for a total of 8 in the previous week - - 7 more in the 20 days before *that* (because digest doc lies and is - based around weeks and months not days), for a total of 15 in the - previous month - """ - # regular employee can't necessarily access "private" addresses - partners = cls.env['res.partner'].search([('type', '!=', 'private')]) - messages = cls.env['mail.message'] - counter = itertools.count() + self.all_digests.invalidate_recordset() - now = fields.Datetime.now() - for count, (low, high) in [(3, (0 * 24, 1 * 24)), - (5, (1 * 24, 7 * 24)), - (7, (7 * 24, 27 * 24)), - ]: - for _ in range(count): - create_date = now - relativedelta(hours=random.randint(low + 1, high - 1)) - messages += random.choice(partners).message_post( - author_id=cls.partner_admin.id, - body=f"Awesome Partner! ({next(counter)})", - email_from=cls.partner_admin.email_formatted, - message_type='comment', - subtype_xmlid='mail.mt_comment', - # adjust top and bottom by 1h to avoid overlapping with the - # range limit and dropping out of the digest's selection thing - create_date=create_date, - ) - cls.env.flush_all() + self.assertEqual(self.digest_1.kpi_res_users_connected_value, 2) + self.assertEqual(self.digest_2.kpi_res_users_connected_value, 0, + msg='This KPI is in an other company') + self.assertEqual(self.digest_3.kpi_res_users_connected_value, 2, + msg='This KPI has no company, should take the current one') @users('admin') def test_digest_numbers(self): - digest = self.env['digest.digest'].browse(self.test_digest.ids) + digest = self.env['digest.digest'].browse(self.digest_1.ids) digest._action_subscribe_users(self.user_employee) # digest creates its mails in auto_delete mode so we need to capture @@ -102,7 +75,7 @@ class TestDigest(mail_test.MailCommon): @users('admin') def test_digest_subscribe(self): - digest_user = self.test_digest.with_user(self.user_employee) + digest_user = self.digest_1.with_user(self.user_employee) self.assertFalse(digest_user.is_subscribed) # subscribe a user so at least one mail gets sent @@ -115,7 +88,7 @@ class TestDigest(mail_test.MailCommon): @users('admin') def test_digest_tone_down(self): - digest = self.env['digest.digest'].browse(self.test_digest.ids) + digest = self.env['digest.digest'].browse(self.digest_1.ids) digest._action_subscribe_users(self.user_employee) # initial data @@ -157,7 +130,7 @@ class TestDigest(mail_test.MailCommon): """, }) with self.mock_mail_gateway(): - self.test_digest._action_send_to_user(self.user_employee) + self.digest_1._action_send_to_user(self.user_employee) self.assertEqual(len(self._new_mails), 1, "A new Email should have been created") sent_mail_body = html.fromstring(self._new_mails.body_html) values_to_check = [ @@ -173,7 +146,7 @@ class TestDigest(mail_test.MailCommon): @users('admin') def test_digest_tone_down_wlogs(self): - digest = self.env['digest.digest'].browse(self.test_digest.ids) + digest = self.env['digest.digest'].browse(self.digest_1.ids) digest._action_subscribe_users(self.user_employee) # initial data diff --git a/addons/hr_recruitment/models/digest.py b/addons/hr_recruitment/models/digest.py index f7072815a18..7a6db70efca 100644 --- a/addons/hr_recruitment/models/digest.py +++ b/addons/hr_recruitment/models/digest.py @@ -14,14 +14,11 @@ class Digest(models.Model): def _compute_kpi_hr_recruitment_new_colleagues_value(self): if not self.env.user.has_group('hr_recruitment.group_hr_recruitment_user'): raise AccessError(_("Do not have access, skip this data for user's digest email")) - for record in self: - start, end, company = record._get_kpi_compute_parameters() - new_colleagues = self.env['hr.employee'].search_count([ - ('create_date', '>=', start), - ('create_date', '<', end), - ('company_id', '=', company.id) - ]) - record.kpi_hr_recruitment_new_colleagues_value = new_colleagues + + self._calculate_company_based_kpi( + 'hr.employee', + 'kpi_hr_recruitment_new_colleagues_value', + ) def _compute_kpis_actions(self, company, user): res = super(Digest, self)._compute_kpis_actions(company, user) diff --git a/addons/im_livechat/models/digest.py b/addons/im_livechat/models/digest.py index 7070f4fde6e..7ebf52070d0 100644 --- a/addons/im_livechat/models/digest.py +++ b/addons/im_livechat/models/digest.py @@ -16,35 +16,34 @@ class Digest(models.Model): def _compute_kpi_livechat_rating_value(self): channels = self.env['mail.channel'].search([('livechat_operator_id', '=', self.env.user.partner_id.id)]) - for record in self: - start, end, company = record._get_kpi_compute_parameters() - domain = [ - ('create_date', '>=', start), ('create_date', '<', end), - ('rated_partner_id', '=', self.env.user.partner_id.id) - ] - ratings = channels.rating_get_grades(domain) - record.kpi_livechat_rating_value = ratings['great'] * 100 / sum(ratings.values()) if sum(ratings.values()) else 0 + start, end, __ = self._get_kpi_compute_parameters() + domain = [ + ('create_date', '>=', start), + ('create_date', '<', end), + ('rated_partner_id', '=', self.env.user.partner_id.id) + ] + ratings = channels.rating_get_grades(domain) + self.kpi_livechat_rating_value = ( + ratings['great'] * 100 / sum(ratings.values()) + if sum(ratings.values()) else 0 + ) def _compute_kpi_livechat_conversations_value(self): - for record in self: - start, end, company = record._get_kpi_compute_parameters() - record.kpi_livechat_conversations_value = self.env['mail.channel'].search_count([ - ('channel_type', '=', 'livechat'), - ('livechat_operator_id', '=', self.env.user.partner_id.id), - ('create_date', '>=', start), ('create_date', '<', end) - ]) + start, end, __ = self._get_kpi_compute_parameters() + self.kpi_livechat_conversations_value = self.env['mail.channel'].search_count([ + ('channel_type', '=', 'livechat'), + ('livechat_operator_id', '=', self.env.user.partner_id.id), + ('create_date', '>=', start), ('create_date', '<', end), + ]) def _compute_kpi_livechat_response_value(self): - for record in self: - start, end, company = record._get_kpi_compute_parameters() - response_time = self.env['im_livechat.report.operator'].sudo()._read_group([ - ('start_date', '>=', start), ('start_date', '<', end), - ('partner_id', '=', self.env.user.partner_id.id)], ['partner_id', 'time_to_answer'], ['partner_id']) - record.kpi_livechat_response_value = sum( - response['time_to_answer'] - for response in response_time - if response['time_to_answer'] > 0 - ) + start, end, __ = self._get_kpi_compute_parameters() + response_time = self.env['im_livechat.report.channel'].sudo()._read_group([ + ('start_date', '>=', start), + ('start_date', '<', end), + ('partner_id', '=', self.env.user.partner_id.id) + ], ['time_to_answer:avg'], []) + self.kpi_livechat_response_value = response_time[0]['time_to_answer'] def _compute_kpis_actions(self, company, user): res = super(Digest, self)._compute_kpis_actions(company, user) diff --git a/addons/im_livechat/tests/__init__.py b/addons/im_livechat/tests/__init__.py index c74b8ea7df9..82d59cc26b2 100644 --- a/addons/im_livechat/tests/__init__.py +++ b/addons/im_livechat/tests/__init__.py @@ -4,6 +4,7 @@ from . import chatbot_common from . import test_chatbot_form_ui from . import test_chatbot_internals +from . import test_digest from . import test_get_mail_channel from . import test_im_livechat_report from . import test_im_livechat_support_page diff --git a/addons/im_livechat/tests/test_digest.py b/addons/im_livechat/tests/test_digest.py new file mode 100644 index 00000000000..2a6113df946 --- /dev/null +++ b/addons/im_livechat/tests/test_digest.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.digest.tests.common import TestDigestCommon +from odoo.tools import mute_logger +from odoo.tests import tagged + + +@tagged('post_install') +class TestLiveChatDigest(TestDigestCommon): + + @classmethod + @mute_logger('odoo.models.unlink') + def setUpClass(cls): + super().setUpClass() + + other_partner = cls.env['res.partner'].create({'name': 'Other Partner'}) + + cls.channels = cls.env['mail.channel'].create([{ + 'name': 'Channel 1', + 'livechat_operator_id': cls.env.user.partner_id.id, + 'channel_type': 'livechat', + }, { + 'name': 'Channel 2', + 'livechat_operator_id': cls.env.user.partner_id.id, + 'channel_type': 'livechat', + }, { + 'name': 'Channel 3', + 'livechat_operator_id': other_partner.id, + 'channel_type': 'livechat', + }]) + + cls.env['rating.rating'].search([]).unlink() + + cls.env['rating.rating'].create([{ + 'rated_partner_id': cls.env.user.partner_id.id, + 'res_id': cls.channels[0].id, + 'res_model_id': cls.env['ir.model']._get('mail.channel').id, + 'consumed': True, + 'rating': 5, + }, { + 'rated_partner_id': cls.env.user.partner_id.id, + 'res_id': cls.channels[0].id, + 'res_model_id': cls.env['ir.model']._get('mail.channel').id, + 'consumed': True, + 'rating': 0, + }, { + 'rated_partner_id': cls.env.user.partner_id.id, + 'res_id': cls.channels[0].id, + 'res_model_id': cls.env['ir.model']._get('mail.channel').id, + 'consumed': True, + 'rating': 3, + }, { + 'rated_partner_id': cls.env.user.partner_id.id, + 'res_id': cls.channels[0].id, + 'res_model_id': cls.env['ir.model']._get('mail.channel').id, + 'consumed': True, + 'rating': 3, + }]) + + def test_kpi_livechat_rating_value(self): + # 1/3 of the ratings have 5/5 note (0 are ignored) + self.assertEqual(round(self.digest_1.kpi_livechat_rating_value, 2), 33.33) diff --git a/addons/point_of_sale/models/digest.py b/addons/point_of_sale/models/digest.py index 21a65cc4fea..6834b50299d 100644 --- a/addons/point_of_sale/models/digest.py +++ b/addons/point_of_sale/models/digest.py @@ -14,14 +14,14 @@ class Digest(models.Model): def _compute_kpi_pos_total_value(self): if not self.env.user.has_group('point_of_sale.group_pos_user'): raise AccessError(_("Do not have access, skip this data for user's digest email")) - for record in self: - start, end, company = record._get_kpi_compute_parameters() - record.kpi_pos_total_value = sum(self.env['pos.order'].search([ - ('date_order', '>=', start), - ('date_order', '<', end), - ('state', 'not in', ['draft', 'cancel', 'invoiced']), - ('company_id', '=', company.id) - ]).mapped('amount_total')) + + self._calculate_company_based_kpi( + 'pos.order', + 'kpi_pos_total_value', + date_field='date_order', + additional_domain=[('state', 'not in', ['draft', 'cancel', 'invoiced'])], + sum_field='amount_total', + ) def _compute_kpis_actions(self, company, user): res = super(Digest, self)._compute_kpis_actions(company, user) diff --git a/addons/project/models/digest_digest.py b/addons/project/models/digest_digest.py index 013722e35a0..e125de3b469 100644 --- a/addons/project/models/digest_digest.py +++ b/addons/project/models/digest_digest.py @@ -14,15 +14,12 @@ class Digest(models.Model): def _compute_project_task_opened_value(self): if not self.env.user.has_group('project.group_project_user'): raise AccessError(_("Do not have access, skip this data for user's digest email")) - for record in self: - start, end, company = record._get_kpi_compute_parameters() - record.kpi_project_task_opened_value = self.env['project.task'].search_count([ - ('stage_id.fold', '=', False), - ('create_date', '>=', start), - ('create_date', '<', end), - ('company_id', '=', company.id), - ('project_id', '!=', False), - ]) + + self._calculate_company_based_kpi( + 'project.task', + 'kpi_project_task_opened_value', + additional_domain=[('stage_id.fold', '=', False), ('project_id', '!=', False)], + ) def _compute_kpis_actions(self, company, user): res = super(Digest, self)._compute_kpis_actions(company, user) diff --git a/addons/rating/models/rating_mixin.py b/addons/rating/models/rating_mixin.py index f9dabc1fafa..a638375026e 100644 --- a/addons/rating/models/rating_mixin.py +++ b/addons/rating/models/rating_mixin.py @@ -134,7 +134,7 @@ class RatingMixin(models.AbstractModel): if domain: base_domain += domain rg_data = self.env['rating.rating'].read_group(base_domain, ['rating'], ['rating', 'res_id']) - # init dict with all posible rate value, except 0 (no value for the rating) + # init dict with all possible rate value, except 0 (no value for the rating) values = dict.fromkeys(range(1, 6), 0) for rating_rg in rg_data: rating_val_round = float_round(rating_rg['rating'], precision_digits=1) diff --git a/addons/sale_management/models/digest.py b/addons/sale_management/models/digest.py index ba577ff5b87..97359837bbc 100644 --- a/addons/sale_management/models/digest.py +++ b/addons/sale_management/models/digest.py @@ -14,14 +14,14 @@ class Digest(models.Model): def _compute_kpi_sale_total_value(self): if not self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): raise AccessError(_("Do not have access, skip this data for user's digest email")) - for record in self: - start, end, company = record._get_kpi_compute_parameters() - all_channels_sales = self.env['sale.report']._read_group([ - ('date', '>=', start), - ('date', '<', end), - ('state', 'not in', ['draft', 'cancel', 'sent']), - ('company_id', '=', company.id)], ['price_total'], ['company_id']) - record.kpi_all_sale_total_value = sum([channel_sale['price_total'] for channel_sale in all_channels_sales]) + + self._calculate_company_based_kpi( + 'sale.report', + 'kpi_all_sale_total_value', + date_field='date', + additional_domain=[('state', 'not in', ['draft', 'cancel', 'sent'])], + sum_field='price_total', + ) def _compute_kpis_actions(self, company, user): res = super(Digest, self)._compute_kpis_actions(company, user) diff --git a/addons/website_sale/models/digest.py b/addons/website_sale/models/digest.py index 4d03289c978..b66aace5948 100644 --- a/addons/website_sale/models/digest.py +++ b/addons/website_sale/models/digest.py @@ -14,16 +14,14 @@ class Digest(models.Model): def _compute_kpi_website_sale_total_value(self): if not self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): raise AccessError(_("Do not have access, skip this data for user's digest email")) - for record in self: - start, end, company = record._get_kpi_compute_parameters() - confirmed_website_sales = self.env['sale.order'].search([ - ('date_order', '>=', start), - ('date_order', '<', end), - ('state', 'not in', ['draft', 'cancel', 'sent']), - ('website_id', '!=', False), - ('company_id', '=', company.id) - ]) - record.kpi_website_sale_total_value = sum(confirmed_website_sales.mapped('amount_total')) + + self._calculate_company_based_kpi( + 'sale.order', + 'kpi_website_sale_total_value', + date_field='date_order', + additional_domain=[('state', 'not in', ['draft', 'cancel', 'sent']), ('website_id', '!=', False)], + sum_field='amount_total', + ) def _compute_kpis_actions(self, company, user): res = super(Digest, self)._compute_kpis_actions(company, user)