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/data/digest_data.xml b/addons/crm/data/digest_data.xml index 05a11305cdc..2d87beddbc3 100644 --- a/addons/crm/data/digest_data.xml +++ b/addons/crm/data/digest_data.xml @@ -9,20 +9,49 @@ - CRM: try mailgateway - 2 + CRM: Create opportunities from incoming emails + 5
% set email = object.env['crm.team'].search([('alias_name','!=', False)],limit=1).alias_id.display_name % if email - Try the mail gateway -
Email sent to ${email} generate opportunities in your pipeline.
-
- Try Now -
+ Create opportunities from incoming emails +

Emails sent to ${email} generate opportunities in your pipeline.

+ + % endif +
+ + + + CRM: Enrich your contacts + 50 + + +
+ Enrich your contacts +

Activate the "Partner Autocomplete" feature to fill in your Partner's details faster.

+ + +
+
+
+ + CRM: Get more leads + 2 + + +
+ Get more leads +

Activate the lead mining feature from your CRM settings and start generating more leads.

+
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/crm/static/src/img/tips-crm-autocomplete.jpg b/addons/crm/static/src/img/tips-crm-autocomplete.jpg new file mode 100644 index 00000000000..2fb9fa9ad93 Binary files /dev/null and b/addons/crm/static/src/img/tips-crm-autocomplete.jpg differ diff --git a/addons/crm/static/src/img/tips-crm-lead-mining.jpg b/addons/crm/static/src/img/tips-crm-lead-mining.jpg new file mode 100644 index 00000000000..3ea82e00dbb Binary files /dev/null and b/addons/crm/static/src/img/tips-crm-lead-mining.jpg differ diff --git a/addons/crm/static/src/img/tips-crm-pipeline.jpg b/addons/crm/static/src/img/tips-crm-pipeline.jpg new file mode 100644 index 00000000000..bb4683cb06c Binary files /dev/null and b/addons/crm/static/src/img/tips-crm-pipeline.jpg differ diff --git a/addons/digest/__manifest__.py b/addons/digest/__manifest__.py index 024305a0dca..6f4cc562e16 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,8 @@ Send KPI Digests periodically ], 'data': [ 'security/ir.model.access.csv', - 'data/digest_template_data.xml', 'data/digest_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/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 375dd660681..d4f29288418 100644 --- a/addons/digest/data/digest_data.xml +++ b/addons/digest/data/digest_data.xml @@ -1,59 +1,245 @@ - - - - - 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: 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/data/digest_template_data.xml b/addons/digest/data/digest_template_data.xml deleted file mode 100644 index 2b9e607796d..00000000000 --- a/addons/digest/data/digest_template_data.xml +++ /dev/null @@ -1,164 +0,0 @@ - - - - Digest: Default main template - - ${'%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: - - - - - - - -

- ${object.fields_get()[kpi]['string']} - %if kpi in kpi_actions: - - View more - - %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 -
-
- - - - -
- ${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 - - %endif -
-
-
- % endfor - % if tips: - - - - -

-
${tips | safe}
-
- % 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 - -

-
-
- ${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..9b690a7f770 --- /dev/null +++ b/addons/digest/data/digest_tips_data.xml @@ -0,0 +1,109 @@ + + + + + Your Odoo Periodic Report + daily + + + True + True + + + + + + Invite Team Members + 1 + + +
+ Invite Team Members +

Go to your general settings to invite new users to your database.

+ +
+
+
+ + + Want to learn more about Odoo? + 2 + + +
+ Want to learn more about Odoo? +

Visit our eLearning Platform

+
+
+
+ + + Odoo Push Notifications + 3 + + +
+ Odoo Push Notifications +

Change your browser settings to "Allow Push Notifications" and never miss a thing.

+ +
+
+
+ + + Reach your full potential with Odoo + 4 + + +
+ Reach your full potential with Odoo +

Take a look at our App videos and discover the next tool that will allow you to work better and faster.

+
+
+
+ + + Did you know? + 10 + + +
+ % 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 your colleagues by tagging them in your messages using “@” followed by their name. They will be instantly notified.

+ +
+

${', '.join(users.mapped('name'))} just signed up. Say hello in the company's discussion channel. +

+
+
+
+
+ + Get things done with activities + 11 + + +
+ Get things done with activities +

Use activities on any business document to schedule meetings, calls and todos.

+ +
+
+
+ + + Follow what you want + 12 + + +
+ Follow what you want +

Keep track of any object in Odoo (opportunity, sales order, etc.) and choose exactly what you want to follow (discussion, status, status change, etc.).

+ +
+
+
+
+
diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py index 8e5623f4dad..c6e51a4fb9a 100644 --- a/addons/digest/models/digest.py +++ b/addons/digest/models/digest.py @@ -2,13 +2,12 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging -import math import pytz from datetime import datetime, date from dateutil.relativedelta import relativedelta -from odoo import api, fields, models, tools +from odoo import api, fields, models, tools, _ from odoo.addons.base.models.ir_mail_server import MailDeliveryException from odoo.exceptions import AccessError from odoo.tools.float_utils import float_round @@ -23,15 +22,12 @@ 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')]", - default=lambda self: self.env.ref('digest.digest_mail_template'), - required=True) currency_id = fields.Many2one(related="company_id.currency_id", string='Currency', readonly=False) company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company.id) available_fields = fields.Char(compute='_compute_available_fields') @@ -77,15 +73,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): @@ -94,53 +96,145 @@ 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: - 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.with_context( + digest_slowdown=digest in to_slowdown, + lang=user.lang + )._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): + rendered_body = self.env['mail.render.mixin']._render_template( + 'digest.digest_mail_main', + 'digest.digest', + self.ids, + engine='qweb', + add_context={ + 'company': user.company_id, + 'user': user, + 'tips_count': tips_count, + 'datetime': datetime, # TDE TEMP FIXME + 'display_mobile_banner': True, + 'kpi_data': self.compute_kpis(user.company_id, user), + 'tips': self.compute_tips(user.company_id, user, tips_count=tips_count), + 'preferences': self.compute_preferences(user.company_id, user), + }, + post_process=True + )[self.id] + full_mail = self.env['mail.render.mixin']._render_encapsulate( + 'digest.digest_mail_layout', + rendered_body, + add_context={ + 'company': user.company_id, + 'user': user, + }, + ) + # create a mail_mail based on values, without attachments + mail_values = { + 'subject': '%s: %s' % (user.company_id.name, self.name), + 'email_from': self.company_id.partner_id.email_formatted if self.company_id else self.env.user.email_formatted, + 'email_to': user.email_formatted, + 'body_html': full_mail, + 'auto_delete': True, + } + mail = self.env['mail.mail'].sudo().create(mail_values) + mail.send(raise_exception=False) + return True + + @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): + """ Compute KPIs to display in the digest template. It is expected to be + a list of KPIs, each containing values for 3 columns display. + + :return list: result [{ + 'kpi_name': 'kpi_mail_message', + 'kpi_fullname': 'Messages', # translated + 'kpi_action': 'crm.crm_lead_action_pipeline', # xml id of an action to execute + 'kpi_col1': { + 'value': '12.0', + 'margin': 32.36, + 'col_subtitle': 'Yesterday', # translated + }, + 'kpi_col2': { ... }, + 'kpi_col3': { ... }, + }, { ... }] """ self.ensure_one() - res = {} - for tf_name, tf in self._compute_timeframes(company).items(): + digest_fields = self._get_kpi_fields() + invalid_fields = [] + kpis = [ + dict(kpi_name=field_name, + kpi_fullname=self._fields[field_name].string, + kpi_action=False, + kpi_col1=dict(), + kpi_col2=dict(), + kpi_col3=dict(), + ) + for field_name in digest_fields + ] + kpis_actions = self._compute_kpis_actions(company, user) + + for col_index, (tf_name, tf) in enumerate(self._compute_timeframes(company)): digest = self.with_context(start_date=tf[0][0], end_date=tf[0][1]).with_user(user).with_company(company) previous_digest = self.with_context(start_date=tf[1][0], end_date=tf[1][1]).with_user(user).with_company(company) - kpis = {} - 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]: + for index, field_name in enumerate(digest_fields): + kpi_values = kpis[index] + kpi_values['kpi_action'] = kpis_actions.get(field_name) + try: + compute_value = digest[field_name + '_value'] + # Context start and end date is different each time so invalidate to recompute. + digest.invalidate_cache([field_name + '_value']) + previous_value = previous_digest[field_name + '_value'] + # Context start and end date is different each time so invalidate to recompute. + previous_digest.invalidate_cache([field_name + '_value']) + except AccessError: # no access rights -> just skip that digest details from that user's digest email + invalid_fields.append(field_name) + continue + margin = self._get_margin_value(compute_value, previous_value) + if self._fields['%s_value' % field_name].type == 'monetary': + converted_amount = self._format_human_readable_amount(compute_value) + compute_value = self._format_currency_amount(converted_amount, company.currency_id) + kpi_values['kpi_col%s' % (col_index + 1)].update({ + 'value': compute_value, + 'margin': margin, + 'col_subtitle': tf_name, + }) - try: - compute_value = digest[field_name + '_value'] - # Context start and end date is different each time so invalidate to recompute. - digest.invalidate_cache([field_name + '_value']) - previous_value = previous_digest[field_name + '_value'] - # Context start and end date is different each time so invalidate to recompute. - previous_digest.invalidate_cache([field_name + '_value']) - except AccessError: # no access rights -> just skip that digest details from that user's digest email - continue - margin = self._get_margin_value(compute_value, previous_value) - if self._fields[field_name+'_value'].type == 'monetary': - converted_amount = self._format_human_readable_amount(compute_value) - kpis.update({field_name: {field_name: self._format_currency_amount(converted_amount, company.currency_id), 'margin': margin}}) - else: - kpis.update({field_name: {field_name: compute_value, 'margin': margin}}) + # filter failed KPIs + return [kpi for kpi in kpis if kpi['kpi_name'] not in invalid_fields] - res.update({tf_name: kpis}) - return res + 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 + ] + tips.user_ids += user + return tip_descriptions - 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_kpis_actions(self, company, user): + def _compute_kpis_actions(self, company, user): """ Give an optional action to display in digest email linked to some KPIs. :return dict: key: kpi name (field name), value: an action that will be @@ -148,8 +242,34 @@ 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 = [] + if self._context.get('digest_slowdown'): + preferences.append(_("We have noticed you did not connect these last few days so we've automatically switched your preference to weekly Digests.")) + elif self.periodicity == 'daily' and user.has_group('base.group_erp_manager'): + preferences.append('%s %s' % ( + _('Prefer a broader overview ?'), + self.id, + _('Switch to weekly Digests') + )) + if user.has_group('base.group_erp_manager'): + preferences.append('%s %s' % ( + _('Want to customize this email?'), + self._name, + self.id, + _('Choose the metrics you care about') + )) + + 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': @@ -164,17 +284,27 @@ class Digest(models.Model): if tz_name: now = pytz.timezone(tz_name).localize(now) start_date = now.date() - return { - 'yesterday': ( + return [ + (_('Yesterday'), ( (start_date + relativedelta(days=-1), start_date), - (start_date + relativedelta(days=-2), start_date + relativedelta(days=-1))), - 'lastweek': ( + (start_date + relativedelta(days=-2), start_date + relativedelta(days=-1))) + ), (_('Last 7 Days'), ( (start_date + relativedelta(weeks=-1), start_date), - (start_date + relativedelta(weeks=-2), start_date + relativedelta(weeks=-1))), - 'lastmonth': ( + (start_date + relativedelta(weeks=-2), start_date + relativedelta(weeks=-1))) + ), (_('Last 30 Days'), ( (start_date + relativedelta(months=-1), start_date), - (start_date + relativedelta(months=-2), start_date + relativedelta(months=-1))), - } + (start_date + relativedelta(months=-2), start_date + relativedelta(months=-1))) + ) + ] + + # ------------------------------------------------------------ + # FORMATTING / TOOLS + # ------------------------------------------------------------ + + 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] + ] def _get_margin_value(self, value, previous_value=0.0): margin = 0.0 @@ -182,6 +312,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 '') @@ -193,12 +335,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.') 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 diff --git a/addons/digest/static/src/img/activity.png b/addons/digest/static/src/img/activity.png deleted file mode 100644 index b1b85043de3..00000000000 Binary files a/addons/digest/static/src/img/activity.png and /dev/null differ diff --git a/addons/digest/static/src/img/notification.png b/addons/digest/static/src/img/notification.png deleted file mode 100644 index 3ba6e7c24a2..00000000000 Binary files a/addons/digest/static/src/img/notification.png and /dev/null differ diff --git a/addons/digest/static/src/img/tips-digest-browser-notifications.jpg b/addons/digest/static/src/img/tips-digest-browser-notifications.jpg new file mode 100644 index 00000000000..38bd5b680b7 Binary files /dev/null and b/addons/digest/static/src/img/tips-digest-browser-notifications.jpg differ diff --git a/addons/digest/static/src/img/tips-digest-invite-team.jpg b/addons/digest/static/src/img/tips-digest-invite-team.jpg new file mode 100644 index 00000000000..3ee2932800a Binary files /dev/null and b/addons/digest/static/src/img/tips-digest-invite-team.jpg differ diff --git a/addons/digest/static/src/img/tips-mail-activities.jpg b/addons/digest/static/src/img/tips-mail-activities.jpg new file mode 100644 index 00000000000..a1c00cb4e91 Binary files /dev/null and b/addons/digest/static/src/img/tips-mail-activities.jpg differ diff --git a/addons/digest/static/src/img/tips-mail-following.jpg b/addons/digest/static/src/img/tips-mail-following.jpg new file mode 100644 index 00000000000..d83e57d4896 Binary files /dev/null and b/addons/digest/static/src/img/tips-mail-following.jpg differ diff --git a/addons/digest/static/src/img/tips-mail-notifications.jpg b/addons/digest/static/src/img/tips-mail-notifications.jpg new file mode 100644 index 00000000000..cfb2164c707 Binary files /dev/null and b/addons/digest/static/src/img/tips-mail-notifications.jpg differ diff --git a/addons/digest/views/digest_views.xml b/addons/digest/views/digest_views.xml index a84df14a2d3..9df32772bee 100644 --- a/addons/digest/views/digest_views.xml +++ b/addons/digest/views/digest_views.xml @@ -46,10 +46,8 @@ - - diff --git a/addons/hr_recruitment/data/digest_data.xml b/addons/hr_recruitment/data/digest_data.xml index 57a1068522e..61790aba91c 100644 --- a/addons/hr_recruitment/data/digest_data.xml +++ b/addons/hr_recruitment/data/digest_data.xml @@ -8,16 +8,15 @@ - HR Recruitment: try mailgateway + Recruitment: Create new applicants from incoming emails 4
- Try the mail gateway -
New applicants can be generated from incoming emails. You just need to set email aliases in configuration of job positions.
- + Create new applicants from incoming emails +

You just need to set email aliases in your job positions' settings.

+
diff --git a/addons/hr_recruitment/models/digest.py b/addons/hr_recruitment/models/digest.py index c24cc80e5d5..c8d17019bea 100644 --- a/addons/hr_recruitment/models/digest.py +++ b/addons/hr_recruitment/models/digest.py @@ -23,7 +23,7 @@ class Digest(models.Model): ]) record.kpi_hr_recruitment_new_colleagues_value = new_colleagues - 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_hr_recruitment_new_colleagues'] = 'hr.open_view_employee_list_my&menu_id=%s' % self.env.ref('hr.menu_hr_root').id return res diff --git a/addons/im_livechat/models/digest.py b/addons/im_livechat/models/digest.py index 6c0fefd7ae4..5b23ab65b97 100644 --- a/addons/im_livechat/models/digest.py +++ b/addons/im_livechat/models/digest.py @@ -42,8 +42,8 @@ class Digest(models.Model): ('partner_id', '=', self.env.user.partner_id.id)], ['partner_id', 'time_to_answer'], ['partner_id']) record.kpi_livechat_response_value = "%.2f" % sum([response['time_to_answer'] for response in response_time]) or 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_livechat_rating'] = 'im_livechat.rating_rating_action_livechat_report' res['kpi_livechat_conversations'] = 'im_livechat.im_livechat_report_operator_action' res['kpi_livechat_response'] = 'im_livechat.im_livechat_report_channel_time_to_answer_action' 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) diff --git a/addons/point_of_sale/models/digest.py b/addons/point_of_sale/models/digest.py index ea115f9b194..21a65cc4fea 100644 --- a/addons/point_of_sale/models/digest.py +++ b/addons/point_of_sale/models/digest.py @@ -23,7 +23,7 @@ class Digest(models.Model): ('company_id', '=', company.id) ]).mapped('amount_total')) - 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_pos_total'] = 'point_of_sale.action_pos_sale_graph&menu_id=%s' % self.env.ref('point_of_sale.menu_point_root').id return res diff --git a/addons/project/data/digest_data.xml b/addons/project/data/digest_data.xml index 43fe90dc995..a5e6cfbc427 100644 --- a/addons/project/data/digest_data.xml +++ b/addons/project/data/digest_data.xml @@ -8,17 +8,16 @@ - Project: try mailgateway - 6 + Project: Create tasks from incoming emails + 5
- Try the mail gateway -
- New tasks can be generated from incoming emails. You just need to set email aliases on your projects.
- + Create tasks from incoming emails +

You just need to set email aliases to your projects.

+
diff --git a/addons/project/models/digest.py b/addons/project/models/digest.py index 9034e647125..b584ca956d6 100644 --- a/addons/project/models/digest.py +++ b/addons/project/models/digest.py @@ -23,7 +23,7 @@ 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_project_task_opened'] = 'project.open_view_project_all&menu_id=%s' % self.env.ref('project.menu_main_pm').id return res diff --git a/addons/sale_expense/data/digest_data.xml b/addons/sale_expense/data/digest_data.xml index 1369710f272..df852b2aff4 100644 --- a/addons/sale_expense/data/digest_data.xml +++ b/addons/sale_expense/data/digest_data.xml @@ -1,16 +1,18 @@ - Expense: try mailgateway + Expense: Submit expenses by email 5 - +
- Submit expenses by email -
Take a snapshot of your expenses and submit your expenses by email.
- + Submit expenses by email +

Take a snapshot of your expenses and submit your expenses by email.

+
diff --git a/addons/sale_management/models/digest.py b/addons/sale_management/models/digest.py index c2bea94c980..8828ad98a77 100644 --- a/addons/sale_management/models/digest.py +++ b/addons/sale_management/models/digest.py @@ -23,7 +23,7 @@ class Digest(models.Model): ('company_id', '=', company.id)], ['price_total'], ['price_total']) record.kpi_all_sale_total_value = sum([channel_sale['price_total'] for channel_sale in all_channels_sales]) - 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_all_sale_total'] = 'sale.report_all_channels_sales_action&menu_id=%s' % self.env.ref('sale.sale_menu_root').id return res diff --git a/addons/website/data/digest_data.xml b/addons/website/data/digest_data.xml new file mode 100644 index 00000000000..a3811fd9d6a --- /dev/null +++ b/addons/website/data/digest_data.xml @@ -0,0 +1,17 @@ + + + + + Website: Use SEO to get more visitors + 100 + + +
+ Use SEO to get more visitors on your website +

Odoo helps you to optimize your website and get a better ranking in search engines like Google.

+ +
+
+
+
+
diff --git a/addons/website/static/src/img/tips-website-seo.jpg b/addons/website/static/src/img/tips-website-seo.jpg new file mode 100644 index 00000000000..305ccdde911 Binary files /dev/null and b/addons/website/static/src/img/tips-website-seo.jpg differ diff --git a/addons/website_sale/models/digest.py b/addons/website_sale/models/digest.py index 276a5b54618..4d03289c978 100644 --- a/addons/website_sale/models/digest.py +++ b/addons/website_sale/models/digest.py @@ -25,7 +25,7 @@ class Digest(models.Model): ]) record.kpi_website_sale_total_value = sum(confirmed_website_sales.mapped('amount_total')) - 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_website_sale_total'] = 'website.backend_dashboard&menu_id=%s' % self.env.ref('website.menu_website_configuration').id return res