[MERGE][IMP] digest, various: improve digest content and tips

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 TEMPLATE

Improve template according to FP specifications, aka

    Header
    Up to 2 tips
    KPIs
    Want to customize?
    Mobile Tip
    Footer

SPECIFICATIONS: QWEB TEMPLATE

Digest now uses 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.

SPECIFICATIONS: DELAY BEFORE SENDING

In this commit we introduce a way to set a delay before sending a tip.
Using a new field, a tip can be send a few days after its module installation.
This require to store the source module on the tip itself. When installed
from data it will be automatically filled using its xml_id if not set

SPECIFICATIONS: TIPS

Improve tips content and display according to FP specifications.

LINKS

Task ID 2197417
Community PR odoo/odoo#51619
Enterprise PR odoo/enterprise#10697
Upgrade PR odoo/upgrade#1256

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2020-06-05 09:24:16 +02:00
committed by GitHub
33 changed files with 741 additions and 341 deletions
+2 -2
View File
@@ -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
+36 -7
View File
@@ -9,20 +9,49 @@
<data>
<record id="digest_tip_crm_0" model="digest.tip">
<field name="name">CRM: try mailgateway</field>
<field name="sequence">2</field>
<field name="name">CRM: Create opportunities from incoming emails</field>
<field name="sequence">5</field>
<field name="group_id" ref="sales_team.group_sale_salesman_all_leads"/>
<field name="tip_description" type="html">
<div>
% set email = object.env['crm.team'].search([('alias_name','!=', False)],limit=1).alias_id.display_name
% if email
<strong style="font-size: 16px;">Try the mail gateway</strong>
<div style="font-size: 14px;">Email sent to <strong>${email}</strong> generate opportunities in your pipeline.<br/>
<div style="text-align:center;margin-top:5px;margin-bottom:2px;">
<a href="mailto:${email}" style="background-color:#56b3b5;padding:2px;color:#FFFFFF;font-weight:bold;text-decoration:none;">Try Now</a>
</div>
<b class="font-16" style="font-size: 20px;line-height: 30px;">Create opportunities from incoming emails</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin-top: 0;">Emails sent to <strong>${email}</strong> generate opportunities in your pipeline.</p>
<img src="/crm/static/src/img/tips-crm-pipeline.jpg" style="max-width: 580px" width="100%" />
<div style="text-align:center;margin-top:10px;">
<a href="mailto:${email}" target="_blank" style="display:inline-block;background-color:#875a7b;line-height:40px;text-decoration: none;color: #ffffff;border-radius: 3px;font-size:13px;font-weight:bold;text-align:center; padding:0 10px;">Try it now</a>
</div>
% endif
</div>
</field>
</record>
<record id="digest_tip_crm_1" model="digest.tip">
<field name="name">CRM: Enrich your contacts</field>
<field name="sequence">50</field>
<field name="group_id" ref="sales_team.group_sale_salesman_all_leads"/>
<field name="tip_description" type="html">
<div>
<b class="font-16" style="font-size: 20px;line-height: 30px;">Enrich your contacts</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin-top: 0;">Activate the "Partner Autocomplete" feature to fill in your Partner's details faster.</p>
<img src="/crm/static/src/img/tips-crm-autocomplete.jpg" style="max-width: 580px" width="100%" />
<div style="text-align:center;margin-top:10px;">
<a href="/web#action=base_setup.action_general_configuration" target="_blank" style="display:inline-block;background-color:#875a7b;line-height:40px;text-decoration: none;color: #ffffff;border-radius: 3px;font-size:13px;font-weight:bold;text-align:center; padding:0 10px;">Try it now</a>
</div>
</div>
</field>
</record>
<record id="digest_tip_crm_2" model="digest.tip">
<field name="name">CRM: Get more leads</field>
<field name="sequence">2</field>
<field name="group_id" ref="sales_team.group_sale_salesman_all_leads"/>
<field name="tip_description" type="html">
<div>
<b class="font-16" style="font-size: 20px;line-height: 30px;">Get more leads</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin-top: 0;">Activate the lead mining feature from your CRM settings and start generating more leads.</p>
<div style="text-align:center;margin-top:10px;">
<a href="https://www.odoo.com/documentation/user/13.0/crm/acquire_leads/lead_mining.html" target="_blank" style="display:inline-block;background-color:#875a7b;line-height:40px;text-decoration: none;color: #ffffff;border-radius: 3px;font-size:13px;font-weight:bold;text-align:center; padding:0 10px;">Find out more</a>
</div>
</div>
</field>
</record>
+2 -2
View File
@@ -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'):
Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

+2 -2
View File
@@ -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',
+23 -2
View File
@@ -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/<int:digest_id>/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/<int:digest_id>/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))
+242 -56
View File
@@ -1,59 +1,245 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<data noupdate="1">
<record id="digest_digest_default" model="digest.digest">
<field name="name">Weekly Stats in Odoo</field>
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="next_run_date" eval="(DateTime.now() + timedelta(days=7)).strftime('%Y-%m-%d')"/>
<field name="kpi_res_users_connected">True</field>
<field name="kpi_mail_message_total">True</field>
</record>
</data>
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<data>
<record id="digest_tip_mail_0" model="digest.tip">
<field name="name">Mail: ping colleagues</field>
<field name="sequence">1</field>
<field name="tip_description" type="html">
<div>
% 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
<strong style="font-size: 16px;">Did you know...?</strong>
<div style="font-size: 14px;">You can ping colleagues by tagging them in your messages using "@". They will be instantly notified.<br/>
<div>
<center>
<img src="/digest/static/src/img/notification.png" width="70%" height="100%"/><br/>
</center>
${', '.join(users.mapped('name'))} signed up. Say hello in the <a href="/web#action=mail.action_discuss&amp;active_id=${channel_id}" style="color: #006d6b;">company's discussion channel.</a>
<!-- Email layout: encapsulation when sending (not used in backend display) -->
<template id="digest_mail_layout">
&lt;!DOCTYPE html&gt;
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="format-detection" content="telephone=no"/>
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=no;"/>
<meta http-equiv="X-UA-Compatible" content="IE=9; IE=8; IE=7; IE=EDGE" />
<style type="text/css">
body {
margin: 0;
padding: 0;
}
@media only screen and (max-width:650px) {
.d-block{
display: block !important;
}
.mobile-hide{
display: none !important;
}
.mobile-w-75{
width: 75% !important;
}
.mobile-w-33{
width: 33% !important;
}
.font-25{
font-size: 25px !important;
line-height: 25px !important;
}
.font-18{
font-size: 18px !important;
line-height: 25px !important;
}
.font-16{
font-size: 16px !important;
line-height: 25px !important;
}
.font-12{
font-size: 12px !important;
line-height: 20px !important;
}
}
</style>
</head>
<body>
<t t-raw="body"/>
</body>
</html>
</template>
<!-- DIGEST MAIN TEMPLATE -->
<template id="digest_mail_main">
<div style="margin:0; padding:0">
<table cellspacing="0" cellpadding="0" align="center" border="0" bgcolor="#eeeeee" style="width:100%; font-family: Arial,Helvetica,Verdana,sans-serif;">
<tr class="mobile-hide" bgcolor="#875a7b"><td height="70"> </td></tr>
<tr bgcolor="#875a7b">
<td align="center" valign="top">
<table bgcolor="#ffffff" cellspacing="0" cellpadding="0" width="650" align="center" border="0" style="border: 1px solid #eeeeee;width: 100%; max-width: 650px; padding: 30px 30px">
<tr>
<td>
<table cellspacing="0" cellpadding="0" border="0" width="580" align="center" style="width:100%; max-width:580px;">
<tr>
<td class="mobile-w-75" style="text-align: left;">
<span class="font-16" style="color:#8F8F8F;font-weight:bold;font-size: 20px; line-height: 30px;" t-field="company.name"/>
<br/>
<span class="font-18" style="color:#282f33;font-weight:bold;font-size: 25px; line-height: 35px; margin-right: 5px;" t-field="object.name"/>
<span class="d-block font-12" style="color:#8F8F8F;font-size: 15px;"><t t-esc="datetime.today().strftime('%B %d, %Y')"/></span>
</td>
<td valign="top" style="text-align:right;">
<a href="/web/login" target="_blank" style="display:block;background-color:#875a7b;line-height:40px;text-decoration: none;color: #ffffff;border-radius: 3px;font-size:13px;font-weight:bold;text-align:center;">Connect</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table t-if="tips" cellspacing="0" cellpadding="0" align="center" border="0" bgcolor="#eeeeee" style="width:100%; font-family: Arial,Helvetica,Verdana,sans-serif;">
<tr>
<td align="center" valign="top">
<table bgcolor="#ffffff" cellspacing="0" cellpadding="0" width="650" align="center" border="0" style="border: 1px solid #eeeeee; border-bottom: none;width: 100%; max-width: 650px; padding:30px 30px">
<tr>
<td>
<table t-foreach="tips" t-as="tip" cellspacing="0" cellpadding="0" border="0" width="580" align="center" style="width:100%; max-width:580px;">
<tr>
<td valign="top">
<t t-raw="tip"/>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table t-if="kpi_data" cellspacing="0" cellpadding="0" align="center" border="0" bgcolor="#eeeeee" style="width:100%; font-family: Arial,Helvetica,Verdana,sans-serif;">
<tr>
<td align="center" valign="top">
<table bgcolor="#ffffff" cellspacing="0" cellpadding="0" width="650" align="center" border="0" style="border: 1px solid #eeeeee; border-bottom: none;width: 100%; max-width: 650px; padding:30px 30px">
<tr t-foreach="kpi_data" t-as="kpi_info">
<td>
<table cellspacing="0" cellpadding="0" border="0" width="580" align="center" style="width:100%; max-width:580px;">
<tr>
<td align="left" style="border-bottom: 1px solid #eeeeee;">
<span style="color:#282f33;font-size: 15px; font-weight: bold; line-height: 30px" t-esc="kpi_info['kpi_fullname']"/>
<a t-if="kpi_info['kpi_action']" t-att-href="'/web#action=%s' % kpi_info['kpi_action']"
style="float:right; background-color:#875A7B;margin-left: 5px; padding: 5px 12px 5px 12px; text-decoration: none; color: #ffffff; border-radius: 3px; font-size:13px; font-weight:bold; margin-bottom: 5px;">
View
</a>
</td>
</tr>
<tr>
<td>
<table cellspacing="0" cellpadding="0" align="center" border="0" style="width:100%; padding: 20px 0;">
<tr valign="top">
<td class="mobile-w-33" width="196" style=" text-align: center;">
<t t-call="digest.digest_tool_kpi">
<t t-set="kpi_value" t-value="kpi_info['kpi_col1']['value']"/>
<t t-set="kpi_margin" t-value="kpi_info['kpi_col1']['margin']"/>
<t t-set="kpi_subtitle" t-value="kpi_info['kpi_col1']['col_subtitle']"/>
</t>
</td>
<td class="mobile-w-33" width="196" style=" text-align: center;">
<t t-call="digest.digest_tool_kpi">
<t t-set="kpi_value" t-value="kpi_info['kpi_col2']['value']"/>
<t t-set="kpi_margin" t-value="kpi_info['kpi_col2']['margin']"/>
<t t-set="kpi_subtitle" t-value="kpi_info['kpi_col2']['col_subtitle']"/>
</t>
</td>
<td class="mobile-w-33" width="196" style=" text-align: center;">
<t t-call="digest.digest_tool_kpi">
<t t-set="kpi_value" t-value="kpi_info['kpi_col3']['value']"/>
<t t-set="kpi_margin" t-value="kpi_info['kpi_col3']['margin']"/>
<t t-set="kpi_subtitle" t-value="kpi_info['kpi_col3']['col_subtitle']"/>
</t>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table t-if="preferences" bgcolor="#ffffff" cellspacing="0" cellpadding="0" width="650" align="center" border="0" style="border-left: 1px solid #eeeeee; border-right: 1px solid #eeeeee; width: 100%; max-width: 650px; padding:30px 30px">
<tr>
<td bgcolor="#fafafa" width="100%" style="padding-top: 20px; padding-bottom: 20px;" align="center">
<p t-foreach="preferences" t-as="preference" class="font-12" style="font-size: 15px; color: #6b6d70">
<t t-raw="preference"/>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table t-if="display_mobile_banner" cellspacing="0" cellpadding="0" align="center" border="0" bgcolor="#eeeeee" style="width:100%; font-family: Arial,Helvetica,Verdana,sans-serif;">
<tr>
<td align="center" valign="top">
<table bgcolor="#ffffff" cellspacing="0" cellpadding="0" width="650" align="center" border="0" style="border: 1px solid #eeeeee; width: 100%; max-width: 650px; padding:30px 30px">
<tr valign="top">
<td>
<t t-call="digest.digest_section_mobile"/>
</td>
</tr>
</table>
</td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" align="center" border="0" bgcolor="#eeeeee" style="width:100%; font-family: Arial,Helvetica,Verdana,sans-serif;">
<tr>
<td align="center" valign="top">
<table bgcolor="#eeeeee" cellspacing="0" cellpadding="0" width="650" align="center" border="0" style="margin-top:30px;font-size: 15px; text-align:center; width: 100%; max-width: 650px;">
<tr>
<td style="color:#8F8F8F;font-weight:bold;font-size: 20px; line-height: 30px;" t-esc="company.name"/>
</tr>
<tr>
<td style="color:#8F8F8F;font-size: 12px; line-height: 30px;">Powered by <a href="https://www.odoo.com" target="_blank" style="color:#875A7B">Odoo</a>.</td>
</tr>
<tr>
<td height="30"> </td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</template>
<!-- DIGEST PARTS -->
<!-- MOBILE BANNER -->
<template id="digest_section_mobile">
<table cellspacing="0" cellpadding="0" border="0" width="580" align="center" style="width:100%; max-width:580px;">
<tr>
<td valign="top" width="168">
<img src="https://www.odoo.com/web/image/24717933/odoo-mobile.png" alt="Odoo Mobile" width="168"/>
</td>
<td valign="top">
<p class="font-16" style="color:#2d2a26;font-size: 18px;line-height: 23px; margin-top: 20px">Run your business from anywhere with <br class="mobile-hide" /><b>Odoo Mobile</b>.</p>
<a href="https://play.google.com/store/apps/details?id=com.odoo.mobile" target="_blank"><img src="https://www.odoo.com/digest/static/src/img/google_play.png" style="display:inline-block;height: 40px; margin-bottom: 5px"/></a>
<span class="mobile-hide"> </span>
<a href="https://itunes.apple.com/us/app/odoo/id1272543640" target="_blank"><img src="https://www.odoo.com/digest/static/src/img/app_store.png" style="display:inline-block;height: 40px; margin-bottom: 5px"/></a>
</td>
</tr>
</table>
</template>
<!-- DIGEST TOOLS -->
<!-- KPI DISPLAY -->
<template id="digest_tool_kpi">
<span class="font-25" style="color:#282f33;font-size: 35px; font-weight: bold; text-decoration: none; line-height: 36px;">
<t t-esc="kpi_value"/>
</span><br/>
<span style="color:#888888;display: inline-block; font-size: 12px; line-height: 18px; text-transform: uppercase;">
<t t-esc="kpi_subtitle"/>
</span>
<t t-if="kpi_margin != 0.0">
<t t-if="kpi_margin &gt; 0.0">
<div style="background-color:#d5f1e2; border: 1px solid #c4ecd7; margin: 10px auto;width: 67px; line-height:27px; text-decoration: none; color: #17613a; border-radius: 5px; font-size:13px;">
▲ <t t-esc="'%.2f' % kpi_margin"/>
</div>
</div>
</div>
</field>
</record>
<record id="digest_tip_mail_1" model="digest.tip">
<field name="name">Mail: get things done with activities</field>
<field name="sequence">7</field>
<field name="tip_description" type="html">
<div>
<strong style="font-size: 16px;">Get things done with activities</strong>
<div style="font-size: 14px;">You don't have any activity scheduled. Use activities on any business document to schedule meetings, calls and todos.</div>
<center>
<img src="/digest/static/src/img/activity.png" width="70%" height="100%"/><br/>
</center>
</div>
</field>
</record>
</t>
<t t-elif="kpi_margin &lt; 0.0">
<div style="background-color:#f7dddc; border: 1px solid #f4cfce; margin: 10px auto;width: 67px; line-height:27px; text-decoration: none; color: #712b29; border-radius: 5px; font-size:13px;">
▼ <t t-esc="'%.2f' % kpi_margin"/>
</div>
</t>
</t>
</template>
<record id="digest_tip_mail_2" model="digest.tip">
<field name="name">Mail: apps videos tour</field>
<field name="group_id" ref='base.group_system'/>
<field name="sequence">0</field>
<field name="tip_description" type="html">
<div style="font-size: 14px;">
To reach your full potential with Odoo, have a look at our <a href="https://www.odoo.com/page/tour">apps videos</a> and discover new tools to work better and faster.
</div>
</field>
</record>
</data>
</odoo>
</data></odoo>
-164
View File
@@ -1,164 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="digest_mail_template" model="mail.template">
<field name="name">Digest: Default main template</field>
<field name="model_id" ref="digest.model_digest_digest"/>
<field name="subject">${'%s: %s' % (ctx.get('user', user).company_id.name, object.name)}</field>
<field name="email_from">${('%s' % (object.company_id.partner_id.email_formatted or user.email_formatted))|safe}</field>
<field name="body_html" type="html">
<table style="width: 100%; border-spacing: 0; font-family: Helvetica,Arial,Verdana,sans-serif;">
<tr>
<td align="center" valign="top" style="border-collapse: collapse; padding: 0">
% 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()
<table style="width: 100%; max-width: 600px; border-spacing: 0; border: 1px solid #e7e7e7; border-bottom: none; color: #6e7172; line-height: 23px; text-align: left;">
<tr>
<td style="border-collapse: collapse; padding: 10px 20px; text-align: left;">
<strong style="color: #000000; font-size: 22px; line-height: 32px;">${object.name}</strong>
<div style="color: #000000; font-size: 15px;">${datetime.date.today().strftime('%B %d, %Y')}</div>
</td>
<td style="text-align: right; padding: 10px 20px">
<img style="padding: 0px; margin: 0px; height: auto; width: 80px;" src="/logo.png?company=${company.id}"/>
<div style="padding-top: 5px;">
<a href="/web/login" style="background-color: #875A7B; margin-left: 5px; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">Connect</a>
</div>
</td>
</tr>
<tr><td colspan="2" style="text-align: center;">
<hr width="95%" style="background-color: rgb(204,204,204); border: medium none; clear: both; display: block; font-size: 0px; min-height: 1px; line-height: 0; margin: 16px 0px 16px 14px;"/>
</td></tr>
</table>
% for kpi in kpis:
<table style="border-spacing: 0; width: 100%; max-width: 600px;">
<tr>
<td style="border-collapse: collapse; background-color: #ffffff; border-left: 1px solid #e7e7e7; border-right: 1px solid #e7e7e7; line-height: 21px; padding: 0 20px 10px 20px; text-align: left;"><br/>
<span style="color: #3d466e; font-size: 18px; font-weight: 500; line-height: 23px;">${object.fields_get()[kpi]['string']}</span>
%if kpi in kpi_actions:
<span style="float: right;">
<a href="/web#action=${kpi_actions[kpi]}">View more</a>
</span>
%endif
</td>
</tr>
<tr>
<td style="border-collapse: collapse; margin: 0; padding:0;">
<table style="width: 100%; border-spacing: 0; background-color: #f9f9f9; border: 1px solid #e7e7e7; border-top: none;">
<tr>
<td style="border-collapse: collapse; margin: 0; padding: 0; display: block; border-top: 2px solid #56b3b5;">
<table style="width: 100%; max-width: 199px; border-spacing: 0;">
<tr>
<td style="border-collapse: collapse; padding: 20px; text-align: center;">
<span style="color: #56b3b5; font-size: 35px; font-weight: bold; text-decoration: none; line-height: 36px;">${data['yesterday'][kpi][kpi]}</span><br/>
<span style="color: #888888; display: inline-block; font-size: 12px; line-height: 18px; text-transform: uppercase;">Yesterday</span>
% if data['yesterday'][kpi]['margin'] != 0.0:
<span style="color: #888888; display: block; font-size: 12px; line-height: 18px; text-transform: uppercase;">
% if data['yesterday'][kpi]['margin'] is greaterthan(0.0):
<span style="color: #0bbc22;">▲</span>${"%.2f" % data['yesterday'][kpi]['margin']} %
% endif
% if data['yesterday'][kpi]['margin'] is lessthan(0.0):
<span style="color: #ff0000;">▼</span>${"%.2f" % data['yesterday'][kpi]['margin']} %
% endif
</span>
% endif
</td>
</tr>
</table>
</td>
<td style="border-collapse: collapse; margin: 0; padding: 0; border-top: 2px solid #9a5b82;">
<table style="width: 100%; max-width: 199px; border-spacing: 0; margin: 0; padding: 0;">
<tr>
<td style="border-collapse: collapse; padding: 20px; text-align: center;">
<span style="color: #9a5b82; font-size: 35px; font-weight: bold; text-decoration: none; line-height: 36px;">${data['lastweek'][kpi][kpi]}</span><br/>
<span style="color: #888888; display: inline-block; font-size: 12px; line-height: 18px; text-transform: uppercase;">Last 7 Days</span>
% if data['lastweek'][kpi]['margin'] != 0.0:
<span style="color: #888888; display: block; font-size: 12px; line-height: 18px; text-transform: uppercase;">
% if data['lastweek'][kpi]['margin'] is greaterthan(0.0):
<span style="color: #0bbc22;">▲</span>${"%.2f" % data['lastweek'][kpi]['margin']} %
% endif
% if data['lastweek'][kpi]['margin'] is lessthan(0.0):
<span style="color: #ff0000;">▼</span>${"%.2f" % data['lastweek'][kpi]['margin']} %
%endif
</span>
%endif
</td>
</tr>
</table>
</td>
<td style="border-collapse: collapse; margin: 0; padding: 15px; border-top: 2px solid #56b3b5;">
<table style="width: 100%; max-width: 199px; border-spacing: 0; margin: 0; padding: 0;">
<tr>
<td style="border-collapse: collapse; margin: 0; text-align: center;">
<span style="color: #56b3b5; font-size: 35px; font-weight: bold; text-decoration: none; line-height: 36px">${data['lastmonth'][kpi][kpi]}</span><br/>
<span style="color: #888888; display: inline-block; font-size: 12px; line-height: 18px; text-transform: uppercase;">Last 30 Days</span>
% if data['lastmonth'][kpi]['margin'] != 0.0:
<span style="color: #888888; display: block; font-size: 12px; line-height: 18px; text-transform: uppercase;">
% if data['lastmonth'][kpi]['margin'] is greaterthan(0.0):
<span style="color: #0bbc22;">▲</span>${"%.2f" % data['lastmonth'][kpi]['margin']} %
% endif
% if data['lastmonth'][kpi]['margin'] is lessthan(0.0):
<span style="color: #ff0000;">▼</span>${"%.2f" % data['lastmonth'][kpi]['margin']} %
%endif
</span>
%endif
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
% endfor
% if tips:
<table style="width: 100%; max-width: 600px; margin-top: 5px; border: 1px solid #e7e7e7;">
<tr>
<td style="border-collapse: collapse; background-color: #ffffff; line-height: 21px; padding: 0px 20px 20px;"><br/>
<div style="color: #3d466e; line-height: 23px;">${tips | safe}</div>
</td>
</tr>
</table>
% endif
<table style="width: 100%; max-width: 600px; margin-top: 5px; border: 1px solid #e7e7e7;">
<tr>
<td style="border-collapse: collapse; background-color: #ffffff; line-height: 21px; padding: 0 20px 10px 20px; text-align: center;"><br/>
<div style="color: #3d466e; font-size: 16px; font-weight: 600; line-height: 23px;">Run your business from anywhere with Odoo Mobile.</div>
</td>
</tr>
<tr>
<td style="padding-bottom:20px;">
<div style="text-align: center;"><a href="https://play.google.com/store/apps/details?id=com.odoo.mobile" target="_blank"><img src="/digest/static/src/img/google_play.png" style="display: inline-block; height: 30px; margin-left: auto; margin-right: 12px;"/></a><a href="https://itunes.apple.com/us/app/odoo/id1272543640" target="_blank"><img src="/digest/static/src/img/app_store.png" style="display: inline-block; height: 30px; margin-left: 12px; margin-right: auto;"/></a>
</div>
</td>
</tr>
</table>
<table style="margin-top: 5px; border: 1px solid #e7e7e7; font-size: 15px; width: 100%; max-width: 600px;">
<tr>
<td style="border-collapse: collapse; margin: 0; padding: 10px 20px;">
% if user.has_group('base.group_system'):
<div style="margin-top: 20px;">
Want to customize this email?
<a href="/web#view_type=form&amp;model=digest.digest&amp;id=${object.id}" target="_blank" style="color: #875A7B;">Choose the metrics you care about</a>
</div>
<br />
% endif
<p style="font-size: 11px; margin-top: 10px;">
<strong>
Sent by
<a href="https://www.odoo.com" style="text-decoration: none; color: #875A7B;">Odoo</a> - <a href="/web#view_type=form&amp;model=digest.digest&amp;id=${object.id}" target="_blank" style="color: #888888;">Unsubscribe</a>
</strong>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table></field>
<field name="lang">${user.lang}</field>
<field name="auto_delete" eval="True"/>
</record>
</odoo>
+109
View File
@@ -0,0 +1,109 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<data noupdate="1">
<record id="digest_digest_default" model="digest.digest">
<field name="name">Your Odoo Periodic Report</field>
<field name="periodicity">daily</field>
<field name="user_ids" eval="[(4, ref('base.user_admin'))]"/>
<field name="next_run_date" eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d')"/>
<field name="kpi_res_users_connected">True</field>
<field name="kpi_mail_message_total">True</field>
</record>
</data>
<data>
<record id="digest_tip_digest_0" model="digest.tip">
<field name="name">Invite Team Members</field>
<field name="sequence">1</field>
<field name="group_id" ref="base.group_erp_manager"/>
<field name="tip_description" type="html">
<div>
<b class="font-16" style="font-size: 20px;line-height: 30px">Invite Team Members</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin-top:0;">Go to your general settings to invite new users to your database.</p>
<img src="/digest/static/src/img/tips-digest-invite-team.jpg" style="max-width: 580px" width="100%"/>
</div>
</field>
</record>
<record id="digest_tip_digest_1" model="digest.tip">
<field name="name"> Want to learn more about Odoo?</field>
<field name="sequence">2</field>
<field name="group_id" ref="base.group_user"/>
<field name="tip_description" type="html">
<div>
<b class="font-16" style="font-size: 20px;line-height: 30px"> Want to learn more about Odoo?</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin-top:0;">Visit our <a href="https://www.odoo.com/slides">eLearning Platform</a></p>
</div>
</field>
</record>
<record id="digest_tip_digest_2" model="digest.tip">
<field name="name">Odoo Push Notifications</field>
<field name="sequence">3</field>
<field name="group_id" ref="base.group_erp_manager"/>
<field name="tip_description" type="html">
<div>
<b class="font-16" style="font-size: 20px;line-height: 30px">Odoo Push Notifications</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin-top:0;">Change your browser settings to "Allow Push Notifications" and never miss a thing.</p>
<img src="/digest/static/src/img/tips-digest-browser-notifications.jpg" style="max-width: 580px" width="100%"/>
</div>
</field>
</record>
<record id="digest_tip_digest_3" model="digest.tip">
<field name="name">Reach your full potential with Odoo</field>
<field name="sequence">4</field>
<field name="group_id" ref="base.group_erp_manager"/>
<field name="tip_description" type="html">
<div>
<b class="font-16" style="font-size: 20px;line-height: 30px">Reach your full potential with Odoo</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin-top:0;">Take a look at our <a href="https://www.odoo.com/page/tour">App videos</a> and discover the next tool that will allow you to work better and faster.</p>
</div>
</field>
</record>
<record id="digest_tip_mail_0" model="digest.tip">
<field name="name">Did you know?</field>
<field name="sequence">10</field>
<field name="group_id" ref="base.group_user"/>
<field name="tip_description" type="html">
<div>
% 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
<b class="font-16" style="font-size: 20px;line-height: 30px">Did you know?</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin-top:0;">You can ping your colleagues by tagging them in your messages using “@” followed by their name. They will be instantly notified.</p>
<img src="/digest/static/src/img/tips-mail-notifications.jpg" style="max-width: 580px" width="100%"/>
<div style="margin-top: 20px; background-color: #f8f9fa">
<p class="font-12" style="text-align:center; color:#888888;font-size: 13px; line-height: 25px; margin:0;"><b>${', '.join(users.mapped('name'))}</b> just signed up. <a href="/web#action=mail.action_discuss&amp;active_id=${channel_id}" style="color: #875a7b; font-weight: bold">Say hello in the company's discussion channel.</a>
</p>
</div>
</div>
</field>
</record>
<record id="digest_tip_mail_1" model="digest.tip">
<field name="name">Get things done with activities</field>
<field name="sequence">11</field>
<field name="group_id" ref="base.group_user"/>
<field name="tip_description" type="html">
<div>
<b class="font-16" style="font-size: 20px;line-height: 30px">Get things done with activities</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin-top:0;">Use activities on any business document to schedule meetings, calls and todos.</p>
<img src="/digest/static/src/img/tips-mail-activities.jpg" style="max-width: 580px" width="100%" />
</div>
</field>
</record>
<record id="digest_tip_mail_2" model="digest.tip">
<field name="name">Follow what you want</field>
<field name="sequence">12</field>
<field name="group_id" ref="base.group_user"/>
<field name="tip_description" type="html">
<div>
<b class="font-16" style="font-size: 20px;line-height: 30px">Follow what you want</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin-top:0;">Keep track of any object in Odoo (opportunity, sales order, etc.) and choose exactly what you want to follow (discussion, status, status change, etc.).</p>
<img src="/digest/static/src/img/tips-mail-following.jpg" style="max-width: 580px" width="100%" />
</div>
</field>
</record>
</data>
</odoo>
+197 -64
View File
@@ -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 <a href="/digest/%s/set_periodicity?periodicity=weekly" target="_blank" style="color:#875A7B; font-weight: bold;">%s</a>' % (
_('Prefer a broader overview ?'),
self.id,
_('Switch to weekly Digests')
))
if user.has_group('base.group_erp_manager'):
preferences.append('%s <a href="/web#view_type=form&amp;model=%s&amp;id=%s" target="_blank" style="color:#875A7B; font-weight: bold;">%s</a>' % (
_('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.')
+1 -1
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_digest_digest_system digest.digest.administration model_digest_digest base.group_erp_manager 1 1 1 1
3 access_digest_digest_user digest.digest.user model_digest_digest base.group_user 1 0 0 0
4 access_digest_tip_system digest.tip.administration model_digest_tip base.group_erp_manager 1 1 1 1
5 access_digest_tip_user digest.tip.user model_digest_tip base.group_user 0 1 0 0 0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

-2
View File
@@ -46,10 +46,8 @@
<group>
<group>
<field name="periodicity" widget="radio" options="{'horizontal': true}"/>
<field name="template_id" groups="base.group_no_one"/>
<field name="next_run_date" groups="base.group_system"/>
<field name="company_id" options="{'no_create': True}" invisible="1"/>
<!-- <field name="digest_history_ids" invisible="1"/> -->
</group>
</group>
<notebook>
+5 -6
View File
@@ -8,16 +8,15 @@
<data>
<record id="digest_tip_hr_recruitment_0" model="digest.tip">
<field name="name">HR Recruitment: try mailgateway</field>
<field name="name">Recruitment: Create new applicants from incoming emails</field>
<field name="sequence">4</field>
<field name="group_id" ref="hr_recruitment.group_hr_recruitment_user"/>
<field name="tip_description" type="html">
<div>
<strong style="font-size: 16px;">Try the mail gateway</strong>
<div style="font-size: 14px;">New applicants can be generated from incoming emails. You just need to set email aliases in configuration of job positions.<br/>
<div style="text-align:center;margin-top:5px;margin-bottom:2px;">
<a href="/web#action=hr_recruitment.action_hr_job_config" style="background-color:#56b3b5;padding:2px;color:#FFFFFF;font-weight:bold;text-decoration:none;">Set Email Aliases</a>
</div>
<b class="font-16" style="font-size: 20px;line-height: 30px">Create new applicants from incoming emails</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin-top: 0">You just need to set email aliases in your job positions' settings.</p>
<div style="text-align:center;margin-top:5px;margin-bottom:10px;">
<a href="/web#action=hr_recruitment.action_hr_job_config" target="_blank" style="display:inline-block;background-color:#875a7b;line-height:40px;text-decoration: none;color: #ffffff;border-radius: 3px;font-size:13px;font-weight:bold;text-align:center; padding:0 10px;">Set Email Aliases</a>
</div>
</div>
</field>
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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'
+77 -6
View File
@@ -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)
+2 -2
View File
@@ -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
+7 -8
View File
@@ -8,17 +8,16 @@
<data>
<record id="digest_tip_project_0" model="digest.tip">
<field name="name">Project: try mailgateway</field>
<field name="sequence">6</field>
<field name="name">Project: Create tasks from incoming emails</field>
<field name="sequence">5</field>
<field name="group_id" ref="project.group_project_manager"/>
<field name="tip_description" type="html">
<div>
<strong style="font-size: 16px;">Try the mail gateway</strong>
<div style="font-size: 14px;">
New tasks can be generated from incoming emails. You just need to set email aliases on your projects.<br/>
<div style="text-align:center;margin-top:5px;margin-bottom:2px;">
<a href="/web#action=project.open_view_project_all_config" style="background-color:#56b3b5;padding:2px;color:#FFFFFF;font-weight:bold;text-decoration:none;">Set Email Aliases</a>
</div>
<b class="font-16" style="font-size: 20px;line-height: 30px">Create tasks from incoming emails</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin: 0">You just need to set email aliases to your projects.</p>
<div style="text-align:center;margin-top:5px;margin-bottom:10px;">
<a href="/web#action=project.open_view_project_all_config" target="_blank" style="display:inline-block;background-color:#875a7b;line-height:40px;text-decoration: none;color: #ffffff;border-radius: 3px;font-size:13px;font-weight:bold;text-align:center; padding:0 10px;">Try it now
</a>
</div>
</div>
</field>
+2 -2
View File
@@ -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
+9 -7
View File
@@ -1,16 +1,18 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<record id="digest_tip_sale_expense_0" model="digest.tip">
<field name="name">Expense: try mailgateway</field>
<field name="name">Expense: Submit expenses by email</field>
<field name="sequence">5</field>
<field name="group_id" ref="sales_team.group_sale_salesman_all_leads"/>
<field name="group_id" ref="base.group_user"/>
<field name="tip_description" type="html">
<div>
<strong style="font-size: 16px;">Submit expenses by email</strong>
<div style="font-size: 14px;">Take a snapshot of your expenses and submit your expenses by email.<br/>
<div style="text-align:center;margin-top:5px;margin-bottom:10px;">
<a href="/web#action=hr_expense.action_hr_expense_configuration" style="background-color:#56b3b5;padding:10px;text-decoration:none;color:#FFFFFF;border-radius:5px;font-size:12px;">Activate Expense Emails</a>
</div>
<b class="font-16" style="font-size: 20px;line-height: 30px">Submit expenses by email</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin-top: 0">Take a snapshot of your expenses and submit your expenses by email.</p>
<div style="text-align:center;margin-top:5px;margin-bottom:10px;">
<a href="/web#action=hr_expense.action_hr_expense_configuration" target="_blank"
style="display:inline-block;background-color:#875a7b;line-height:40px;text-decoration: none;color: #ffffff;border-radius: 3px;font-size:13px;font-weight:bold;text-align:center;padding:0 10px;">
Activate Expense Emails
</a>
</div>
</div>
</field>
+2 -2
View File
@@ -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
+17
View File
@@ -0,0 +1,17 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo>
<data>
<record id="digest_tip_website_0" model="digest.tip">
<field name="name">Website: Use SEO to get more visitors</field>
<field name="sequence">100</field>
<field name="group_id" ref="website.group_website_designer"/>
<field name="tip_description" type="html">
<div>
<b class="font-16" style="font-size: 20px;line-height: 30px">Use SEO to get more visitors on your website</b>
<p class="font-12" style="color:#888888;font-size: 15px; line-height: 25px; margin-top: 0">Odoo helps you to optimize your website and get a better ranking in search engines like Google.</p>
<img src="/digest/static/src/img/tips-website-seo.jpg" style="max-width: 580px" width="100%"/>
</div>
</field>
</record>
</data>
</odoo>
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

+2 -2
View File
@@ -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