[FIX] mail: try to parse scheduled date input
``scheduled_date`` field of ``mail.mail`` is a char field since its addition in 2015 (see odoo/odoo@364b4ba06d). As its first usage was in combination with mail templates, a char field was used to simplify its implementation. However this technically allows to store whatever value in that field. Using it in a filter with a datetime argument is quite strange. A workaround is to try to parse as much as possible the inputs, remove timezone information, try to localize it, and have it in regular server format to enable filtering on it. We consider value should be set in UTC. If we have a specific timezone set on the input we localize it to UTC. Otherwise we consider the input was done in UTC, as all datetime fields. It is the role of the business code generating mail.mail to either give the timezone, either already convert into UTC. This might solve the following bug Step to reproduce: activate the developer mode go to settings - technical - emails create a new email and set the Scheduled Send Date in the future run the scheduled action Mail: Email Queue Manager Current behavior: the email is sent and the action does not consider the filter Expected behavior: the email is not sent and will only be sent when the scheduler detects that scheduled_date exceeds the current time. Task-2833300 opw-2823106 closes odoo/odoo#94937 X-original-commit: 5c113cb9d54e051132a9a50deb2fcf61f9c9dc2c Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com> Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
committed by
Thibault Delavallée
parent
dfb122b960
commit
55130e50f2
@@ -7619,6 +7619,11 @@ msgstr ""
|
||||
msgid "Wrong operation name (%s)"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#: model_terms:ir.ui.view,arch_db:mail.view_mail_form
|
||||
msgid "YYYY-MM-DD HH:MM:SS"
|
||||
msgstr ""
|
||||
|
||||
#. module: mail
|
||||
#. openerp-web
|
||||
#: code:addons/mail/static/src/models/tracking_value_item.js:0
|
||||
|
||||
@@ -9,8 +9,10 @@ import psycopg2
|
||||
import smtplib
|
||||
import threading
|
||||
import re
|
||||
import pytz
|
||||
|
||||
from collections import defaultdict
|
||||
from dateutil.parser import parse
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo import tools
|
||||
@@ -127,7 +129,12 @@ class MailMail(models.Model):
|
||||
for values in values_list:
|
||||
if 'is_notification' not in values and values.get('mail_message_id'):
|
||||
values['is_notification'] = True
|
||||
|
||||
if values.get('scheduled_date'):
|
||||
parsed_datetime = self._parse_scheduled_datetime(values['scheduled_date'])
|
||||
if parsed_datetime:
|
||||
values['scheduled_date'] = parsed_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
else:
|
||||
values['scheduled_date'] = False
|
||||
new_mails = super(MailMail, self).create(values_list)
|
||||
|
||||
new_mails_w_attach = self
|
||||
@@ -140,6 +147,12 @@ class MailMail(models.Model):
|
||||
return new_mails
|
||||
|
||||
def write(self, vals):
|
||||
if vals.get('scheduled_date'):
|
||||
parsed_datetime = self._parse_scheduled_datetime(vals['scheduled_date'])
|
||||
if parsed_datetime:
|
||||
vals['scheduled_date'] = parsed_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
else:
|
||||
vals['scheduled_date'] = False
|
||||
res = super(MailMail, self).write(vals)
|
||||
if vals.get('attachment_ids'):
|
||||
for mail in self:
|
||||
@@ -204,11 +217,13 @@ class MailMail(models.Model):
|
||||
messages to send (by default all 'outgoing'
|
||||
messages are sent).
|
||||
"""
|
||||
filters = ['&',
|
||||
('state', '=', 'outgoing'),
|
||||
'|',
|
||||
('scheduled_date', '<', datetime.datetime.now()),
|
||||
('scheduled_date', '=', False)]
|
||||
filters = [
|
||||
'&',
|
||||
('state', '=', 'outgoing'),
|
||||
'|',
|
||||
('scheduled_date', '=', False),
|
||||
('scheduled_date', '<=', datetime.datetime.utcnow()),
|
||||
]
|
||||
if 'filters' in self._context:
|
||||
filters.extend(self._context['filters'])
|
||||
# TODO: make limit configurable
|
||||
@@ -270,6 +285,42 @@ class MailMail(models.Model):
|
||||
|
||||
return True
|
||||
|
||||
def _parse_scheduled_datetime(self, scheduled_datetime):
|
||||
""" Taking an arbitrary datetime (either as a date, a datetime or a string)
|
||||
try to parse it and return a datetime timezoned to UTC.
|
||||
|
||||
If no specific timezone information is given, we consider it as being
|
||||
given in UTC, as all datetime values given to the server. Trying to
|
||||
guess its timezone based on user or flow would be strange as this is
|
||||
not standard. When manually creating datetimes for mail.mail scheduled
|
||||
date, business code should ensure either a timezone info is set, either
|
||||
it is converted into UTC.
|
||||
|
||||
Using yearfirst when parsing str datetimes eases parser's job when
|
||||
dealing with the hard-to-parse trio (01/04/09 -> ?). In most use cases
|
||||
year will be given first as this is the expected default formatting.
|
||||
|
||||
:return datetime: parsed datetime (or False if parser failed)
|
||||
"""
|
||||
if isinstance(scheduled_datetime, datetime.datetime):
|
||||
parsed_datetime = scheduled_datetime
|
||||
elif isinstance(scheduled_datetime, datetime.date):
|
||||
parsed_datetime = datetime.combine(scheduled_datetime, datetime.time.min)
|
||||
else:
|
||||
try:
|
||||
parsed_datetime = parse(scheduled_datetime, yearfirst=True)
|
||||
except (ValueError, TypeError):
|
||||
parsed_datetime = False
|
||||
if parsed_datetime:
|
||||
if not parsed_datetime.tzinfo:
|
||||
parsed_datetime = pytz.utc.localize(parsed_datetime)
|
||||
else:
|
||||
try:
|
||||
parsed_datetime = parsed_datetime.astimezone(pytz.utc)
|
||||
except Exception:
|
||||
pass
|
||||
return parsed_datetime
|
||||
|
||||
# ------------------------------------------------------
|
||||
# mail_mail formatting, tools and send mechanism
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
domain="[('type', '!=', 'private'), ('active', '=', True)]"/>
|
||||
<field name="email_cc"/>
|
||||
<field name="reply_to"/>
|
||||
<field name="scheduled_date"/>
|
||||
<field name="scheduled_date" placeholder="YYYY-MM-DD HH:MM:SS"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Body" name="body">
|
||||
|
||||
@@ -2,18 +2,21 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import psycopg2
|
||||
import pytz
|
||||
import smtplib
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from freezegun import freeze_time
|
||||
from OpenSSL.SSL import Error as SSLError
|
||||
from socket import gaierror, timeout
|
||||
from unittest.mock import call, patch
|
||||
|
||||
from odoo import api, Command
|
||||
from odoo import api, Command, tools
|
||||
from odoo.addons.base.models.ir_mail_server import MailDeliveryException
|
||||
from odoo.addons.test_mail.tests.common import TestMailCommon
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import common, tagged, users
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tools import mute_logger, DEFAULT_SERVER_DATETIME_FORMAT
|
||||
|
||||
|
||||
@tagged('mail_mail')
|
||||
@@ -169,6 +172,57 @@ class TestMailMail(TestMailCommon):
|
||||
mail.send()
|
||||
self.assertEqual(self._mails[0]['headers']['Return-Path'], '%s@%s' % (self.alias_bounce, self.alias_domain))
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.tests')
|
||||
def test_mail_mail_schedule(self):
|
||||
"""Test that a mail scheduled in the past/future are sent or not"""
|
||||
now = datetime(2022, 6, 28, 14, 0, 0)
|
||||
scheduled_datetimes = [
|
||||
# falsy values
|
||||
False, '', 'This is not a date format',
|
||||
# datetimes (UTC/GMT +10 hours for Australia/Brisbane)
|
||||
now, pytz.timezone('Australia/Brisbane').localize(now),
|
||||
# string
|
||||
(now - timedelta(days=1)).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
(now + timedelta(days=1)).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
(now + timedelta(days=1)).strftime("%H:%M:%S %d-%m-%Y"),
|
||||
# tz: is actually 1 hour before now in UTC
|
||||
(now + timedelta(hours=3)).strftime("%H:%M:%S %d-%m-%Y") + " +0400",
|
||||
# tz: is actually 1 hour after now in UTC
|
||||
(now + timedelta(hours=-3)).strftime("%H:%M:%S %d-%m-%Y") + " -0400",
|
||||
]
|
||||
expected_datetimes = [
|
||||
False, '', False,
|
||||
now, now - pytz.timezone('Australia/Brisbane').utcoffset(now),
|
||||
now - timedelta(days=1), now + timedelta(days=1), now + timedelta(days=1),
|
||||
now + timedelta(hours=-1),
|
||||
now + timedelta(hours=1),
|
||||
]
|
||||
expected_states = [
|
||||
# falsy values = send now
|
||||
'sent', 'sent', 'sent',
|
||||
'sent', 'sent',
|
||||
'sent', 'outgoing', 'outgoing',
|
||||
'sent', 'outgoing'
|
||||
]
|
||||
|
||||
mails = self.env['mail.mail'].create([
|
||||
{'body_html': '<p>Test</p>',
|
||||
'email_to': 'test@example.com',
|
||||
'scheduled_date': scheduled_datetime,
|
||||
} for scheduled_datetime in scheduled_datetimes
|
||||
])
|
||||
|
||||
for mail, expected_datetime, scheduled_datetime in zip(mails, expected_datetimes, scheduled_datetimes):
|
||||
expected = expected_datetime.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) if expected_datetime else expected_datetime
|
||||
self.assertEqual(mail.scheduled_date, expected,
|
||||
'Scheduled date: %s should be stored as %s, received %s' % (scheduled_datetime, expected, mail.scheduled_date))
|
||||
self.assertEqual(mail.state, 'outgoing')
|
||||
|
||||
with freeze_time(now):
|
||||
self.env['mail.mail'].process_email_queue()
|
||||
for mail, expected_state in zip(mails, expected_states):
|
||||
self.assertEqual(mail.state, expected_state)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_mail_send_exceptions_origin(self):
|
||||
""" Test various use case with exceptions and errors and see how they are
|
||||
|
||||
Reference in New Issue
Block a user