1616 lines
74 KiB
Python
1616 lines
74 KiB
Python
from datetime import timedelta
|
|
|
|
from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE
|
|
from odoo.addons.test_marketing_automation.tests.common import TestMACommon
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.fields import Datetime
|
|
from odoo.tests import tagged, users
|
|
from odoo.tools import mute_logger
|
|
|
|
|
|
class ActivityTriggersCase(TestMACommon):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.date_reference = Datetime.from_string('2023-11-08 09:00:00')
|
|
|
|
# --------------------------------------------------
|
|
# TEST RECORDS, using marketing.test.sms (customers)
|
|
#
|
|
# 2 times
|
|
# - 3 records with partners
|
|
# - 1 records wo partner, but email/mobile
|
|
# - 1 record wo partner/email/mobile
|
|
# --------------------------------------------------
|
|
cls.test_records_base = cls._create_marketauto_records(model='marketing.test.sms', count=2)
|
|
cls.test_records = cls.test_records_base
|
|
cls.test_customers = cls.test_records.customer_id
|
|
cls.test_records_ko = cls.test_records_base[4] + cls.test_records_base[9]
|
|
cls.test_records_ok = cls.test_records - cls.test_records_ko
|
|
|
|
cls.campaign = cls.env['marketing.campaign'].create({
|
|
'domain': [('id', 'in', cls.test_records.ids)],
|
|
'model_id': cls.env['ir.model']._get_id('marketing.test.sms'),
|
|
'name': 'Test Campaign',
|
|
})
|
|
|
|
cls.test_mailing_mail = cls._create_mailing(
|
|
'marketing.test.sms',
|
|
email_from=cls.user_marketing_automation.email_formatted,
|
|
keep_archives=True,
|
|
mailing_type='mail',
|
|
user_id=cls.user_marketing_automation.id,
|
|
)
|
|
cls.test_mailing_sms = cls._create_mailing(
|
|
'marketing.test.sms',
|
|
mailing_type='sms',
|
|
user_id=cls.user_marketing_automation.id,
|
|
sms_force_send=True,
|
|
)
|
|
cls.test_wa_template = cls._create_wa_template(
|
|
'marketing.test.sms',
|
|
)
|
|
|
|
cls.test_sa_descr = cls.env['ir.actions.server'].create([
|
|
{
|
|
'code': f"""
|
|
for record in records:
|
|
record.write({{'description': (record.description or '') + ' - {description}'}})""",
|
|
'model_id': cls.env['ir.model']._get_id('marketing.test.sms'),
|
|
'name': f'Update {description}',
|
|
'state': 'code',
|
|
} for description in [
|
|
# mail
|
|
'mail_open', 'mail_not_open',
|
|
'mail_reply', 'mail_not_reply',
|
|
'mail_click', 'mail_not_click',
|
|
'mail_bounce',
|
|
# sms
|
|
'sms_click', 'sms_not_click',
|
|
'sms_bounce',
|
|
# whatsapp
|
|
'whatsapp_click', 'whatsapp_not_click',
|
|
'whatsapp_read', 'whatsapp_not_read',
|
|
'whatsapp_replied', 'whatsapp_not_replied',
|
|
'whatsapp_bounced',
|
|
# other
|
|
'action',
|
|
]
|
|
])
|
|
(
|
|
# mail
|
|
cls.test_sa_descr_mail_open, cls.test_sa_descr_mail_not_open,
|
|
cls.test_sa_descr_mail_reply, cls.test_sa_descr_mail_not_reply,
|
|
cls.test_sa_descr_mail_click, cls.test_sa_descr_mail_not_click,
|
|
cls.test_sa_descr_mail_bounce,
|
|
# sms
|
|
cls.test_sa_descr_sms_click, cls.test_sa_descr_sms_not_click,
|
|
cls.test_sa_descr_sms_bounce,
|
|
# whatsapp
|
|
cls.test_sa_descr_wa_click, cls.test_sa_descr_wa_not_click,
|
|
cls.test_sa_descr_wa_read, cls.test_sa_descr_wa_not_read,
|
|
cls.test_sa_descr_wa_replied, cls.test_sa_descr_wa_not_replied,
|
|
cls.test_sa_descr_wa_bounced,
|
|
# other
|
|
cls.test_sa_descr_action,
|
|
) = cls.test_sa_descr
|
|
cls.test_sa_unlink = cls.env['ir.actions.server'].create({
|
|
'code': "records.unlink()",
|
|
'model_id': cls.env['ir.model']._get_id('marketing.test.sms'),
|
|
'name': 'Unlink',
|
|
'state': 'code',
|
|
})
|
|
|
|
cls.env.flush_all()
|
|
|
|
|
|
@tagged('marketing_automation', 'ma_sync')
|
|
class TestActivityTriggers(ActivityTriggersCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
# begin activities: MAIL, SMS and WHATSAPP
|
|
cls.activity_begin_mail = cls._create_activity(
|
|
cls.campaign,
|
|
create_date=cls.date_reference,
|
|
mailing=cls.test_mailing_mail,
|
|
interval_number=1, interval_type='hours',
|
|
trigger_type='begin',
|
|
)
|
|
cls.activity_begin_sms = cls._create_activity(
|
|
cls.campaign,
|
|
create_date=cls.date_reference,
|
|
mailing=cls.test_mailing_sms,
|
|
interval_number=1, interval_type='hours',
|
|
trigger_type='begin',
|
|
)
|
|
cls.activity_begin_wa = cls._create_activity(
|
|
cls.campaign,
|
|
create_date=cls.date_reference,
|
|
wa_template=cls.test_wa_template,
|
|
interval_number=1, interval_type='hours',
|
|
trigger_type='begin',
|
|
)
|
|
cls.activity_begin_sa = cls._create_activity(
|
|
cls.campaign,
|
|
create_date=cls.date_reference,
|
|
action=cls.test_sa_descr_action,
|
|
interval_number=1, interval_type='hours',
|
|
trigger_type='begin',
|
|
)
|
|
|
|
(
|
|
# sub activities mail triggers
|
|
cls.activity_sa_mail_not_open,
|
|
cls.activity_sa_mail_open,
|
|
cls.activity_sa_mail_not_reply,
|
|
cls.activity_sa_mail_reply,
|
|
cls.activity_sa_mail_not_click,
|
|
cls.activity_sa_mail_click,
|
|
cls.activity_sa_mail_bounce,
|
|
# sub activities for sms triggers
|
|
cls.activity_sa_sms_not_click,
|
|
cls.activity_sa_sms_click,
|
|
cls.activity_sa_sms_bounce,
|
|
# sub activities for whatsapp triggers
|
|
cls.activity_sa_wa_not_read,
|
|
cls.activity_sa_wa_read,
|
|
cls.activity_sa_wa_not_replied,
|
|
cls.activity_sa_wa_replied,
|
|
cls.activity_sa_wa_not_click,
|
|
cls.activity_sa_wa_click,
|
|
cls.activity_sa_wa_bounced,
|
|
) = (
|
|
cls._create_activity(
|
|
cls.campaign,
|
|
action=server_action,
|
|
parent_id=parent.id,
|
|
interval_number=itv,
|
|
interval_type='days',
|
|
trigger_type=trigger_type,
|
|
)
|
|
for trigger_type, server_action, parent, itv in (
|
|
# mail
|
|
("mail_not_open", cls.test_sa_descr_mail_not_open, cls.activity_begin_mail, 5),
|
|
("mail_open", cls.test_sa_descr_mail_open, cls.activity_begin_mail, 1),
|
|
("mail_not_reply", cls.test_sa_descr_mail_not_reply, cls.activity_begin_mail, 5),
|
|
("mail_reply", cls.test_sa_descr_mail_reply, cls.activity_begin_mail, 1),
|
|
("mail_not_click", cls.test_sa_descr_mail_not_click, cls.activity_begin_mail, 5),
|
|
("mail_click", cls.test_sa_descr_mail_click, cls.activity_begin_mail, 1),
|
|
("mail_bounce", cls.test_sa_descr_mail_bounce, cls.activity_begin_mail, 1),
|
|
# sms
|
|
("sms_not_click", cls.test_sa_descr_sms_not_click, cls.activity_begin_sms, 5),
|
|
("sms_click", cls.test_sa_descr_sms_click, cls.activity_begin_sms, 1),
|
|
("sms_bounce", cls.test_sa_descr_sms_bounce, cls.activity_begin_sms, 1),
|
|
# whatsapp
|
|
("whatsapp_not_read", cls.test_sa_descr_wa_not_read, cls.activity_begin_wa, 5),
|
|
("whatsapp_read", cls.test_sa_descr_wa_read, cls.activity_begin_wa, 1),
|
|
("whatsapp_not_replied", cls.test_sa_descr_wa_not_replied, cls.activity_begin_wa, 5),
|
|
("whatsapp_replied", cls.test_sa_descr_wa_replied, cls.activity_begin_wa, 1),
|
|
("whatsapp_not_click", cls.test_sa_descr_wa_not_click, cls.activity_begin_wa, 5),
|
|
("whatsapp_click", cls.test_sa_descr_wa_click, cls.activity_begin_wa, 1),
|
|
("whatsapp_bounced", cls.test_sa_descr_wa_bounced, cls.activity_begin_wa, 1),
|
|
)
|
|
)
|
|
# sub activities other
|
|
cls.activity_mail_activity = cls._create_activity(
|
|
cls.campaign,
|
|
action=cls.test_sa_descr_action,
|
|
parent_id=cls.activity_begin_mail.id,
|
|
interval_number=2,
|
|
interval_type='hours',
|
|
trigger_type='activity',
|
|
)
|
|
cls.activity_sms_activity = cls._create_activity(
|
|
cls.campaign,
|
|
action=cls.test_sa_descr_action,
|
|
parent_id=cls.activity_begin_sms.id,
|
|
interval_number=2,
|
|
interval_type='hours',
|
|
trigger_type='activity',
|
|
)
|
|
cls.activity_wa_activity = cls._create_activity(
|
|
cls.campaign,
|
|
action=cls.test_sa_descr_action,
|
|
parent_id=cls.activity_begin_wa.id,
|
|
interval_number=2,
|
|
interval_type='hours',
|
|
trigger_type='activity',
|
|
)
|
|
|
|
cls.env.flush_all()
|
|
|
|
def test_assert_initial_values(self):
|
|
""" Check initial state, notably partners """
|
|
self.assertEqual(len(self.test_records), 10)
|
|
self.assertEqual(len(self.test_customers), 6)
|
|
|
|
self._launch_campaign(self.campaign, date_reference=self.date_reference)
|
|
for activity in (
|
|
self.activity_begin_mail + self.activity_begin_sms +
|
|
self.activity_begin_wa + self.activity_begin_sa
|
|
):
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': self.test_records,
|
|
'status': 'scheduled',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
},
|
|
}],
|
|
activity,
|
|
)
|
|
|
|
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread')
|
|
@users('user_marketing_automation')
|
|
def test_reaction_triggers(self):
|
|
""" Test triggers (open / not_open, read / not read, ...) for mail, sms
|
|
and whatsapp. """
|
|
campaign = self.campaign.with_env(self.env)
|
|
test_records = self.test_records.with_env(self.env)
|
|
test_records_ok = self.test_records_ok.with_env(self.env)
|
|
test_records_ko = self.test_records_ko.with_env(self.env)
|
|
|
|
self._launch_campaign(campaign, date_reference=self.date_reference)
|
|
self.assertActivityWoTrace(
|
|
self.activity_sa_mail_open + self.activity_sa_mail_not_open +
|
|
self.activity_sa_mail_reply + self.activity_sa_mail_not_reply +
|
|
self.activity_sa_mail_click + self.activity_sa_mail_not_click +
|
|
self.activity_sa_mail_bounce +
|
|
self.activity_sa_sms_not_click + self.activity_sa_sms_click +
|
|
self.activity_sa_sms_bounce +
|
|
self.activity_sa_wa_not_read + self.activity_sa_wa_read +
|
|
self.activity_sa_wa_not_replied + self.activity_sa_wa_replied +
|
|
self.activity_sa_wa_not_click + self.activity_sa_wa_click +
|
|
self.activity_sa_wa_bounced +
|
|
self.activity_mail_activity + self.activity_sms_activity + self.activity_wa_activity
|
|
)
|
|
|
|
# First traces are processed, email, SMSes and WA msgs are sent (or failed)
|
|
date_send = self.date_reference + timedelta(hours=1) # ok for send mailing
|
|
date_opened = date_send + timedelta(hours=2) # simulating opened
|
|
date_not_action = date_send + timedelta(days=5) # 5 days delay before triggering 'did not reply' or similar
|
|
with self.mock_datetime_and_now(date_send), \
|
|
self.mockWhatsappGateway(), self.patchWhatsappCronTrigger(), \
|
|
self.mock_mail_gateway(), self.mockSMSGateway():
|
|
campaign.execute_activities()
|
|
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': test_records_ok,
|
|
'records_to_partner': {r.id: r.customer_id for r in test_records_ok},
|
|
'status': 'processed',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
},
|
|
'trace_status': 'sent',
|
|
}, {
|
|
'records': test_records_ko,
|
|
'status': 'canceled',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
'state_msg': 'Email cancelled',
|
|
},
|
|
'trace_failure_type': 'mail_email_missing',
|
|
'trace_status': 'cancel',
|
|
}],
|
|
self.activity_begin_mail,
|
|
)
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': test_records_ok,
|
|
'status': 'processed',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
},
|
|
'trace_status': 'pending', # forced send -> waiting provider update
|
|
}] + [{
|
|
'records': record,
|
|
'status': 'canceled',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
'state_msg': 'SMS cancelled',
|
|
},
|
|
'trace_content': f"Test SMS for {record.name} click on",
|
|
'trace_failure_type': 'sms_number_missing',
|
|
'trace_status': 'cancel',
|
|
} for record in test_records_ko],
|
|
self.activity_begin_sms,
|
|
)
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': test_records_ok,
|
|
'status': 'processed',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
},
|
|
'trace_content': "<p>Hello your much wow template value</p>",
|
|
'trace_status': 'sent',
|
|
}] + [{
|
|
'records': record,
|
|
'status': 'error',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
'state_msg': 'WhatsApp failed',
|
|
},
|
|
'trace_content': "<p>Hello your much wow template value</p>",
|
|
'trace_failure_type': 'phone_invalid',
|
|
'trace_status': 'error',
|
|
} for record in test_records_ko],
|
|
self.activity_begin_wa,
|
|
)
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': test_records,
|
|
'status': 'processed',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
},
|
|
}],
|
|
self.activity_begin_sa,
|
|
)
|
|
|
|
for sub_activity, schedule_date in [
|
|
# mail
|
|
(self.activity_sa_mail_open, False), # mail open: no date, as user input
|
|
(self.activity_sa_mail_not_open, date_not_action),
|
|
(self.activity_sa_mail_reply, False), # mail reply: no date, as user input
|
|
(self.activity_sa_mail_not_reply, date_not_action),
|
|
(self.activity_sa_mail_click, False), # mail click: no date, as user input
|
|
(self.activity_sa_mail_not_click, date_not_action),
|
|
(self.activity_sa_mail_bounce, False), # mail bounce: no date, as user input
|
|
# sms
|
|
(self.activity_sa_sms_click, False), # sms click: no date, as user input
|
|
(self.activity_sa_sms_not_click, date_not_action),
|
|
(self.activity_sa_sms_bounce, False), # sms bounce: no date, as user input
|
|
# whatsapp
|
|
(self.activity_sa_wa_read, False), # wa read: no date, as user input
|
|
(self.activity_sa_wa_not_read, date_not_action),
|
|
(self.activity_sa_wa_replied, False), # wa replied: no date, as user input
|
|
(self.activity_sa_wa_not_replied, date_not_action),
|
|
(self.activity_sa_wa_click, False), # wa click: no date, as user input
|
|
(self.activity_sa_wa_not_click, date_not_action),
|
|
(self.activity_sa_wa_bounced, False), # wa bounce: no date, as user input
|
|
# other
|
|
(self.activity_mail_activity, date_send + timedelta(hours=2)),
|
|
(self.activity_sms_activity, date_send + timedelta(hours=2)),
|
|
(self.activity_wa_activity, date_send + timedelta(hours=2)),
|
|
]:
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': test_records,
|
|
'status': 'scheduled',
|
|
'fields_values': {
|
|
'schedule_date': schedule_date,
|
|
},
|
|
}],
|
|
sub_activity,
|
|
)
|
|
|
|
# Simulate open / reply / click
|
|
# - active traces should be processed, schedule date updated
|
|
# - opposite traces should be canceled
|
|
for action, action_sa, opposite_sa in (
|
|
# mail
|
|
("mail_open", self.activity_sa_mail_open, self.activity_sa_mail_not_open),
|
|
("mail_reply", self.activity_sa_mail_reply, self.activity_sa_mail_not_reply),
|
|
("mail_click", self.activity_sa_mail_click, self.activity_sa_mail_not_click),
|
|
# sms
|
|
("sms_click", self.activity_sa_sms_click, self.activity_sa_sms_not_click),
|
|
# whatsapp
|
|
("whatsapp_read", self.activity_sa_wa_read, self.activity_sa_wa_not_read),
|
|
("whatsapp_replied", self.activity_sa_wa_replied, self.activity_sa_wa_not_replied),
|
|
("whatsapp_click", self.activity_sa_wa_click, self.activity_sa_wa_not_click),
|
|
):
|
|
todo = test_records_ok[:5]
|
|
with self.mock_datetime_and_now(date_opened):
|
|
for record in todo:
|
|
if action == "mail_open":
|
|
self.gateway_mail_trace_open(self.activity_begin_mail.mass_mailing_id, record)
|
|
state_msg = 'Parent activity mail opened'
|
|
elif action == "mail_reply":
|
|
self.gateway_mail_trace_reply(MAIL_TEMPLATE, self.activity_begin_mail.mass_mailing_id, record)
|
|
state_msg = 'Parent activity mail replied'
|
|
elif action == "mail_click":
|
|
self.gateway_mail_trace_click(self.activity_begin_mail.mass_mailing_id, record, "LINK")
|
|
state_msg = 'Parent activity mail clicked'
|
|
elif action == "sms_click":
|
|
self.gateway_sms_click(self.activity_begin_sms.mass_mailing_id, record, use_sent_sms=False, sms_status='pending')
|
|
state_msg = 'Parent activity SMS clicked'
|
|
elif action == "whatsapp_read":
|
|
self.whatsapp_msg_read_with_records(record)
|
|
state_msg = 'Parent Whatsapp message got opened'
|
|
elif action == "whatsapp_replied":
|
|
self.whatsapp_answer_with_records(record, mock=False)
|
|
state_msg = 'Parent Whatsapp was replied to'
|
|
elif action == "whatsapp_click":
|
|
self.whatsapp_msg_click_with_records(record, button_index=0)
|
|
state_msg = 'Parent Whatsapp message was clicked'
|
|
# sub action scheduled
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': todo,
|
|
'status': 'scheduled',
|
|
'fields_values': {
|
|
'schedule_date': date_opened + timedelta(days=1),
|
|
'state_msg': False,
|
|
},
|
|
}, {
|
|
'records': test_records - todo,
|
|
'status': 'scheduled',
|
|
'fields_values': {
|
|
'schedule_date': False,
|
|
},
|
|
}],
|
|
action_sa,
|
|
)
|
|
# opposition action is canceled
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': todo,
|
|
'status': 'canceled',
|
|
'fields_values': {
|
|
'schedule_date': date_opened,
|
|
'state_msg': state_msg,
|
|
},
|
|
}, {
|
|
'records': test_records - todo,
|
|
'status': 'scheduled',
|
|
'fields_values': {
|
|
'schedule_date': date_not_action,
|
|
},
|
|
}],
|
|
opposite_sa,
|
|
)
|
|
|
|
@mute_logger('odoo.addons.mail.models.mail_mail')
|
|
@users('user_marketing_automation')
|
|
def test_reaction_triggers_bounce_fail(self):
|
|
""" Test specific 'bounce' and 'failed' triggers, that should
|
|
cancel a lot of things as if means contact is failing. """
|
|
campaign = self.campaign.with_env(self.env)
|
|
test_records = self.test_records.with_env(self.env)
|
|
test_records_ok = self.test_records_ok.with_env(self.env)
|
|
test_records_ko = self.test_records_ko.with_env(self.env)
|
|
bounced = test_records_ok[:2] # always post update
|
|
failed = test_records_ok[2:4] # fails at sending
|
|
post_failed = test_records_ok[4] # fails as post update
|
|
|
|
self._launch_campaign(campaign, date_reference=self.date_reference)
|
|
|
|
# First traces are processed, email, SMSes and WA msgs are sent (or failed)
|
|
# see 'test_reaction_triggers' that has the same beginning, with more asserts
|
|
date_send = self.date_reference + timedelta(hours=1) # ok for send mailing
|
|
date_opened = date_send + timedelta(hours=2) # simulating opened
|
|
with self.mock_datetime_and_now(date_send), \
|
|
self.mockWhatsappGateway(), self.patchWhatsappCronTrigger(), \
|
|
self.mock_mail_gateway(), self.mock_send_email_delivery_exception(failed.mapped('email_from'), error="OutboundSpamException"), \
|
|
self.mockSMSGateway(nbr_t_error={r.phone_sanitized: 'credit' for r in failed}):
|
|
campaign.execute_activities()
|
|
|
|
# Simulate bounce: should cancel all brother traces
|
|
for action, action_sa in (
|
|
# mail
|
|
("mail_bounce", self.activity_sa_mail_bounce),
|
|
# sms
|
|
("sms_bounce", self.activity_sa_sms_bounce),
|
|
# whatsapp
|
|
("whatsapp_bounced", self.activity_sa_wa_bounced),
|
|
):
|
|
with self.mock_datetime_and_now(date_opened):
|
|
for record in bounced:
|
|
if action == "mail_bounce":
|
|
self.gateway_mail_trace_bounce(self.activity_begin_mail.mass_mailing_id, record)
|
|
state_msg = 'Parent activity mail bounced'
|
|
elif action == "sms_bounce":
|
|
self.gateway_sms_bounce(self.activity_begin_sms.mass_mailing_id, record, error_code='invalid_destination')
|
|
state_msg = 'Parent activity SMS bounced'
|
|
elif action == "whatsapp_bounced":
|
|
self.whatsapp_msg_bounce_with_records(record, error_code=131026)
|
|
state_msg = 'Parent whatsapp was bounced'
|
|
# note that fail is not a trigger currently, hence do not change anything
|
|
for record in post_failed:
|
|
if action == "mail_bounce":
|
|
self.gateway_mail_trace_fail(self.activity_begin_mail.mass_mailing_id, record, failure_reason="Error without exception.", failure_type='unknown')
|
|
state_msg = 'Parent activity mail bounced'
|
|
if action == "sms_bounce":
|
|
self.gateway_sms_failed(self.activity_begin_sms.mass_mailing_id, record, error_code='not_delivered')
|
|
state_msg = 'Parent activity SMS bounced'
|
|
# TDE TODO: add other whatsapp reaction checks
|
|
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': bounced,
|
|
'status': 'scheduled',
|
|
'fields_values': {
|
|
'schedule_date': date_opened + timedelta(days=1),
|
|
'state_msg': False,
|
|
},
|
|
}, {
|
|
'records': test_records - bounced,
|
|
'status': 'scheduled',
|
|
'fields_values': {
|
|
'schedule_date': False,
|
|
'state_msg': False,
|
|
},
|
|
}],
|
|
action_sa,
|
|
)
|
|
for brother in (action_sa.parent_id.child_ids - action_sa):
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': bounced,
|
|
'status': 'canceled',
|
|
'fields_values': {
|
|
'schedule_date': date_opened,
|
|
'state_msg': state_msg,
|
|
},
|
|
}, {
|
|
'records': test_records - bounced,
|
|
'status': 'scheduled',
|
|
}],
|
|
brother,
|
|
)
|
|
|
|
# parent (mail / sms / WA sending) action update
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': test_records_ok - (bounced + failed + post_failed),
|
|
'records_to_partner': {r.id: r.customer_id for r in (test_records_ok - (bounced + failed))},
|
|
'status': 'processed',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
'state_msg': False,
|
|
},
|
|
'trace_status': 'sent',
|
|
}, {
|
|
'records': failed,
|
|
'records_to_partner': {r.id: r.customer_id for r in failed},
|
|
'status': 'error',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
'state_msg': 'Email failed',
|
|
},
|
|
'mail_values': { # note that bounce did not update the mail.mail itself, only the trace
|
|
'failure_reason': "OutboundSpamException",
|
|
},
|
|
'trace_failure_reason': False, # FIXME: should be "OutboundSpamException"
|
|
'trace_failure_type': 'mail_spam',
|
|
'trace_status': 'error',
|
|
}, {
|
|
'records': post_failed,
|
|
'records_to_partner': {r.id: r.customer_id for r in post_failed},
|
|
'status': 'error',
|
|
'fields_values': {
|
|
'schedule_date': date_opened, # set_failed updated schedule_date to error dt, unsure this is right
|
|
'state_msg': 'SMS failed', # FIXME: well sms override is a bit too broad ^^
|
|
},
|
|
'mail_values': { # note that bounce did not update the mail.mail itself, only the trace
|
|
'failure_reason': "Error without exception.",
|
|
},
|
|
'trace_failure_reason': False, # FIXME: should be "Error without exception."
|
|
'trace_failure_type': 'unknown',
|
|
'trace_status': 'error',
|
|
}, {
|
|
'records': bounced,
|
|
'records_to_partner': {r.id: r.customer_id for r in bounced},
|
|
'status': 'processed',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
'state_msg': False,
|
|
},
|
|
'mail_values': { # note that bounce did not update the mail.mail itself, only the trace
|
|
'failure_reason': False,
|
|
'failure_type': False,
|
|
'state': 'sent',
|
|
},
|
|
'trace_failure_reason': 'This is the bounce email',
|
|
'trace_failure_type': 'mail_bounce',
|
|
'trace_status': 'bounce',
|
|
}, {
|
|
'records': test_records_ko,
|
|
'status': 'canceled',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
'state_msg': 'Email cancelled',
|
|
},
|
|
'trace_failure_type': 'mail_email_missing',
|
|
'trace_status': 'cancel',
|
|
}],
|
|
self.activity_begin_mail,
|
|
)
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': (test_records_ok - (bounced + failed + post_failed)),
|
|
'status': 'processed',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
'state_msg': False,
|
|
},
|
|
'trace_status': 'pending', # forced send
|
|
}, {
|
|
'records': failed,
|
|
'status': 'error',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
'state_msg': 'SMS failed',
|
|
},
|
|
'trace_failure_reason': False,
|
|
'trace_failure_type': 'sms_credit',
|
|
'trace_status': 'error',
|
|
}, {
|
|
'records': post_failed,
|
|
'status': 'processed', # FIXME different from mail - asynchronous error update may explain it ?
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
'state_msg': False,
|
|
},
|
|
'sms_values': { # async fail does not update sms.sms values itself (generally unlinked anyway)
|
|
'failure_type': False,
|
|
'state': 'pending',
|
|
},
|
|
'trace_failure_reason': False,
|
|
'trace_failure_type': 'sms_not_delivered',
|
|
'trace_status': 'error',
|
|
}, {
|
|
'records': bounced,
|
|
'status': 'processed',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
'state_msg': False,
|
|
},
|
|
'sms_values': { # bounce does not update sms.sms values itself (generally unlinked anyway)
|
|
'failure_type': False,
|
|
'state': 'pending',
|
|
},
|
|
'trace_failure_reason': False, # FIXME nothing logged ?
|
|
'trace_failure_type': 'sms_invalid_destination',
|
|
'trace_status': 'bounce',
|
|
}, {
|
|
'records': test_records_ko,
|
|
'status': 'canceled',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
'state_msg': 'SMS cancelled',
|
|
},
|
|
'trace_failure_type': 'sms_number_missing',
|
|
'trace_status': 'cancel',
|
|
}],
|
|
self.activity_begin_sms,
|
|
)
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': (test_records_ok - bounced),
|
|
'status': 'processed',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
},
|
|
'trace_content': "<p>Hello your much wow template value</p>",
|
|
'trace_status': 'sent',
|
|
}, {
|
|
'records': bounced,
|
|
'status': 'canceled', # FIXME: contrary to mail / sms, bounce = canceled
|
|
'fields_values': {
|
|
'schedule_date': date_opened, # update at failing dt
|
|
'state_msg': 'WhatsApp canceled',
|
|
},
|
|
'trace_failure_reason': '131026 : Message failed to send due to an unknown error.',
|
|
'trace_failure_type': 'whatsapp_unrecoverable',
|
|
'trace_status': 'bounce',
|
|
}, {
|
|
'records': test_records_ko,
|
|
'status': 'error',
|
|
'fields_values': {
|
|
'schedule_date': self.date_reference + timedelta(hours=1),
|
|
'state_msg': 'WhatsApp failed',
|
|
},
|
|
'trace_content': "<p>Hello your much wow template value</p>",
|
|
'trace_failure_type': 'phone_invalid',
|
|
'trace_status': 'error',
|
|
}],
|
|
self.activity_begin_wa,
|
|
)
|
|
|
|
@users('user_marketing_automation')
|
|
def test_reaction_triggers_multiple(self):
|
|
""" Test multiple triggers (e.g. several replies) do not trigger
|
|
child traces several times. """
|
|
campaign = self.campaign.with_env(self.env)
|
|
test_records = self.test_records.with_env(self.env)
|
|
test_records_ok = self.test_records_ok.with_env(self.env)
|
|
|
|
self.test_reaction_triggers()
|
|
|
|
date_opened = self.date_reference + timedelta(hours=1) + timedelta(hours=2) # see previous test
|
|
date_subaction = date_opened + timedelta(days=1)
|
|
|
|
# now that some records have been opened / replied / clicked, we can process
|
|
# sub activities linked to those triggers
|
|
with self.mock_datetime_and_now(date_subaction), \
|
|
self.mockWhatsappGateway(), self.patchWhatsappCronTrigger(), \
|
|
self.mock_mail_gateway(), self.mockSMSGateway():
|
|
campaign.execute_activities()
|
|
|
|
todo = test_records_ok[:5]
|
|
for subaction in (
|
|
# mail
|
|
self.activity_sa_mail_open, self.activity_sa_mail_reply, self.activity_sa_mail_click,
|
|
# sms
|
|
self.activity_sa_sms_click,
|
|
# whatsapp
|
|
self.activity_sa_wa_read, self.activity_sa_wa_replied, self.activity_sa_wa_click, self.activity_sa_wa_click,
|
|
):
|
|
with self.subTest(subaction_name=subaction.name):
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': todo,
|
|
'status': 'processed',
|
|
'fields_values': {
|
|
'schedule_date': date_subaction,
|
|
'state_msg': False,
|
|
},
|
|
}, {
|
|
'records': test_records - todo,
|
|
'status': 'scheduled',
|
|
'fields_values': {
|
|
'schedule_date': False,
|
|
},
|
|
}],
|
|
subaction,
|
|
)
|
|
|
|
# receive replies again, check traces are still processed and not set
|
|
# back to schedule
|
|
# Simulate open / reply / click
|
|
# - active traces should be processed, schedule date updated
|
|
# - opposite traces should be canceled
|
|
for action, action_sa, opposite_sa in (
|
|
# mail
|
|
("mail_open", self.activity_sa_mail_open, self.activity_sa_mail_not_open),
|
|
("mail_reply", self.activity_sa_mail_reply, self.activity_sa_mail_not_reply),
|
|
("mail_click", self.activity_sa_mail_click, self.activity_sa_mail_not_click),
|
|
# sms
|
|
("sms_click", self.activity_sa_sms_click, self.activity_sa_sms_not_click),
|
|
# whatsapp
|
|
("whatsapp_read", self.activity_sa_wa_read, self.activity_sa_wa_not_read),
|
|
("whatsapp_replied", self.activity_sa_wa_replied, self.activity_sa_wa_not_replied),
|
|
# ("whatsapp_click", self.activity_sa_wa_click, self.activity_sa_wa_not_click), # tool not supporting it
|
|
):
|
|
todo = test_records_ok[:5]
|
|
with self.subTest(action=action):
|
|
with self.mock_datetime_and_now(date_opened):
|
|
for record in todo:
|
|
if action == "mail_open":
|
|
self.gateway_mail_trace_open(self.activity_begin_mail.mass_mailing_id, record)
|
|
elif action == "mail_reply":
|
|
self.gateway_mail_trace_reply(MAIL_TEMPLATE, self.activity_begin_mail.mass_mailing_id, record)
|
|
elif action == "mail_click":
|
|
self.gateway_mail_trace_click_simple(self.activity_begin_mail.mass_mailing_id, record)
|
|
elif action == "sms_click":
|
|
self.gateway_sms_click_simple(self.activity_begin_sms.mass_mailing_id, record)
|
|
elif action == "whatsapp_read":
|
|
self.whatsapp_msg_read_with_records(record, find_in_mock=False, mock=False)
|
|
elif action == "whatsapp_replied":
|
|
self.whatsapp_answer_with_records(record, find_in_mock=False, mock=False)
|
|
elif action == "whatsapp_click":
|
|
self.whatsapp_msg_click_with_records(record, button_index=0)
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': todo,
|
|
'status': 'processed', # should not be back to scheduled
|
|
'fields_values': {
|
|
'schedule_date': date_subaction,
|
|
},
|
|
}, {
|
|
'records': test_records - todo,
|
|
'status': 'scheduled',
|
|
'fields_values': {
|
|
'schedule_date': False,
|
|
},
|
|
}],
|
|
action_sa,
|
|
)
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': todo,
|
|
'status': 'canceled',
|
|
}, {
|
|
'records': test_records - todo,
|
|
'status': 'scheduled',
|
|
}],
|
|
opposite_sa,
|
|
)
|
|
|
|
@users('user_marketing_automation')
|
|
def test_schedule_date_based_on_triggers_new(self):
|
|
""" Test triggers when adding a new activity """
|
|
self._launch_campaign(self.campaign)
|
|
date_send = self.date_reference + timedelta(hours=1)
|
|
|
|
with self.mock_datetime_and_now(date_send), self.mock_mail_gateway():
|
|
self.campaign.execute_activities()
|
|
|
|
# add a new begin activity
|
|
new_activity = self._create_activity_mail(
|
|
self.campaign, self.env.user,
|
|
act_values={
|
|
"interval_number": 1,
|
|
"interval_type": "days",
|
|
"trigger_type": "begin",
|
|
},
|
|
)
|
|
|
|
# add new participants that should have traces for the begin activities
|
|
# so that we also check they are not duplicated at rescheduling
|
|
new_test_records = self._create_marketauto_records(model='marketing.test.sms', count=1)
|
|
self.campaign.write({"domain": [("id", "in", (self.test_records + new_test_records).ids)]})
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=3)):
|
|
self.campaign.sync_participants()
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
"records": new_test_records,
|
|
"status": "scheduled",
|
|
"fields_values": {
|
|
"schedule_date": self.date_reference + timedelta(days=1, hours=3),
|
|
},
|
|
}],
|
|
new_activity,
|
|
)
|
|
for activity in (
|
|
self.activity_begin_mail + self.activity_begin_sms +
|
|
self.activity_begin_wa + self.activity_begin_sa
|
|
):
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
"records": new_test_records,
|
|
"status": "scheduled",
|
|
"fields_values": {
|
|
"schedule_date": self.date_reference + timedelta(hours=4), # sync date + activity timedelta
|
|
},
|
|
}],
|
|
activity,
|
|
strict=False,
|
|
)
|
|
|
|
@mute_logger('odoo.addons.mail.models.mail_mail')
|
|
@users('user_marketing_automation')
|
|
def test_schedule_date_based_on_triggers_update(self):
|
|
""" Test triggers when updating activities """
|
|
self._launch_campaign(self.campaign, date_reference=self.date_reference)
|
|
date_send = self.date_reference + timedelta(hours=1)
|
|
date_subaction = date_send + timedelta(days=5)
|
|
|
|
with self.mock_datetime_and_now(date_send), self.mock_mail_gateway():
|
|
self.campaign.execute_activities()
|
|
|
|
# changing already-done activities: should not impact anything as child
|
|
# traces are based upon parent traces, already done
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=2)):
|
|
(
|
|
self.activity_begin_mail + self.activity_begin_sms +
|
|
self.activity_begin_wa + self.activity_begin_sa
|
|
).write({
|
|
"interval_number": 2,
|
|
"interval_type": "days",
|
|
})
|
|
self.campaign.action_update_participants()
|
|
|
|
for sub_activity, expected_schedule_date in [
|
|
# mail
|
|
(self.activity_sa_mail_not_open, date_subaction),
|
|
(self.activity_sa_mail_open, False), # mail open: no date, as user input
|
|
(self.activity_sa_mail_not_reply, date_subaction),
|
|
(self.activity_sa_mail_reply, False), # mail reply: no date, as user input
|
|
(self.activity_sa_mail_not_click, date_subaction),
|
|
(self.activity_sa_mail_click, False), # mail click: no date, as user input
|
|
(self.activity_sa_mail_bounce, False), # mail bounce: no date, as user input
|
|
# sms
|
|
(self.activity_sa_sms_not_click, date_subaction),
|
|
(self.activity_sa_sms_click, False), # sms click: no date, as user input
|
|
(self.activity_sa_sms_bounce, False), # sms bounce: no date, as user input
|
|
# whatsapp
|
|
(self.activity_sa_wa_not_read, date_subaction),
|
|
(self.activity_sa_wa_read, False), # wa read: no date, as user input
|
|
(self.activity_sa_wa_not_replied, date_subaction),
|
|
(self.activity_sa_wa_replied, False), # wa replied: no date, as user input
|
|
(self.activity_sa_wa_not_click, date_subaction),
|
|
(self.activity_sa_wa_click, False), # wa click: no date, as user input
|
|
(self.activity_sa_wa_bounced, False), # wa bounce: no date, as user input
|
|
# other
|
|
(self.activity_mail_activity, date_send + timedelta(hours=2)),
|
|
(self.activity_sms_activity, date_send + timedelta(hours=2)),
|
|
(self.activity_wa_activity, date_send + timedelta(hours=2)),
|
|
]:
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': self.test_records,
|
|
'status': 'scheduled',
|
|
'fields_values': {
|
|
'schedule_date': expected_schedule_date,
|
|
},
|
|
}],
|
|
sub_activity,
|
|
)
|
|
|
|
# change scheduled activities: should not impact "no deadline" activities
|
|
# such as mail_open, mail_click as they depend on user action
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=3)):
|
|
(
|
|
# mail
|
|
self.activity_sa_mail_not_open + self.activity_sa_mail_open +
|
|
self.activity_sa_mail_not_reply + self.activity_sa_mail_reply +
|
|
self.activity_sa_mail_not_click + self.activity_sa_mail_click +
|
|
self.activity_sa_mail_bounce +
|
|
# sms
|
|
self.activity_sa_sms_not_click + self.activity_sa_sms_click +
|
|
self.activity_sa_sms_bounce +
|
|
# whatsapp
|
|
self.activity_sa_wa_not_read + self.activity_sa_wa_read +
|
|
self.activity_sa_wa_not_replied + self.activity_sa_wa_replied +
|
|
self.activity_sa_wa_not_click + self.activity_sa_wa_click +
|
|
self.activity_sa_wa_bounced +
|
|
# other
|
|
self.activity_mail_activity + self.activity_sms_activity + self.activity_wa_activity
|
|
).write({
|
|
"interval_number": 2,
|
|
"interval_type": "days",
|
|
})
|
|
self.campaign.action_update_participants()
|
|
date_subaction = date_send + timedelta(days=2)
|
|
|
|
for sub_activity, expected_schedule_date in [
|
|
# mail
|
|
(self.activity_sa_mail_not_open, date_subaction),
|
|
(self.activity_sa_mail_open, False), # mail open: no date, as user input
|
|
(self.activity_sa_mail_not_reply, date_subaction),
|
|
(self.activity_sa_mail_reply, False), # mail reply: no date, as user input
|
|
(self.activity_sa_mail_not_click, date_subaction),
|
|
(self.activity_sa_mail_click, False), # mail click: no date, as user input
|
|
(self.activity_sa_mail_bounce, False), # mail bounce: no date, as user input
|
|
# sms
|
|
(self.activity_sa_sms_not_click, date_subaction),
|
|
(self.activity_sa_sms_click, False), # sms click: no date, as user input
|
|
(self.activity_sa_sms_bounce, False), # sms bounce: no date, as user input
|
|
# whatsapp
|
|
(self.activity_sa_wa_not_read, date_subaction),
|
|
# (self.activity_sa_wa_read, False), # wa read: no date, as user input
|
|
(self.activity_sa_wa_not_replied, date_subaction),
|
|
# (self.activity_sa_wa_replied, False), # wa reply: no date, as user input
|
|
(self.activity_sa_wa_not_click, date_subaction),
|
|
(self.activity_sa_wa_click, False), # wa click: no date, as user input
|
|
(self.activity_sa_wa_bounced, False), # wa bounce: no date, as user input
|
|
# other
|
|
(self.activity_mail_activity, date_subaction),
|
|
(self.activity_sms_activity, date_subaction),
|
|
(self.activity_wa_activity, date_subaction),
|
|
]:
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': self.test_records,
|
|
'status': 'scheduled',
|
|
'fields_values': {
|
|
'schedule_date': expected_schedule_date,
|
|
},
|
|
}],
|
|
sub_activity,
|
|
)
|
|
|
|
# update write_date on some traces by triggering trace update
|
|
new_now = self.date_reference + timedelta(hours=5)
|
|
date_subaction_new = new_now + timedelta(days=2)
|
|
with self.mock_datetime_and_now(new_now):
|
|
self.gateway_mail_trace_open(self.activity_begin_mail.mass_mailing_id, self.test_records[0])
|
|
self.gateway_mail_trace_reply(MAIL_TEMPLATE, self.activity_begin_mail.mass_mailing_id, self.test_records[1])
|
|
self.gateway_mail_trace_click(self.activity_begin_mail.mass_mailing_id, self.test_records[2], "LINK")
|
|
|
|
for record, sub_activity, exp_status, exp_schedule_date in [
|
|
# mail
|
|
(self.test_records[0], self.activity_sa_mail_not_open, "canceled", new_now),
|
|
(self.test_records[0], self.activity_sa_mail_open, "scheduled", date_subaction_new),
|
|
]:
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': record,
|
|
'status': exp_status,
|
|
'fields_values': {
|
|
'schedule_date': exp_schedule_date,
|
|
},
|
|
}],
|
|
sub_activity,
|
|
strict=False,
|
|
)
|
|
|
|
# update sub actions, updated date should be based on trace date
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=3)):
|
|
(
|
|
# mail
|
|
self.activity_sa_mail_not_open + self.activity_sa_mail_open +
|
|
self.activity_sa_mail_not_reply + self.activity_sa_mail_reply +
|
|
self.activity_sa_mail_not_click + self.activity_sa_mail_click +
|
|
self.activity_sa_mail_bounce +
|
|
# sms
|
|
self.activity_sa_sms_not_click + self.activity_sa_sms_click +
|
|
self.activity_sa_sms_bounce +
|
|
# whatsapp
|
|
self.activity_sa_wa_not_read + self.activity_sa_wa_read +
|
|
self.activity_sa_wa_not_replied + self.activity_sa_wa_replied +
|
|
self.activity_sa_wa_not_click + self.activity_sa_wa_click +
|
|
self.activity_sa_wa_bounced
|
|
).write({
|
|
"interval_number": 5,
|
|
"interval_type": "days",
|
|
})
|
|
self.campaign.action_update_participants()
|
|
date_subaction_upd = new_now + timedelta(days=5)
|
|
|
|
for record, sub_activity, exp_status, exp_schedule_date in [
|
|
# opened mail
|
|
(self.test_records[0], self.activity_sa_mail_not_open, "canceled", new_now),
|
|
(self.test_records[0], self.activity_sa_mail_open, "scheduled", date_subaction_upd),
|
|
(self.test_records[0], self.activity_sa_mail_not_reply, "scheduled", date_subaction_upd - timedelta(hours=4)), # still cheduled on opened mail, also unsure those hours ??
|
|
(self.test_records[0], self.activity_sa_mail_reply, "scheduled", False), # user input, should not be scheduled
|
|
# replied mail
|
|
(self.test_records[1], self.activity_sa_mail_not_open, "canceled", new_now),
|
|
(self.test_records[1], self.activity_sa_mail_open, "scheduled", date_subaction_upd), # mail open: shoudln't it be no date, as user input ?
|
|
(self.test_records[1], self.activity_sa_mail_not_reply, "canceled", new_now),
|
|
(self.test_records[1], self.activity_sa_mail_reply, "scheduled", date_subaction_upd), # date_subaction_new ?
|
|
]:
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': record,
|
|
'status': exp_status,
|
|
'fields_values': {
|
|
'schedule_date': exp_schedule_date,
|
|
},
|
|
}],
|
|
sub_activity,
|
|
strict=False,
|
|
)
|
|
|
|
@users('user_marketing_automation')
|
|
def test_updating_activity_hierarchy(self):
|
|
""" Test activity hierarchy updates are restricted when traces exist """
|
|
self._launch_campaign(self.campaign, date_reference=self.date_reference)
|
|
date_send = self.date_reference + timedelta(hours=1)
|
|
|
|
self.assertTrue(self.activity_begin_sms.trace_ids)
|
|
with self.assertRaises(ValidationError):
|
|
self.activity_begin_sms.write({
|
|
"trigger_type": "activity",
|
|
"parent_id": self.activity_begin_mail,
|
|
})
|
|
|
|
self.assertFalse(self.activity_mail_activity.trace_ids)
|
|
with self.mock_datetime_and_now(date_send):
|
|
self.activity_mail_activity.write({
|
|
"trigger_type": "activity",
|
|
"parent_id": self.activity_begin_sms,
|
|
})
|
|
self.activity_begin_sms.execute()
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
"records": self.test_records,
|
|
"status": "scheduled",
|
|
"fields_values": {
|
|
"schedule_date": date_send + timedelta(hours=2),
|
|
},
|
|
}],
|
|
self.activity_mail_activity,
|
|
strict=True,
|
|
)
|
|
|
|
@users('user_marketing_automation')
|
|
def test_updating_activity_parent_in_test(self):
|
|
""" Test activity hierarchy updates with test traces """
|
|
self.campaign.domain = [(0, '=', 1)]
|
|
self._launch_campaign(self.campaign, date_reference=self.date_reference)
|
|
|
|
self.env['marketing.campaign.test'].create({
|
|
'campaign_id': self.campaign.id,
|
|
'res_id': self.test_records[0].id,
|
|
}).action_launch_test()
|
|
|
|
self.assertEqual(len(self.activity_begin_sms.trace_ids), 1)
|
|
test_trace = self.activity_begin_sms.trace_ids[0]
|
|
self.assertEqual(test_trace.state, 'scheduled')
|
|
|
|
self.activity_begin_sms.write({
|
|
'trigger_type': 'activity',
|
|
'parent_id': self.activity_begin_mail,
|
|
})
|
|
self.activity_begin_sms.write({
|
|
'interval_number': 5,
|
|
'interval_type': 'days',
|
|
})
|
|
self.assertTrue(self.activity_begin_sms.require_sync)
|
|
|
|
self.campaign.action_update_participants()
|
|
expected_date = test_trace.participant_id.create_date + timedelta(days=5)
|
|
self.assertEqual(test_trace.schedule_date, expected_date)
|
|
|
|
|
|
@tagged('marketing_automation', 'ma_sync')
|
|
class TestSynchronize(ActivityTriggersCase):
|
|
|
|
@mute_logger('odoo.addons.mail.models.mail_thread')
|
|
@users('user_marketing_automation')
|
|
def test_post_launch_synchronize(self):
|
|
""" Test participants creation then update when adding new activities
|
|
on a running campaign. We should not duplicate traces, notably when
|
|
a participant had traces on a new activity that is then updated
|
|
automatically. """
|
|
campaign = self.env['marketing.campaign'].create({
|
|
'domain': [('id', 'in', self.test_records.ids)],
|
|
'model_id': self.env['ir.model']._get_id('marketing.test.sms'),
|
|
'name': 'Test Campaign',
|
|
})
|
|
# begin activities, parents for each type category to test
|
|
activity_begin_mail = self._create_activity(
|
|
campaign,
|
|
create_date=self.date_reference,
|
|
mailing=self.test_mailing_mail,
|
|
interval_number=1, interval_type='hours',
|
|
trigger_type='begin',
|
|
)
|
|
activity_begin_sms = self._create_activity(
|
|
campaign,
|
|
create_date=self.date_reference,
|
|
mailing=self.test_mailing_sms,
|
|
interval_number=1, interval_type='hours',
|
|
trigger_type='begin',
|
|
)
|
|
activity_begin_wa = self._create_activity(
|
|
campaign,
|
|
create_date=self.date_reference,
|
|
wa_template=self.test_wa_template,
|
|
interval_number=1, interval_type='hours',
|
|
trigger_type='begin',
|
|
)
|
|
_activity_begin_sa = self._create_activity(
|
|
campaign,
|
|
create_date=self.date_reference,
|
|
action=self.test_sa_descr_action,
|
|
interval_number=1, interval_type='hours',
|
|
trigger_type='begin',
|
|
)
|
|
|
|
# init participants and traces
|
|
with self.mock_datetime_and_now(self.date_reference):
|
|
self._launch_campaign(campaign, self.date_reference)
|
|
campaign.action_update_participants()
|
|
# run begin activities so that we have traces for replies / ...
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=1)), \
|
|
self.mockWhatsappGateway(), self.patchWhatsappCronTrigger(), \
|
|
self.mock_mail_gateway(), self.mockSMSGateway():
|
|
campaign.execute_activities()
|
|
|
|
for parent, existing, additional, exp_assert_values in (
|
|
# mail: records 0-1 are open, 2-3 are replied to, 6-7 are clicked, 8 is bounced
|
|
# ------------------------------------------------------------
|
|
(
|
|
activity_begin_mail,
|
|
("mail_not_open", self.test_sa_descr_mail_not_open, 0),
|
|
(
|
|
("mail_open", self.test_sa_descr_mail_open),
|
|
("mail_reply", self.test_sa_descr_mail_reply),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # open still holds, unsure
|
|
}, {
|
|
'records': self.test_records_ok, # reply still holds, unsure
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_mail,
|
|
("mail_open", self.test_sa_descr_mail_open, 0),
|
|
(
|
|
("mail_not_open", self.test_sa_descr_mail_not_open),
|
|
("mail_not_reply", self.test_sa_descr_mail_not_reply),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok[4] + self.test_records_ok[7:], # not_open canceled by open - note that reply and click implies open
|
|
}, {
|
|
'records': self.test_records_ok, # not_reply still holds even if opened
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_mail,
|
|
("mail_not_reply", self.test_sa_descr_mail_not_reply, 0),
|
|
(
|
|
("mail_open", self.test_sa_descr_mail_open),
|
|
("mail_reply", self.test_sa_descr_mail_reply),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # open still holds, unsure
|
|
}, {
|
|
'records': self.test_records_ok, # reply still holds, unsure
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_mail,
|
|
("mail_reply", self.test_sa_descr_mail_reply, 0),
|
|
(
|
|
("mail_not_open", self.test_sa_descr_mail_not_open),
|
|
("mail_not_reply", self.test_sa_descr_mail_not_reply),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # not_open still holds even if replied (to fix probably)
|
|
}, {
|
|
'records': self.test_records_ok[:2] + self.test_records_ok[4:], # not_reply canceled by reply
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_mail,
|
|
("mail_not_click", self.test_sa_descr_mail_not_click, 0),
|
|
(
|
|
("mail_open", self.test_sa_descr_mail_open),
|
|
("mail_reply", self.test_sa_descr_mail_reply),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # open still holds, unsure
|
|
}, {
|
|
'records': self.test_records_ok, # reply still holds, unsure
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_mail,
|
|
("mail_click", self.test_sa_descr_mail_click, 0),
|
|
(
|
|
("mail_not_open", self.test_sa_descr_mail_not_open),
|
|
("mail_not_reply", self.test_sa_descr_mail_not_reply),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # not_open still holds even if clicked (to fix probably)
|
|
}, {
|
|
'records': self.test_records_ok, # not_reply still holds
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_mail,
|
|
("mail_bounce", self.test_sa_descr_mail_bounce, 0),
|
|
(
|
|
("mail_open", self.test_sa_descr_mail_open),
|
|
("mail_not_open", self.test_sa_descr_mail_not_open),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # open still holds even if bounced (to check)
|
|
}, {
|
|
'records': self.test_records_ok, # not_reply still holds
|
|
},
|
|
),
|
|
),
|
|
# sms
|
|
# ------------------------------------------------------------
|
|
(
|
|
activity_begin_sms,
|
|
("sms_not_click", self.test_sa_descr_sms_not_click, 0),
|
|
(
|
|
("sms_click", self.test_sa_descr_sms_click),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # click still holds, unsure
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_sms,
|
|
("sms_click", self.test_sa_descr_sms_click, 0),
|
|
(
|
|
("sms_not_click", self.test_sa_descr_sms_not_click),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok[2:], # not_click canceled by click
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_sms,
|
|
("sms_bounce", self.test_sa_descr_sms_bounce, 0),
|
|
(
|
|
("sms_click", self.test_sa_descr_sms_click),
|
|
("sms_not_click", self.test_sa_descr_sms_not_click),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # check me
|
|
}, {
|
|
'records': self.test_records_ok, # check me
|
|
},
|
|
),
|
|
),
|
|
# whatsapp
|
|
# ------------------------------------------------------------
|
|
(
|
|
activity_begin_wa,
|
|
("whatsapp_not_read", self.test_sa_descr_wa_not_read, 0),
|
|
(
|
|
("whatsapp_read", self.test_sa_descr_wa_read),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # read still holds, unsure
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_wa,
|
|
("whatsapp_read", self.test_sa_descr_wa_read, 0),
|
|
(
|
|
("whatsapp_not_read", self.test_sa_descr_wa_not_read),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok[2:], # not_read canceled by read
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_wa,
|
|
("whatsapp_not_replied", self.test_sa_descr_wa_not_replied, 0),
|
|
(
|
|
("whatsapp_read", self.test_sa_descr_wa_read),
|
|
("whatsapp_replied", self.test_sa_descr_wa_replied),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # read still holds, unsure
|
|
}, {
|
|
'records': self.test_records_ok, # reply still holds, unsure
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_wa,
|
|
("whatsapp_replied", self.test_sa_descr_wa_replied, 0),
|
|
(
|
|
("whatsapp_not_read", self.test_sa_descr_wa_not_read),
|
|
("whatsapp_not_replied", self.test_sa_descr_wa_not_replied),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # not_read still holds even if replied (to fix probably)
|
|
}, {
|
|
'records': self.test_records_ok, # FIXME: not_replied should be canceled by read
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_wa,
|
|
("whatsapp_not_click", self.test_sa_descr_wa_not_click, 0),
|
|
(
|
|
("whatsapp_read", self.test_sa_descr_wa_read),
|
|
("whatsapp_replied", self.test_sa_descr_wa_replied),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # open still holds, unsure
|
|
}, {
|
|
'records': self.test_records_ok, # replied still holds, unsure
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_wa,
|
|
("whatsapp_click", self.test_sa_descr_wa_click, 0),
|
|
(
|
|
("whatsapp_not_read", self.test_sa_descr_wa_not_read),
|
|
("whatsapp_not_replied", self.test_sa_descr_wa_not_replied),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # not_read still holds even if replied (to fix probably)
|
|
}, {
|
|
'records': self.test_records_ok, # not_replied still holds, logic (clicked, not necessarily replied)
|
|
},
|
|
),
|
|
),
|
|
(
|
|
activity_begin_wa,
|
|
("whatsapp_bounced", self.test_sa_descr_wa_bounced, 0),
|
|
(
|
|
("whatsapp_read", self.test_sa_descr_wa_read),
|
|
("whatsapp_not_read", self.test_sa_descr_wa_not_read),
|
|
("whatsapp_replied", self.test_sa_descr_wa_replied),
|
|
("whatsapp_not_replied", self.test_sa_descr_wa_not_replied),
|
|
),
|
|
(
|
|
{
|
|
'records': self.test_records_ok, # read still holds even if bounced (to fix probably)
|
|
}, {
|
|
'records': self.test_records_ok, # not_read still holds, unsure
|
|
}, {
|
|
'records': self.test_records_ok, # replied still holds even if bounced (to fix probably)
|
|
}, {
|
|
'records': self.test_records_ok, # not_replied still holds, unsure
|
|
},
|
|
),
|
|
),
|
|
):
|
|
trigger_type, server_action, interval = existing
|
|
with self.subTest(parent_name=parent.name, trigger_type=trigger_type):
|
|
# create activity, schedule it to have traces
|
|
new_activities = self._create_activity(
|
|
campaign,
|
|
create_date=self.date_reference + timedelta(hours=1),
|
|
action=server_action,
|
|
interval_number=interval, interval_type='days',
|
|
parent_id=parent.id,
|
|
trigger_type=trigger_type,
|
|
)
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=2)):
|
|
campaign.action_update_participants() # scheduled newly create activity
|
|
|
|
if parent.activity_type == 'email':
|
|
# update traces: open
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
|
|
self.gateway_mail_trace_open(parent.mass_mailing_id, self.test_records[0:2])
|
|
# update traces: reply
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
|
|
self.gateway_mail_trace_reply(MAIL_TEMPLATE, parent.mass_mailing_id, self.test_records[2:4])
|
|
# update traces: click
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
|
|
self.gateway_mail_trace_click(parent.mass_mailing_id, self.test_records[6:8], "LINK")
|
|
# update traces: bounce
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
|
|
self.gateway_mail_trace_bounce(parent.mass_mailing_id, self.test_records[8])
|
|
elif parent.activity_type == 'sms':
|
|
# update traces: click
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
|
|
self.gateway_sms_click(parent.mass_mailing_id, self.test_records[0], use_sent_sms=False, sms_status='pending')
|
|
self.gateway_sms_click(parent.mass_mailing_id, self.test_records[1], use_sent_sms=False, sms_status='pending')
|
|
# update traces: bounce
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
|
|
self.gateway_sms_bounce(parent.mass_mailing_id, self.test_records[2])
|
|
elif parent.activity_type == 'whatsapp':
|
|
# update traces: open
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
|
|
self.whatsapp_msg_read_with_records(self.test_records[0:2])
|
|
else:
|
|
raise NotImplementedError()
|
|
|
|
# add brother-like activity, check status
|
|
for to_create in additional:
|
|
trigger_type, server_action = to_create
|
|
new_activities += self._create_activity(
|
|
campaign,
|
|
create_date=self.date_reference + timedelta(hours=5),
|
|
action=server_action,
|
|
interval_number=1, interval_type='days',
|
|
parent_id=parent.id,
|
|
trigger_type=trigger_type,
|
|
)
|
|
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=8)):
|
|
campaign.action_update_participants()
|
|
|
|
# final expected status: for parent
|
|
if parent.activity_type == 'email':
|
|
self.assertMarketAutoTraces( # begin mail activity sent for everyone
|
|
[{
|
|
'records': self.test_records_ok[0:2],
|
|
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[0:2]},
|
|
'status': 'processed',
|
|
'trace_status': 'open', # gateway trigerred
|
|
}, {
|
|
'records': self.test_records_ok[2:4],
|
|
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[2:4]},
|
|
'status': 'processed',
|
|
'trace_status': 'reply', # gateway trigerred
|
|
}, {
|
|
'records': self.test_records_ok[5:7],
|
|
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[5:7]},
|
|
'mailing_trace_values': {
|
|
'links_click_datetime': self.date_reference + timedelta(hours=4),
|
|
},
|
|
'status': 'processed',
|
|
'trace_status': 'open', # click is not a status
|
|
}, {
|
|
'records': self.test_records_ok[7],
|
|
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[7]},
|
|
'status': 'processed',
|
|
'mail_values': { # note that bounce did not update the mail.mail itself, only the trace
|
|
'failure_reason': False,
|
|
'failure_type': False,
|
|
'state': 'sent',
|
|
},
|
|
'trace_failure_reason': 'This is the bounce email',
|
|
'trace_failure_type': 'mail_bounce',
|
|
'trace_status': 'bounce', # click is not a status
|
|
}, {
|
|
'records': self.test_records_ok[4] + self.test_records_ok[8:],
|
|
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[4] + self.test_records_ok[8:]},
|
|
'status': 'processed',
|
|
'trace_status': 'sent',
|
|
}, {
|
|
'records': self.test_records_ko,
|
|
'status': 'canceled',
|
|
'trace_failure_type': 'mail_email_missing',
|
|
'trace_status': 'cancel',
|
|
}],
|
|
parent,
|
|
)
|
|
elif parent.activity_type == 'sms':
|
|
self.assertMarketAutoTraces( # begin sms activity sent for everyone
|
|
[{
|
|
'records': self.test_records_ok[0:2],
|
|
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[0:2]},
|
|
'mailing_trace_values': {
|
|
'links_click_datetime': self.date_reference + timedelta(hours=4),
|
|
},
|
|
'sms_values': {
|
|
'state': 'pending', # sms_force_send activated, hence not ouggoing
|
|
},
|
|
'status': 'processed',
|
|
'trace_status': 'open', # click is not a status
|
|
}, {
|
|
'check_sms': False, # sms untouched, still old values (not bounced)
|
|
'records': self.test_records_ok[2],
|
|
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[2]},
|
|
'status': 'processed',
|
|
'trace_failure_type': 'sms_invalid_destination',
|
|
'trace_status': 'bounce',
|
|
}, {
|
|
'records': self.test_records_ok[3:],
|
|
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[2:4]},
|
|
'status': 'processed',
|
|
'trace_status': 'pending', # sms_force_send activated, hence not outgoing
|
|
}, {
|
|
'records': self.test_records_ko,
|
|
'status': 'canceled',
|
|
'trace_failure_type': 'sms_number_missing',
|
|
'trace_status': 'cancel',
|
|
}],
|
|
parent,
|
|
)
|
|
elif parent.activity_type == 'whatsapp':
|
|
self.assertMarketAutoTraces( # begin wa activity sent for everyone
|
|
[{
|
|
'records': self.test_records_ok[0:2],
|
|
'status': 'processed',
|
|
'trace_status': 'read', # trace_status is actually directly wa message state here
|
|
}, {
|
|
'records': self.test_records_ok[2:],
|
|
'status': 'processed',
|
|
'trace_status': 'sent', # trace_status is actually directly wa message state here
|
|
}, {
|
|
'records': self.test_records_ko,
|
|
'status': 'error',
|
|
'trace_failure_type': 'phone_invalid',
|
|
'trace_status': 'error',
|
|
}],
|
|
parent,
|
|
)
|
|
|
|
# final expected status: for children
|
|
for new_activity, check_values in zip(new_activities[1:], exp_assert_values, strict=True):
|
|
with self.subTest(new_activity_name=new_activity.name):
|
|
self.assertMarketAutoTraces(
|
|
[{
|
|
'records': self.test_records_ok,
|
|
'status': 'scheduled',
|
|
**check_values,
|
|
}],
|
|
new_activity,
|
|
)
|
|
|
|
# cleanup
|
|
new_activities.unlink()
|
|
campaign.last_sync_date = self.date_reference
|