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. """