mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-21 02:42:05 +02:00
Initial commit: Mail packages
This commit is contained in:
commit
4e53507711
1948 changed files with 751201 additions and 0 deletions
6
odoo-bringout-oca-ocb-sms/sms/wizard/__init__.py
Normal file
6
odoo-bringout-oca-ocb-sms/sms/wizard/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import sms_composer
|
||||
from . import sms_resend
|
||||
from . import sms_template_preview
|
||||
from . import sms_template_reset
|
||||
372
odoo-bringout-oca-ocb-sms/sms/wizard/sms_composer.py
Normal file
372
odoo-bringout-oca-ocb-sms/sms/wizard/sms_composer.py
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from ast import literal_eval
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.phone_validation.tools import phone_validation
|
||||
from odoo.addons.sms.tools.sms_tools import sms_content_to_rendered_html
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class SendSMS(models.TransientModel):
|
||||
_name = 'sms.composer'
|
||||
_description = 'Send SMS Wizard'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
result = super(SendSMS, self).default_get(fields)
|
||||
|
||||
result['res_model'] = result.get('res_model') or self.env.context.get('active_model')
|
||||
|
||||
if not result.get('res_ids'):
|
||||
if not result.get('res_id') and self.env.context.get('active_ids') and len(self.env.context.get('active_ids')) > 1:
|
||||
result['res_ids'] = repr(self.env.context.get('active_ids'))
|
||||
if not result.get('res_id'):
|
||||
if not result.get('res_ids') and self.env.context.get('active_id'):
|
||||
result['res_id'] = self.env.context.get('active_id')
|
||||
|
||||
return result
|
||||
|
||||
# documents
|
||||
composition_mode = fields.Selection([
|
||||
('numbers', 'Send to numbers'),
|
||||
('comment', 'Post on a document'),
|
||||
('mass', 'Send SMS in batch')], string='Composition Mode',
|
||||
compute='_compute_composition_mode', precompute=True, readonly=False, required=True, store=True)
|
||||
res_model = fields.Char('Document Model Name')
|
||||
res_model_description = fields.Char('Document Model Description', compute='_compute_res_model_description')
|
||||
res_id = fields.Integer('Document ID')
|
||||
res_ids = fields.Char('Document IDs')
|
||||
res_ids_count = fields.Integer(
|
||||
'Visible records count', compute='_compute_res_ids_count', compute_sudo=False,
|
||||
help='Number of recipients that will receive the SMS if sent in mass mode, without applying the Active Domain value')
|
||||
comment_single_recipient = fields.Boolean(
|
||||
'Single Mode', compute='_compute_comment_single_recipient', compute_sudo=False,
|
||||
help='Indicates if the SMS composer targets a single specific recipient')
|
||||
# options for comment and mass mode
|
||||
mass_keep_log = fields.Boolean('Keep a note on document', default=True)
|
||||
mass_force_send = fields.Boolean('Send directly', default=False)
|
||||
mass_use_blacklist = fields.Boolean('Use blacklist', default=True)
|
||||
# recipients
|
||||
recipient_valid_count = fields.Integer('# Valid recipients', compute='_compute_recipients', compute_sudo=False)
|
||||
recipient_invalid_count = fields.Integer('# Invalid recipients', compute='_compute_recipients', compute_sudo=False)
|
||||
recipient_single_description = fields.Text('Recipients (Partners)', compute='_compute_recipient_single', compute_sudo=False)
|
||||
recipient_single_number = fields.Char('Stored Recipient Number', compute='_compute_recipient_single', compute_sudo=False)
|
||||
recipient_single_number_itf = fields.Char(
|
||||
'Recipient Number', compute='_compute_recipient_single',
|
||||
readonly=False, compute_sudo=False, store=True,
|
||||
help='Phone number of the recipient. If changed, it will be recorded on recipient\'s profile.')
|
||||
recipient_single_valid = fields.Boolean("Is valid", compute='_compute_recipient_single_valid', compute_sudo=False)
|
||||
number_field_name = fields.Char('Number Field')
|
||||
numbers = fields.Char('Recipients (Numbers)')
|
||||
sanitized_numbers = fields.Char('Sanitized Number', compute='_compute_sanitized_numbers', compute_sudo=False)
|
||||
# content
|
||||
template_id = fields.Many2one('sms.template', string='Use Template', domain="[('model', '=', res_model)]")
|
||||
body = fields.Text(
|
||||
'Message', compute='_compute_body',
|
||||
precompute=True, readonly=False, store=True, required=True)
|
||||
|
||||
@api.depends('res_ids_count')
|
||||
@api.depends_context('sms_composition_mode')
|
||||
def _compute_composition_mode(self):
|
||||
for composer in self:
|
||||
if self.env.context.get('sms_composition_mode') == 'guess' or not composer.composition_mode:
|
||||
if composer.res_ids_count > 1:
|
||||
composer.composition_mode = 'mass'
|
||||
else:
|
||||
composer.composition_mode = 'comment'
|
||||
|
||||
@api.depends('res_model')
|
||||
def _compute_res_model_description(self):
|
||||
self.res_model_description = False
|
||||
for composer in self.filtered('res_model'):
|
||||
composer.res_model_description = self.env['ir.model']._get(composer.res_model).display_name
|
||||
|
||||
@api.depends('res_model', 'res_id', 'res_ids')
|
||||
def _compute_res_ids_count(self):
|
||||
for composer in self:
|
||||
composer.res_ids_count = len(literal_eval(composer.res_ids)) if composer.res_ids else 0
|
||||
|
||||
@api.depends('res_id', 'composition_mode')
|
||||
def _compute_comment_single_recipient(self):
|
||||
for composer in self:
|
||||
composer.comment_single_recipient = bool(composer.res_id and composer.composition_mode == 'comment')
|
||||
|
||||
@api.depends('res_model', 'res_id', 'res_ids', 'composition_mode', 'number_field_name', 'sanitized_numbers')
|
||||
def _compute_recipients(self):
|
||||
for composer in self:
|
||||
composer.recipient_valid_count = 0
|
||||
composer.recipient_invalid_count = 0
|
||||
|
||||
if composer.composition_mode not in ('comment', 'mass') or not composer.res_model:
|
||||
continue
|
||||
|
||||
records = composer._get_records()
|
||||
if records:
|
||||
res = records._sms_get_recipients_info(force_field=composer.number_field_name, partner_fallback=not composer.comment_single_recipient)
|
||||
composer.recipient_valid_count = len([rid for rid, rvalues in res.items() if rvalues['sanitized']])
|
||||
composer.recipient_invalid_count = len([rid for rid, rvalues in res.items() if not rvalues['sanitized']])
|
||||
else:
|
||||
composer.recipient_invalid_count = 0 if (
|
||||
composer.sanitized_numbers or composer.composition_mode == 'mass'
|
||||
) else 1
|
||||
|
||||
@api.depends('res_model', 'number_field_name')
|
||||
def _compute_recipient_single(self):
|
||||
for composer in self:
|
||||
records = composer._get_records()
|
||||
if not records or not composer.comment_single_recipient:
|
||||
composer.recipient_single_description = False
|
||||
composer.recipient_single_number = ''
|
||||
composer.recipient_single_number_itf = ''
|
||||
continue
|
||||
records.ensure_one()
|
||||
res = records._sms_get_recipients_info(force_field=composer.number_field_name, partner_fallback=True)
|
||||
composer.recipient_single_description = res[records.id]['partner'].name or records._sms_get_default_partners().display_name
|
||||
composer.recipient_single_number = res[records.id]['sanitized'] or res[records.id]['number'] or ''
|
||||
if not composer.recipient_single_number_itf:
|
||||
composer.recipient_single_number_itf = res[records.id]['sanitized'] or res[records.id]['number'] or ''
|
||||
if not composer.number_field_name:
|
||||
composer.number_field_name = res[records.id]['field_store']
|
||||
|
||||
@api.depends('recipient_single_number', 'recipient_single_number_itf')
|
||||
def _compute_recipient_single_valid(self):
|
||||
for composer in self:
|
||||
value = composer.recipient_single_number_itf or composer.recipient_single_number
|
||||
if value:
|
||||
records = composer._get_records()
|
||||
sanitized = phone_validation.phone_sanitize_numbers_w_record([value], records)[value]['sanitized']
|
||||
composer.recipient_single_valid = bool(sanitized)
|
||||
else:
|
||||
composer.recipient_single_valid = False
|
||||
|
||||
@api.depends('numbers', 'res_model', 'res_id')
|
||||
def _compute_sanitized_numbers(self):
|
||||
for composer in self:
|
||||
if composer.numbers:
|
||||
record = composer._get_records() if composer.res_model and composer.res_id else self.env.user
|
||||
numbers = [number.strip() for number in composer.numbers.split(',')]
|
||||
sanitize_res = phone_validation.phone_sanitize_numbers_w_record(numbers, record)
|
||||
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']]
|
||||
if invalid_numbers:
|
||||
raise UserError(_('Following numbers are not correctly encoded: %s', repr(invalid_numbers)))
|
||||
composer.sanitized_numbers = ','.join(sanitized_numbers)
|
||||
else:
|
||||
composer.sanitized_numbers = False
|
||||
|
||||
@api.depends('composition_mode', 'res_model', 'res_id', 'template_id')
|
||||
def _compute_body(self):
|
||||
for record in self:
|
||||
if record.template_id and record.composition_mode == 'comment' and record.res_id:
|
||||
record.body = record.template_id._render_field('body', [record.res_id], compute_lang=True)[record.res_id]
|
||||
elif record.template_id:
|
||||
record.body = record.template_id.body
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Actions
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def action_send_sms(self):
|
||||
if self.composition_mode in ('numbers', 'comment'):
|
||||
if self.comment_single_recipient and not self.recipient_single_valid:
|
||||
raise UserError(_('Invalid recipient number. Please update it.'))
|
||||
elif not self.comment_single_recipient and self.recipient_invalid_count:
|
||||
raise UserError(_('%s invalid recipients', self.recipient_invalid_count))
|
||||
self._action_send_sms()
|
||||
return False
|
||||
|
||||
def action_send_sms_mass_now(self):
|
||||
if not self.mass_force_send:
|
||||
self.write({'mass_force_send': True})
|
||||
return self.action_send_sms()
|
||||
|
||||
def _action_send_sms(self):
|
||||
records = self._get_records()
|
||||
if self.composition_mode == 'numbers':
|
||||
return self._action_send_sms_numbers()
|
||||
elif self.composition_mode == 'comment':
|
||||
if records is None or not isinstance(records, self.pool['mail.thread']):
|
||||
return self._action_send_sms_numbers()
|
||||
if self.comment_single_recipient:
|
||||
return self._action_send_sms_comment_single(records)
|
||||
else:
|
||||
return self._action_send_sms_comment(records)
|
||||
else:
|
||||
return self._action_send_sms_mass(records)
|
||||
|
||||
def _action_send_sms_numbers(self):
|
||||
numbers = self.sanitized_numbers.split(',') if self.sanitized_numbers else [self.recipient_single_number_itf or self.recipient_single_number or '']
|
||||
self.env['sms.api']._send_sms_batch([{
|
||||
'res_id': 0,
|
||||
'number': number,
|
||||
'content': self.body,
|
||||
} for number in numbers])
|
||||
return True
|
||||
|
||||
def _action_send_sms_comment_single(self, records=None):
|
||||
# If we have a recipient_single_original number, it's possible this number has been corrected in the popup
|
||||
# if invalid. As a consequence, the test cannot be based on recipient_invalid_count, which count is based
|
||||
# on the numbers in the database.
|
||||
records = records if records is not None else self._get_records()
|
||||
records.ensure_one()
|
||||
if not self.number_field_name or self.number_field_name not in records:
|
||||
self.numbers = self.recipient_single_number_itf or self.recipient_single_number
|
||||
elif self.recipient_single_number_itf and self.recipient_single_number_itf != self.recipient_single_number:
|
||||
records.write({self.number_field_name: self.recipient_single_number_itf})
|
||||
return self._action_send_sms_comment(records=records)
|
||||
|
||||
def _action_send_sms_comment(self, records=None):
|
||||
records = records if records is not None else self._get_records()
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
|
||||
|
||||
messages = self.env['mail.message']
|
||||
all_bodies = self._prepare_body_values(records)
|
||||
|
||||
for record in records:
|
||||
messages += record._message_sms(
|
||||
all_bodies[record.id],
|
||||
subtype_id=subtype_id,
|
||||
number_field=self.number_field_name,
|
||||
sms_numbers=self.sanitized_numbers.split(',') if self.sanitized_numbers else None)
|
||||
return messages
|
||||
|
||||
def _action_send_sms_mass(self, records=None):
|
||||
records = records if records is not None else self._get_records()
|
||||
|
||||
sms_record_values = self._prepare_mass_sms_values(records)
|
||||
sms_all = self._prepare_mass_sms(records, sms_record_values)
|
||||
|
||||
if sms_all and self.mass_keep_log and records and isinstance(records, self.pool['mail.thread']):
|
||||
log_values = self._prepare_mass_log_values(records, sms_record_values)
|
||||
records._message_log_batch(**log_values)
|
||||
|
||||
if sms_all and self.mass_force_send:
|
||||
sms_all.filtered(lambda sms: sms.state == 'outgoing').send(auto_commit=False, raise_exception=False)
|
||||
return self.env['sms.sms'].sudo().search([('id', 'in', sms_all.ids)])
|
||||
return sms_all
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Mass mode specific
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _get_blacklist_record_ids(self, records, recipients_info):
|
||||
""" Get a list of blacklisted records. Those will be directly canceled
|
||||
with the right error code. """
|
||||
if self.mass_use_blacklist:
|
||||
bl_numbers = self.env['phone.blacklist'].sudo().search([]).mapped('number')
|
||||
return [r.id for r in records if recipients_info[r.id]['sanitized'] in bl_numbers]
|
||||
return []
|
||||
|
||||
def _get_optout_record_ids(self, records, recipients_info):
|
||||
""" Compute opt-outed contacts, not necessarily blacklisted. Void by default
|
||||
as no opt-out mechanism exist in SMS, see SMS Marketing. """
|
||||
return []
|
||||
|
||||
def _get_done_record_ids(self, records, recipients_info):
|
||||
""" Get a list of already-done records. Order of record set is used to
|
||||
spot duplicates so pay attention to it if necessary. """
|
||||
done_ids, done = [], []
|
||||
for record in records:
|
||||
sanitized = recipients_info[record.id]['sanitized']
|
||||
if sanitized in done:
|
||||
done_ids.append(record.id)
|
||||
else:
|
||||
done.append(sanitized)
|
||||
return done_ids
|
||||
|
||||
def _prepare_recipient_values(self, records):
|
||||
recipients_info = records._sms_get_recipients_info(force_field=self.number_field_name)
|
||||
return recipients_info
|
||||
|
||||
def _prepare_body_values(self, records):
|
||||
if self.template_id and self.body == self.template_id.body:
|
||||
all_bodies = self.template_id._render_field('body', records.ids, compute_lang=True)
|
||||
else:
|
||||
all_bodies = self.env['mail.render.mixin']._render_template(self.body, records._name, records.ids)
|
||||
return all_bodies
|
||||
|
||||
def _prepare_mass_sms_values(self, records):
|
||||
all_bodies = self._prepare_body_values(records)
|
||||
all_recipients = self._prepare_recipient_values(records)
|
||||
blacklist_ids = self._get_blacklist_record_ids(records, all_recipients)
|
||||
optout_ids = self._get_optout_record_ids(records, all_recipients)
|
||||
done_ids = self._get_done_record_ids(records, all_recipients)
|
||||
|
||||
result = {}
|
||||
for record in records:
|
||||
recipients = all_recipients[record.id]
|
||||
sanitized = recipients['sanitized']
|
||||
if sanitized and record.id in blacklist_ids:
|
||||
state = 'canceled'
|
||||
failure_type = 'sms_blacklist'
|
||||
elif sanitized and record.id in optout_ids:
|
||||
state = 'canceled'
|
||||
failure_type = 'sms_optout'
|
||||
elif sanitized and record.id in done_ids:
|
||||
state = 'canceled'
|
||||
failure_type = 'sms_duplicate'
|
||||
elif not sanitized:
|
||||
state = 'canceled'
|
||||
failure_type = 'sms_number_format' if recipients['number'] else 'sms_number_missing'
|
||||
else:
|
||||
state = 'outgoing'
|
||||
failure_type = ''
|
||||
|
||||
result[record.id] = {
|
||||
'body': all_bodies[record.id],
|
||||
'partner_id': recipients['partner'].id,
|
||||
'number': sanitized if sanitized else recipients['number'],
|
||||
'state': state,
|
||||
'failure_type': failure_type,
|
||||
}
|
||||
return result
|
||||
|
||||
def _prepare_mass_sms(self, records, sms_record_values):
|
||||
sms_create_vals = [sms_record_values[record.id] for record in records]
|
||||
return self.env['sms.sms'].sudo().create(sms_create_vals)
|
||||
|
||||
def _prepare_log_body_values(self, sms_records_values):
|
||||
result = {}
|
||||
for record_id, sms_values in sms_records_values.items():
|
||||
result[record_id] = sms_content_to_rendered_html(sms_values['body'])
|
||||
return result
|
||||
|
||||
def _prepare_mass_log_values(self, records, sms_records_values):
|
||||
return {
|
||||
'bodies': self._prepare_log_body_values(sms_records_values),
|
||||
'message_type': 'sms',
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Tools
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _get_composer_values(self, composition_mode, res_model, res_id, body, template_id):
|
||||
result = {}
|
||||
if composition_mode == 'comment':
|
||||
if not body and template_id and res_id:
|
||||
template = self.env['sms.template'].browse(template_id)
|
||||
result['body'] = template._render_template(template.body, res_model, [res_id])[res_id]
|
||||
elif template_id:
|
||||
template = self.env['sms.template'].browse(template_id)
|
||||
result['body'] = template.body
|
||||
else:
|
||||
if not body and template_id:
|
||||
template = self.env['sms.template'].browse(template_id)
|
||||
result['body'] = template.body
|
||||
return result
|
||||
|
||||
def _get_records(self):
|
||||
if not self.res_model:
|
||||
return None
|
||||
if self.res_ids:
|
||||
records = self.env[self.res_model].browse(literal_eval(self.res_ids))
|
||||
elif self.res_id:
|
||||
records = self.env[self.res_model].browse(self.res_id)
|
||||
else:
|
||||
records = self.env[self.res_model]
|
||||
|
||||
records = records.with_context(mail_notify_author=True)
|
||||
return records
|
||||
76
odoo-bringout-oca-ocb-sms/sms/wizard/sms_composer_views.xml
Normal file
76
odoo-bringout-oca-ocb-sms/sms/wizard/sms_composer_views.xml
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="sms_composer_view_form" model="ir.ui.view">
|
||||
<field name="name">sms.composer.view.form</field>
|
||||
<field name="model">sms.composer</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Send an SMS">
|
||||
<!-- Single mode information (invalid number) -->
|
||||
<div colspan="2" class="alert alert-danger text-center mb-0" role="alert"
|
||||
attrs="{'invisible': ['|', '|', ('res_model_description', '=', False), ('comment_single_recipient', '=', False), ('recipient_single_valid', '=', True)]}">
|
||||
<p class="my-0">
|
||||
<strong>Invalid number:</strong>
|
||||
<span> make sure to set a country on the </span>
|
||||
<span><field name="res_model_description"/></span>
|
||||
<span> or to specify the country code.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Mass mode information (res_ids versus active domain) -->
|
||||
<div colspan="2" class="alert alert-info text-center mb-0" role="alert"
|
||||
attrs="{'invisible': ['|', ('comment_single_recipient', '=', True), ('recipient_invalid_count', '=', 0)]}">
|
||||
<p class="my-0">
|
||||
<field class="oe_inline fw-bold" name="recipient_invalid_count"/> out of
|
||||
<field class="oe_inline fw-bold" name="res_ids_count"/> recipients have an invalid phone number and will not receive this text message.
|
||||
</p>
|
||||
</div>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="composition_mode" invisible="1"/>
|
||||
<field name="comment_single_recipient" invisible="1"/>
|
||||
<field name="res_id" invisible="1"/>
|
||||
<field name="res_ids" invisible="1"/>
|
||||
<field name="res_model" invisible="1"/>
|
||||
<field name="mass_force_send" invisible="1"/>
|
||||
<field name="recipient_single_valid" invisible="1"/>
|
||||
<field name="recipient_single_number" invisible="1"/>
|
||||
<field name="number_field_name" invisible="1"/>
|
||||
<field name="numbers" invisible="1"/>
|
||||
<field name="sanitized_numbers" invisible="1"/>
|
||||
|
||||
<label for="recipient_single_description" string="Recipient"
|
||||
class="fw-bold"
|
||||
attrs="{'invisible': [('comment_single_recipient', '=', False)]}"/>
|
||||
<div attrs="{'invisible': [('comment_single_recipient', '=', False)]}">
|
||||
<field name="recipient_single_description" class="oe_inline" attrs="{'invisible': [('recipient_single_description', '=', False)]}"/>
|
||||
<field name="recipient_single_number_itf" class="oe_inline" nolabel="1" onchange_on_keydown="True" placeholder="e.g. +1 415 555 0100"/>
|
||||
</div>
|
||||
<field name="body" widget="sms_widget" attrs="{'invisible': ['|', ('comment_single_recipient', '=', False), ('recipient_single_valid', '=', True)]}"/>
|
||||
<field name="body" widget="sms_widget" attrs="{'invisible': [('comment_single_recipient', '=', True), ('recipient_single_valid', '=', False)]}" default_focus="1"/>
|
||||
<field name="mass_keep_log" invisible="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<!-- attrs doesn't work for 'disabled'-->
|
||||
<button string="Send SMS" type="object" class="oe_highlight" name="action_send_sms" data-hotkey="q"
|
||||
attrs="{'invisible': ['|',('composition_mode', 'not in', ('comment', 'numbers')),('recipient_single_valid', '=', False)]}"/>
|
||||
<button string="Send SMS" type="object" class="oe_highlight" name="action_send_sms" data-hotkey="q"
|
||||
attrs="{'invisible': ['|',('composition_mode', 'not in', ('comment', 'numbers')),('recipient_single_valid', '=', True)]}" disabled='1'/>
|
||||
<button string="Put in queue" type="object" class="oe_highlight" name="action_send_sms" data-hotkey="q"
|
||||
attrs="{'invisible': [('composition_mode', '!=', 'mass')]}"/>
|
||||
<button string="Send Now" type="object" name="action_send_sms_mass_now" data-hotkey="w"
|
||||
attrs="{'invisible': [('composition_mode', '!=', 'mass')]}"/>
|
||||
<button string="Close" class="btn btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sms_composer_action_form" model="ir.actions.act_window">
|
||||
<field name="name">Send SMS Text Message</field>
|
||||
<field name="res_model">sms.composer</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
121
odoo-bringout-oca-ocb-sms/sms/wizard/sms_resend.py
Normal file
121
odoo-bringout-oca-ocb-sms/sms/wizard/sms_resend.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, exceptions, fields, models
|
||||
|
||||
|
||||
class SMSRecipient(models.TransientModel):
|
||||
_name = 'sms.resend.recipient'
|
||||
_description = 'Resend Notification'
|
||||
_rec_name = 'sms_resend_id'
|
||||
|
||||
sms_resend_id = fields.Many2one('sms.resend', required=True)
|
||||
notification_id = fields.Many2one('mail.notification', required=True, ondelete='cascade')
|
||||
resend = fields.Boolean(string='Try Again', default=True)
|
||||
failure_type = fields.Selection(
|
||||
related='notification_id.failure_type', string='Error Message', related_sudo=True, readonly=True)
|
||||
partner_id = fields.Many2one('res.partner', 'Partner', related='notification_id.res_partner_id', readonly=True)
|
||||
partner_name = fields.Char(string='Recipient Name', readonly='True')
|
||||
sms_number = fields.Char(string='Phone Number')
|
||||
|
||||
|
||||
class SMSResend(models.TransientModel):
|
||||
_name = 'sms.resend'
|
||||
_description = 'SMS Resend'
|
||||
_rec_name = 'mail_message_id'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
result = super(SMSResend, self).default_get(fields)
|
||||
if 'recipient_ids' in fields and result.get('mail_message_id'):
|
||||
mail_message_id = self.env['mail.message'].browse(result['mail_message_id'])
|
||||
result['recipient_ids'] = [(0, 0, {
|
||||
'notification_id': notif.id,
|
||||
'resend': True,
|
||||
'failure_type': notif.failure_type,
|
||||
'partner_name': notif.res_partner_id.display_name or mail_message_id.record_name,
|
||||
'sms_number': notif.sms_number,
|
||||
}) for notif in mail_message_id.notification_ids if notif.notification_type == 'sms' and notif.notification_status in ('exception', 'bounce')]
|
||||
return result
|
||||
|
||||
mail_message_id = fields.Many2one('mail.message', 'Message', readonly=True, required=True)
|
||||
recipient_ids = fields.One2many('sms.resend.recipient', 'sms_resend_id', string='Recipients')
|
||||
can_cancel = fields.Boolean(compute='_compute_can_cancel')
|
||||
can_resend = fields.Boolean(compute='_compute_can_resend')
|
||||
has_insufficient_credit = fields.Boolean(compute='_compute_has_insufficient_credit')
|
||||
has_unregistered_account = fields.Boolean(compute='_compute_has_unregistered_account')
|
||||
|
||||
@api.depends("recipient_ids.failure_type")
|
||||
def _compute_has_unregistered_account(self):
|
||||
self.has_unregistered_account = self.recipient_ids.filtered(lambda p: p.failure_type == 'sms_acc')
|
||||
|
||||
@api.depends("recipient_ids.failure_type")
|
||||
def _compute_has_insufficient_credit(self):
|
||||
self.has_insufficient_credit = self.recipient_ids.filtered(lambda p: p.failure_type == 'sms_credit')
|
||||
|
||||
@api.depends("recipient_ids.resend")
|
||||
def _compute_can_cancel(self):
|
||||
self.can_cancel = self.recipient_ids.filtered(lambda p: not p.resend)
|
||||
|
||||
@api.depends('recipient_ids.resend')
|
||||
def _compute_can_resend(self):
|
||||
self.can_resend = any([recipient.resend for recipient in self.recipient_ids])
|
||||
|
||||
def _check_access(self):
|
||||
if not self.mail_message_id or not self.mail_message_id.model or not self.mail_message_id.res_id:
|
||||
raise exceptions.UserError(_('You do not have access to the message and/or related document.'))
|
||||
record = self.env[self.mail_message_id.model].browse(self.mail_message_id.res_id)
|
||||
record.check_access_rights('read')
|
||||
record.check_access_rule('read')
|
||||
|
||||
def action_resend(self):
|
||||
self._check_access()
|
||||
|
||||
all_notifications = self.env['mail.notification'].sudo().search([
|
||||
('mail_message_id', '=', self.mail_message_id.id),
|
||||
('notification_type', '=', 'sms'),
|
||||
('notification_status', 'in', ('exception', 'bounce'))
|
||||
])
|
||||
sudo_self = self.sudo()
|
||||
to_cancel_ids = [r.notification_id.id for r in sudo_self.recipient_ids if not r.resend]
|
||||
to_resend_ids = [r.notification_id.id for r in sudo_self.recipient_ids if r.resend]
|
||||
|
||||
if to_cancel_ids:
|
||||
all_notifications.filtered(lambda n: n.id in to_cancel_ids).write({'notification_status': 'canceled'})
|
||||
|
||||
if to_resend_ids:
|
||||
record = self.env[self.mail_message_id.model].browse(self.mail_message_id.res_id)
|
||||
|
||||
sms_pid_to_number = dict((r.partner_id.id, r.sms_number) for r in self.recipient_ids if r.resend and r.partner_id)
|
||||
pids = list(sms_pid_to_number.keys())
|
||||
numbers = [r.sms_number for r in self.recipient_ids if r.resend and not r.partner_id]
|
||||
|
||||
recipients_data = []
|
||||
all_recipients_data = self.env['mail.followers']._get_recipient_data(record, 'sms', False, pids=pids)[record.id]
|
||||
for pid, pdata in all_recipients_data.items():
|
||||
if pid and pdata['notif'] == 'sms':
|
||||
recipients_data.append(pdata)
|
||||
if recipients_data or numbers:
|
||||
record._notify_thread_by_sms(
|
||||
self.mail_message_id, recipients_data,
|
||||
sms_numbers=numbers, sms_pid_to_number=sms_pid_to_number,
|
||||
resend_existing=True, put_in_queue=False
|
||||
)
|
||||
|
||||
self.mail_message_id._notify_message_notification_update()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def action_cancel(self):
|
||||
self._check_access()
|
||||
|
||||
sudo_self = self.sudo()
|
||||
sudo_self.mapped('recipient_ids.notification_id').write({'notification_status': 'canceled'})
|
||||
self.mail_message_id._notify_message_notification_update()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def action_buy_credits(self):
|
||||
url = self.env['iap.account'].get_credits_url(service_name='sms')
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': url,
|
||||
}
|
||||
46
odoo-bringout-oca-ocb-sms/sms/wizard/sms_resend_views.xml
Normal file
46
odoo-bringout-oca-ocb-sms/sms/wizard/sms_resend_views.xml
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data>
|
||||
<record id="mail_resend_message_view_form" model="ir.ui.view">
|
||||
<field name="name">sms.resend.form</field>
|
||||
<field name="model">sms.resend</field>
|
||||
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Edit Partners">
|
||||
<field name="mail_message_id" invisible="1"/>
|
||||
<field name="can_resend" invisible="1"/>
|
||||
<field name="has_insufficient_credit" invisible="1"/>
|
||||
<field name="has_unregistered_account" invisible="1"/>
|
||||
<field name="recipient_ids">
|
||||
<tree string="Recipient" editable="top" create="0" delete="0">
|
||||
<field name="partner_name"/>
|
||||
<field name="sms_number"/>
|
||||
<field name="failure_type" string="Reason" class="text-wrap"/>
|
||||
<field name="resend" widget="boolean_toggle"/>
|
||||
<field name="notification_id" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
<footer>
|
||||
<button string="Buy credits" name="action_buy_credits" type="object" class="btn-primary o_mail_send"
|
||||
attrs="{'invisible': [('has_insufficient_credit', '=', False)]}" data-hotkey="q"/>
|
||||
<button string="Set up an account" name="action_buy_credits" type="object" class="btn-primary o_mail_send"
|
||||
attrs="{'invisible': [('has_unregistered_account', '=', False)]}" data-hotkey="q"/>
|
||||
<button string="Send & Close" name="action_resend" type="object" class="btn-primary o_mail_send"
|
||||
attrs="{'invisible': ['|', ('has_unregistered_account', '=', False), ('can_resend', '=', False)]}" data-hotkey="w"/>
|
||||
<button string="Ignore all" name="action_cancel" type="object" class="btn-primary"
|
||||
attrs="{'invisible': ['|', '|', ('has_insufficient_credit', '=', True), ('has_unregistered_account', '=', True), '&', ('has_unregistered_account', '=', True), ('can_resend', '=', True)]}" data-hotkey="x"/>
|
||||
<button string="Ignore all" name="action_cancel" type="object" class="btn-secondary"
|
||||
attrs="{'invisible': ['!', '|', '|', ('has_insufficient_credit', '=', True), ('has_unregistered_account', '=', True), '&', ('has_unregistered_account', '=', True), ('can_resend', '=', True)]}" data-hotkey="x"/>
|
||||
<button string="Close" class="btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sms_resend_action" model="ir.actions.act_window">
|
||||
<field name="name">Sending Failures</field>
|
||||
<field name="res_model">sms.resend</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</data></odoo>
|
||||
49
odoo-bringout-oca-ocb-sms/sms/wizard/sms_template_preview.py
Normal file
49
odoo-bringout-oca-ocb-sms/sms/wizard/sms_template_preview.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SMSTemplatePreview(models.TransientModel):
|
||||
_name = "sms.template.preview"
|
||||
_description = "SMS Template Preview"
|
||||
|
||||
@api.model
|
||||
def _selection_target_model(self):
|
||||
return [(model.model, model.name) for model in self.env['ir.model'].sudo().search([])]
|
||||
|
||||
@api.model
|
||||
def _selection_languages(self):
|
||||
return self.env['res.lang'].get_installed()
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
result = super(SMSTemplatePreview, self).default_get(fields)
|
||||
sms_template_id = self.env.context.get('default_sms_template_id')
|
||||
if not sms_template_id or 'resource_ref' not in fields:
|
||||
return result
|
||||
sms_template = self.env['sms.template'].browse(sms_template_id)
|
||||
res = self.env[sms_template.model_id.model].search([], limit=1)
|
||||
if res:
|
||||
result['resource_ref'] = '%s,%s' % (sms_template.model_id.model, res.id)
|
||||
return result
|
||||
|
||||
sms_template_id = fields.Many2one('sms.template', required=True, ondelete='cascade')
|
||||
lang = fields.Selection(_selection_languages, string='Template Preview Language')
|
||||
model_id = fields.Many2one('ir.model', related="sms_template_id.model_id")
|
||||
body = fields.Char('Body', compute='_compute_sms_template_fields')
|
||||
resource_ref = fields.Reference(string='Record reference', selection='_selection_target_model')
|
||||
no_record = fields.Boolean('No Record', compute='_compute_no_record')
|
||||
|
||||
@api.depends('model_id')
|
||||
def _compute_no_record(self):
|
||||
for preview in self:
|
||||
preview.no_record = (self.env[preview.model_id.model].search_count([]) == 0) if preview.model_id else True
|
||||
|
||||
@api.depends('lang', 'resource_ref')
|
||||
def _compute_sms_template_fields(self):
|
||||
for wizard in self:
|
||||
if wizard.sms_template_id and wizard.resource_ref:
|
||||
wizard.body = wizard.sms_template_id._render_field('body', [wizard.resource_ref.id], set_lang=wizard.lang)[wizard.resource_ref.id]
|
||||
else:
|
||||
wizard.body = wizard.sms_template_id.body
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- SMS Template Preview -->
|
||||
<record model="ir.ui.view" id="sms_template_preview_form">
|
||||
<field name="name">sms.template.preview.form</field>
|
||||
<field name="model">sms.template.preview</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="SMS Preview">
|
||||
<h3>Preview of <field name="sms_template_id" readonly="1" nolabel="1" class="oe_inline"/></h3>
|
||||
<field name="no_record" invisible="1"/>
|
||||
<div class="o_row">
|
||||
<span>Choose an example <field name="model_id" readonly="1"/> record:</span>
|
||||
<div>
|
||||
<field name="resource_ref" class="oe_inline" options="{'hide_model': True, 'no_create': True, 'no_open': True}" attrs="{'invisible': [('no_record', '=', True)]}"/>
|
||||
<span class="text-warning" attrs="{'invisible': [('no_record', '=', False)]}">No records</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>Choose a language: <field name="lang" class="oe_inline ml8"/></p>
|
||||
<label for="body" string="SMS content"/>
|
||||
<hr/>
|
||||
<field name="body" readonly="1" nolabel="1" options='{"safe": True}'/>
|
||||
<hr/>
|
||||
<footer>
|
||||
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sms_template_preview_action" model="ir.actions.act_window">
|
||||
<field name="name">Template Preview</field>
|
||||
<field name="res_model">sms.template.preview</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="sms_template_preview_form"/>
|
||||
<field name="target">new</field>
|
||||
<field name="context">{'default_sms_template_id':active_id}</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
29
odoo-bringout-oca-ocb-sms/sms/wizard/sms_template_reset.py
Normal file
29
odoo-bringout-oca-ocb-sms/sms/wizard/sms_template_reset.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
|
||||
|
||||
class SMSTemplateReset(models.TransientModel):
|
||||
_name = 'sms.template.reset'
|
||||
_description = 'SMS Template Reset'
|
||||
|
||||
template_ids = fields.Many2many('sms.template')
|
||||
|
||||
def reset_template(self):
|
||||
if not self.template_ids:
|
||||
return False
|
||||
self.template_ids.reset_template()
|
||||
if self.env.context.get('params', {}).get('view_type') == 'list':
|
||||
next_action = {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
else:
|
||||
next_action = {'type': 'ir.actions.act_window_close'}
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'type': 'success',
|
||||
'message': _('SMS Templates have been reset'),
|
||||
'next': next_action,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="sms_template_reset_view_form" model="ir.ui.view">
|
||||
<field name="name">sms.template.reset.view.form</field>
|
||||
<field name="model">sms.template.reset</field>
|
||||
<field name="priority">1000</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<div>
|
||||
Are you sure you want to reset these sms templates to their original configuration? Changes and translations will be lost.
|
||||
</div>
|
||||
<footer>
|
||||
<button string="Proceed" class="btn btn-primary" type="object" name="reset_template" data-hotkey="q"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sms_template_reset_action" model="ir.actions.act_window">
|
||||
<field name="name">Reset SMS Template</field>
|
||||
<field name="res_model">sms.template.reset</field>
|
||||
<field name="binding_model_id" ref="sms.model_sms_template"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="context">{
|
||||
'default_template_ids': active_ids
|
||||
}</field>
|
||||
<field name="view_id" ref="sms_template_reset_view_form"/>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Add table
Add a link
Reference in a new issue