[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:
committed by
jem-odoo
parent
f17655dd77
commit
ef7871826e
@@ -35,8 +35,8 @@
|
||||
<field name="email_to">${(object.partner_id.email_formatted or '"%s" <%s>' % (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&text=${object.event_id.name}&dates=${date_begin}/${date_end}&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&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&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&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_datetime(object.event_id.date_begin, tz='UTC', dt_format='%Y%m%dT%H%M%S')}&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" <%s>' % (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&text=${object.event_id.name}&dates=${date_begin}/${date_end}&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&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_tz(object.event_id.date_begin, tz='UTC', format='%Y%m%dT%H%M%S')}&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&view=d&type=20&title=${object.event_id.name}&in_loc=${location}&st=${format_datetime(object.event_id.date_begin, tz='UTC', dt_format='%Y%m%dT%H%M%S')}&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,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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;"/>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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. """
|
||||
|
||||
Reference in New Issue
Block a user