From e1432afd9cfbd148e651fe13cb4459593e72e32d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 28 May 2020 13:28:56 +0000 Subject: [PATCH 01/10] [IMP] mail: add first lightweight support of qweb in rendering mixin PURPOSE Make digest email and tips more appealing. The goals of these tips are * to encourage the adoption of other apps (Did you know ?); * to make Odoo look more fun (Fun tips and tricks, young and dynamic style); * to show social proof and increase trust (emphasis on already existing projects / customers to); SPECIFICATIONS In this commit we simply add a first support of tools to render qweb templates like rendering jinja. Instead of giving the template to render the xml id of the view is passed. This code is meant to be improved in a near future with new use cases that will soon arise. LINKS Task ID 2197417 PR odoo/odoo#51619 --- addons/mail/models/mail_render_mixin.py | 83 +++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/addons/mail/models/mail_render_mixin.py b/addons/mail/models/mail_render_mixin.py index a0d633538dc..ddd32659a03 100644 --- a/addons/mail/models/mail_render_mixin.py +++ b/addons/mail/models/mail_render_mixin.py @@ -183,10 +183,74 @@ class MailRenderMixin(models.AbstractModel): return html + @api.model + def _render_encapsulate(self, layout_xmlid, html, add_context=None, context_record=None): + try: + template = self.env.ref(layout_xmlid, raise_if_not_found=True) + except ValueError: + _logger.warning('QWeb template %s not found when rendering encapsulation template.' % (layout_xmlid)) + else: + record_name = context_record.display_name if context_record else '' + model_description = self.env['ir.model']._get(context_record._name).display_name if context_record else False + template_ctx = { + 'body': html, + 'record_name': record_name, + 'model_description': model_description, + 'company': context_record['company_id'] if (context_record and 'company_id' in context_record) else self.env.company, + 'record': context_record, + } + if add_context: + template_ctx.update(**add_context) + + html = template._render(template_ctx, engine='ir.qweb', minimal_qcontext=True) + html = self.env['mail.render.mixin']._replace_local_links(html) + return html + # ------------------------------------------------------------ # RENDERING # ------------------------------------------------------------ + @api.model + def _render_qweb_eval_context(self): + """ Prepare qweb evaluation context, containing for all rendering + + * ``user``: current user browse record; + * ``ctx```: current context; + * various formatting tools; + """ + render_context = { + 'format_date': lambda date, date_format=False, lang_code=False: format_date(self.env, date, date_format, lang_code), + 'format_datetime': lambda dt, tz=False, dt_format=False, lang_code=False: format_datetime(self.env, dt, tz, dt_format, lang_code), + 'format_amount': lambda amount, currency, lang_code=False: tools.format_amount(self.env, amount, currency, lang_code), + 'format_duration': lambda value: tools.format_duration(value), + 'user': self.env.user, + 'ctx': self._context, + } + return render_context + + @api.model + def _render_template_qweb(self, template_src, model, res_ids, add_context=None): + view = self.env.ref(template_src, raise_if_not_found=False) or self.env['ir.ui.view'] + results = dict.fromkeys(res_ids, u"") + if not view: + return results + + # prepare template variables + variables = self._render_qweb_eval_context() + if add_context: + variables.update(**add_context) + + for record in self.env[model].browse(res_ids): + variables['object'] = record + try: + render_result = view._render(variables, engine='ir.qweb', minimal_qcontext=True) + except Exception as e: + _logger.info("Failed to render template : %s (%d)" % (template_src, view.id), exc_info=True) + raise UserError(_("Failed to render template : %s (%d)") % (template_src, view.id)) + results[record.id] = render_result + + return results + @api.model def _render_jinja_eval_context(self): """ Prepare jinja evaluation context, containing for all rendering @@ -207,7 +271,7 @@ class MailRenderMixin(models.AbstractModel): return render_context @api.model - def _render_template_jinja(self, template_txt, model, res_ids): + def _render_template_jinja(self, template_txt, model, res_ids, add_context=None): """ Render a string-based template on records given by a model and a list of IDs, using jinja. @@ -238,6 +302,9 @@ class MailRenderMixin(models.AbstractModel): # prepare template variables variables = self._render_jinja_eval_context() + if add_context: + variables.update(**add_context) + # TDE CHECKME # records = self.env[model].browse(it for it in res_ids if it) # filter to avoid browsing [None] if any(r is None for r in res_ids): @@ -271,11 +338,12 @@ class MailRenderMixin(models.AbstractModel): return rendered @api.model - def _render_template(self, template_txt, model, res_ids, engine='jinja', post_process=False): + def _render_template(self, template_src, model, res_ids, engine='jinja', add_context=None, post_process=False): """ Render the given string on records designed by model / res_ids using the given rendering engine. Currently only jinja is supported. - :param str template_txt: template text to render + :param str template_src: template text to render (jinja) or xml id of view (qweb) + this could be cleaned but hey, we are in a rush :param str model: model name of records on which we want to perform rendering :param list res_ids: list of ids of records (all belonging to same model) :param string engine: jinja @@ -286,10 +354,13 @@ class MailRenderMixin(models.AbstractModel): """ if not isinstance(res_ids, (list, tuple)): raise ValueError(_('Template rendering should be called only using on a list of IDs.')) - if engine != 'jinja': - raise ValueError(_('Template rendering supports only jinja.')) + if engine not in ('jinja', 'qweb'): + raise ValueError(_('Template rendering supports only jinja or qweb.')) - rendered = self._render_template_jinja(template_txt, model, res_ids) + if engine == 'qweb': + rendered = self._render_template_qweb(template_src, model, res_ids, add_context=add_context) + else: + rendered = self._render_template_jinja(template_src, model, res_ids, add_context=add_context) if post_process: rendered = self._render_template_postprocess(rendered) From 48e94a6eba53c77453e810410647a0bfc45c118a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 3 Jun 2020 09:34:34 +0000 Subject: [PATCH 02/10] [FIX] digest: ensure cron sends all ready digests Currently only digest planned "today" are sent. Which means that digest that missed a cron tick are not sent anymore. We fix that by making the cron run on all digest whose scheduled date is in the past or today. LINKS Task ID 2197417 PR odoo/odoo#51619 --- addons/digest/models/digest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py index 8e5623f4dad..ca2104efb1e 100644 --- a/addons/digest/models/digest.py +++ b/addons/digest/models/digest.py @@ -196,7 +196,7 @@ class Digest(models.Model): @api.model def _cron_send_digest_email(self): - digests = self.search([('next_run_date', '=', fields.Date.today()), ('state', '=', 'activated')]) + digests = self.search([('next_run_date', '<=', fields.Date.today()), ('state', '=', 'activated')]) for digest in digests: try: digest.action_send() From c329c618cf804b668e8b8f8e66052b0413c3d93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 2 Jun 2020 11:59:51 +0000 Subject: [PATCH 03/10] [REF] digest: reorganize code to ease future changes PURPOSE Make digest email and tips more appealing. The goals of these tips are * to encourage the adoption of other apps (Did you know ?); * to make Odoo look more fun (Fun tips and tricks, young and dynamic style); * to show social proof and increase trust (emphasis on already existing projects / customers to); SPECIFICATIONS Slightly reorganize code to ease future changes, notably in KPIs computation that will be made a bit more generic. LINKS Task ID 2197417 PR #51619 --- addons/digest/models/digest.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py index ca2104efb1e..609f7198478 100644 --- a/addons/digest/models/digest.py +++ b/addons/digest/models/digest.py @@ -80,6 +80,10 @@ class Digest(models.Model): vals['next_run_date'] = date.today() + relativedelta(days=3) return super(Digest, self).create(vals) + # ------------------------------------------------------------ + # ACTIONS + # ------------------------------------------------------------ + def action_subscribe(self): if self.env.user not in self.user_ids: self.sudo().user_ids |= self.env.user @@ -101,6 +105,20 @@ class Digest(models.Model): digest.template_id.with_context(user=user).send_mail(digest.id, force_send=True, raise_exception=True, email_values={'email_to': user.email, 'subject': subject}) digest.next_run_date = digest._get_next_run_date() + + @api.model + def _cron_send_digest_email(self): + digests = self.search([('next_run_date', '<=', fields.Date.today()), ('state', '=', 'activated')]) + for digest in digests: + try: + digest.action_send() + except MailDeliveryException as e: + _logger.warning('MailDeliveryException while sending digest %d. Digest is now scheduled for next cron update.') + + # ------------------------------------------------------------ + # KPIS + # ------------------------------------------------------------ + def compute_kpis(self, company, user): self.ensure_one() res = {} @@ -176,6 +194,10 @@ class Digest(models.Model): (start_date + relativedelta(months=-2), start_date + relativedelta(months=-1))), } + # ------------------------------------------------------------ + # FORMATTING / TOOLS + # ------------------------------------------------------------ + def _get_margin_value(self, value, previous_value=0.0): margin = 0.0 if (value != previous_value) and (value != 0.0 and previous_value != 0.0): @@ -193,12 +215,3 @@ class Digest(models.Model): return "%3.2f%s%s" % (amount, unit, suffix) amount /= 1000.0 return "%.2f%s%s" % (amount, 'T', suffix) - - @api.model - def _cron_send_digest_email(self): - digests = self.search([('next_run_date', '<=', fields.Date.today()), ('state', '=', 'activated')]) - for digest in digests: - try: - digest.action_send() - except MailDeliveryException as e: - _logger.warning('MailDeliveryException while sending digest %d. Digest is now scheduled for next cron update.') From add1a04a9451a684b5503d92a07f0ddf37f6921e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 26 May 2020 13:58:55 +0000 Subject: [PATCH 04/10] [REF] digest: make digest template bioutifoul MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PURPOSE Make digest email and tips more appealing. The goals of these tips are * to encourage the adoption of other apps (Did you know ?); * to make Odoo look more fun (Fun tips and tricks, young and dynamic style); * to show social proof and increase trust (emphasis on already existing projects / customers to); SPECIFICATIONS Improve template according to FP specifications, aka Header Tip KPIs Preference text Want to customize? Mobile Tip Footer LINKS Task ID 2197417 PR odoo/odoo#51619 Upgrade PR odoo/upgrade#1256 Co-Authored-By: Elisabeth Dickinson Co-Authored-By: Thibault Delavallée --- addons/digest/__manifest__.py | 5 +- addons/digest/data/digest_data.xml | 111 ++++---- addons/digest/data/digest_template_data.xml | 293 +++++++++++--------- addons/digest/data/digest_tips_data.xml | 59 ++++ addons/digest/models/digest.py | 34 ++- 5 files changed, 302 insertions(+), 200 deletions(-) create mode 100644 addons/digest/data/digest_tips_data.xml diff --git a/addons/digest/__manifest__.py b/addons/digest/__manifest__.py index 024305a0dca..b27fbd29ed7 100644 --- a/addons/digest/__manifest__.py +++ b/addons/digest/__manifest__.py @@ -7,7 +7,7 @@ Send KPI Digests periodically ============================= """, - 'version': '1.0', + 'version': '1.1', 'depends': [ 'mail', 'portal', @@ -15,8 +15,9 @@ Send KPI Digests periodically ], 'data': [ 'security/ir.model.access.csv', - 'data/digest_template_data.xml', 'data/digest_data.xml', + 'data/digest_template_data.xml', + 'data/digest_tips_data.xml', 'data/ir_cron_data.xml', 'data/res_config_settings_data.xml', 'views/digest_views.xml', diff --git a/addons/digest/data/digest_data.xml b/addons/digest/data/digest_data.xml index 375dd660681..67bd019bc00 100644 --- a/addons/digest/data/digest_data.xml +++ b/addons/digest/data/digest_data.xml @@ -1,59 +1,56 @@ - - - - - Weekly Stats in Odoo - - - True - True - - + + - - - Mail: ping colleagues - 1 - -
- % set users = object.env['res.users'].search([('share', '=' , False)], limit=10, order='id desc') - % set channel_id = object.env.ref('mail.channel_all_employees').id - Did you know...? -
You can ping colleagues by tagging them in your messages using "@". They will be instantly notified.
-
-
-
-
- ${', '.join(users.mapped('name'))} signed up. Say hello in the company's discussion channel. -
-
-
-
-
- - Mail: get things done with activities - 7 - -
- Get things done with activities -
You don't have any activity scheduled. Use activities on any business document to schedule meetings, calls and todos.
-
-
-
-
-
-
+ + +
diff --git a/addons/digest/data/digest_template_data.xml b/addons/digest/data/digest_template_data.xml index 2b9e607796d..dc5109c4b56 100644 --- a/addons/digest/data/digest_template_data.xml +++ b/addons/digest/data/digest_template_data.xml @@ -6,103 +6,126 @@ ${'%s: %s' % (ctx.get('user', user).company_id.name, object.name)} ${('%s' % (object.company_id.partner_id.email_formatted or user.email_formatted))|safe} - - -
- % set user = ctx.get('user', user) - % set company = user.company_id - % set data = object.compute_kpis(company, user) - % set tips = object.compute_tips(company, user) - % set kpi_actions = object.compute_kpis_actions(company, user) - % set kpis = data.yesterday.keys() - - - - - - -
- ${object.name} -
${datetime.date.today().strftime('%B %d, %Y')}
-
- -
- Connect -
-
-
-
- % for kpi in kpis: - +
+ % set user = ctx.get('user', user) + % set company = user.company_id + % set data = object.compute_kpis(company, user) + % set tips = object.compute_tips(company, user, tips_count=ctx.get('tips_count', 1)) + % set kpi_actions = object.compute_kpis_actions(company, user) + % set kpis = data.yesterday.keys() +
+ + + + +
+ - +

- ${object.fields_get()[kpi]['string']} - %if kpi in kpi_actions: - - View more - - %endif +
+ + + + + +
+ ${company.name} +
+ ${object.name} + ${datetime.date.today().strftime('%B %d, %Y')} +
+ Connect +
+
+ % if tips: + + +
+ -
- + + +
+ - - + +
- - - - -
- ${data['yesterday'][kpi][kpi]}
- Yesterday - % if data['yesterday'][kpi]['margin'] != 0.0: - - % if data['yesterday'][kpi]['margin'] is greaterthan(0.0): - ▲${"%.2f" % data['yesterday'][kpi]['margin']} % - % endif - % if data['yesterday'][kpi]['margin'] is lessthan(0.0): - ▼${"%.2f" % data['yesterday'][kpi]['margin']} % - % endif - - % endif -
+
+ ${tip | safe} - - - +
- ${data['lastweek'][kpi][kpi]}
- Last 7 Days - % if data['lastweek'][kpi]['margin'] != 0.0: - - % if data['lastweek'][kpi]['margin'] is greaterthan(0.0): - ▲${"%.2f" % data['lastweek'][kpi]['margin']} % - % endif - % if data['lastweek'][kpi]['margin'] is lessthan(0.0): - ▼${"%.2f" % data['lastweek'][kpi]['margin']} % - %endif - +
+
+
+ % endif + + +
+ % for kpi in kpis: + + + + + + + + + + +
+ + + + + + - - -
+ ${object.fields_get()[kpi]['string']} + %if kpi in kpi_actions: + View + %endif +
+ + + - -
+ ${data['yesterday'][kpi][kpi]}
+ Yesterday + % if data['yesterday'][kpi]['margin'] != 0.0: + % if data['yesterday'][kpi]['margin'] is greaterthan(0.0): +
+ ▲ ${"%.2f" % data['yesterday'][kpi]['margin']} +
+ %endif + % if data['yesterday'][kpi]['margin'] is lessthan(0.0): +
+ ▼ ${"%.2f" % data['yesterday'][kpi]['margin']} +
+ %endif %endif
-
- - - + @@ -113,52 +136,62 @@
- ${data['lastmonth'][kpi][kpi]}
- Last 30 Days +
+ ${data['lastweek'][kpi][kpi]}
+ Last 7 Days + % if data['lastweek'][kpi]['margin'] != 0.0: + % if data['lastweek'][kpi]['margin'] is greaterthan(0.0): +
+ ▲ ${"%.2f" % data['lastweek'][kpi]['margin']} +
+ %endif + % if data['lastweek'][kpi]['margin'] is lessthan(0.0): +
+ ▼ ${"%.2f" % data['lastweek'][kpi]['margin']} +
+ %endif + %endif +
+ ${data['lastmonth'][kpi][kpi]}
+ Last 30 Days % if data['lastmonth'][kpi]['margin'] != 0.0: - - % if data['lastmonth'][kpi]['margin'] is greaterthan(0.0): - ▲${"%.2f" % data['lastmonth'][kpi]['margin']} % - % endif - % if data['lastmonth'][kpi]['margin'] is lessthan(0.0): - ▼${"%.2f" % data['lastmonth'][kpi]['margin']} % - %endif - + % if data['lastmonth'][kpi]['margin'] is greaterthan(0.0): +
+ ▲ ${"%.2f" % data['lastmonth'][kpi]['margin']} +
+ %endif + % if data['lastmonth'][kpi]['margin'] is lessthan(0.0): +
+ ▼ ${"%.2f" % data['lastmonth'][kpi]['margin']} +
+ %endif %endif
- % endfor - % if tips: - + % endfor + % if user.has_group('base.group_system'): +
-

-
${tips | safe}
+
+

Want to customize this email? + Choose the metrics you care about

- % endif - - - - - - - -

-
Run your business from anywhere with Odoo Mobile.
-
-
-
-
- - - - -
- % if user.has_group('base.group_system'): -
- Want to customize this email? - Choose the metrics you care about -
-
- % endif -

- - Sent by - Odoo - Unsubscribe - -

-
-
+ % endif +
+ + + + +
+ + + + + +
+ Odoo Mobile + +

Run your business from anywhere with
Odoo Mobile.

+ + + + +
+
+
+ + + + + + + + + + +
${company.name}
Powered by Odoo.
+
+ + ${user.lang} - + diff --git a/addons/digest/data/digest_tips_data.xml b/addons/digest/data/digest_tips_data.xml new file mode 100644 index 00000000000..375dd660681 --- /dev/null +++ b/addons/digest/data/digest_tips_data.xml @@ -0,0 +1,59 @@ + + + + + Weekly Stats in Odoo + + + True + True + + + + + + Mail: ping colleagues + 1 + +
+ % set users = object.env['res.users'].search([('share', '=' , False)], limit=10, order='id desc') + % set channel_id = object.env.ref('mail.channel_all_employees').id + Did you know...? +
You can ping colleagues by tagging them in your messages using "@". They will be instantly notified.
+
+
+
+
+ ${', '.join(users.mapped('name'))} signed up. Say hello in the company's discussion channel. +
+
+
+
+
+ + Mail: get things done with activities + 7 + +
+ Get things done with activities +
You don't have any activity scheduled. Use activities on any business document to schedule meetings, calls and todos.
+
+
+
+
+
+
+ + + Mail: apps videos tour + + 0 + +
+ To reach your full potential with Odoo, have a look at our apps videos and discover new tools to work better and faster. +
+
+
+ +
+
diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py index 609f7198478..4b33ebc7937 100644 --- a/addons/digest/models/digest.py +++ b/addons/digest/models/digest.py @@ -101,10 +101,20 @@ class Digest(models.Model): def action_send(self): for digest in self: for user in digest.user_ids: - subject = '%s: %s' % (user.company_id.name, digest.name) - digest.template_id.with_context(user=user).send_mail(digest.id, force_send=True, raise_exception=True, email_values={'email_to': user.email, 'subject': subject}) + digest._action_send_to_user(user, tips_count=1) digest.next_run_date = digest._get_next_run_date() + def _action_send_to_user(self, user, tips_count=1): + subject = '%s: %s' % (user.company_id.name, self.name) + self.template_id.with_context(user=user, tips_count=tips_count).send_mail( + self.id, + force_send=True, + notif_layout="digest.digest_mail_layout", + raise_exception=True, + email_values={ + 'email_to': user.email, 'subject': subject + } + ) @api.model def _cron_send_digest_email(self): @@ -148,15 +158,17 @@ class Digest(models.Model): res.update({tf_name: kpis}) return res - def compute_tips(self, company, user): - tip = self.env['digest.tip'].search([('user_ids', '!=', user.id), '|', ('group_id', 'in', user.groups_id.ids), ('group_id', '=', False)], limit=1) - if not tip: - return False - tip.user_ids += user - body = tools.html_sanitize(tip.tip_description) - # FIXME: sanitize ? links ? - tip_description = self.env['mail.render.mixin']._render_template(body, 'digest.tip', self.ids)[self.id] - return tip_description + def compute_tips(self, company, user, tips_count=1): + tips = self.env['digest.tip'].search([ + ('user_ids', '!=', user.id), + '|', ('group_id', 'in', user.groups_id.ids), ('group_id', '=', False) + ], limit=tips_count) + tip_descriptions = [ + self.env['mail.render.mixin']._render_template(tools.html_sanitize(tip.tip_description), 'digest.tip', tip.ids, post_process=True)[tip.id] + for tip in tips + ] + # tip.user_ids += user + return tip_descriptions def compute_kpis_actions(self, company, user): """ Give an optional action to display in digest email linked to some KPIs. From 14abd4fdf714ce280ab07998073328eb4d76bc09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 28 May 2020 10:38:32 +0000 Subject: [PATCH 05/10] [IMP] digest: improve digest by adding daily sending and preferences PURPOSE Make digest email and tips more appealing. The goals of these tips are * to encourage the adoption of other apps (Did you know ?); * to make Odoo look more fun (Fun tips and tricks, young and dynamic style); * to show social proof and increase trust (emphasis on already existing projects / customers to); SPECIFICATIONS Add a daily digest option, allowing to send digests on a daily basis. It will be the default settings to help users coming back to odoo. Add a slowdown heuristics. If not any user targeted by a daily digest logs himself within 3 days, digest is slowed down to a weekly setting to avoid spam. Add a preference section in template, to hold notably links to some configuration steps sent mainly for admin / CEOs. Improve access on models. LINKS Task ID 2197417 PR odoo/odoo#51619 --- addons/digest/controllers/portal.py | 25 ++++++++- addons/digest/data/digest_data.xml | 12 +++++ addons/digest/data/digest_template_data.xml | 12 +++++ addons/digest/data/digest_tips_data.xml | 5 +- addons/digest/models/digest.py | 57 +++++++++++++++++---- addons/digest/security/ir.model.access.csv | 2 +- 6 files changed, 99 insertions(+), 14 deletions(-) diff --git a/addons/digest/controllers/portal.py b/addons/digest/controllers/portal.py index 0e7f8a5b4e5..451956a7bfa 100644 --- a/addons/digest/controllers/portal.py +++ b/addons/digest/controllers/portal.py @@ -1,15 +1,36 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from werkzeug.exceptions import Forbidden +from werkzeug.urls import url_encode + +from odoo import _ from odoo.http import Controller, request, route class DigestController(Controller): @route('/digest//unsubscribe', type='http', website=True, auth='user') - def digest_unsubscribe(self, digest_id, **post): - digest = request.env['digest.digest'].sudo().browse(digest_id) + def digest_unsubscribe(self, digest_id): + digest = request.env['digest.digest'].browse(digest_id).exists() digest.action_unsubcribe() return request.render('digest.portal_digest_unsubscribed', { 'digest': digest, }) + + @route('/digest//set_periodicity', type='http', website=True, auth='user') + def digest_set_periodicity(self, digest_id, periodicity='weekly'): + if not request.env.user.has_group('base.group_erp_manager'): + raise Forbidden() + if periodicity not in ('daily', 'weekly', 'monthly', 'quarterly'): + raise ValueError(_('Invalid periodicity set on digest')) + + digest = request.env['digest.digest'].browse(digest_id).exists() + digest.action_set_periodicity(periodicity) + + url_params = { + 'model': digest._name, + 'id': digest.id, + 'active_id': digest.id, + } + return request.redirect('/web?#%s' % url_encode(url_params)) diff --git a/addons/digest/data/digest_data.xml b/addons/digest/data/digest_data.xml index 67bd019bc00..80678308c79 100644 --- a/addons/digest/data/digest_data.xml +++ b/addons/digest/data/digest_data.xml @@ -53,4 +53,16 @@ + + + diff --git a/addons/digest/data/digest_template_data.xml b/addons/digest/data/digest_template_data.xml index dc5109c4b56..448fa8b2033 100644 --- a/addons/digest/data/digest_template_data.xml +++ b/addons/digest/data/digest_template_data.xml @@ -12,6 +12,7 @@ % set data = object.compute_kpis(company, user) % set tips = object.compute_tips(company, user, tips_count=ctx.get('tips_count', 1)) % set kpi_actions = object.compute_kpis_actions(company, user) + % set preferences = object.compute_preferences(company, user) % set kpis = data.yesterday.keys() @@ -46,6 +47,7 @@
+ % for tip in tips:
@@ -53,6 +55,7 @@
+ % endfor
@@ -137,6 +140,15 @@
% endfor + % if preferences: + + + + +
+ ${preferences | safe} +
+ % endif % if user.has_group('base.group_system'): diff --git a/addons/digest/data/digest_tips_data.xml b/addons/digest/data/digest_tips_data.xml index 375dd660681..ae7ed018ed8 100644 --- a/addons/digest/data/digest_tips_data.xml +++ b/addons/digest/data/digest_tips_data.xml @@ -2,9 +2,10 @@ - Weekly Stats in Odoo + Awesome Stats with Odoo + daily - + True True diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py index 4b33ebc7937..3c39f49e244 100644 --- a/addons/digest/models/digest.py +++ b/addons/digest/models/digest.py @@ -2,7 +2,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging -import math import pytz from datetime import datetime, date @@ -23,10 +22,11 @@ class Digest(models.Model): # Digest description name = fields.Char(string='Name', required=True, translate=True) user_ids = fields.Many2many('res.users', string='Recipients', domain="[('share', '=', False)]") - periodicity = fields.Selection([('weekly', 'Weekly'), + periodicity = fields.Selection([('daily', 'Daily'), + ('weekly', 'Weekly'), ('monthly', 'Monthly'), ('quarterly', 'Quarterly')], - string='Periodicity', default='weekly', required=True) + string='Periodicity', default='daily', required=True) next_run_date = fields.Date(string='Next Send Date') template_id = fields.Many2one('mail.template', string='Email Template', domain="[('model','=','digest.digest')]", @@ -77,19 +77,21 @@ class Digest(models.Model): @api.model def create(self, vals): - vals['next_run_date'] = date.today() + relativedelta(days=3) - return super(Digest, self).create(vals) + digest = super(Digest, self).create(vals) + if not digest.next_run_date: + digest.next_run_date = digest._get_next_run_date() + return digest # ------------------------------------------------------------ # ACTIONS # ------------------------------------------------------------ def action_subscribe(self): - if self.env.user not in self.user_ids: + if self.env.user.has_group('base.group_user') and self.env.user not in self.user_ids: self.sudo().user_ids |= self.env.user def action_unsubcribe(self): - if self.env.user in self.user_ids: + if self.env.user.has_group('base.group_user') and self.env.user in self.user_ids: self.sudo().user_ids -= self.env.user def action_activate(self): @@ -98,10 +100,16 @@ class Digest(models.Model): def action_deactivate(self): self.state = 'deactivated' + def action_set_periodicity(self, periodicity): + self.periodicity = periodicity + def action_send(self): + to_slowdown = self._check_daily_logs() for digest in self: for user in digest.user_ids: - digest._action_send_to_user(user, tips_count=1) + digest.with_context(digest_slowdown=digest in to_slowdown)._action_send_to_user(user, tips_count=1) + if digest in to_slowdown: + digest.write({'periodicity': 'weekly'}) digest.next_run_date = digest._get_next_run_date() def _action_send_to_user(self, user, tips_count=1): @@ -167,7 +175,7 @@ class Digest(models.Model): self.env['mail.render.mixin']._render_template(tools.html_sanitize(tip.tip_description), 'digest.tip', tip.ids, post_process=True)[tip.id] for tip in tips ] - # tip.user_ids += user + tip.user_ids += user return tip_descriptions def compute_kpis_actions(self, company, user): @@ -178,8 +186,27 @@ class Digest(models.Model): """ return {} + def compute_preferences(self, company, user): + """ Give an optional text for preferences, like a shortcut for configuration. + + :return string: html to put in template + """ + preferences = self.env['mail.render.mixin']._render_template( + 'digest.digest_section_preferences', + 'digest.digest', + self.ids, + engine='qweb', + add_context={ + 'company': company, + 'user': user, + }, + post_process=True)[self.id] + return preferences + def _get_next_run_date(self): self.ensure_one() + if self.periodicity == 'daily': + delta = relativedelta(days=1) if self.periodicity == 'weekly': delta = relativedelta(weeks=1) elif self.periodicity == 'monthly': @@ -216,6 +243,18 @@ class Digest(models.Model): margin = float_round((float(value-previous_value) / previous_value or 1) * 100, precision_digits=2) return margin + def _check_daily_logs(self): + three_days_ago = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - relativedelta(days=3) + to_slowdown = self.env['digest.digest'] + for digest in self.filtered(lambda digest: digest.periodicity == 'daily'): + users_logs = self.env['res.users.log'].sudo().search_count([ + ('create_uid', 'in', digest.user_ids.ids), + ('create_date', '>=', three_days_ago) + ]) + if not users_logs: + to_slowdown += digest + return to_slowdown + def _format_currency_amount(self, amount, currency_id): pre = currency_id.position == 'before' symbol = u'{symbol}'.format(symbol=currency_id.symbol or '') diff --git a/addons/digest/security/ir.model.access.csv b/addons/digest/security/ir.model.access.csv index 2891b43b673..49ef355262e 100644 --- a/addons/digest/security/ir.model.access.csv +++ b/addons/digest/security/ir.model.access.csv @@ -2,4 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_digest_digest_system,digest.digest.administration,model_digest_digest,base.group_erp_manager,1,1,1,1 access_digest_digest_user,digest.digest.user,model_digest_digest,base.group_user,1,0,0,0 access_digest_tip_system,digest.tip.administration,model_digest_tip,base.group_erp_manager,1,1,1,1 -access_digest_tip_user,digest.tip.user,model_digest_tip,base.group_user,0,0,0,0 +access_digest_tip_user,digest.tip.user,model_digest_tip,base.group_user,1,0,0,0 From 99c2c2ee1e7fc5f6c3298a447ae3845257b44e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 28 May 2020 14:46:49 +0000 Subject: [PATCH 06/10] [REF] digest: move template to QWeb PURPOSE Make digest email and tips more appealing. The goals of these tips are * to encourage the adoption of other apps (Did you know ?); * to make Odoo look more fun (Fun tips and tricks, young and dynamic style); * to show social proof and increase trust (emphasis on already existing projects / customers to); SPECIFICATIONS Digest now use qweb views instead of a standard mail.template record for rendering as * template is very custom; * probability of breaking it while edition the mail template is high; it is easier to maintain and extend in qweb; * only body was really used, other mail-related fields were not used; We therefore remove the mail template data and the field used for it on the digest model. Template is now forced to a Qweb view. We can also remove the template_id field. Indeed we think that having different templates for digests is a really advanced use case we do not want to support. Finally kpis and action computation is rewritten. It now returns an unique structure containing all information in a more neutral way, holding values for 3 columns. Purpose is to be less date-oriented in data construct and allow people to use digest qweb template to display a 3-columns KPIs content even if not related to yesterday / last 7 days / last 30 days. LINKS Task ID 2197417 Community PR odoo/odoo#51619 Upgrade PR odoo/upgrade#1256 --- addons/account/models/digest.py | 4 +- addons/crm/models/digest.py | 4 +- addons/digest/__manifest__.py | 1 - addons/digest/data/digest_data.xml | 199 +++++++++++++++++-- addons/digest/data/digest_template_data.xml | 209 -------------------- addons/digest/data/digest_tips_data.xml | 2 +- addons/digest/models/digest.py | 185 +++++++++++------ addons/digest/views/digest_views.xml | 2 - addons/hr_recruitment/models/digest.py | 4 +- addons/im_livechat/models/digest.py | 4 +- addons/point_of_sale/models/digest.py | 4 +- addons/project/models/digest.py | 4 +- addons/sale_management/models/digest.py | 4 +- addons/website_sale/models/digest.py | 4 +- 14 files changed, 332 insertions(+), 298 deletions(-) delete mode 100644 addons/digest/data/digest_template_data.xml diff --git a/addons/account/models/digest.py b/addons/account/models/digest.py index da7d673053b..d3a798ebb25 100644 --- a/addons/account/models/digest.py +++ b/addons/account/models/digest.py @@ -27,7 +27,7 @@ class Digest(models.Model): query_res = self._cr.fetchone() record.kpi_account_total_revenue_value = query_res and query_res[0] or 0.0 - def compute_kpis_actions(self, company, user): - res = super(Digest, self).compute_kpis_actions(company, user) + def _compute_kpis_actions(self, company, user): + res = super(Digest, self)._compute_kpis_actions(company, user) res['kpi_account_total_revenue'] = 'account.action_move_out_invoice_type&menu_id=%s' % self.env.ref('account.menu_finance').id return res diff --git a/addons/crm/models/digest.py b/addons/crm/models/digest.py index 5ec2bd07add..acb529f1a32 100644 --- a/addons/crm/models/digest.py +++ b/addons/crm/models/digest.py @@ -37,8 +37,8 @@ class Digest(models.Model): ('company_id', '=', company.id) ]) - def compute_kpis_actions(self, company, user): - res = super(Digest, self).compute_kpis_actions(company, user) + def _compute_kpis_actions(self, company, user): + res = super(Digest, self)._compute_kpis_actions(company, user) res['kpi_crm_lead_created'] = 'crm.crm_lead_action_pipeline&menu_id=%s' % self.env.ref('crm.crm_menu_root').id res['kpi_crm_opportunities_won'] = 'crm.crm_lead_action_pipeline&menu_id=%s' % self.env.ref('crm.crm_menu_root').id if user.has_group('crm.group_use_lead'): diff --git a/addons/digest/__manifest__.py b/addons/digest/__manifest__.py index b27fbd29ed7..6f4cc562e16 100644 --- a/addons/digest/__manifest__.py +++ b/addons/digest/__manifest__.py @@ -16,7 +16,6 @@ Send KPI Digests periodically 'data': [ 'security/ir.model.access.csv', 'data/digest_data.xml', - 'data/digest_template_data.xml', 'data/digest_tips_data.xml', 'data/ir_cron_data.xml', 'data/res_config_settings_data.xml', diff --git a/addons/digest/data/digest_data.xml b/addons/digest/data/digest_data.xml index 80678308c79..d4f29288418 100644 --- a/addons/digest/data/digest_data.xml +++ b/addons/digest/data/digest_data.xml @@ -49,20 +49,197 @@ - + - -