19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:39 +01:00
parent 5df8c07b59
commit daa394e8b0
2114 changed files with 564841 additions and 299642 deletions

View file

@ -7,4 +7,5 @@ from . import mailing_mailing
from . import mailing_trace
from . import res_users
from . import sms_sms
from . import sms_tracker
from . import utm

View file

@ -9,6 +9,3 @@ class MailingContact(models.Model):
_inherit = ['mailing.contact', 'mail.thread.phone']
mobile = fields.Char(string='Mobile')
def _phone_get_number_fields(self):
return ['mobile']

View file

@ -27,6 +27,22 @@ class MailingList(models.Model):
action['context'] = dict(action.get('context', {}), search_default_filter_valid_sms_recipient=1)
return action
def action_send_mailing_sms(self):
view = self.env.ref('mass_mailing_sms.mailing_mailing_view_form_sms')
action = self.env["ir.actions.actions"]._for_xml_id('mass_mailing_sms.mailing_mailing_action_sms')
action.update({
'context': {
'default_contact_list_ids': self.ids,
'default_model_id': self.env['ir.model']._get_id('mailing.list'),
'default_mailing_type': 'sms',
},
'target': 'current',
'view_type': 'form',
'views': [(view.id, 'form')],
})
return action
def _get_contact_statistics_fields(self):
""" See super method docstring for more info.
Adds:
@ -57,7 +73,8 @@ class MailingList(models.Model):
on one list but not on another, one opted in and the other one opted out,
send mailing anyway.
:return list: opt-outed record IDs
:return: opt-outed record IDs
:rtype: list
"""
subscriptions = self.subscription_ids if self else mailing.contact_list_ids.subscription_ids
opt_out_contacts = subscriptions.filtered(lambda sub: sub.opt_out).mapped('contact_id')

View file

@ -3,22 +3,22 @@
import logging
from urllib.parse import urljoin
from odoo import api, fields, models, _
from odoo.addons.link_tracker.models.link_tracker import LINK_TRACKER_MIN_CODE_LENGTH
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.fields import Domain
from odoo.tools.urls import urljoin
from odoo.addons.link_tracker.models.link_tracker import LINK_TRACKER_MIN_CODE_LENGTH
_logger = logging.getLogger(__name__)
class Mailing(models.Model):
class MailingMailing(models.Model):
_inherit = 'mailing.mailing'
@api.model
def default_get(self, fields):
res = super(Mailing, self).default_get(fields)
res = super().default_get(fields)
if fields is not None and 'keep_archives' in fields and res.get('mailing_type') == 'sms':
res['keep_archives'] = True
return res
@ -55,15 +55,16 @@ class Mailing(models.Model):
ab_testing_sms_winner_selection = fields.Selection(
related="campaign_id.ab_testing_sms_winner_selection",
default="clicks_ratio", readonly=False, copy=True)
ab_testing_mailings_sms_count = fields.Integer(related="campaign_id.ab_testing_mailings_sms_count")
@api.depends('mailing_type')
def _compute_medium_id(self):
super(Mailing, self)._compute_medium_id()
super()._compute_medium_id()
for mailing in self:
if mailing.mailing_type == 'sms' and (not mailing.medium_id or mailing.medium_id == self.env.ref('utm.utm_medium_email')):
mailing.medium_id = self.env.ref('mass_mailing_sms.utm_medium_sms').id
elif mailing.mailing_type == 'mail' and (not mailing.medium_id or mailing.medium_id == self.env.ref('mass_mailing_sms.utm_medium_sms')):
mailing.medium_id = self.env.ref('utm.utm_medium_email').id
if mailing.mailing_type == 'sms' and (not mailing.medium_id or mailing.medium_id == self.env['utm.medium']._fetch_or_create_utm_medium('email')):
mailing.medium_id = self.env['utm.medium']._fetch_or_create_utm_medium("sms", module="mass_mailing_sms").id
elif mailing.mailing_type == 'mail' and (not mailing.medium_id or mailing.medium_id == self.env['utm.medium']._fetch_or_create_utm_medium("sms", module="mass_mailing_sms")):
mailing.medium_id = self.env['utm.medium']._fetch_or_create_utm_medium('email').id
@api.depends('sms_template_id', 'mailing_type')
def _compute_body_plaintext(self):
@ -73,23 +74,18 @@ class Mailing(models.Model):
@api.depends('mailing_trace_ids.failure_type')
def _compute_sms_has_iap_failure(self):
failures = ['sms_acc', 'sms_credit']
if not self.ids:
self.sms_has_insufficient_credit = self.sms_has_unregistered_account = False
else:
traces = self.env['mailing.trace'].sudo().read_group([
('mass_mailing_id', 'in', self.ids),
('trace_type', '=', 'sms'),
('failure_type', 'in', failures)
], ['mass_mailing_id', 'failure_type'], ['mass_mailing_id', 'failure_type'], lazy=False)
self.sms_has_insufficient_credit = self.sms_has_unregistered_account = False
traces = self.env['mailing.trace'].sudo()._read_group([
('mass_mailing_id', 'in', self.ids),
('trace_type', '=', 'sms'),
('failure_type', 'in', ['sms_acc', 'sms_credit'])
], ['mass_mailing_id', 'failure_type'])
trace_dict = dict.fromkeys(self.ids, {key: False for key in failures})
for t in traces:
trace_dict[t['mass_mailing_id'][0]][t['failure_type']] = bool(t['__count'])
for mail in self:
mail.sms_has_insufficient_credit = trace_dict[mail.id]['sms_credit']
mail.sms_has_unregistered_account = trace_dict[mail.id]['sms_acc']
for mass_mailing, failure_type in traces:
if failure_type == 'sms_credit':
mass_mailing.sms_has_insufficient_credit = True
elif failure_type == 'sms_acc':
mass_mailing.sms_has_unregistered_account = True
# --------------------------------------------------
# ORM OVERRIDES
@ -112,7 +108,7 @@ class Mailing(models.Model):
mass_sms = self.filtered(lambda m: m.mailing_type == 'sms')
if mass_sms:
mass_sms.action_retry_failed_sms()
return super(Mailing, self - mass_sms).action_retry_failed()
return super(MailingMailing, self - mass_sms).action_retry_failed()
def action_retry_failed_sms(self):
failed_sms = self.env['sms.sms'].sudo().search([
@ -125,21 +121,21 @@ class Mailing(models.Model):
def action_test(self):
if self.mailing_type == 'sms':
ctx = dict(self.env.context, default_mailing_id=self.id)
ctx = dict(self.env.context, default_mailing_id=self.id, dialog_size='medium')
return {
'name': _('Test SMS marketing'),
'name': _('Test Mailing'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mailing.sms.test',
'target': 'new',
'context': ctx,
}
return super(Mailing, self).action_test()
return super().action_test()
def _action_view_traces_filtered(self, view_filter):
action = super(Mailing, self)._action_view_traces_filtered(view_filter)
action = super()._action_view_traces_filtered(view_filter)
if self.mailing_type == 'sms':
action['views'] = [(self.env.ref('mass_mailing_sms.mailing_trace_view_tree_sms').id, 'tree'),
action['views'] = [(self.env.ref('mass_mailing_sms.mailing_trace_view_tree_sms').id, 'list'),
(self.env.ref('mass_mailing_sms.mailing_trace_view_form_sms').id, 'form')]
return action
@ -158,7 +154,8 @@ class Mailing(models.Model):
""" Give list of opt-outed records, depending on specific model-based
computation if available.
:return list: opt-outed record IDs
:returns: opt-outed record IDs
:rtype: list
"""
self.ensure_one()
opt_out = []
@ -178,18 +175,12 @@ class Mailing(models.Model):
partner_fields = []
if isinstance(target, self.pool['mail.thread.phone']):
phone_fields = ['phone_sanitized']
elif isinstance(target, self.pool['mail.thread']):
else:
phone_fields = [
fname for fname in target._sms_get_number_fields()
fname for fname in target._phone_get_number_fields()
if fname in target._fields and target._fields[fname].store
]
partner_fields = target._sms_get_partner_fields()
else:
phone_fields = []
if 'mobile' in target._fields and target._fields['mobile'].store:
phone_fields.append('mobile')
if 'phone' in target._fields and target._fields['phone'].store:
phone_fields.append('phone')
partner_fields = target._mail_get_partner_fields()
partner_field = next(
(fname for fname in partner_fields if target._fields[fname].store and target._fields[fname].type == 'many2one'),
False
@ -213,7 +204,7 @@ class Mailing(models.Model):
join_add_query = ''
else:
# phone fields are checked on res.partner model
partner_phone_fields = ['mobile', 'phone']
partner_phone_fields = ['phone']
select_query = 'target.id, ' + ', '.join('partner.%s' % fname for fname in partner_phone_fields)
where_query = ' OR '.join('partner.%s IS NOT NULL' % fname for fname in partner_phone_fields)
join_add_query = 'JOIN res_partner partner ON (target.%s = partner.id)' % partner_field
@ -225,8 +216,8 @@ class Mailing(models.Model):
'join_add_query': join_add_query,
}
params = {'mailing_id': self.id, 'target_model': self.mailing_model_real}
self._cr.execute(query, params)
query_res = self._cr.fetchall()
self.env.cr.execute(query, params)
query_res = self.env.cr.fetchall()
seen_list = set(number for item in query_res for number in item[1:] if number)
seen_ids = set(item[0] for item in query_res)
_logger.info("Mass SMS %s targets %s: already reached %s SMS", self, target._name, len(seen_list))
@ -245,13 +236,14 @@ class Mailing(models.Model):
'mass_keep_log': self.keep_archives,
'mass_force_send': self.sms_force_send,
'mass_sms_allow_unsubscribe': self.sms_allow_unsubscribe,
'use_exclusion_list': self.use_exclusion_list,
}
def action_send_mail(self, res_ids=None):
def _action_send_mail(self, res_ids=None):
mass_sms = self.filtered(lambda m: m.mailing_type == 'sms')
if mass_sms:
mass_sms.action_send_sms(res_ids=res_ids)
return super(Mailing, self - mass_sms).action_send_mail(res_ids=res_ids)
return super(MailingMailing, self - mass_sms)._action_send_mail(res_ids=res_ids)
def action_send_sms(self, res_ids=None):
for mailing in self:
@ -260,12 +252,6 @@ class Mailing(models.Model):
if res_ids:
composer = self.env['sms.composer'].with_context(active_id=False).create(mailing._send_sms_get_composer_values(res_ids))
composer._action_send_sms()
mailing.write({
'state': 'done',
'sent_date': fields.Datetime.now(),
'kpi_mail_required': not mailing.sent_date,
})
return True
# ------------------------------------------------------
@ -278,7 +264,7 @@ class Mailing(models.Model):
Each item in the returned list will be displayed as a table, with a title and
1, 2 or 3 columns.
"""
values = super(Mailing, self)._prepare_statistics_email_values()
values = super()._prepare_statistics_email_values()
if self.mailing_type == 'sms':
mailing_type = self._get_pretty_mailing_type()
values['title'] = _('24H Stats of %(mailing_type)s "%(mailing_name)s"',
@ -310,19 +296,12 @@ class Mailing(models.Model):
def _get_pretty_mailing_type(self):
if self.mailing_type == 'sms':
return _('SMS Text Message')
return super(Mailing, self)._get_pretty_mailing_type()
return super()._get_pretty_mailing_type()
# --------------------------------------------------
# TOOLS
# --------------------------------------------------
def _get_default_mailing_domain(self):
mailing_domain = super(Mailing, self)._get_default_mailing_domain()
if self.mailing_type == 'sms' and 'phone_sanitized_blacklisted' in self.env[self.mailing_model_name]._fields:
mailing_domain = expression.AND([mailing_domain, [('phone_sanitized_blacklisted', '=', False)]])
return mailing_domain
def convert_links(self):
sms_mailings = self.filtered(lambda m: m.mailing_type == 'sms')
res = {}
@ -330,22 +309,23 @@ class Mailing(models.Model):
tracker_values = mailing._get_link_tracker_values()
body = mailing._shorten_links_text(mailing.body_plaintext, tracker_values)
res[mailing.id] = body
res.update(super(Mailing, self - sms_mailings).convert_links())
res.update(super(MailingMailing, self - sms_mailings).convert_links())
return res
def get_sms_link_replacements_placeholders(self):
"""Get placeholders for replaced links in sms widget for accurate computation of sms counts.
Reminders and assumptions:
* Links wille be transformed to the format "[base_url]/r/[link_tracker_code]/s/[sms_id]".
* unsubscribe is formatted as: "\nSTOP SMS : [base_url]/sms/[mailing_id]/[trace_code]".
:return: Character counts used for links, formatted as `{link: str, unsubscribe: str}`.
* Links wille be transformed to the format ``"[base_url]/r/[link_tracker_code]/s/[sms_id]"``.
* unsubscribe is formatted as: ``"STOP SMS : [base_url]/sms/[mailing_id]/[trace_code]"``.
:returns: Character counts used for links, formatted as ``{link: str, unsubscribe: str}``.
"""
if self:
self.ensure_one()
self.check_access_rights('write')
self.check_access('write')
max_sms = self.env['sms.sms'].sudo().search_read([], ['id'], order='id desc', limit=1)
sms_id_length = max(len(str(max_sms[0]['id'])), 5) if max_sms else 5 # Assumes a mailing won't be more than 10⁵ sms at once
@ -378,6 +358,7 @@ class Mailing(models.Model):
values = super()._get_ab_testing_description_values()
if self.mailing_type == 'sms':
values.update({
'ab_testing_count': self.ab_testing_mailings_sms_count,
'ab_testing_winner_selection': self.ab_testing_sms_winner_selection,
})
return values

View file

@ -5,7 +5,6 @@ import random
import string
from odoo import api, fields, models
from odoo.osv import expression
class MailingTrace(models.Model):
@ -17,35 +16,87 @@ class MailingTrace(models.Model):
trace_type = fields.Selection(selection_add=[
('sms', 'SMS')
], ondelete={'sms': 'set default'})
sms_sms_id = fields.Many2one('sms.sms', string='SMS', index='btree_not_null', ondelete='set null')
sms_sms_id_int = fields.Integer(
string='SMS ID (tech)',
sms_id = fields.Many2one('sms.sms', string='SMS', store=False, compute='_compute_sms_id')
sms_id_int = fields.Integer(
string='SMS ID',
index='btree_not_null'
# Integer because the related sms.sms can be deleted separately from its statistics.
# However the ID is needed for several action and controllers.
# However, the ID is needed for several action and controllers.
)
sms_tracker_ids = fields.One2many('sms.tracker', 'mailing_trace_id', string='SMS Trackers')
sms_number = fields.Char('Number')
sms_code = fields.Char('Code')
failure_type = fields.Selection(selection_add=[
('sms_number_missing', 'Missing Number'),
('sms_number_format', 'Wrong Number Format'),
('sms_credit', 'Insufficient Credit'),
('sms_country_not_supported', 'Country Not Supported'),
('sms_registration_needed', 'Country-specific Registration Required'),
('sms_server', 'Server Error'),
('sms_acc', 'Unregistered Account'),
# mass mode specific codes
('sms_blacklist', 'Blacklisted'),
('sms_duplicate', 'Duplicate'),
('sms_optout', 'Opted Out'),
# delivery report errors
('sms_expired', 'Expired'),
('sms_invalid_destination', 'Invalid Destination'),
('sms_not_allowed', 'Not Allowed'),
('sms_not_delivered', 'Not Delivered'),
('sms_rejected', 'Rejected'),
# twilio specific: to move in bridge module in master
('twilio_authentication', 'Authentication Error"'),
('twilio_callback', 'Incorrect callback URL'),
('twilio_from_missing', 'Missing From Number'),
('twilio_from_to', 'From / To identic'),
])
@api.depends('sms_id_int', 'trace_type')
def _compute_sms_id(self):
self.sms_id = False
sms_traces = self.filtered(lambda t: t.trace_type == 'sms' and bool(t.sms_id_int))
if not sms_traces:
return
existing_sms_ids = self.env['sms.sms'].sudo().search([
('id', 'in', sms_traces.mapped('sms_id_int')), ('to_delete', '!=', True)
]).ids
for sms_trace in sms_traces.filtered(lambda n: n.sms_id_int in set(existing_sms_ids)):
sms_trace.sms_id = sms_trace.sms_id_int
@api.model_create_multi
def create(self, values_list):
for values in values_list:
if 'sms_sms_id' in values:
values['sms_sms_id_int'] = values['sms_sms_id']
def create(self, vals_list):
for values in vals_list:
if values.get('trace_type') == 'sms' and not values.get('sms_code'):
values['sms_code'] = self._get_random_code()
return super(MailingTrace, self).create(values_list)
return super().create(vals_list)
@api.model
def fields_get(self, allfields=None, attributes=None):
# As we are adding keys in stable, better be sure no-one is getting crashes
# due to missing translations
# TODO: remove in master
res = super().fields_get(allfields=allfields, attributes=attributes)
existing_selection = res.get('failure_type', {}).get('selection')
if existing_selection is None:
return res
updated_stable = {
'twilio_authentication', 'twilio_callback',
'twilio_from_missing', 'twilio_from_to',
}
need_update = updated_stable - set(dict(self._fields['failure_type'].selection))
if need_update:
self.env['ir.model.fields'].invalidate_model(['selection_ids'])
self.env['ir.model.fields.selection']._update_selection(
self._name,
'failure_type',
self._fields['failure_type'].selection,
)
self.env.registry.clear_cache()
return super().fields_get(allfields=allfields, attributes=attributes)
return res
def _get_random_code(self):
""" Generate a random code for trace. Uniqueness is not really necessary

View file

@ -6,47 +6,54 @@ import json
from odoo import api, fields, models, modules, _
class Users(models.Model):
_name = 'res.users'
_inherit = ['res.users']
class ResUsers(models.Model):
_inherit = 'res.users'
@api.model
def systray_get_activities(self):
def _get_activity_groups(self):
""" Split mass_mailing and mass_mailing_sms activities in systray by
removing the single mailing.mailing activity represented and
doing a new query to split them by mailing_type.
"""
activities = super(Users, self).systray_get_activities()
activities = super()._get_activity_groups()
view_type = self.env['mailing.mailing']._systray_view
for activity in activities:
if activity.get('model') == 'mailing.mailing':
activities.remove(activity)
query = """SELECT m.mailing_type, count(*), act.res_model as model, act.res_id,
CASE
WHEN %(today)s::date - act.date_deadline::date = 0 Then 'today'
WHEN %(today)s::date - act.date_deadline::date > 0 Then 'overdue'
WHEN %(today)s::date - act.date_deadline::date < 0 Then 'planned'
END AS states
FROM mail_activity AS act
JOIN mailing_mailing AS m ON act.res_id = m.id
WHERE act.res_model = 'mailing.mailing' AND act.user_id = %(user_id)s
GROUP BY m.mailing_type, states, act.res_model, act.res_id;
query = """
WITH mailing_states AS (
SELECT m.mailing_type, act.res_id,
CASE
WHEN %(today)s::date - MIN(act.date_deadline)::date = 0 Then 'today'
WHEN %(today)s::date - MIN(act.date_deadline)::date > 0 Then 'overdue'
WHEN %(today)s::date - MIN(act.date_deadline)::date < 0 Then 'planned'
END AS states
FROM mail_activity AS act
JOIN mailing_mailing AS m ON act.res_id = m.id
WHERE act.res_model = 'mailing.mailing' AND act.user_id = %(user_id)s AND act.active in (TRUE, %(active)s)
GROUP BY m.mailing_type, act.res_id
)
SELECT mailing_type, states, array_agg(res_id) AS res_ids, COUNT(res_id) AS count
FROM mailing_states
GROUP BY mailing_type, states
"""
self.env.cr.execute(query, {
'today': fields.Date.context_today(self),
'user_id': self.env.uid,
'active': self.env.context.get('active_test', True),
})
activity_data = self.env.cr.dictfetchall()
user_activities = {}
for act in activity_data:
if not user_activities.get(act['mailing_type']):
if act['mailing_type'] == 'sms':
module = 'mass_mailing_sms'
module_name = 'mass_mailing_sms'
name = _('SMS Marketing')
else:
module = 'mass_mailing'
module_name = 'mass_mailing'
name = _('Email Marketing')
icon = module and modules.module.get_module_icon(module)
icon = modules.Manifest.for_addon(module_name).icon
res_ids = set()
user_activities[act['mailing_type']] = {
'id': self.env['ir.model']._get('mailing.mailing').id,
@ -54,18 +61,22 @@ class Users(models.Model):
'model': 'mailing.mailing',
'type': 'activity',
'icon': icon,
'domain': [('active', 'in', [True, False])],
'total_count': 0, 'today_count': 0, 'overdue_count': 0, 'planned_count': 0,
'res_ids': res_ids,
"view_type": view_type,
}
user_activities[act['mailing_type']]['res_ids'].add(act['res_id'])
user_activities[act['mailing_type']]['res_ids'].update(act['res_ids'])
user_activities[act['mailing_type']]['%s_count' % act['states']] += act['count']
if act['states'] in ('today', 'overdue'):
user_activities[act['mailing_type']]['total_count'] += act['count']
for mailing_type in user_activities.keys():
user_activities[mailing_type].update({
'actions': [{'icon': 'fa-clock-o', 'name': 'Summary',}],
'domain': json.dumps([['activity_ids.res_id', 'in', list(user_activities[mailing_type]['res_ids'])]])
'domain': json.dumps([
['active', 'in', [True, False]],
['activity_ids.res_id', 'in', list(user_activities[mailing_type]['res_ids'])],
])
})
activities.extend(list(user_activities.values()))
break

View file

@ -3,14 +3,18 @@
import re
from odoo import fields, models, tools
from odoo import fields, models
from odoo.tools.mail import TEXT_URL_REGEX
class SmsSms(models.Model):
_inherit = ['sms.sms']
_inherit = 'sms.sms'
mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing')
mailing_trace_ids = fields.One2many('mailing.trace', 'sms_sms_id', string='Statistics')
# Linking to another field than the comodel id allows to use the ORM to create
# "linked" records (see _prepare_sms_values) without adding a foreign key.
# See commit message for why this is useful.
mailing_trace_ids = fields.One2many('mailing.trace', 'sms_id_int', string='Statistics')
def _update_body_short_links(self):
""" Override to tweak shortened URLs by adding statistics ids, allowing to
@ -22,25 +26,8 @@ class SmsSms(models.Model):
continue
body = sms.body
for url in set(re.findall(tools.TEXT_URL_REGEX, body)):
for url in set(re.findall(TEXT_URL_REGEX, body)):
if url.startswith(sms.get_base_url() + '/r/'):
body = re.sub(re.escape(url) + r'(?![\w@:%.+&~#=/-])', url + f'/s/{sms.id}', body)
res[sms.id] = body
return res
def _postprocess_iap_sent_sms(self, iap_results, failure_reason=None, unlink_failed=False, unlink_sent=True):
all_sms_ids = [item['res_id'] for item in iap_results]
if any(sms.mailing_id for sms in self.env['sms.sms'].sudo().browse(all_sms_ids)):
for state in self.IAP_TO_SMS_STATE.keys():
sms_ids = [item['res_id'] for item in iap_results if item['state'] == state]
traces = self.env['mailing.trace'].sudo().search([
('sms_sms_id_int', 'in', sms_ids)
])
if traces and state == 'success':
traces.set_sent()
elif traces:
traces.set_failed(failure_type=self.IAP_TO_SMS_STATE[state])
return super(SmsSms, self)._postprocess_iap_sent_sms(
iap_results, failure_reason=failure_reason,
unlink_failed=unlink_failed, unlink_sent=unlink_sent
)

View file

@ -0,0 +1,79 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class SmsTracker(models.Model):
_inherit = "sms.tracker"
SMS_STATE_TO_TRACE_STATUS = {
'error': 'error',
'process': 'process',
'outgoing': 'outgoing',
'canceled': 'cancel',
'pending': 'pending',
'sent': 'sent',
}
mailing_trace_id = fields.Many2one('mailing.trace', ondelete='cascade', index='btree_not_null')
def _action_update_from_provider_error(self, provider_error):
error_status, failure_type, failure_reason = super()._action_update_from_provider_error(provider_error)
self._update_sms_traces(error_status or 'error', failure_type=failure_type, failure_reason=failure_reason)
return error_status, failure_type, failure_reason
def _action_update_from_sms_state(self, sms_state, failure_type=False, failure_reason=False):
super()._action_update_from_sms_state(sms_state, failure_type=failure_type, failure_reason=failure_reason)
trace_status = self.SMS_STATE_TO_TRACE_STATUS[sms_state]
traces = self._update_sms_traces(trace_status, failure_type=failure_type, failure_reason=failure_reason)
self._update_sms_mailings(trace_status, traces)
def _update_sms_traces(self, trace_status, failure_type=False, failure_reason=False):
if not self.mailing_trace_id: # avoid a search below
return self.env['mailing.trace']
# See _update_sms_notifications
statuses_to_ignore = {
'cancel': ['cancel', 'process', 'pending', 'sent'],
'outgoing': ['outgoing', 'process', 'pending', 'sent'],
'process': ['process', 'pending', 'sent'],
'pending': ['pending', 'sent'],
'bounce': ['bounce'],
'sent': ['sent'],
'error': ['error'],
}[trace_status]
traces = self.mailing_trace_id.filtered(lambda t: t.trace_status not in statuses_to_ignore)
if traces:
# TDE note: check to use set_sent / ... tools updating marketing automation bits
traces_values = {
'trace_status': trace_status,
'failure_type': failure_type,
'failure_reason': failure_reason,
}
traces.write(traces_values)
traces.filtered(
lambda t: t.trace_status not in ['outgoing', 'process', 'error', 'cancel'] and not t.sent_datetime
).sent_datetime = self.env.cr.now()
return traces
def _update_sms_mailings(self, trace_status, traces):
traces.flush_recordset(['trace_status'])
if trace_status == 'process':
traces.mass_mailing_id.write({'state': 'sending'})
return
mailings_to_mark_done = self.env['mailing.mailing'].search([
('id', 'in', traces.mass_mailing_id.ids),
'!', ('mailing_trace_ids.trace_status', '=', 'process'), # = not any trace with 'process' status
('state', '!=', 'done'),
])
if mailings_to_mark_done:
if self.env.user.is_public: # From webhook event
mailings_to_mark_done._track_set_author(self.env.ref('base.partner_root'))
for mailing in mailings_to_mark_done:
mailing.write({
'state': 'done',
'sent_date': fields.Datetime.now(),
'kpi_mail_required': not mailing.sent_date
})

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import _, api, fields, models
from odoo.exceptions import UserError
@ -23,35 +24,24 @@ class UtmCampaign(models.Model):
('manual', 'Manual'),
('clicks_ratio', 'Highest Click Rate')], string="SMS Winner Selection", default="clicks_ratio")
@api.depends('mailing_mail_ids', 'mailing_sms_ids')
def _compute_ab_testing_total_pc(self):
super()._compute_ab_testing_total_pc()
for campaign in self:
campaign.ab_testing_total_pc += sum([
mailing.ab_testing_pc for mailing in campaign.mailing_sms_ids.filtered('ab_testing_enabled')
])
@api.depends('mailing_sms_ids')
def _compute_mailing_sms_count(self):
if self.ids:
mailing_sms_data = self.env['mailing.mailing'].read_group(
[('campaign_id', 'in', self.ids), ('mailing_type', '=', 'sms')],
['campaign_id', 'ab_testing_enabled'],
['campaign_id', 'ab_testing_enabled'],
lazy=False,
)
ab_testing_mapped_sms_data = {}
mapped_sms_data = {}
for data in mailing_sms_data:
if data['ab_testing_enabled']:
ab_testing_mapped_sms_data.setdefault(data['campaign_id'][0], []).append(data['__count'])
mapped_sms_data.setdefault(data['campaign_id'][0], []).append(data['__count'])
else:
mapped_sms_data = dict()
ab_testing_mapped_sms_data = dict()
mailing_sms_data = self.env['mailing.mailing']._read_group(
[('campaign_id', 'in', self.ids), ('mailing_type', '=', 'sms')],
['campaign_id', 'ab_testing_enabled'],
['__count'],
)
ab_testing_mapped_sms_data = defaultdict(list)
mapped_sms_data = defaultdict(list)
for campaign, ab_testing_enabled, count in mailing_sms_data:
if ab_testing_enabled:
ab_testing_mapped_sms_data[campaign.id].append(count)
mapped_sms_data[campaign.id].append(count)
for campaign in self:
campaign.mailing_sms_count = sum(mapped_sms_data.get(campaign._origin.id or campaign.id, []))
campaign.ab_testing_mailings_sms_count = sum(ab_testing_mapped_sms_data.get(campaign._origin.id or campaign.id, []))
campaign.mailing_sms_count = sum(mapped_sms_data[campaign.id])
campaign.ab_testing_mailings_sms_count = sum(ab_testing_mapped_sms_data[campaign.id])
def action_create_mass_sms(self):
action = self.env["ir.actions.actions"]._for_xml_id("mass_mailing.action_create_mass_mailings_from_campaign")
@ -99,3 +89,7 @@ class UtmMedium(models.Model):
"functional flows, such as the SMS Marketing.",
utm_medium_sms.name
))
@property
def SELF_REQUIRED_UTM_MEDIUMS_REF(self):
return super().SELF_REQUIRED_UTM_MEDIUMS_REF | {"mass_mailing_sms.utm_medium_sms": "SMS"}