File: sms_composer.py

package info (click to toggle)
odoo 18.0.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 878,716 kB
  • sloc: javascript: 927,937; python: 685,670; xml: 388,524; sh: 1,033; sql: 415; makefile: 26
file content (375 lines) | stat: -rw-r--r-- 18,773 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from ast import literal_eval
from uuid import uuid4

from odoo import api, fields, models, _
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_non_stored', compute_sudo=False)
    recipient_single_number = fields.Char('Stored Recipient Number', compute='_compute_recipient_single_non_stored', compute_sudo=False)
    recipient_single_number_itf = fields.Char(
        'Recipient Number', compute='_compute_recipient_single_stored',
        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 and isinstance(records, self.pool['mail.thread']):
                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_stored(self):
        for composer in self:
            records = composer._get_records()
            if not records or not isinstance(records, self.pool['mail.thread']) or not composer.comment_single_recipient:
                composer.recipient_single_number_itf = ''
                continue
            records.ensure_one()
            res = records._sms_get_recipients_info(force_field=composer.number_field_name, partner_fallback=False)
            if not composer.recipient_single_number_itf:
                composer.recipient_single_number_itf = res[records.id]['number'] or ''
            if not composer.number_field_name:
                composer.number_field_name = res[records.id]['field_store']

    @api.depends('res_model', 'number_field_name')
    def _compute_recipient_single_non_stored(self):
        for composer in self:
            records = composer._get_records()
            if not records or not isinstance(records, self.pool['mail.thread']) or not composer.comment_single_recipient:
                composer.recipient_single_description = False
                composer.recipient_single_number = ''
                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._mail_get_partners()[records[0].id].display_name
            composer.recipient_single_number = res[records.id]['number'] or ''

    @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()
                composer.recipient_single_valid = bool(records._phone_format(number=value)) if len(records) == 1 else False
            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(',')]
                sanitized_numbers = [record._phone_format(number=number) for number in numbers]
                invalid_numbers = [number for sanitized, number in zip(sanitized_numbers, numbers) if not sanitized]
                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):
        sms_values = [{'body': self.body, 'number': number} for number in self.sanitized_numbers.split(',')]
        self.env['sms.sms'].sudo().create(sms_values).send()
        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],
                'failure_type': failure_type,
                'number': sanitized if sanitized else recipients['number'],
                'partner_id': recipients['partner'].id,
                'state': state,
                'uuid': uuid4().hex,
            }
        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