-% 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)}
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. """
|
|