[REF] mail: template: move formatting to tools

This commit moves formatting helpers method used in mail.template to tools. The goal
is to reuse them anywhere in odoo, and not only in mail templates. Thoses method are
used to format date, datetime, amount, ... according to user localization (currency,
language, pattern, ...).

This commit also improve the formatting methods of 'date' and 'datetime': we are now
taking the 'lang_code' into account. Indeed, as the implementation uses `babel` lib,
it is easy and usefull to add the language option, to display the given dates into a
particular language. This parameter is optional to keep compatiblity with existing
code.

Task-2025396

Code Courtesy of @tde-banana-odoo
This commit is contained in:
Thibault Delavallée
2019-06-28 06:51:16 +00:00
committed by jem-odoo
parent f17655dd77
commit ef7871826e
6 changed files with 107 additions and 65 deletions
+6 -6
View File
@@ -35,8 +35,8 @@
<field name="email_to">${(object.partner_id.email_formatted or '"%s" &lt;%s&gt;' % (object.name, object.email) or '') | safe}</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
% set date_begin = format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%SZ')
% set date_end = format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%SZ')
% set date_begin = format_datetime(object.event_id.date_begin, tz='UTC', dt_format='%Y%m%dT%H%M%SZ')
% set date_end = format_datetime(object.event_id.date_end, tz='UTC', dt_format='%Y%m%dT%H%M%SZ')
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
@@ -178,7 +178,7 @@
<strong>Add this event to your calendar</strong>
<a href="https://www.google.com/calendar/render?action=TEMPLATE&amp;text=${object.event_id.name}&amp;dates=${date_begin}/${date_end}&amp;location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> Google</a>
<a href="/event/${slug(object.event_id)}/ics" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> iCal/Outlook</a>
<a href="https://calendar.yahoo.com/?v=60&amp;view=d&amp;type=20&amp;title=${object.event_id.name}&amp;in_loc=${location}&amp;st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&amp;et=${format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new">
<a href="https://calendar.yahoo.com/?v=60&amp;view=d&amp;type=20&amp;title=${object.event_id.name}&amp;in_loc=${location}&amp;st=${format_datetime(object.event_id.date_begin, tz='UTC', dt_format='%Y%m%dT%H%M%S')}&amp;et=${format_datetime(object.event_id.date_end, tz='UTC', dt_format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new">
<img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> Yahoo</a>
</td>
</tr>
@@ -232,8 +232,8 @@
<field name="email_to">${(object.partner_id.email_formatted or '"%s" &lt;%s&gt;' % (object.name, object.email) or '') | safe}</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
% set date_begin = format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%SZ')
% set date_end = format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%SZ')
% set date_begin = format_datetime(object.event_id.date_begin, tz='UTC', dt_format='%Y%m%dT%H%M%SZ')
% set date_end = format_datetime(object.event_id.date_end, tz='UTC', dt_format='%Y%m%dT%H%M%SZ')
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
<tbody>
<!-- HEADER -->
@@ -375,7 +375,7 @@
<strong>Add this event to your calendar</strong>
<a href="https://www.google.com/calendar/render?action=TEMPLATE&amp;text=${object.event_id.name}&amp;dates=${date_begin}/${date_end}&amp;location=${location}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> Google</a>
<a href="/event/${slug(object.event_id)}/ics" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;"><img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> iCal/Outlook</a>
<a href="https://calendar.yahoo.com/?v=60&amp;view=d&amp;type=20&amp;title=${object.event_id.name}&amp;in_loc=${location}&amp;st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&amp;et=${format_tz(object.event_id.date_end, tz='UTC', format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new">
<a href="https://calendar.yahoo.com/?v=60&amp;view=d&amp;type=20&amp;title=${object.event_id.name}&amp;in_loc=${location}&amp;st=${format_datetime(object.event_id.date_begin, tz='UTC', dt_format='%Y%m%dT%H%M%S')}&amp;et=${format_datetime(object.event_id.date_end, tz='UTC', dt_format='%Y%m%dT%H%M%S')}" style="padding:3px 5px;border:1px solid #875A7B;color:#875A7B;text-decoration:none;border-radius:3px;" target="new">
<img src="/web_editor/font_to_img/61525/rgb(135,90,123)/16" style="vertical-align:middle;" height="16" alt=""/> Yahoo</a>
</td>
</tr>
+4 -4
View File
@@ -4,7 +4,7 @@ import logging
import pytz
from odoo import _, api, fields, models
from odoo.addons.mail.models.mail_template import format_tz
from odoo.tools import format_datetime
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools.translate import html_translate
@@ -219,7 +219,7 @@ class EventEvent(models.Model):
@api.depends('date_tz', 'date_begin')
def _compute_date_begin_tz(self):
if self.date_begin:
self.date_begin_located = format_tz(self.with_context(use_babel=True).env, self.date_begin, tz=self.date_tz)
self.date_begin_located = format_datetime(self.env, self.date_begin, tz=self.date_tz, dt_format='medium')
else:
self.date_begin_located = False
@@ -227,7 +227,7 @@ class EventEvent(models.Model):
@api.depends('date_tz', 'date_end')
def _compute_date_end_tz(self):
if self.date_end:
self.date_end_located = format_tz(self.with_context(use_babel=True).env, self.date_end, tz=self.date_tz)
self.date_end_located = format_datetime(self.env, self.date_end, tz=self.date_tz, dt_format='medium')
else:
self.date_end_located = False
@@ -561,7 +561,7 @@ class EventRegistration(models.Model):
elif event_date.month == (today + relativedelta(months=+1)).month:
return _('next month')
else:
return _('on ') + format_tz(self.with_context({'use_babel': True}).env, self.event_begin_date, tz=self.event_id.date_tz or 'UTC')
return _('on ') + format_datetime(self.env, self.event_begin_date, tz=self.event_id.date_tz, dt_format='medium')
@api.multi
def summary(self):
+15 -53
View File
@@ -6,10 +6,9 @@ import base64
import copy
import datetime
import dateutil.relativedelta as relativedelta
import functools
import logging
import functools
import lxml
from werkzeug import urls
from odoo import _, api, fields, models, tools
@@ -18,59 +17,18 @@ from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
def format_date(env, date, pattern=False):
if not date:
return ''
def format_date(env, date, pattern=False, lang_code=False):
try:
return tools.format_date(env, date, date_format=pattern)
return tools.format_date(env, date, date_format=pattern, lang_code=lang_code)
except babel.core.UnknownLocaleError:
return date
def format_tz(env, dt, tz=False, format=False):
record_user_timestamp = env.user.sudo().with_context(tz=tz or env.user.sudo().tz or 'UTC')
timestamp = fields.Datetime.from_string(dt)
ts = fields.Datetime.context_timestamp(record_user_timestamp, timestamp)
# Babel allows to format datetime in a specific language without change locale
# So month 1 = January in English, and janvier in French
# Be aware that the default value for format is 'medium', instead of 'short'
# medium: Jan 5, 2016, 10:20:31 PM | 5 janv. 2016 22:20:31
# short: 1/5/16, 10:20 PM | 5/01/16 22:20
if env.context.get('use_babel'):
# Formatting available here : http://babel.pocoo.org/en/latest/dates.html#date-fields
from babel.dates import format_datetime
return format_datetime(ts, format or 'medium', locale=env.context.get("lang") or 'en_US')
if format:
return str(ts.strftime(format))
else:
lang = env.context.get("lang")
langs = env['res.lang']
if lang:
langs = env['res.lang'].search([("code", "=", lang)])
format_date = langs.date_format or '%B-%d-%Y'
format_time = langs.time_format or '%I-%M %p'
fdate = ts.strftime(format_date)
ftime = ts.strftime(format_time)
return "%s %s%s" % (fdate, ftime, (' (%s)' % tz) if tz else '')
def format_amount(env, amount, currency):
fmt = "%.{0}f".format(currency.decimal_places)
lang = env['res.lang']._lang_get(env.context.get('lang') or 'en_US')
formatted_amount = lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\
.replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'-\N{ZERO WIDTH NO-BREAK SPACE}')
pre = post = u''
if currency.position == 'before':
pre = u'{symbol}\N{NO-BREAK SPACE}'.format(symbol=currency.symbol or '')
else:
post = u'\N{NO-BREAK SPACE}{symbol}'.format(symbol=currency.symbol or '')
return u'{pre}{0}{post}'.format(formatted_amount, pre=pre, post=post)
def format_datetime(env, dt, tz=False, dt_format='medium', lang_code=False):
try:
return tools.format_datetime(env, dt, tz=tz, dt_format=dt_format, lang_code=lang_code)
except babel.core.UnknownLocaleError:
return dt
try:
# We use a jinja2 sandboxed environment to render mako templates.
@@ -320,9 +278,9 @@ class MailTemplate(models.Model):
for record in records:
res_to_rec[record.id] = record
variables = {
'format_date': lambda date, format=False, context=self._context: format_date(self.env, date, format),
'format_tz': lambda dt, tz=False, format=False, context=self._context: format_tz(self.env, dt, tz, format),
'format_amount': lambda amount, currency, context=self._context: format_amount(self.env, amount, currency),
'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),
'user': self.env.user,
'ctx': self._context, # context kw would clash with mako internals
}
@@ -500,6 +458,10 @@ class MailTemplate(models.Model):
return multi_mode and results or results[res_ids[0]]
# ----------------------------------------
# EMAIL
# ----------------------------------------
@api.multi
def send_mail(self, res_id, force_send=False, raise_exception=False, email_values=None, notif_layout=False):
""" Generates a new mail.mail. Template is rendered on record given by
+1 -1
View File
@@ -9,7 +9,7 @@
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Hello<br/><br/>
A new ${object.slide_type} <strong>${object.name}</strong> has been published on ${object.channel_id.name} at ${format_tz(object.write_date, tz=user.tz)}
A new ${object.slide_type} <strong>${object.name}</strong> has been published on ${object.channel_id.name} at ${format_datetime(object.write_date, tz=user.tz)}
<div style="margin: 16px 8px 16px 8px; text-align: center;">
<a href="${object.website_url}">
<img alt="${object.name}" src="${ctx['base_url']}/web/image/slide.slide/${object.id}/image" style="height:auto; width:150px; margin: 16px;"/>
+24 -1
View File
@@ -165,7 +165,8 @@ class TestDateRangeFunction(unittest.TestCase):
class TestFormatLangDate(TransactionCase):
def test_00_accepted_types(self):
date_datetime = datetime.datetime.strptime('2017-01-31 12:00:00', "%Y-%m-%d %H:%M:%S")
datetime_str = '2017-01-31 12:00:00'
date_datetime = datetime.datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S")
date_date = date_datetime.date()
date_str = '2017-01-31'
@@ -176,6 +177,12 @@ class TestFormatLangDate(TransactionCase):
self.assertEqual(misc.format_date(self.env, False), '')
self.assertEqual(misc.format_date(self.env, None), '')
self.assertEqual(misc.format_datetime(self.env, date_datetime), 'Jan 31, 2017, 1:00:00 PM')
self.assertEqual(misc.format_datetime(self.env, datetime_str), 'Jan 31, 2017, 1:00:00 PM')
self.assertEqual(misc.format_datetime(self.env, ''), '')
self.assertEqual(misc.format_datetime(self.env, False), '')
self.assertEqual(misc.format_datetime(self.env, None), '')
def test_01_code_and_format(self):
date_str = '2017-01-31'
lang = self.env['res.lang']
@@ -183,6 +190,7 @@ class TestFormatLangDate(TransactionCase):
# Activate French and Simplified Chinese (test with non-ASCII characters)
lang.search([('active', '=', False), ('code', 'in', ['fr_FR', 'zh_CN'])]).write({'active': True})
# -- test `date`
# Change a single parameter
self.assertEqual(misc.format_date(lang.with_context(lang='fr_FR').env, date_str), '31/01/2017')
self.assertEqual(misc.format_date(lang.env, date_str, lang_code='fr_FR'), '31/01/2017')
@@ -195,3 +203,18 @@ class TestFormatLangDate(TransactionCase):
# Change 3 parameters
self.assertEqual(misc.format_date(lang.with_context(lang='zh_CN').env, date_str, lang_code='en_US', date_format='MMM d, y'), 'Jan 31, 2017')
# -- test `datetime`
datetime_str = '2017-01-31 10:33:00'
# Change languages and timezones
self.assertEqual(misc.format_datetime(lang.with_context(lang='fr_FR').env, datetime_str, tz='Europe/Brussels'), '31 janv. 2017 à 11:33:00')
self.assertEqual(misc.format_datetime(lang.with_context(lang='zh_CN').env, datetime_str, tz='America/New_York'), '2017\u5E741\u670831\u65E5 \u4E0A\u53485:33:00') # '2017年1月31日 上午5:33:00'
# Change language, timezone and format
self.assertEqual(misc.format_datetime(lang.with_context(lang='fr_FR').env, datetime_str, tz='America/New_York', dt_format='short'), '31/01/2017 05:33')
self.assertEqual(misc.format_datetime(lang.with_context(lang='en_US').env, datetime_str, tz='Europe/Brussels', dt_format='MMM d, y'), 'Jan 31, 2017')
# Check given `lang_code` overwites context lang
self.assertEqual(misc.format_datetime(lang.env, datetime_str, tz='Europe/Brussels', dt_format='long', lang_code='fr_FR'), '31 janvier 2017 à 11:33:00 +0100')
self.assertEqual(misc.format_datetime(lang.with_context(lang='zh_CN').env, datetime_str, tz='Europe/Brussels', dt_format='long', lang_code='en_US'), 'January 31, 2017 at 11:33:00 AM +0100')
+57
View File
@@ -16,6 +16,7 @@ import os
import collections
import passlib.utils
import pickle as pickle_
import pytz
import re
import socket
import sys
@@ -1196,6 +1197,62 @@ def format_date(env, value, lang_code=False, date_format=False):
return babel.dates.format_date(value, format=date_format, locale=locale)
def format_datetime(env, value, tz=False, dt_format='medium', lang_code=False):
""" Formats the datetime in a given format.
:param {str, datetime} value: naive datetime to format either in string or in datetime
:param {str} tz: name of the timezone in which the given datetime should be localized
:param {str} dt_format: one of “full”, “long”, “medium”, or “short”, or a custom date/time pattern compatible with `babel` lib
:param {str} lang_code: ISO code of the language to use to render the given datetime
"""
if not value:
return ''
if isinstance(value, str):
timestamp = odoo.fields.Datetime.from_string(value)
else:
timestamp = value
tz_name = tz or env.user.tz or 'UTC'
utc_datetime = pytz.utc.localize(timestamp, is_dst=False)
try:
context_tz = pytz.timezone(tz_name)
localized_datetime = utc_datetime.astimezone(context_tz)
except Exception:
localized_datetime = utc_datetime
lang = env['res.lang']._lang_get(lang_code or env.context.get('lang') or 'en_US')
locale = babel.Locale.parse(lang.code)
if not dt_format:
date_format = posix_to_ldml(lang.date_format, locale=locale)
time_format = posix_to_ldml(lang.time_format, locale=locale)
dt_format = '%s %s' % (date_format, time_format)
# Babel allows to format datetime in a specific language without change locale
# So month 1 = January in English, and janvier in French
# Be aware that the default value for format is 'medium', instead of 'short'
# medium: Jan 5, 2016, 10:20:31 PM | 5 janv. 2016 22:20:31
# short: 1/5/16, 10:20 PM | 5/01/16 22:20
# Formatting available here : http://babel.pocoo.org/en/latest/dates.html#date-fields
return babel.dates.format_datetime(localized_datetime, dt_format, locale=locale)
def format_amount(env, amount, currency, lang_code=False):
fmt = "%.{0}f".format(currency.decimal_places)
lang = env['res.lang']._lang_get(lang_code or env.context.get('lang') or 'en_US')
formatted_amount = lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\
.replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'-\N{ZERO WIDTH NO-BREAK SPACE}')
pre = post = u''
if currency.position == 'before':
pre = u'{symbol}\N{NO-BREAK SPACE}'.format(symbol=currency.symbol or '')
else:
post = u'\N{NO-BREAK SPACE}{symbol}'.format(symbol=currency.symbol or '')
return u'{pre}{0}{post}'.format(formatted_amount, pre=pre, post=post)
def _consteq(str1, str2):
""" Constant-time string comparison. Suitable to compare bytestrings of fixed,
known length only, because length difference is optimized. """