From ef7871826edce400884fe032366781f59aafd8a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 3 Apr 2019 13:35:21 +0000 Subject: [PATCH] [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 --- addons/event/data/email_template_data.xml | 12 ++-- addons/event/models/event.py | 8 +-- addons/mail/models/mail_template.py | 68 +++++------------------ addons/website_slides/data/mail_data.xml | 2 +- odoo/addons/base/tests/test_misc.py | 25 ++++++++- odoo/tools/misc.py | 57 +++++++++++++++++++ 6 files changed, 107 insertions(+), 65 deletions(-) diff --git a/addons/event/data/email_template_data.xml b/addons/event/data/email_template_data.xml index 4aec1ff6a13..c0b3ece2d08 100644 --- a/addons/event/data/email_template_data.xml +++ b/addons/event/data/email_template_data.xml @@ -35,8 +35,8 @@ ${(object.partner_id.email_formatted or '"%s" <%s>' % (object.name, object.email) or '') | safe}
-% 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') @@ -178,7 +178,7 @@ Add this event to your calendar Google iCal/Outlook - + Yahoo @@ -232,8 +232,8 @@ ${(object.partner_id.email_formatted or '"%s" <%s>' % (object.name, object.email) or '') | safe}
-% 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') @@ -375,7 +375,7 @@ Add this event to your calendar Google iCal/Outlook - + Yahoo diff --git a/addons/event/models/event.py b/addons/event/models/event.py index 40667f0c3b0..1c1f24a099b 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -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): diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index a334487f364..3edfbe28e66 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -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 diff --git a/addons/website_slides/data/mail_data.xml b/addons/website_slides/data/mail_data.xml index 246fec87e28..2cf45c4e0c5 100644 --- a/addons/website_slides/data/mail_data.xml +++ b/addons/website_slides/data/mail_data.xml @@ -9,7 +9,7 @@

Hello

- A new ${object.slide_type} ${object.name} has been published on ${object.channel_id.name} at ${format_tz(object.write_date, tz=user.tz)} + A new ${object.slide_type} ${object.name} has been published on ${object.channel_id.name} at ${format_datetime(object.write_date, tz=user.tz)}

${object.name} diff --git a/odoo/addons/base/tests/test_misc.py b/odoo/addons/base/tests/test_misc.py index 08c790387e9..9690243547a 100644 --- a/odoo/addons/base/tests/test_misc.py +++ b/odoo/addons/base/tests/test_misc.py @@ -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') diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py index 44c3dd4bc41..c8bb5cb8a3a 100644 --- a/odoo/tools/misc.py +++ b/odoo/tools/misc.py @@ -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. """