mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-22 00:42:08 +02:00
19.0 vanilla
This commit is contained in:
parent
5df8c07b59
commit
daa394e8b0
2114 changed files with 564841 additions and 299642 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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"}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue