summaryrefslogtreecommitdiff
path: root/addons/digest/models
diff options
context:
space:
mode:
Diffstat (limited to 'addons/digest/models')
-rw-r--r--addons/digest/models/__init__.py7
-rw-r--r--addons/digest/models/digest.py337
-rw-r--r--addons/digest/models/digest_tip.py22
-rw-r--r--addons/digest/models/res_config_settings.py10
-rw-r--r--addons/digest/models/res_users.py18
5 files changed, 394 insertions, 0 deletions
diff --git a/addons/digest/models/__init__.py b/addons/digest/models/__init__.py
new file mode 100644
index 00000000..1882e860
--- /dev/null
+++ b/addons/digest/models/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import digest
+from . import digest_tip
+from . import res_config_settings
+from . import res_users
diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py
new file mode 100644
index 00000000..46733f84
--- /dev/null
+++ b/addons/digest/models/digest.py
@@ -0,0 +1,337 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+import pytz
+
+from datetime import datetime, date
+from dateutil.relativedelta import relativedelta
+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.tools.float_utils import float_round
+
+_logger = logging.getLogger(__name__)
+
+
+class Digest(models.Model):
+ _name = 'digest.digest'
+ _description = 'Digest'
+
+ # 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([('daily', 'Daily'),
+ ('weekly', 'Weekly'),
+ ('monthly', 'Monthly'),
+ ('quarterly', 'Quarterly')],
+ string='Periodicity', default='daily', required=True)
+ next_run_date = fields.Date(string='Next Send Date')
+ 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')
+ is_subscribed = fields.Boolean('Is user subscribed', compute='_compute_is_subscribed')
+ state = fields.Selection([('activated', 'Activated'), ('deactivated', 'Deactivated')], string='Status', readonly=True, default='activated')
+ # First base-related KPIs
+ kpi_res_users_connected = fields.Boolean('Connected Users')
+ kpi_res_users_connected_value = fields.Integer(compute='_compute_kpi_res_users_connected_value')
+ kpi_mail_message_total = fields.Boolean('Messages')
+ kpi_mail_message_total_value = fields.Integer(compute='_compute_kpi_mail_message_total_value')
+
+ def _compute_is_subscribed(self):
+ for digest in self:
+ digest.is_subscribed = self.env.user in digest.user_ids
+
+ def _compute_available_fields(self):
+ for digest in self:
+ kpis_values_fields = []
+ for field_name, field in digest._fields.items():
+ if field.type == 'boolean' and field_name.startswith(('kpi_', 'x_kpi_', 'x_studio_kpi_')) and digest[field_name]:
+ kpis_values_fields += [field_name + '_value']
+ digest.available_fields = ', '.join(kpis_values_fields)
+
+ def _get_kpi_compute_parameters(self):
+ return fields.Date.to_string(self._context.get('start_date')), fields.Date.to_string(self._context.get('end_date')), self.env.company
+
+ 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
+
+ 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
+
+ @api.onchange('periodicity')
+ def _onchange_periodicity(self):
+ self.next_run_date = self._get_next_run_date()
+
+ @api.model
+ def create(self, 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.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.has_group('base.group_user') and self.env.user in self.user_ids:
+ self.sudo().user_ids -= self.env.user
+
+ def action_activate(self):
+ self.state = 'activated'
+
+ 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.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, consum_tips=True):
+ web_base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+
+ rendered_body = self.env['mail.render.mixin']._render_template(
+ 'digest.digest_mail_main',
+ 'digest.digest',
+ self.ids,
+ engine='qweb',
+ add_context={
+ 'title': self.name,
+ 'top_button_label': _('Connect'),
+ 'top_button_url': url_join(web_base_url, '/web/login'),
+ 'company': user.company_id,
+ 'user': user,
+ 'tips_count': tips_count,
+ 'formatted_date': datetime.today().strftime('%B %d, %Y'),
+ '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, consumed=consum_tips),
+ '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.', digest.id)
+
+ # ------------------------------------------------------------
+ # 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()
+ digest_fields = self._get_kpi_fields()
+ invalid_fields = []
+ kpis = [
+ dict(kpi_name=field_name,
+ kpi_fullname=self.env['ir.model.fields']._get(self._name, field_name).field_description,
+ 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)
+ 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 = tools.format_decimalized_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,
+ })
+
+ # filter failed KPIs
+ return [kpi for kpi in kpis if kpi['kpi_name'] not in invalid_fields]
+
+ def compute_tips(self, company, user, tips_count=1, consumed=True):
+ 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
+ ]
+ if consumed:
+ tips.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.
+
+ :return dict: key: kpi name (field name), value: an action that will be
+ concatenated with /web#action={action}
+ """
+ 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('<p>%s<br /><a href="/digest/%s/set_periodicity?periodicity=weekly" target="_blank" style="color:#875A7B; font-weight: bold;">%s</a></p>' % (
+ _('Prefer a broader overview ?'),
+ self.id,
+ _('Switch to weekly Digests')
+ ))
+ if user.has_group('base.group_erp_manager'):
+ preferences.append('<p>%s<br /><a href="/web#view_type=form&amp;model=%s&amp;id=%s" target="_blank" style="color:#875A7B; font-weight: bold;">%s</a></p>' % (
+ _('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':
+ delta = relativedelta(months=1)
+ elif self.periodicity == 'quarterly':
+ delta = relativedelta(months=3)
+ return date.today() + delta
+
+ def _compute_timeframes(self, company):
+ now = datetime.utcnow()
+ tz_name = company.resource_calendar_id.tz
+ if tz_name:
+ now = pytz.timezone(tz_name).localize(now)
+ start_date = now.date()
+ return [
+ (_('Yesterday'), (
+ (start_date + relativedelta(days=-1), start_date),
+ (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)))
+ ), (_('Last 30 Days'), (
+ (start_date + relativedelta(months=-1), start_date),
+ (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
+ if (value != previous_value) and (value != 0.0 and previous_value != 0.0):
+ 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 '')
+ return u'{pre}{0}{post}'.format(amount, pre=symbol if pre else '', post=symbol if not pre else '')
diff --git a/addons/digest/models/digest_tip.py b/addons/digest/models/digest_tip.py
new file mode 100644
index 00000000..c382700f
--- /dev/null
+++ b/addons/digest/models/digest_tip.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import fields, models
+from odoo.tools.translate import html_translate
+
+
+class DigestTip(models.Model):
+ _name = 'digest.tip'
+ _description = 'Digest Tips'
+ _order = 'sequence'
+
+ sequence = fields.Integer(
+ 'Sequence', default=1,
+ help='Used to display digest tip in email template base on order')
+ name = fields.Char('Name', translate=True)
+ user_ids = fields.Many2many(
+ 'res.users', string='Recipients',
+ help='Users having already received this tip')
+ tip_description = fields.Html('Tip description', translate=html_translate)
+ group_id = fields.Many2one(
+ 'res.groups', string='Authorized Group',
+ default=lambda self: self.env.ref('base.group_user'))
diff --git a/addons/digest/models/res_config_settings.py b/addons/digest/models/res_config_settings.py
new file mode 100644
index 00000000..59f13bdf
--- /dev/null
+++ b/addons/digest/models/res_config_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ digest_emails = fields.Boolean(string="Digest Emails", config_parameter='digest.default_digest_emails')
+ digest_id = fields.Many2one('digest.digest', string='Digest Email', config_parameter='digest.default_digest_id')
diff --git a/addons/digest/models/res_users.py b/addons/digest/models/res_users.py
new file mode 100644
index 00000000..397d672e
--- /dev/null
+++ b/addons/digest/models/res_users.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import api, models
+
+
+class ResUsers(models.Model):
+ _inherit = "res.users"
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ """ Automatically subscribe employee users to default digest if activated """
+ users = super(ResUsers, self).create(vals_list)
+ default_digest_emails = self.env['ir.config_parameter'].sudo().get_param('digest.default_digest_emails')
+ default_digest_id = self.env['ir.config_parameter'].sudo().get_param('digest.default_digest_id')
+ if default_digest_emails and default_digest_id:
+ digest = self.env['digest.digest'].sudo().browse(int(default_digest_id)).exists()
+ digest.user_ids |= users.filtered_domain([('share', '=', False)])
+ return users