Initial commit: Mail packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:51 +02:00
commit 4e53507711
1948 changed files with 751201 additions and 0 deletions

View file

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mailing_sms_test
from . import sms_composer

View file

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from odoo.addons.phone_validation.tools import phone_validation
class MassSMSTest(models.TransientModel):
_name = 'mailing.sms.test'
_description = 'Test SMS Mailing'
def _default_numbers(self):
return self.env.user.partner_id.phone_sanitized or ""
numbers = fields.Text(string='Number(s)', required=True,
default=_default_numbers, help='Carriage-return-separated list of phone numbers')
mailing_id = fields.Many2one('mailing.mailing', string='Mailing', required=True, ondelete='cascade')
def action_send_sms(self):
self.ensure_one()
numbers = [number.strip() for number in self.numbers.splitlines()]
sanitize_res = phone_validation.phone_sanitize_numbers_w_record(numbers, self.env.user)
sanitized_numbers = [info['sanitized'] for info in sanitize_res.values() if info['sanitized']]
invalid_numbers = [number for number, info in sanitize_res.items() if info['code']]
record = self.env[self.mailing_id.mailing_model_real].search([], limit=1)
body = self.mailing_id.body_plaintext
if record:
# Returns a proper error if there is a syntax error with qweb
body = self.env['mail.render.mixin']._render_template(body, self.mailing_id.mailing_model_real, record.ids)[record.id]
# res_id is used to map the result to the number to log notifications as IAP does not return numbers...
# TODO: clean IAP to make it return a clean dict with numbers / use custom keys / rename res_id to external_id
sent_sms_list = self.env['sms.api']._send_sms_batch([{
'res_id': number,
'number': number,
'content': body,
} for number in sanitized_numbers])
error_messages = {}
if any(sent_sms.get('state') != 'success' for sent_sms in sent_sms_list):
error_messages = self.env['sms.api']._get_sms_api_error_messages()
notification_messages = []
if invalid_numbers:
notification_messages.append(_('The following numbers are not correctly encoded: %s',
', '.join(invalid_numbers)))
for sent_sms in sent_sms_list:
if sent_sms.get('state') == 'success':
notification_messages.append(
_('Test SMS successfully sent to %s', sent_sms.get('res_id')))
elif sent_sms.get('state'):
notification_messages.append(
_('Test SMS could not be sent to %s:<br>%s',
sent_sms.get('res_id'),
error_messages.get(sent_sms['state'], _("An error occurred.")))
)
if notification_messages:
self.mailing_id._message_log(body='<ul>%s</ul>' % ''.join(
['<li>%s</li>' % notification_message for notification_message in notification_messages]
))
return True

View file

@ -0,0 +1,29 @@
<?xml version="1.0"?>
<odoo>
<record id="mailing_sms_test_view_form" model="ir.ui.view">
<field name="name">mailing.sms.test.view.form</field>
<field name="model">mailing.sms.test</field>
<field name="arch" type="xml">
<form string="Send a Sample SMS">
<p class="text-muted">
Send a sample SMS for testing purpose to the numbers below (carriage-return-separated list).
</p>
<group>
<field name="numbers" placeholder="+32 495 85 85 77&#10;+33 545 55 55 55"/>
<field name="mailing_id" invisible="1"/>
</group>
<footer>
<button string="Send" name="action_send_sms" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<record id="mailing_sms_test_action" model="ir.actions.act_window">
<field name="name">Test SMS Marketing</field>
<field name="res_model">mailing.sms.test</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View file

@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import werkzeug.urls
from odoo import api, fields, models, _
class SMSComposer(models.TransientModel):
_inherit = 'sms.composer'
# mass mode with mass sms
mass_sms_allow_unsubscribe = fields.Boolean('Include opt-out link', default=True)
mailing_id = fields.Many2one('mailing.mailing', string='Mailing')
utm_campaign_id = fields.Many2one('utm.campaign', string='Campaign', ondelete='set null')
# ------------------------------------------------------------
# Mass mode specific
# ------------------------------------------------------------
def _get_unsubscribe_url(self, res_id, trace_code, number):
return werkzeug.urls.url_join(
self.get_base_url(),
'/sms/%s/%s' % (self.mailing_id.id, trace_code)
)
@api.model
def _get_unsubscribe_info(self, url):
return _('STOP SMS : %(unsubscribe_url)s', unsubscribe_url=url)
def _prepare_mass_sms_trace_values(self, record, sms_values):
trace_code = self.env['mailing.trace']._get_random_code()
trace_values = {
'model': self.res_model,
'res_id': record.id,
'trace_type': 'sms',
'mass_mailing_id': self.mailing_id.id,
'sms_number': sms_values['number'],
'sms_code': trace_code,
}
if sms_values['state'] == 'error':
trace_values['failure_type'] = sms_values['failure_type']
trace_values['trace_status'] = 'error'
elif sms_values['state'] == 'canceled':
trace_values['failure_type'] = sms_values['failure_type']
trace_values['trace_status'] = 'cancel'
else:
if self.mass_sms_allow_unsubscribe:
stop_sms = self._get_unsubscribe_info(self._get_unsubscribe_url(record.id, trace_code, sms_values['number']))
sms_values['body'] = '%s\n%s' % (sms_values['body'] or '', stop_sms)
return trace_values
def _get_optout_record_ids(self, records, recipients_info):
""" Fetch opt-out records based on mailing. """
res = super(SMSComposer, self)._get_optout_record_ids(records, recipients_info)
if self.mailing_id:
optout_res_ids = self.mailing_id._get_opt_out_list_sms()
res += optout_res_ids
return res
def _get_done_record_ids(self, records, recipients_info):
""" A/B testing could lead to records having been already mailed. """
res = super(SMSComposer, self)._get_done_record_ids(records, recipients_info)
if self.mailing_id:
seen_ids, seen_list = self.mailing_id._get_seen_list_sms()
res += seen_ids
return res
def _prepare_body_values(self, records):
all_bodies = super(SMSComposer, self)._prepare_body_values(records)
if self.mailing_id:
tracker_values = self.mailing_id._get_link_tracker_values()
for sms_id, body in all_bodies.items():
body = self.env['mail.render.mixin'].sudo()._shorten_links_text(body, tracker_values)
all_bodies[sms_id] = body
return all_bodies
def _prepare_mass_sms_values(self, records):
result = super(SMSComposer, self)._prepare_mass_sms_values(records)
if self.composition_mode == 'mass' and self.mailing_id:
for record in records:
sms_values = result[record.id]
trace_values = self._prepare_mass_sms_trace_values(record, sms_values)
sms_values.update({
'mailing_id': self.mailing_id.id,
'mailing_trace_ids': [(0, 0, trace_values)],
})
return result
def _prepare_mass_sms(self, records, sms_record_values):
sms_all = super(SMSComposer, self)._prepare_mass_sms(records, sms_record_values)
if self.mailing_id:
updated_bodies = sms_all._update_body_short_links()
for sms in sms_all:
sms.body = updated_bodies[sms.id]
return sms_all

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sms_composer_view_form" model="ir.ui.view">
<field name="name">sms.composer.views.inherit.sms</field>
<field name="model">sms.composer</field>
<field name="inherit_id" ref="sms.sms_composer_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='res_model']" position="after">
<field name="utm_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"
invisible="1"/>
<field name="mailing_id" invisible="1"/>
</xpath>
</field>
</record>
</odoo>