mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-21 04:02:00 +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/__init__.py
Normal file
6
odoo-bringout-oca-ocb-sms/sms/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
from . import tools
|
||||
from . import wizard
|
||||
59
odoo-bringout-oca-ocb-sms/sms/__manifest__.py
Normal file
59
odoo-bringout-oca-ocb-sms/sms/__manifest__.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'SMS gateway',
|
||||
'version': '2.4',
|
||||
'category': 'Hidden/Tools',
|
||||
'summary': 'SMS Text Messaging',
|
||||
'description': """
|
||||
This module gives a framework for SMS text messaging
|
||||
----------------------------------------------------
|
||||
|
||||
The service is provided by the In App Purchase Odoo platform.
|
||||
""",
|
||||
'depends': [
|
||||
'base',
|
||||
'iap_mail',
|
||||
'mail',
|
||||
'phone_validation'
|
||||
],
|
||||
'data': [
|
||||
'data/ir_cron_data.xml',
|
||||
'wizard/sms_composer_views.xml',
|
||||
'wizard/sms_template_preview_views.xml',
|
||||
'wizard/sms_resend_views.xml',
|
||||
'wizard/sms_template_reset_views.xml',
|
||||
'views/ir_actions_server_views.xml',
|
||||
'views/mail_notification_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/res_partner_views.xml',
|
||||
'views/sms_sms_views.xml',
|
||||
'views/sms_template_views.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/sms_security.xml',
|
||||
],
|
||||
'demo': [
|
||||
'data/sms_demo.xml',
|
||||
'data/mail_demo.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
'assets': {
|
||||
'mail.assets_messaging': [
|
||||
'sms/static/src/models/*.js',
|
||||
],
|
||||
'mail.assets_discuss_public': [
|
||||
'sms/static/src/components/sms_button/*',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
'sms/static/src/js/fields_phone_widget.js',
|
||||
'sms/static/src/components/*/*',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'sms/static/tests/sms_widget_test.js',
|
||||
'sms/static/tests/qunit_suite_tests/**/*.js',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
14
odoo-bringout-oca-ocb-sms/sms/data/ir_cron_data.xml
Normal file
14
odoo-bringout-oca-ocb-sms/sms/data/ir_cron_data.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
<record forcecreate="True" id="ir_cron_sms_scheduler_action" model="ir.cron">
|
||||
<field name="name">SMS: SMS Queue Manager</field>
|
||||
<field name="model_id" ref="model_sms_sms"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._process_queue()</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field eval="False" name="doall"/>
|
||||
</record>
|
||||
</data></odoo>
|
||||
90
odoo-bringout-oca-ocb-sms/sms/data/mail_demo.xml
Normal file
90
odoo-bringout-oca-ocb-sms/sms/data/mail_demo.xml
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
<record id="message_demo_partner_1_0" model="mail.message">
|
||||
<field name="model">res.partner</field>
|
||||
<field name="res_id" ref="base.res_partner_address_28"/>
|
||||
<field name="body" type="html"><p>Hello! This is an example of incoming email.</p></field>
|
||||
<field name="message_type">email</field>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(days=5)).strftime('%Y-%m-%d %H:%M:00')"/>
|
||||
</record>
|
||||
<record id="message_demo_partner_1_1" model="mail.message">
|
||||
<field name="model">res.partner</field>
|
||||
<field name="res_id" ref="base.res_partner_address_28"/>
|
||||
<field name="body" type="html"><p>Hello! This is an example of user comment.</p></field>
|
||||
<field name="message_type">comment</field>
|
||||
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(days=4)).strftime('%Y-%m-%d %H:%M:00')"/>
|
||||
</record>
|
||||
<record id="message_demo_partner_1_2_notif_0" model="mail.notification">
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="mail_message_id" ref="message_demo_partner_1_1"/>
|
||||
<field name="res_partner_id" ref="base.res_partner_address_28"/>
|
||||
<field name="notification_type">email</field>
|
||||
<field name="notification_status">exception</field>
|
||||
<field name="failure_type">mail_smtp</field>
|
||||
</record>
|
||||
|
||||
<record id="message_demo_partner_1_2" model="mail.message">
|
||||
<field name="model">res.partner</field>
|
||||
<field name="res_id" ref="base.res_partner_address_28"/>
|
||||
<field name="body" type="html"><p>Hello! This is an example of SMS.</p></field>
|
||||
<field name="message_type">sms</field>
|
||||
<field name="subtype_id" ref="mail.mt_note"/>
|
||||
<field name="author_id" ref="base.partner_demo"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(days=3)).strftime('%Y-%m-%d %H:%M:00')"/>
|
||||
</record>
|
||||
<record id="message_demo_partner_1_3" model="mail.message">
|
||||
<field name="model">res.partner</field>
|
||||
<field name="res_id" ref="base.res_partner_address_28"/>
|
||||
<field name="body" type="html"><p>Hello! This is an example of another SMS with notifications and an unregistered account.</p></field>
|
||||
<field name="message_type">sms</field>
|
||||
<field name="subtype_id" ref="mail.mt_note"/>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(days=2)).strftime('%Y-%m-%d %H:%M:00')"/>
|
||||
</record>
|
||||
<record id="message_demo_partner_1_3_notif_0" model="mail.notification">
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="mail_message_id" ref="message_demo_partner_1_3"/>
|
||||
<field name="res_partner_id" ref="base.res_partner_address_28"/>
|
||||
<field name="notification_type">sms</field>
|
||||
<field name="notification_status">exception</field>
|
||||
<field name="failure_type">sms_acc</field>
|
||||
</record>
|
||||
<record id="message_demo_partner_1_4" model="mail.message">
|
||||
<field name="model">res.partner</field>
|
||||
<field name="res_id" ref="base.res_partner_address_28"/>
|
||||
<field name="body" type="html"><p>Hello! This is an example of a sent SMS with notifications.</p></field>
|
||||
<field name="message_type">sms</field>
|
||||
<field name="subtype_id" ref="mail.mt_note"/>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(days=1,hours=22)).strftime('%Y-%m-%d %H:%M:00')"/>
|
||||
</record>
|
||||
<record id="message_demo_partner_1_4_notif_0" model="mail.notification">
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="mail_message_id" ref="message_demo_partner_1_4"/>
|
||||
<field name="res_partner_id" ref="base.res_partner_address_28"/>
|
||||
<field name="notification_type">sms</field>
|
||||
<field name="notification_status">sent</field>
|
||||
</record>
|
||||
<record id="message_demo_partner_1_5" model="mail.message">
|
||||
<field name="model">res.partner</field>
|
||||
<field name="res_id" ref="base.res_partner_address_16"/>
|
||||
<field name="body" type="html"><p>Hello! This is an example of another SMS with notifications without credits.</p></field>
|
||||
<field name="message_type">sms</field>
|
||||
<field name="subtype_id" ref="mail.mt_note"/>
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="date" eval="(DateTime.today() - timedelta(days=1)).strftime('%Y-%m-%d %H:%M:00')"/>
|
||||
</record>
|
||||
<record id="message_demo_partner_1_5_notif_0" model="mail.notification">
|
||||
<field name="author_id" ref="base.partner_admin"/>
|
||||
<field name="mail_message_id" ref="message_demo_partner_1_5"/>
|
||||
<field name="res_partner_id" ref="base.res_partner_address_16"/>
|
||||
<field name="notification_type">sms</field>
|
||||
<field name="notification_status">exception</field>
|
||||
<field name="failure_type">sms_credit</field>
|
||||
</record>
|
||||
|
||||
</data></odoo>
|
||||
8
odoo-bringout-oca-ocb-sms/sms/data/sms_demo.xml
Normal file
8
odoo-bringout-oca-ocb-sms/sms/data/sms_demo.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
<record id="sms_template_demo_0" model="sms.template">
|
||||
<field name="name">Customer: automated SMS</field>
|
||||
<field name="model_id" ref="base.model_res_partner"/>
|
||||
<field name="body">Dear {{ object.display_name }} this is an automated SMS.</field>
|
||||
</record>
|
||||
</data></odoo>
|
||||
1340
odoo-bringout-oca-ocb-sms/sms/i18n/af.po
Normal file
1340
odoo-bringout-oca-ocb-sms/sms/i18n/af.po
Normal file
File diff suppressed because it is too large
Load diff
1336
odoo-bringout-oca-ocb-sms/sms/i18n/am.po
Normal file
1336
odoo-bringout-oca-ocb-sms/sms/i18n/am.po
Normal file
File diff suppressed because it is too large
Load diff
1413
odoo-bringout-oca-ocb-sms/sms/i18n/ar.po
Normal file
1413
odoo-bringout-oca-ocb-sms/sms/i18n/ar.po
Normal file
File diff suppressed because it is too large
Load diff
1372
odoo-bringout-oca-ocb-sms/sms/i18n/az.po
Normal file
1372
odoo-bringout-oca-ocb-sms/sms/i18n/az.po
Normal file
File diff suppressed because it is too large
Load diff
1352
odoo-bringout-oca-ocb-sms/sms/i18n/be.po
Normal file
1352
odoo-bringout-oca-ocb-sms/sms/i18n/be.po
Normal file
File diff suppressed because it is too large
Load diff
1389
odoo-bringout-oca-ocb-sms/sms/i18n/bg.po
Normal file
1389
odoo-bringout-oca-ocb-sms/sms/i18n/bg.po
Normal file
File diff suppressed because it is too large
Load diff
1348
odoo-bringout-oca-ocb-sms/sms/i18n/bs.po
Normal file
1348
odoo-bringout-oca-ocb-sms/sms/i18n/bs.po
Normal file
File diff suppressed because it is too large
Load diff
1427
odoo-bringout-oca-ocb-sms/sms/i18n/ca.po
Normal file
1427
odoo-bringout-oca-ocb-sms/sms/i18n/ca.po
Normal file
File diff suppressed because it is too large
Load diff
1390
odoo-bringout-oca-ocb-sms/sms/i18n/cs.po
Normal file
1390
odoo-bringout-oca-ocb-sms/sms/i18n/cs.po
Normal file
File diff suppressed because it is too large
Load diff
1386
odoo-bringout-oca-ocb-sms/sms/i18n/da.po
Normal file
1386
odoo-bringout-oca-ocb-sms/sms/i18n/da.po
Normal file
File diff suppressed because it is too large
Load diff
1422
odoo-bringout-oca-ocb-sms/sms/i18n/de.po
Normal file
1422
odoo-bringout-oca-ocb-sms/sms/i18n/de.po
Normal file
File diff suppressed because it is too large
Load diff
144
odoo-bringout-oca-ocb-sms/sms/i18n/el.po
Normal file
144
odoo-bringout-oca-ocb-sms/sms/i18n/el.po
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * sms
|
||||
#
|
||||
# Translators:
|
||||
# Martin Trigaux, 2018
|
||||
# George Tarasidis <george_tarasidis@yahoo.com>, 2018
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server saas~11.5\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-09-21 13:17+0000\n"
|
||||
"PO-Revision-Date: 2018-09-21 13:17+0000\n"
|
||||
"Last-Translator: George Tarasidis <george_tarasidis@yahoo.com>, 2018\n"
|
||||
"Language-Team: Greek (https://www.transifex.com/odoo/teams/41243/el/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: el\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: sms
|
||||
#. openerp-web
|
||||
#: code:addons/sms/static/src/js/sms_widget.js:93
|
||||
#, python-format
|
||||
msgid "%s chars, fits in %s SMS (%s) "
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: model_terms:ir.ui.view,arch_db:sms.send_sms_view_form
|
||||
msgid "Cancel"
|
||||
msgstr "Ακύρωση"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model,name:sms.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr "Επαφή"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms__create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Δημιουργήθηκε από"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms__create_date
|
||||
msgid "Created on"
|
||||
msgstr "Δημιουργήθηκε στις"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_api__display_name
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms__display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Εμφάνιση Ονόματος"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model,name:sms.model_mail_thread
|
||||
msgid "Email Thread"
|
||||
msgstr "Νήμα Email"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_api__id
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms__id
|
||||
msgid "ID"
|
||||
msgstr "Κωδικός"
|
||||
|
||||
#. module: sms
|
||||
#: code:addons/sms/models/mail_thread.py:53
|
||||
#, python-format
|
||||
msgid "Insufficient credit, unable to send SMS message: %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_api____last_update
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "Τελευταία τροποποίηση στις"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Τελευταία Ενημέρωση από"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Τελευταία Ενημέρωση στις"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms__message
|
||||
msgid "Message"
|
||||
msgstr "Μήνυμα"
|
||||
|
||||
#. module: sms
|
||||
#: code:addons/sms/wizard/send_sms.py:82
|
||||
#, python-format
|
||||
msgid "Missing mobile number for %s."
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: code:addons/sms/models/mail_thread.py:55
|
||||
#, python-format
|
||||
msgid "No mobile number defined, unable to send SMS message: %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms__recipients
|
||||
msgid "Recipients"
|
||||
msgstr "Αποδέκτες"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model,name:sms.model_sms_api
|
||||
msgid "SMS API"
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#. openerp-web
|
||||
#: code:addons/sms/static/src/xml/sms_widget.xml:4
|
||||
#, python-format
|
||||
msgid "SMS Pricing"
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: code:addons/sms/models/mail_thread.py:48
|
||||
#, python-format
|
||||
msgid "SMS message sent: %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: model_terms:ir.ui.view,arch_db:sms.send_sms_view_form
|
||||
msgid "Send"
|
||||
msgstr "Αποστολή"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.actions.act_window,name:sms.send_sms_action
|
||||
#: model:ir.actions.act_window,name:sms.send_sms_form_action
|
||||
#: model:ir.model,name:sms.model_sms_send_sms
|
||||
#: model_terms:ir.ui.view,arch_db:sms.partner_form_send_sms_form_view
|
||||
msgid "Send SMS"
|
||||
msgstr "Αποστολή SMS"
|
||||
|
||||
#. module: sms
|
||||
#: model_terms:ir.ui.view,arch_db:sms.send_sms_view_form
|
||||
msgid "Send an SMS"
|
||||
msgstr "Αποστολή ενός SMS"
|
||||
1417
odoo-bringout-oca-ocb-sms/sms/i18n/es.po
Normal file
1417
odoo-bringout-oca-ocb-sms/sms/i18n/es.po
Normal file
File diff suppressed because it is too large
Load diff
1419
odoo-bringout-oca-ocb-sms/sms/i18n/es_MX.po
Normal file
1419
odoo-bringout-oca-ocb-sms/sms/i18n/es_MX.po
Normal file
File diff suppressed because it is too large
Load diff
1420
odoo-bringout-oca-ocb-sms/sms/i18n/et.po
Normal file
1420
odoo-bringout-oca-ocb-sms/sms/i18n/et.po
Normal file
File diff suppressed because it is too large
Load diff
1414
odoo-bringout-oca-ocb-sms/sms/i18n/fa.po
Normal file
1414
odoo-bringout-oca-ocb-sms/sms/i18n/fa.po
Normal file
File diff suppressed because it is too large
Load diff
1425
odoo-bringout-oca-ocb-sms/sms/i18n/fi.po
Normal file
1425
odoo-bringout-oca-ocb-sms/sms/i18n/fi.po
Normal file
File diff suppressed because it is too large
Load diff
1416
odoo-bringout-oca-ocb-sms/sms/i18n/fr.po
Normal file
1416
odoo-bringout-oca-ocb-sms/sms/i18n/fr.po
Normal file
File diff suppressed because it is too large
Load diff
1358
odoo-bringout-oca-ocb-sms/sms/i18n/gu.po
Normal file
1358
odoo-bringout-oca-ocb-sms/sms/i18n/gu.po
Normal file
File diff suppressed because it is too large
Load diff
1382
odoo-bringout-oca-ocb-sms/sms/i18n/he.po
Normal file
1382
odoo-bringout-oca-ocb-sms/sms/i18n/he.po
Normal file
File diff suppressed because it is too large
Load diff
1360
odoo-bringout-oca-ocb-sms/sms/i18n/hi.po
Normal file
1360
odoo-bringout-oca-ocb-sms/sms/i18n/hi.po
Normal file
File diff suppressed because it is too large
Load diff
1374
odoo-bringout-oca-ocb-sms/sms/i18n/hr.po
Normal file
1374
odoo-bringout-oca-ocb-sms/sms/i18n/hr.po
Normal file
File diff suppressed because it is too large
Load diff
1370
odoo-bringout-oca-ocb-sms/sms/i18n/hu.po
Normal file
1370
odoo-bringout-oca-ocb-sms/sms/i18n/hu.po
Normal file
File diff suppressed because it is too large
Load diff
1336
odoo-bringout-oca-ocb-sms/sms/i18n/hy.po
Normal file
1336
odoo-bringout-oca-ocb-sms/sms/i18n/hy.po
Normal file
File diff suppressed because it is too large
Load diff
1408
odoo-bringout-oca-ocb-sms/sms/i18n/id.po
Normal file
1408
odoo-bringout-oca-ocb-sms/sms/i18n/id.po
Normal file
File diff suppressed because it is too large
Load diff
1355
odoo-bringout-oca-ocb-sms/sms/i18n/is.po
Normal file
1355
odoo-bringout-oca-ocb-sms/sms/i18n/is.po
Normal file
File diff suppressed because it is too large
Load diff
1415
odoo-bringout-oca-ocb-sms/sms/i18n/it.po
Normal file
1415
odoo-bringout-oca-ocb-sms/sms/i18n/it.po
Normal file
File diff suppressed because it is too large
Load diff
1379
odoo-bringout-oca-ocb-sms/sms/i18n/ja.po
Normal file
1379
odoo-bringout-oca-ocb-sms/sms/i18n/ja.po
Normal file
File diff suppressed because it is too large
Load diff
1359
odoo-bringout-oca-ocb-sms/sms/i18n/km.po
Normal file
1359
odoo-bringout-oca-ocb-sms/sms/i18n/km.po
Normal file
File diff suppressed because it is too large
Load diff
1385
odoo-bringout-oca-ocb-sms/sms/i18n/ko.po
Normal file
1385
odoo-bringout-oca-ocb-sms/sms/i18n/ko.po
Normal file
File diff suppressed because it is too large
Load diff
1084
odoo-bringout-oca-ocb-sms/sms/i18n/lb.po
Normal file
1084
odoo-bringout-oca-ocb-sms/sms/i18n/lb.po
Normal file
File diff suppressed because it is too large
Load diff
1343
odoo-bringout-oca-ocb-sms/sms/i18n/lo.po
Normal file
1343
odoo-bringout-oca-ocb-sms/sms/i18n/lo.po
Normal file
File diff suppressed because it is too large
Load diff
1374
odoo-bringout-oca-ocb-sms/sms/i18n/lt.po
Normal file
1374
odoo-bringout-oca-ocb-sms/sms/i18n/lt.po
Normal file
File diff suppressed because it is too large
Load diff
1382
odoo-bringout-oca-ocb-sms/sms/i18n/lv.po
Normal file
1382
odoo-bringout-oca-ocb-sms/sms/i18n/lv.po
Normal file
File diff suppressed because it is too large
Load diff
1345
odoo-bringout-oca-ocb-sms/sms/i18n/ml.po
Normal file
1345
odoo-bringout-oca-ocb-sms/sms/i18n/ml.po
Normal file
File diff suppressed because it is too large
Load diff
1366
odoo-bringout-oca-ocb-sms/sms/i18n/mn.po
Normal file
1366
odoo-bringout-oca-ocb-sms/sms/i18n/mn.po
Normal file
File diff suppressed because it is too large
Load diff
1360
odoo-bringout-oca-ocb-sms/sms/i18n/ms.po
Normal file
1360
odoo-bringout-oca-ocb-sms/sms/i18n/ms.po
Normal file
File diff suppressed because it is too large
Load diff
1366
odoo-bringout-oca-ocb-sms/sms/i18n/nb.po
Normal file
1366
odoo-bringout-oca-ocb-sms/sms/i18n/nb.po
Normal file
File diff suppressed because it is too large
Load diff
1412
odoo-bringout-oca-ocb-sms/sms/i18n/nl.po
Normal file
1412
odoo-bringout-oca-ocb-sms/sms/i18n/nl.po
Normal file
File diff suppressed because it is too large
Load diff
1340
odoo-bringout-oca-ocb-sms/sms/i18n/no.po
Normal file
1340
odoo-bringout-oca-ocb-sms/sms/i18n/no.po
Normal file
File diff suppressed because it is too large
Load diff
1429
odoo-bringout-oca-ocb-sms/sms/i18n/pl.po
Normal file
1429
odoo-bringout-oca-ocb-sms/sms/i18n/pl.po
Normal file
File diff suppressed because it is too large
Load diff
1373
odoo-bringout-oca-ocb-sms/sms/i18n/pt.po
Normal file
1373
odoo-bringout-oca-ocb-sms/sms/i18n/pt.po
Normal file
File diff suppressed because it is too large
Load diff
1418
odoo-bringout-oca-ocb-sms/sms/i18n/pt_BR.po
Normal file
1418
odoo-bringout-oca-ocb-sms/sms/i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load diff
1415
odoo-bringout-oca-ocb-sms/sms/i18n/ro.po
Normal file
1415
odoo-bringout-oca-ocb-sms/sms/i18n/ro.po
Normal file
File diff suppressed because it is too large
Load diff
1406
odoo-bringout-oca-ocb-sms/sms/i18n/ru.po
Normal file
1406
odoo-bringout-oca-ocb-sms/sms/i18n/ru.po
Normal file
File diff suppressed because it is too large
Load diff
1379
odoo-bringout-oca-ocb-sms/sms/i18n/sk.po
Normal file
1379
odoo-bringout-oca-ocb-sms/sms/i18n/sk.po
Normal file
File diff suppressed because it is too large
Load diff
1386
odoo-bringout-oca-ocb-sms/sms/i18n/sl.po
Normal file
1386
odoo-bringout-oca-ocb-sms/sms/i18n/sl.po
Normal file
File diff suppressed because it is too large
Load diff
1348
odoo-bringout-oca-ocb-sms/sms/i18n/sms.pot
Normal file
1348
odoo-bringout-oca-ocb-sms/sms/i18n/sms.pot
Normal file
File diff suppressed because it is too large
Load diff
1336
odoo-bringout-oca-ocb-sms/sms/i18n/sq.po
Normal file
1336
odoo-bringout-oca-ocb-sms/sms/i18n/sq.po
Normal file
File diff suppressed because it is too large
Load diff
1395
odoo-bringout-oca-ocb-sms/sms/i18n/sr.po
Normal file
1395
odoo-bringout-oca-ocb-sms/sms/i18n/sr.po
Normal file
File diff suppressed because it is too large
Load diff
134
odoo-bringout-oca-ocb-sms/sms/i18n/sr@latin.po
Normal file
134
odoo-bringout-oca-ocb-sms/sms/i18n/sr@latin.po
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * sms
|
||||
#
|
||||
# Translators:
|
||||
# Djordje Marjanovic <djordje_m@yahoo.com>, 2017
|
||||
# Ljubisa Jovev <ljubisa.jovev@gmail.com>, 2017
|
||||
# Martin Trigaux <mat@odoo.com>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 11.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-10-10 11:35+0000\n"
|
||||
"PO-Revision-Date: 2017-10-10 11:35+0000\n"
|
||||
"Last-Translator: Martin Trigaux <mat@odoo.com>, 2017\n"
|
||||
"Language-Team: Serbian (Latin) (https://www.transifex.com/odoo/teams/41243/sr%40latin/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Language: sr@latin\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
|
||||
#. module: sms
|
||||
#: model_terms:ir.ui.view,arch_db:sms.send_sms_view_form
|
||||
msgid "Cancel"
|
||||
msgstr "Odustani"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model,name:sms.model_res_partner
|
||||
msgid "Contact"
|
||||
msgstr "Kontakt"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms_create_uid
|
||||
msgid "Created by"
|
||||
msgstr "Kreirao"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms_create_date
|
||||
msgid "Created on"
|
||||
msgstr "Datum kreiranja"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_api_display_name
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms_display_name
|
||||
msgid "Display Name"
|
||||
msgstr "Naziv za prikaz"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model,name:sms.model_mail_thread
|
||||
msgid "Email Thread"
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_api_id
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms_id
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
#. module: sms
|
||||
#: code:addons/sms/models/mail_thread.py:53
|
||||
#, python-format
|
||||
msgid "Insufficient credit, unable to send SMS message: %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_api___last_update
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms___last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr "Zadnja promena"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms_write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr "Promenio"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms_write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr "Vreme promene"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms_message
|
||||
msgid "Message"
|
||||
msgstr "Poruka"
|
||||
|
||||
#. module: sms
|
||||
#: code:addons/sms/wizard/send_sms.py:48
|
||||
#, python-format
|
||||
msgid "Missing mobile number for %s."
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: code:addons/sms/models/mail_thread.py:55
|
||||
#, python-format
|
||||
msgid "No mobile number defined, unable to send SMS message: %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model.fields,field_description:sms.field_sms_send_sms_recipients
|
||||
msgid "Recipients"
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: code:addons/sms/models/mail_thread.py:48
|
||||
#, python-format
|
||||
msgid "SMS message sent: %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: model_terms:ir.ui.view,arch_db:sms.send_sms_view_form
|
||||
msgid "Send"
|
||||
msgstr "Pošalji"
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.actions.act_window,name:sms.send_sms_action
|
||||
#: model:ir.actions.act_window,name:sms.send_sms_form_action
|
||||
msgid "Send SMS"
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: model_terms:ir.ui.view,arch_db:sms.send_sms_view_form
|
||||
msgid "Send an SMS"
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model,name:sms.model_sms_api
|
||||
msgid "sms.api"
|
||||
msgstr ""
|
||||
|
||||
#. module: sms
|
||||
#: model:ir.model,name:sms.model_sms_send_sms
|
||||
msgid "sms.send_sms"
|
||||
msgstr ""
|
||||
1416
odoo-bringout-oca-ocb-sms/sms/i18n/sv.po
Normal file
1416
odoo-bringout-oca-ocb-sms/sms/i18n/sv.po
Normal file
File diff suppressed because it is too large
Load diff
1336
odoo-bringout-oca-ocb-sms/sms/i18n/sw.po
Normal file
1336
odoo-bringout-oca-ocb-sms/sms/i18n/sw.po
Normal file
File diff suppressed because it is too large
Load diff
1336
odoo-bringout-oca-ocb-sms/sms/i18n/ta.po
Normal file
1336
odoo-bringout-oca-ocb-sms/sms/i18n/ta.po
Normal file
File diff suppressed because it is too large
Load diff
1397
odoo-bringout-oca-ocb-sms/sms/i18n/th.po
Normal file
1397
odoo-bringout-oca-ocb-sms/sms/i18n/th.po
Normal file
File diff suppressed because it is too large
Load diff
1421
odoo-bringout-oca-ocb-sms/sms/i18n/tr.po
Normal file
1421
odoo-bringout-oca-ocb-sms/sms/i18n/tr.po
Normal file
File diff suppressed because it is too large
Load diff
1409
odoo-bringout-oca-ocb-sms/sms/i18n/uk.po
Normal file
1409
odoo-bringout-oca-ocb-sms/sms/i18n/uk.po
Normal file
File diff suppressed because it is too large
Load diff
1410
odoo-bringout-oca-ocb-sms/sms/i18n/vi.po
Normal file
1410
odoo-bringout-oca-ocb-sms/sms/i18n/vi.po
Normal file
File diff suppressed because it is too large
Load diff
1384
odoo-bringout-oca-ocb-sms/sms/i18n/zh_CN.po
Normal file
1384
odoo-bringout-oca-ocb-sms/sms/i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load diff
1378
odoo-bringout-oca-ocb-sms/sms/i18n/zh_TW.po
Normal file
1378
odoo-bringout-oca-ocb-sms/sms/i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load diff
15
odoo-bringout-oca-ocb-sms/sms/models/__init__.py
Normal file
15
odoo-bringout-oca-ocb-sms/sms/models/__init__.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import ir_actions_server
|
||||
from . import ir_model
|
||||
from . import mail_followers
|
||||
from . import mail_message
|
||||
from . import mail_notification
|
||||
from . import mail_thread
|
||||
from . import mail_thread_phone
|
||||
from . import models
|
||||
from . import res_partner
|
||||
from . import sms_api
|
||||
from . import sms_sms
|
||||
from . import sms_template
|
||||
71
odoo-bringout-oca-ocb-sms/sms/models/ir_actions_server.py
Normal file
71
odoo-bringout-oca-ocb-sms/sms/models/ir_actions_server.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ServerActions(models.Model):
|
||||
""" Add SMS option in server actions. """
|
||||
_name = 'ir.actions.server'
|
||||
_inherit = ['ir.actions.server']
|
||||
|
||||
state = fields.Selection(selection_add=[
|
||||
('sms', 'Send SMS Text Message'),
|
||||
], ondelete={'sms': 'cascade'})
|
||||
# SMS
|
||||
sms_template_id = fields.Many2one(
|
||||
'sms.template', 'SMS Template',
|
||||
compute='_compute_sms_template_id',
|
||||
ondelete='set null', readonly=False, store=True,
|
||||
domain="[('model_id', '=', model_id)]",
|
||||
)
|
||||
sms_method = fields.Selection(
|
||||
selection=[('sms', 'SMS'), ('comment', 'Post as Message'), ('note', 'Post as Note')],
|
||||
string='Send as (SMS)',
|
||||
compute='_compute_sms_method',
|
||||
readonly=False, store=True,
|
||||
help='Choose method for SMS sending:\nSMS: mass SMS\nPost as Message: log on document\nPost as Note: mass SMS with archives')
|
||||
|
||||
@api.depends('model_id', 'state')
|
||||
def _compute_sms_template_id(self):
|
||||
to_reset = self.filtered(
|
||||
lambda act: act.state != 'sms' or \
|
||||
(act.model_id != act.sms_template_id.model_id)
|
||||
)
|
||||
if to_reset:
|
||||
to_reset.sms_template_id = False
|
||||
|
||||
@api.depends('state')
|
||||
def _compute_sms_method(self):
|
||||
to_reset = self.filtered(lambda act: act.state != 'sms')
|
||||
if to_reset:
|
||||
to_reset.sms_method = False
|
||||
other = self - to_reset
|
||||
if other:
|
||||
other.sms_method = 'sms'
|
||||
|
||||
def _check_model_coherency(self):
|
||||
super()._check_model_coherency()
|
||||
for action in self:
|
||||
if action.state == 'sms' and (action.model_id.transient or not action.model_id.is_mail_thread):
|
||||
raise ValidationError(_("Sending SMS can only be done on a not transient mail.thread model"))
|
||||
|
||||
def _run_action_sms_multi(self, eval_context=None):
|
||||
# TDE CLEANME: when going to new api with server action, remove action
|
||||
if not self.sms_template_id or self._is_recompute():
|
||||
return False
|
||||
|
||||
records = eval_context.get('records') or eval_context.get('record')
|
||||
if not records:
|
||||
return False
|
||||
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
default_res_model=records._name,
|
||||
default_res_ids=records.ids,
|
||||
default_composition_mode='comment' if self.sms_method == 'comment' else 'mass',
|
||||
default_template_id=self.sms_template_id.id,
|
||||
default_mass_keep_log=self.sms_method == 'note',
|
||||
).create({})
|
||||
composer.action_send_sms()
|
||||
return False
|
||||
41
odoo-bringout-oca-ocb-sms/sms/models/ir_model.py
Normal file
41
odoo-bringout-oca-ocb-sms/sms/models/ir_model.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class IrModel(models.Model):
|
||||
_inherit = 'ir.model'
|
||||
|
||||
is_mail_thread_sms = fields.Boolean(
|
||||
string="Mail Thread SMS", default=False,
|
||||
store=False, compute='_compute_is_mail_thread_sms', search='_search_is_mail_thread_sms',
|
||||
help="Whether this model supports messages and notifications through SMS",
|
||||
)
|
||||
|
||||
@api.depends('is_mail_thread')
|
||||
def _compute_is_mail_thread_sms(self):
|
||||
for model in self:
|
||||
if model.is_mail_thread:
|
||||
ModelObject = self.env[model.model]
|
||||
potential_fields = ModelObject._sms_get_number_fields() + ModelObject._sms_get_partner_fields()
|
||||
if any(fname in ModelObject._fields for fname in potential_fields):
|
||||
model.is_mail_thread_sms = True
|
||||
continue
|
||||
model.is_mail_thread_sms = False
|
||||
|
||||
def _search_is_mail_thread_sms(self, operator, value):
|
||||
thread_models = self.search([('is_mail_thread', '=', True)])
|
||||
valid_models = self.env['ir.model']
|
||||
for model in thread_models:
|
||||
if model.model not in self.env:
|
||||
continue
|
||||
ModelObject = self.env[model.model]
|
||||
potential_fields = ModelObject._sms_get_number_fields() + ModelObject._sms_get_partner_fields()
|
||||
if any(fname in ModelObject._fields for fname in potential_fields):
|
||||
valid_models |= model
|
||||
|
||||
search_sms = (operator == '=' and value) or (operator == '!=' and not value)
|
||||
if search_sms:
|
||||
return [('id', 'in', valid_models.ids)]
|
||||
return [('id', 'not in', valid_models.ids)]
|
||||
29
odoo-bringout-oca-ocb-sms/sms/models/mail_followers.py
Normal file
29
odoo-bringout-oca-ocb-sms/sms/models/mail_followers.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 models
|
||||
|
||||
|
||||
class Followers(models.Model):
|
||||
_inherit = ['mail.followers']
|
||||
|
||||
def _get_recipient_data(self, records, message_type, subtype_id, pids=None):
|
||||
if message_type != 'sms' or not (pids or records):
|
||||
return super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids)
|
||||
|
||||
if pids is None and records:
|
||||
records_pids = dict(
|
||||
(record.id, record._sms_get_default_partners().ids)
|
||||
for record in records
|
||||
)
|
||||
elif pids and records:
|
||||
records_pids = dict((record.id, pids) for record in records)
|
||||
else:
|
||||
records_pids = {0: pids if pids else []}
|
||||
recipients_data = super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids)
|
||||
for rid, rdata in recipients_data.items():
|
||||
sms_pids = records_pids.get(rid) or []
|
||||
for pid, pdata in rdata.items():
|
||||
if pid in sms_pids:
|
||||
pdata['notif'] = 'sms'
|
||||
return recipients_data
|
||||
54
odoo-bringout-oca-ocb-sms/sms/models/mail_message.py
Normal file
54
odoo-bringout-oca-ocb-sms/sms/models/mail_message.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
|
||||
from odoo import exceptions, fields, models
|
||||
from odoo.tools import groupby
|
||||
|
||||
|
||||
class MailMessage(models.Model):
|
||||
""" Override MailMessage class in order to add a new type: SMS messages.
|
||||
Those messages comes with their own notification method, using SMS
|
||||
gateway. """
|
||||
_inherit = 'mail.message'
|
||||
|
||||
message_type = fields.Selection(selection_add=[
|
||||
('sms', 'SMS')
|
||||
], ondelete={'sms': lambda recs: recs.write({'message_type': 'email'})})
|
||||
has_sms_error = fields.Boolean(
|
||||
'Has SMS error', compute='_compute_has_sms_error', search='_search_has_sms_error')
|
||||
|
||||
def _compute_has_sms_error(self):
|
||||
sms_error_from_notification = self.env['mail.notification'].sudo().search([
|
||||
('notification_type', '=', 'sms'),
|
||||
('mail_message_id', 'in', self.ids),
|
||||
('notification_status', '=', 'exception')]).mapped('mail_message_id')
|
||||
for message in self:
|
||||
message.has_sms_error = message in sms_error_from_notification
|
||||
|
||||
def _search_has_sms_error(self, operator, operand):
|
||||
if operator == '=' and operand:
|
||||
return ['&', ('notification_ids.notification_status', '=', 'exception'), ('notification_ids.notification_type', '=', 'sms')]
|
||||
raise NotImplementedError()
|
||||
|
||||
def message_format(self, format_reply=True):
|
||||
""" Override in order to retrieves data about SMS (recipient name and
|
||||
SMS status)
|
||||
|
||||
TDE FIXME: clean the overall message_format thingy
|
||||
"""
|
||||
message_values = super(MailMessage, self).message_format(format_reply=format_reply)
|
||||
all_sms_notifications = self.env['mail.notification'].sudo().search([
|
||||
('mail_message_id', 'in', [r['id'] for r in message_values]),
|
||||
('notification_type', '=', 'sms')
|
||||
])
|
||||
msgid_to_notif = defaultdict(lambda: self.env['mail.notification'].sudo())
|
||||
for notif in all_sms_notifications:
|
||||
msgid_to_notif[notif.mail_message_id.id] += notif
|
||||
|
||||
for message in message_values:
|
||||
customer_sms_data = [(notif.id, notif.res_partner_id.display_name or notif.sms_number, notif.notification_status) for notif in msgid_to_notif.get(message['id'], [])]
|
||||
message['sms_ids'] = customer_sms_data
|
||||
return message_values
|
||||
21
odoo-bringout-oca-ocb-sms/sms/models/mail_notification.py
Normal file
21
odoo-bringout-oca-ocb-sms/sms/models/mail_notification.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class MailNotification(models.Model):
|
||||
_inherit = 'mail.notification'
|
||||
|
||||
notification_type = fields.Selection(selection_add=[
|
||||
('sms', 'SMS')
|
||||
], ondelete={'sms': 'cascade'})
|
||||
sms_id = fields.Many2one('sms.sms', string='SMS', index='btree_not_null', ondelete='set null')
|
||||
sms_number = fields.Char('SMS Number', groups='base.group_user')
|
||||
failure_type = fields.Selection(selection_add=[
|
||||
('sms_number_missing', 'Missing Number'),
|
||||
('sms_number_format', 'Wrong Number Format'),
|
||||
('sms_credit', 'Insufficient Credit'),
|
||||
('sms_server', 'Server Error'),
|
||||
('sms_acc', 'Unregistered Account')
|
||||
])
|
||||
276
odoo-bringout-oca-ocb-sms/sms/models/mail_thread.py
Normal file
276
odoo-bringout-oca-ocb-sms/sms/models/mail_thread.py
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, models, fields
|
||||
from odoo.addons.phone_validation.tools import phone_validation
|
||||
from odoo.addons.sms.tools.sms_tools import sms_content_to_rendered_html
|
||||
from odoo.tools import html2plaintext
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
_inherit = 'mail.thread'
|
||||
|
||||
message_has_sms_error = fields.Boolean(
|
||||
'SMS Delivery error', compute='_compute_message_has_sms_error', search='_search_message_has_sms_error',
|
||||
help="If checked, some messages have a delivery error.")
|
||||
|
||||
def _compute_message_has_sms_error(self):
|
||||
res = {}
|
||||
if self.ids:
|
||||
self.env.cr.execute("""
|
||||
SELECT msg.res_id, COUNT(msg.res_id)
|
||||
FROM mail_message msg
|
||||
INNER JOIN mail_notification notif
|
||||
ON notif.mail_message_id = msg.id
|
||||
WHERE notif.notification_type = 'sms'
|
||||
AND notif.notification_status = 'exception'
|
||||
AND notif.author_id = %(author_id)s
|
||||
AND msg.model = %(model_name)s
|
||||
AND msg.res_id in %(res_ids)s
|
||||
AND msg.message_type != 'user_notification'
|
||||
GROUP BY msg.res_id
|
||||
""", {'author_id': self.env.user.partner_id.id, 'model_name': self._name, 'res_ids': tuple(self.ids)})
|
||||
res.update(self._cr.fetchall())
|
||||
|
||||
for record in self:
|
||||
record.message_has_sms_error = bool(res.get(record._origin.id, 0))
|
||||
|
||||
@api.model
|
||||
def _search_message_has_sms_error(self, operator, operand):
|
||||
return ['&', ('message_ids.has_sms_error', operator, operand), ('message_ids.author_id', '=', self.env.user.partner_id.id)]
|
||||
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, *args, body='', message_type='notification', **kwargs):
|
||||
# When posting an 'SMS' `message_type`, make sure that the body is used as-is in the sms,
|
||||
# and reformat the message body for the notification (mainly making URLs clickable).
|
||||
if message_type == 'sms':
|
||||
kwargs['sms_content'] = body
|
||||
body = sms_content_to_rendered_html(body)
|
||||
return super().message_post(*args, body=body, message_type=message_type, **kwargs)
|
||||
|
||||
def _message_sms_schedule_mass(self, body='', template=False, **composer_values):
|
||||
""" Shortcut method to schedule a mass sms sending on a recordset.
|
||||
|
||||
:param template: an optional sms.template record;
|
||||
"""
|
||||
composer_context = {
|
||||
'default_res_model': self._name,
|
||||
'default_composition_mode': 'mass',
|
||||
'default_template_id': template.id if template else False,
|
||||
'default_res_ids': self.ids,
|
||||
}
|
||||
if body and not template:
|
||||
composer_context['default_body'] = body
|
||||
|
||||
create_vals = {
|
||||
'mass_force_send': False,
|
||||
'mass_keep_log': True,
|
||||
}
|
||||
if composer_values:
|
||||
create_vals.update(composer_values)
|
||||
|
||||
composer = self.env['sms.composer'].with_context(**composer_context).create(create_vals)
|
||||
return composer._action_send_sms()
|
||||
|
||||
def _message_sms_with_template(self, template=False, template_xmlid=False, template_fallback='', partner_ids=False, **kwargs):
|
||||
""" Shortcut method to perform a _message_sms with an sms.template.
|
||||
|
||||
:param template: a valid sms.template record;
|
||||
:param template_xmlid: XML ID of an sms.template (if no template given);
|
||||
:param template_fallback: plaintext (inline_template-enabled) in case template
|
||||
and template xml id are falsy (for example due to deleted data);
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not template and template_xmlid:
|
||||
template = self.env.ref(template_xmlid, raise_if_not_found=False)
|
||||
if template:
|
||||
body = template._render_field('body', self.ids, compute_lang=True)[self.id]
|
||||
else:
|
||||
body = self.env['sms.template']._render_template(template_fallback, self._name, self.ids)[self.id]
|
||||
return self._message_sms(body, partner_ids=partner_ids, **kwargs)
|
||||
|
||||
def _message_sms(self, body, subtype_id=False, partner_ids=False, number_field=False,
|
||||
sms_numbers=None, sms_pid_to_number=None, **kwargs):
|
||||
""" Main method to post a message on a record using SMS-based notification
|
||||
method.
|
||||
|
||||
:param body: content of SMS;
|
||||
:param subtype_id: mail.message.subtype used in mail.message associated
|
||||
to the sms notification process;
|
||||
:param partner_ids: if set is a record set of partners to notify;
|
||||
:param number_field: if set is a name of field to use on current record
|
||||
to compute a number to notify;
|
||||
:param sms_numbers: see ``_notify_thread_by_sms``;
|
||||
:param sms_pid_to_number: see ``_notify_thread_by_sms``;
|
||||
"""
|
||||
self.ensure_one()
|
||||
sms_pid_to_number = sms_pid_to_number if sms_pid_to_number is not None else {}
|
||||
|
||||
if number_field or (partner_ids is False and sms_numbers is None):
|
||||
info = self._sms_get_recipients_info(force_field=number_field)[self.id]
|
||||
info_partner_ids = info['partner'].ids if info['partner'] else False
|
||||
info_number = info['sanitized'] if info['sanitized'] else info['number']
|
||||
if info_partner_ids and info_number:
|
||||
sms_pid_to_number[info_partner_ids[0]] = info_number
|
||||
if info_partner_ids:
|
||||
partner_ids = info_partner_ids + (partner_ids or [])
|
||||
if not info_partner_ids:
|
||||
if info_number:
|
||||
sms_numbers = [info_number] + (sms_numbers or [])
|
||||
# will send a falsy notification allowing to fix it through SMS wizards
|
||||
elif not sms_numbers:
|
||||
sms_numbers = [False]
|
||||
|
||||
if subtype_id is False:
|
||||
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
|
||||
|
||||
return self.message_post(
|
||||
body=body, partner_ids=partner_ids or [], # TDE FIXME: temp fix otherwise crash mail_thread.py
|
||||
message_type='sms', subtype_id=subtype_id,
|
||||
sms_numbers=sms_numbers, sms_pid_to_number=sms_pid_to_number,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def _notify_thread(self, message, msg_vals=False, **kwargs):
|
||||
recipients_data = super(MailThread, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
|
||||
self._notify_thread_by_sms(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
return recipients_data
|
||||
|
||||
def _notify_thread_by_sms(self, message, recipients_data, msg_vals=False,
|
||||
sms_content=None, sms_numbers=None, sms_pid_to_number=None,
|
||||
resend_existing=False, put_in_queue=False, **kwargs):
|
||||
""" Notification method: by SMS.
|
||||
|
||||
:param message: ``mail.message`` record to notify;
|
||||
:param recipients_data: list of recipients information (based on res.partner
|
||||
records), formatted like
|
||||
[{'active': partner.active;
|
||||
'id': id of the res.partner being recipient to notify;
|
||||
'groups': res.group IDs if linked to a user;
|
||||
'notif': 'inbox', 'email', 'sms' (SMS App);
|
||||
'share': partner.partner_share;
|
||||
'type': 'customer', 'portal', 'user;'
|
||||
}, {...}].
|
||||
See ``MailThread._notify_get_recipients``;
|
||||
:param msg_vals: dictionary of values used to create the message. If given it
|
||||
may be used to access values related to ``message`` without accessing it
|
||||
directly. It lessens query count in some optimized use cases by avoiding
|
||||
access message content in db;
|
||||
|
||||
:param sms_content: plaintext version of body, mainly to avoid
|
||||
conversion glitches by splitting html and plain text content formatting
|
||||
(e.g.: links, styling.).
|
||||
If not given, `msg_vals`'s `body` is used and converted from html to plaintext;
|
||||
:param sms_numbers: additional numbers to notify in addition to partners
|
||||
and classic recipients;
|
||||
:param pid_to_number: force a number to notify for a given partner ID
|
||||
instead of taking its mobile / phone number;
|
||||
:param resend_existing: check for existing notifications to update based on
|
||||
mailed recipient, otherwise create new notifications;
|
||||
:param put_in_queue: use cron to send queued SMS instead of sending them
|
||||
directly;
|
||||
"""
|
||||
sms_pid_to_number = sms_pid_to_number if sms_pid_to_number is not None else {}
|
||||
sms_numbers = sms_numbers if sms_numbers is not None else []
|
||||
sms_create_vals = []
|
||||
sms_all = self.env['sms.sms'].sudo()
|
||||
|
||||
# pre-compute SMS data
|
||||
body = sms_content or html2plaintext(msg_vals['body'] if msg_vals and 'body' in msg_vals else message.body)
|
||||
sms_base_vals = {
|
||||
'body': body,
|
||||
'mail_message_id': message.id,
|
||||
'state': 'outgoing',
|
||||
}
|
||||
|
||||
# notify from computed recipients_data (followers, specific recipients)
|
||||
partners_data = [r for r in recipients_data if r['notif'] == 'sms']
|
||||
partner_ids = [r['id'] for r in partners_data]
|
||||
if partner_ids:
|
||||
for partner in self.env['res.partner'].sudo().browse(partner_ids):
|
||||
number = sms_pid_to_number.get(partner.id) or partner.mobile or partner.phone
|
||||
sanitize_res = phone_validation.phone_sanitize_numbers_w_record([number], partner)[number]
|
||||
number = sanitize_res['sanitized'] or number
|
||||
sms_create_vals.append(dict(
|
||||
sms_base_vals,
|
||||
partner_id=partner.id,
|
||||
number=number
|
||||
))
|
||||
|
||||
# notify from additional numbers
|
||||
if sms_numbers:
|
||||
sanitized = phone_validation.phone_sanitize_numbers_w_record(sms_numbers, self)
|
||||
tocreate_numbers = [
|
||||
value['sanitized'] or original
|
||||
for original, value in sanitized.items()
|
||||
]
|
||||
existing_partners_numbers = {vals_dict['number'] for vals_dict in sms_create_vals}
|
||||
sms_create_vals += [dict(
|
||||
sms_base_vals,
|
||||
partner_id=False,
|
||||
number=n,
|
||||
state='outgoing' if n else 'error',
|
||||
failure_type='' if n else 'sms_number_missing',
|
||||
) for n in tocreate_numbers if n not in existing_partners_numbers]
|
||||
|
||||
# create sms and notification
|
||||
existing_pids, existing_numbers = [], []
|
||||
if sms_create_vals:
|
||||
sms_all |= self.env['sms.sms'].sudo().create(sms_create_vals)
|
||||
|
||||
if resend_existing:
|
||||
existing = self.env['mail.notification'].sudo().search([
|
||||
'|', ('res_partner_id', 'in', partner_ids),
|
||||
'&', ('res_partner_id', '=', False), ('sms_number', 'in', sms_numbers),
|
||||
('notification_type', '=', 'sms'),
|
||||
('mail_message_id', '=', message.id)
|
||||
])
|
||||
for n in existing:
|
||||
if n.res_partner_id.id in partner_ids and n.mail_message_id == message:
|
||||
existing_pids.append(n.res_partner_id.id)
|
||||
if not n.res_partner_id and n.sms_number in sms_numbers and n.mail_message_id == message:
|
||||
existing_numbers.append(n.sms_number)
|
||||
|
||||
notif_create_values = [{
|
||||
'author_id': message.author_id.id,
|
||||
'mail_message_id': message.id,
|
||||
'res_partner_id': sms.partner_id.id,
|
||||
'sms_number': sms.number,
|
||||
'notification_type': 'sms',
|
||||
'sms_id': sms.id,
|
||||
'is_read': True, # discard Inbox notification
|
||||
'notification_status': 'ready' if sms.state == 'outgoing' else 'exception',
|
||||
'failure_type': '' if sms.state == 'outgoing' else sms.failure_type,
|
||||
} for sms in sms_all if (sms.partner_id and sms.partner_id.id not in existing_pids) or (not sms.partner_id and sms.number not in existing_numbers)]
|
||||
if notif_create_values:
|
||||
self.env['mail.notification'].sudo().create(notif_create_values)
|
||||
|
||||
if existing_pids or existing_numbers:
|
||||
for sms in sms_all:
|
||||
notif = next((n for n in existing if
|
||||
(n.res_partner_id.id in existing_pids and n.res_partner_id.id == sms.partner_id.id) or
|
||||
(not n.res_partner_id and n.sms_number in existing_numbers and n.sms_number == sms.number)), False)
|
||||
if notif:
|
||||
notif.write({
|
||||
'notification_type': 'sms',
|
||||
'notification_status': 'ready',
|
||||
'sms_id': sms.id,
|
||||
'sms_number': sms.number,
|
||||
})
|
||||
|
||||
if sms_all and not put_in_queue:
|
||||
sms_all.filtered(lambda sms: sms.state == 'outgoing').send(auto_commit=False, raise_exception=False)
|
||||
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def notify_cancel_by_type(self, notification_type):
|
||||
super().notify_cancel_by_type(notification_type)
|
||||
if notification_type == 'sms':
|
||||
# TDE CHECK: delete pending SMS
|
||||
self._notify_cancel_by_type_generic('sms')
|
||||
return True
|
||||
16
odoo-bringout-oca-ocb-sms/sms/models/mail_thread_phone.py
Normal file
16
odoo-bringout-oca-ocb-sms/sms/models/mail_thread_phone.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class PhoneMixin(models.AbstractModel):
|
||||
_inherit = 'mail.thread.phone'
|
||||
|
||||
def _sms_get_number_fields(self):
|
||||
""" Add fields coming from mail.thread.phone implementation. """
|
||||
phone_fields = self._phone_get_number_fields()
|
||||
sms_fields = super(PhoneMixin, self)._sms_get_number_fields()
|
||||
for fname in (f for f in sms_fields if f not in phone_fields):
|
||||
phone_fields.append(fname)
|
||||
return phone_fields
|
||||
117
odoo-bringout-oca-ocb-sms/sms/models/models.py
Normal file
117
odoo-bringout-oca-ocb-sms/sms/models/models.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
from odoo import models
|
||||
from odoo.addons.phone_validation.tools import phone_validation
|
||||
|
||||
|
||||
class BaseModel(models.AbstractModel):
|
||||
_inherit = 'base'
|
||||
|
||||
def _sms_get_partner_fields(self):
|
||||
""" This method returns the fields to use to find the contact to link
|
||||
whensending an SMS. Having partner is not necessary, having only phone
|
||||
number fields is possible. However it gives more flexibility to
|
||||
notifications management when having partners. """
|
||||
fields = []
|
||||
if hasattr(self, 'partner_id'):
|
||||
fields.append('partner_id')
|
||||
if hasattr(self, 'partner_ids'):
|
||||
fields.append('partner_ids')
|
||||
return fields
|
||||
|
||||
def _sms_get_default_partners(self):
|
||||
""" This method will likely need to be overridden by inherited models.
|
||||
:returns partners: recordset of res.partner
|
||||
"""
|
||||
partners = self.env['res.partner']
|
||||
for fname in self._sms_get_partner_fields():
|
||||
partners = partners.union(*self.mapped(fname)) # ensure ordering
|
||||
return partners
|
||||
|
||||
def _sms_get_number_fields(self):
|
||||
""" This method returns the fields to use to find the number to use to
|
||||
send an SMS on a record. """
|
||||
if 'mobile' in self:
|
||||
return ['mobile']
|
||||
return []
|
||||
|
||||
def _sms_get_recipients_info(self, force_field=False, partner_fallback=True):
|
||||
"""" Get SMS recipient information on current record set. This method
|
||||
checks for numbers and sanitation in order to centralize computation.
|
||||
|
||||
Example of use cases
|
||||
|
||||
* click on a field -> number is actually forced from field, find customer
|
||||
linked to record, force its number to field or fallback on customer fields;
|
||||
* contact -> find numbers from all possible phone fields on record, find
|
||||
customer, force its number to found field number or fallback on customer fields;
|
||||
|
||||
:param force_field: either give a specific field to find phone number, either
|
||||
generic heuristic is used to find one based on ``_sms_get_number_fields``;
|
||||
:param partner_fallback: if no value found in the record, check its customer
|
||||
values based on ``_sms_get_default_partners``;
|
||||
|
||||
:return dict: record.id: {
|
||||
'partner': a res.partner recordset that is the customer (void or singleton)
|
||||
linked to the recipient. See ``_sms_get_default_partners``;
|
||||
'sanitized': sanitized number to use (coming from record's field or partner's
|
||||
phone fields). Set to False is number impossible to parse and format;
|
||||
'number': original number before sanitation;
|
||||
'partner_store': whether the number comes from the customer phone fields. If
|
||||
False it means number comes from the record itself, even if linked to a
|
||||
customer;
|
||||
'field_store': field in which the number has been found (generally mobile or
|
||||
phone, see ``_sms_get_number_fields``);
|
||||
} for each record in self
|
||||
"""
|
||||
result = dict.fromkeys(self.ids, False)
|
||||
tocheck_fields = [force_field] if force_field else self._sms_get_number_fields()
|
||||
for record in self:
|
||||
all_numbers = [record[fname] for fname in tocheck_fields if fname in record]
|
||||
all_partners = record._sms_get_default_partners()
|
||||
|
||||
valid_number = False
|
||||
for fname in [f for f in tocheck_fields if f in record]:
|
||||
valid_number = phone_validation.phone_sanitize_numbers_w_record([record[fname]], record)[record[fname]]['sanitized']
|
||||
if valid_number:
|
||||
break
|
||||
|
||||
if valid_number:
|
||||
result[record.id] = {
|
||||
'partner': all_partners[0] if all_partners else self.env['res.partner'],
|
||||
'sanitized': valid_number,
|
||||
'number': record[fname],
|
||||
'partner_store': False,
|
||||
'field_store': fname,
|
||||
}
|
||||
elif all_partners and partner_fallback:
|
||||
partner = self.env['res.partner']
|
||||
for partner in all_partners:
|
||||
for fname in self.env['res.partner']._sms_get_number_fields():
|
||||
valid_number = phone_validation.phone_sanitize_numbers_w_record([partner[fname]], record)[partner[fname]]['sanitized']
|
||||
if valid_number:
|
||||
break
|
||||
|
||||
if not valid_number:
|
||||
fname = 'mobile' if partner.mobile else ('phone' if partner.phone else 'mobile')
|
||||
|
||||
result[record.id] = {
|
||||
'partner': partner,
|
||||
'sanitized': valid_number if valid_number else False,
|
||||
'number': partner[fname],
|
||||
'partner_store': True,
|
||||
'field_store': fname,
|
||||
}
|
||||
else:
|
||||
# did not find any sanitized number -> take first set value as fallback;
|
||||
# if none, just assign False to the first available number field
|
||||
value, fname = next(
|
||||
((value, fname) for value, fname in zip(all_numbers, tocheck_fields) if value),
|
||||
(False, tocheck_fields[0] if tocheck_fields else False)
|
||||
)
|
||||
result[record.id] = {
|
||||
'partner': self.env['res.partner'],
|
||||
'sanitized': False,
|
||||
'number': value,
|
||||
'partner_store': False,
|
||||
'field_store': fname
|
||||
}
|
||||
return result
|
||||
20
odoo-bringout-oca-ocb-sms/sms/models/res_partner.py
Normal file
20
odoo-bringout-oca-ocb-sms/sms/models/res_partner.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_name = 'res.partner'
|
||||
_inherit = ['mail.thread.phone', 'res.partner']
|
||||
|
||||
def _sms_get_default_partners(self):
|
||||
""" Override of mail.thread method.
|
||||
SMS recipients on partners are the partners themselves.
|
||||
"""
|
||||
return self
|
||||
|
||||
def _phone_get_number_fields(self):
|
||||
""" This method returns the fields to use to find the number to use to
|
||||
send an SMS on a record. """
|
||||
return ['mobile', 'phone']
|
||||
79
odoo-bringout-oca-ocb-sms/sms/models/sms_api.py
Normal file
79
odoo-bringout-oca-ocb-sms/sms/models/sms_api.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, exceptions, models
|
||||
from odoo.addons.iap.tools import iap_tools
|
||||
|
||||
DEFAULT_ENDPOINT = 'https://iap-sms.odoo.com'
|
||||
|
||||
|
||||
class SmsApi(models.AbstractModel):
|
||||
_name = 'sms.api'
|
||||
_description = 'SMS API'
|
||||
|
||||
@api.model
|
||||
def _contact_iap(self, local_endpoint, params):
|
||||
if not self.env.registry.ready: # Don't reach IAP servers during module installation
|
||||
raise exceptions.AccessError("Unavailable during module installation.")
|
||||
|
||||
account = self.env['iap.account'].get('sms')
|
||||
params['account_token'] = account.account_token
|
||||
endpoint = self.env['ir.config_parameter'].sudo().get_param('sms.endpoint', DEFAULT_ENDPOINT)
|
||||
# TODO PRO, the default timeout is 15, do we have to increase it ?
|
||||
return iap_tools.iap_jsonrpc(endpoint + local_endpoint, params=params)
|
||||
|
||||
@api.model
|
||||
def _send_sms(self, numbers, message):
|
||||
""" Send a single message to several numbers
|
||||
|
||||
:param numbers: list of E164 formatted phone numbers
|
||||
:param message: content to send
|
||||
|
||||
:raises ? TDE FIXME
|
||||
"""
|
||||
params = {
|
||||
'numbers': numbers,
|
||||
'message': message,
|
||||
}
|
||||
return self._contact_iap('/iap/message_send', params)
|
||||
|
||||
@api.model
|
||||
def _send_sms_batch(self, messages):
|
||||
""" Send SMS using IAP in batch mode
|
||||
|
||||
:param messages: list of SMS to send, structured as dict [{
|
||||
'res_id': integer: ID of sms.sms,
|
||||
'number': string: E164 formatted phone number,
|
||||
'content': string: content to send
|
||||
}]
|
||||
|
||||
:return: return of /iap/sms/1/send controller which is a list of dict [{
|
||||
'res_id': integer: ID of sms.sms,
|
||||
'state': string: 'insufficient_credit' or 'wrong_number_format' or 'success',
|
||||
'credit': integer: number of credits spent to send this SMS,
|
||||
}]
|
||||
|
||||
:raises: normally none
|
||||
"""
|
||||
params = {
|
||||
'messages': messages
|
||||
}
|
||||
return self._contact_iap('/iap/sms/2/send', params)
|
||||
|
||||
@api.model
|
||||
def _get_sms_api_error_messages(self):
|
||||
""" Returns a dict containing the error message to display for every known error 'state'
|
||||
resulting from the '_send_sms_batch' method.
|
||||
We prefer a dict instead of a message-per-error-state based method so we only call
|
||||
the 'get_credits_url' once, to avoid extra RPC calls. """
|
||||
|
||||
buy_credits_url = self.sudo().env['iap.account'].get_credits_url(service_name='sms')
|
||||
buy_credits = '<a href="%s" target="_blank">%s</a>' % (
|
||||
buy_credits_url,
|
||||
_('Buy credits.')
|
||||
)
|
||||
return {
|
||||
'unregistered': _("You don't have an eligible IAP account."),
|
||||
'insufficient_credit': ' '.join([_('You don\'t have enough credits on your IAP account.'), buy_credits]),
|
||||
'wrong_number_format': _("The number you're trying to reach is not correctly formatted."),
|
||||
}
|
||||
217
odoo-bringout-oca-ocb-sms/sms/models/sms_sms.py
Normal file
217
odoo-bringout-oca-ocb-sms/sms/models/sms_sms.py
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SmsSms(models.Model):
|
||||
_name = 'sms.sms'
|
||||
_description = 'Outgoing SMS'
|
||||
_rec_name = 'number'
|
||||
_order = 'id DESC'
|
||||
|
||||
IAP_TO_SMS_STATE = {
|
||||
'success': 'sent',
|
||||
'insufficient_credit': 'sms_credit',
|
||||
'wrong_number_format': 'sms_number_format',
|
||||
'server_error': 'sms_server',
|
||||
'unregistered': 'sms_acc'
|
||||
}
|
||||
|
||||
number = fields.Char('Number')
|
||||
body = fields.Text()
|
||||
partner_id = fields.Many2one('res.partner', 'Customer')
|
||||
mail_message_id = fields.Many2one('mail.message', index=True)
|
||||
state = fields.Selection([
|
||||
('outgoing', 'In Queue'),
|
||||
('sent', 'Sent'),
|
||||
('error', 'Error'),
|
||||
('canceled', 'Canceled')
|
||||
], 'SMS Status', readonly=True, copy=False, default='outgoing', required=True)
|
||||
failure_type = fields.Selection([
|
||||
('sms_number_missing', 'Missing Number'),
|
||||
('sms_number_format', 'Wrong Number Format'),
|
||||
('sms_credit', 'Insufficient Credit'),
|
||||
('sms_server', 'Server Error'),
|
||||
('sms_acc', 'Unregistered Account'),
|
||||
# mass mode specific codes
|
||||
('sms_blacklist', 'Blacklisted'),
|
||||
('sms_duplicate', 'Duplicate'),
|
||||
('sms_optout', 'Opted Out'),
|
||||
], copy=False)
|
||||
|
||||
def action_set_canceled(self):
|
||||
self.state = 'canceled'
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('sms_id', 'in', self.ids),
|
||||
# sent is sent -> cannot reset
|
||||
('notification_status', 'not in', ['canceled', 'sent']),
|
||||
])
|
||||
if notifications:
|
||||
notifications.write({'notification_status': 'canceled'})
|
||||
if not self._context.get('sms_skip_msg_notification', False):
|
||||
notifications.mail_message_id._notify_message_notification_update()
|
||||
|
||||
def action_set_error(self, failure_type):
|
||||
self.state = 'error'
|
||||
self.failure_type = failure_type
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('sms_id', 'in', self.ids),
|
||||
# sent can be set to error due to IAP feedback
|
||||
('notification_status', '!=', 'exception'),
|
||||
])
|
||||
if notifications:
|
||||
notifications.write({'notification_status': 'exception', 'failure_type': failure_type})
|
||||
if not self._context.get('sms_skip_msg_notification', False):
|
||||
notifications.mail_message_id._notify_message_notification_update()
|
||||
|
||||
def action_set_outgoing(self):
|
||||
self.write({
|
||||
'state': 'outgoing',
|
||||
'failure_type': False
|
||||
})
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('sms_id', 'in', self.ids),
|
||||
# sent is sent -> cannot reset
|
||||
('notification_status', 'not in', ['ready', 'sent']),
|
||||
])
|
||||
if notifications:
|
||||
notifications.write({'notification_status': 'ready', 'failure_type': False})
|
||||
if not self._context.get('sms_skip_msg_notification', False):
|
||||
notifications.mail_message_id._notify_message_notification_update()
|
||||
|
||||
def send(self, unlink_failed=False, unlink_sent=True, auto_commit=False, raise_exception=False):
|
||||
""" Main API method to send SMS.
|
||||
|
||||
:param unlink_failed: unlink failed SMS after IAP feedback;
|
||||
:param unlink_sent: unlink sent SMS after IAP feedback;
|
||||
:param auto_commit: commit after each batch of SMS;
|
||||
:param raise_exception: raise if there is an issue contacting IAP;
|
||||
"""
|
||||
self = self.filtered(lambda sms: sms.state == 'outgoing')
|
||||
for batch_ids in self._split_batch():
|
||||
self.browse(batch_ids)._send(unlink_failed=unlink_failed, unlink_sent=unlink_sent, raise_exception=raise_exception)
|
||||
# auto-commit if asked except in testing mode
|
||||
if auto_commit is True and not getattr(threading.current_thread(), 'testing', False):
|
||||
self._cr.commit()
|
||||
|
||||
def resend_failed(self):
|
||||
sms_to_send = self.filtered(lambda sms: sms.state == 'error')
|
||||
sms_to_send.state = 'outgoing'
|
||||
notification_title = _('Warning')
|
||||
notification_type = 'danger'
|
||||
|
||||
if sms_to_send:
|
||||
sms_to_send.send()
|
||||
success_sms = len(sms_to_send) - len(sms_to_send.exists())
|
||||
if success_sms > 0:
|
||||
notification_title = _('Success')
|
||||
notification_type = 'success'
|
||||
notification_message = _('%s out of the %s selected SMS Text Messages have successfully been resent.', success_sms, len(self))
|
||||
else:
|
||||
notification_message = _('The SMS Text Messages could not be resent.')
|
||||
else:
|
||||
notification_message = _('There are no SMS Text Messages to resend.')
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': notification_title,
|
||||
'message': notification_message,
|
||||
'type': notification_type,
|
||||
}
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _process_queue(self, ids=None):
|
||||
""" Send immediately queued messages, committing after each message is sent.
|
||||
This is not transactional and should not be called during another transaction!
|
||||
|
||||
:param list ids: optional list of emails ids to send. If passed no search
|
||||
is performed, and these ids are used instead.
|
||||
"""
|
||||
domain = [('state', '=', 'outgoing')]
|
||||
|
||||
filtered_ids = self.search(domain, limit=10000).ids # TDE note: arbitrary limit we might have to update
|
||||
if ids:
|
||||
ids = list(set(filtered_ids) & set(ids))
|
||||
else:
|
||||
ids = filtered_ids
|
||||
ids.sort()
|
||||
|
||||
res = None
|
||||
try:
|
||||
# auto-commit except in testing mode
|
||||
auto_commit = not getattr(threading.current_thread(), 'testing', False)
|
||||
res = self.browse(ids).send(unlink_failed=False, unlink_sent=True, auto_commit=auto_commit, raise_exception=False)
|
||||
except Exception:
|
||||
_logger.exception("Failed processing SMS queue")
|
||||
return res
|
||||
|
||||
def _split_batch(self):
|
||||
batch_size = int(self.env['ir.config_parameter'].sudo().get_param('sms.session.batch.size', 500))
|
||||
for sms_batch in tools.split_every(batch_size, self.ids):
|
||||
yield sms_batch
|
||||
|
||||
def _send(self, unlink_failed=False, unlink_sent=True, raise_exception=False):
|
||||
""" This method tries to send SMS after checking the number (presence and
|
||||
formatting). """
|
||||
iap_data = [{
|
||||
'res_id': record.id,
|
||||
'number': record.number,
|
||||
'content': record.body,
|
||||
} for record in self]
|
||||
|
||||
try:
|
||||
iap_results = self.env['sms.api']._send_sms_batch(iap_data)
|
||||
except Exception as e:
|
||||
_logger.info('Sent batch %s SMS: %s: failed with exception %s', len(self.ids), self.ids, e)
|
||||
if raise_exception:
|
||||
raise
|
||||
self._postprocess_iap_sent_sms(
|
||||
[{'res_id': sms.id, 'state': 'server_error'} for sms in self],
|
||||
unlink_failed=unlink_failed, unlink_sent=unlink_sent)
|
||||
else:
|
||||
_logger.info('Send batch %s SMS: %s: gave %s', len(self.ids), self.ids, iap_results)
|
||||
self._postprocess_iap_sent_sms(iap_results, unlink_failed=unlink_failed, unlink_sent=unlink_sent)
|
||||
|
||||
def _postprocess_iap_sent_sms(self, iap_results, failure_reason=None, unlink_failed=False, unlink_sent=True):
|
||||
todelete_sms_ids = []
|
||||
if unlink_failed:
|
||||
todelete_sms_ids += [item['res_id'] for item in iap_results if item['state'] != 'success']
|
||||
if unlink_sent:
|
||||
todelete_sms_ids += [item['res_id'] for item in iap_results if item['state'] == 'success']
|
||||
|
||||
for state in self.IAP_TO_SMS_STATE.keys():
|
||||
sms_ids = [item['res_id'] for item in iap_results if item['state'] == state]
|
||||
if sms_ids:
|
||||
if state != 'success' and not unlink_failed:
|
||||
self.env['sms.sms'].sudo().browse(sms_ids).write({
|
||||
'state': 'error',
|
||||
'failure_type': self.IAP_TO_SMS_STATE[state],
|
||||
})
|
||||
if state == 'success' and not unlink_sent:
|
||||
self.env['sms.sms'].sudo().browse(sms_ids).write({
|
||||
'state': 'sent',
|
||||
'failure_type': False,
|
||||
})
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('notification_type', '=', 'sms'),
|
||||
('sms_id', 'in', sms_ids),
|
||||
('notification_status', 'not in', ('sent', 'canceled')),
|
||||
])
|
||||
if notifications:
|
||||
notifications.write({
|
||||
'notification_status': 'sent' if state == 'success' else 'exception',
|
||||
'failure_type': self.IAP_TO_SMS_STATE[state] if state != 'success' else False,
|
||||
'failure_reason': failure_reason if failure_reason else False,
|
||||
})
|
||||
self.mail_message_id._notify_message_notification_update()
|
||||
|
||||
if todelete_sms_ids:
|
||||
self.browse(todelete_sms_ids).sudo().unlink()
|
||||
78
odoo-bringout-oca-ocb-sms/sms/models/sms_template.py
Normal file
78
odoo-bringout-oca-ocb-sms/sms/models/sms_template.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class SMSTemplate(models.Model):
|
||||
"Templates for sending SMS"
|
||||
_name = "sms.template"
|
||||
_inherit = ['mail.render.mixin', 'template.reset.mixin']
|
||||
_description = 'SMS Templates'
|
||||
|
||||
_unrestricted_rendering = True
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(SMSTemplate, self).default_get(fields)
|
||||
if not fields or 'model_id' in fields and not res.get('model_id') and res.get('model'):
|
||||
res['model_id'] = self.env['ir.model']._get(res['model']).id
|
||||
return res
|
||||
|
||||
name = fields.Char('Name', translate=True)
|
||||
model_id = fields.Many2one(
|
||||
'ir.model', string='Applies to', required=True,
|
||||
domain=['&', ('is_mail_thread_sms', '=', True), ('transient', '=', False)],
|
||||
help="The type of document this template can be used with", ondelete='cascade')
|
||||
model = fields.Char('Related Document Model', related='model_id.model', index=True, store=True, readonly=True)
|
||||
body = fields.Char('Body', translate=True, required=True)
|
||||
# Use to create contextual action (same as for email template)
|
||||
sidebar_action_id = fields.Many2one('ir.actions.act_window', 'Sidebar action', readonly=True, copy=False,
|
||||
help="Sidebar action to make this template available on records "
|
||||
"of the related document model")
|
||||
|
||||
# Overrides of mail.render.mixin
|
||||
@api.depends('model')
|
||||
def _compute_render_model(self):
|
||||
for template in self:
|
||||
template.render_model = template.model
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# CRUD
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def copy(self, default=None):
|
||||
default = dict(default or {},
|
||||
name=_("%s (copy)", self.name))
|
||||
return super(SMSTemplate, self).copy(default=default)
|
||||
|
||||
def unlink(self):
|
||||
self.sudo().mapped('sidebar_action_id').unlink()
|
||||
return super(SMSTemplate, self).unlink()
|
||||
|
||||
def action_create_sidebar_action(self):
|
||||
ActWindow = self.env['ir.actions.act_window']
|
||||
view = self.env.ref('sms.sms_composer_view_form')
|
||||
|
||||
for template in self:
|
||||
button_name = _('Send SMS (%s)', template.name)
|
||||
action = ActWindow.create({
|
||||
'name': button_name,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'sms.composer',
|
||||
# Add default_composition_mode to guess to determine if need to use mass or comment composer
|
||||
'context': "{'default_template_id' : %d, 'sms_composition_mode': 'guess', 'default_res_ids': active_ids, 'default_res_id': active_id}" % (template.id),
|
||||
'view_mode': 'form',
|
||||
'view_id': view.id,
|
||||
'target': 'new',
|
||||
'binding_model_id': template.model_id.id,
|
||||
})
|
||||
template.write({'sidebar_action_id': action.id})
|
||||
return True
|
||||
|
||||
def action_unlink_sidebar_action(self):
|
||||
for template in self:
|
||||
if template.sidebar_action_id:
|
||||
template.sidebar_action_id.unlink()
|
||||
return True
|
||||
11
odoo-bringout-oca-ocb-sms/sms/security/ir.model.access.csv
Normal file
11
odoo-bringout-oca-ocb-sms/sms/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sms_sms_all,access.sms.sms.all,model_sms_sms,,0,0,0,0
|
||||
access_sms_sms_system,access.sms.sms.system,model_sms_sms,base.group_system,1,1,1,1
|
||||
access_sms_template_all,access.sms.template.all,model_sms_template,,0,0,0,0
|
||||
access_sms_template_user,access.sms.template.user,model_sms_template,base.group_user,1,0,0,0
|
||||
access_sms_template_system,access.sms.template.system,model_sms_template,base.group_system,1,1,1,1
|
||||
access_sms_composer,access.sms.composer,model_sms_composer,base.group_user,1,1,1,0
|
||||
access_sms_resend_recipient,access.sms.resend.recipient,model_sms_resend_recipient,base.group_user,1,1,1,0
|
||||
access_sms_resend,access.sms.resend,model_sms_resend,base.group_user,1,1,1,0
|
||||
access_sms_template_preview,access.sms.template.preview,model_sms_template_preview,base.group_user,1,1,1,0
|
||||
access_sms_template_reset,access.sms.template.reset,model_sms_template_reset,mail.group_mail_template_editor,1,1,1,1
|
||||
|
9
odoo-bringout-oca-ocb-sms/sms/security/sms_security.xml
Normal file
9
odoo-bringout-oca-ocb-sms/sms/security/sms_security.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="ir_rule_sms_template_system" model="ir.rule">
|
||||
<field name="name">SMS Template: system group granted all</field>
|
||||
<field name="model_id" ref="sms.model_sms_template"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
</odoo>
|
||||
BIN
odoo-bringout-oca-ocb-sms/sms/static/description/icon.png
Normal file
BIN
odoo-bringout-oca-ocb-sms/sms/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
41
odoo-bringout-oca-ocb-sms/sms/static/description/icon.svg
Normal file
41
odoo-bringout-oca-ocb-sms/sms/static/description/icon.svg
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 70 70">
|
||||
<defs>
|
||||
<mask id="mask" x="0" y="0" width="70" height="70" maskUnits="userSpaceOnUse">
|
||||
<g id="b">
|
||||
<path id="a" d="M4,0H65c4,0,5,1,5,5V65c0,4-1,5-5,5H4c-3,0-4-1-4-5V5C0,1,1,0,4,0Z" fill="#fff" fill-rule="evenodd"/>
|
||||
</g>
|
||||
</mask>
|
||||
<linearGradient id="linear-gradient" x1="-1172.36" y1="477.94" x2="-1173.36" y2="476.94" gradientTransform="matrix(70, 0, 0, -70, 82134.99, 33455.73)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#7cc098"/>
|
||||
<stop offset="1" stop-color="#5f8a71"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g mask="url(#mask)">
|
||||
<g>
|
||||
<path d="M0,0H70V70H0Z" fill-rule="evenodd" fill="url(#linear-gradient)"/>
|
||||
<path d="M4,1H65c2.67,0,4.33.67,5,2V0H0V3C.67,1.67,2,1,4,1Z" fill="#fff" fill-opacity="0.38" fill-rule="evenodd"/>
|
||||
<path d="M4,69H65c2.67,0,4.33-1,5-3v4H0V66A3.92,3.92,0,0,0,4,69Z" fill-opacity="0.38" fill-rule="evenodd"/>
|
||||
<path d="M4,69a3.66,3.66,0,0,1-4-4V34.17L20.65,13.52l24.78-.66,2,3.78v9.44l-6.11,6.11,2.41,2.11-1.09,1.09,1.31,2L47.32,34l1.52,0,3.8-3.83,1.87,12-7.08,7.1-.56,7.6L34.81,69Z" fill-opacity="0.15" fill-rule="evenodd"/>
|
||||
<g>
|
||||
<g opacity="0.4" style="isolation: isolate">
|
||||
<path d="M21.59,39.06,23,39a2.29,2.29,0,0,0,.49,1,1.5,1.5,0,0,0,1.05.33,1.87,1.87,0,0,0,1-.27.87.87,0,0,0,.33-.66A.64.64,0,0,0,25.7,39a1.88,1.88,0,0,0-.49-.33l-1.15-.33a4.49,4.49,0,0,1-1.59-.71,1.82,1.82,0,0,1-.66-1.42,1.52,1.52,0,0,1,.33-1,1.83,1.83,0,0,1,.88-.71,3.31,3.31,0,0,1,1.37-.22,3.2,3.2,0,0,1,2,.6,2.05,2.05,0,0,1,.72,1.54l-1.43,0a1.3,1.3,0,0,0-.38-.77,1.44,1.44,0,0,0-.88-.22,1.73,1.73,0,0,0-1,.28.51.51,0,0,0-.22.44.52.52,0,0,0,.22.44,3.47,3.47,0,0,0,1.32.49,7.23,7.23,0,0,1,1.53.49,2,2,0,0,1,.77.71,2.34,2.34,0,0,1,.27,1.16A2.3,2.3,0,0,1,27,40.6a2.08,2.08,0,0,1-.94.76,4.1,4.1,0,0,1-1.53.28A3.3,3.3,0,0,1,22.47,41,3.1,3.1,0,0,1,21.59,39.06Z"/>
|
||||
<path d="M28.39,41.36V34.29h2.13l1.26,4.83L33,34.29h2.14v7.07H33.87V35.83L32.5,41.36H31.13l-1.37-5.53v5.53Z"/>
|
||||
<path d="M36.17,39.06,37.54,39A2.29,2.29,0,0,0,38,40a1.56,1.56,0,0,0,1,.28,1.77,1.77,0,0,0,1-.28.83.83,0,0,0,.33-.65.61.61,0,0,0-.16-.44,2.07,2.07,0,0,0-.49-.33c-.17-.06-.55-.17-1.16-.33A3.37,3.37,0,0,1,37,37.53a1.88,1.88,0,0,1-.65-1.43,1.51,1.51,0,0,1,.33-1,1.93,1.93,0,0,1,.87-.72A3.52,3.52,0,0,1,39,34.18a3.15,3.15,0,0,1,2,.61,2,2,0,0,1,.71,1.53l-1.42.06a1.25,1.25,0,0,0-.39-.77,1.33,1.33,0,0,0-.87-.22,1.81,1.81,0,0,0-1,.27.52.52,0,0,0-.22.44.51.51,0,0,0,.22.44,3.09,3.09,0,0,0,1.32.49,7.13,7.13,0,0,1,1.53.5,2,2,0,0,1,.77.71,2.44,2.44,0,0,1,.27,1.15,2.29,2.29,0,0,1-.33,1.15,1.8,1.8,0,0,1-.93.77,4.36,4.36,0,0,1-1.53.27A3.42,3.42,0,0,1,37,41,3,3,0,0,1,36.17,39.06Z"/>
|
||||
<path d="M57.14,37.2l-4.55-4.93a1.37,1.37,0,0,0-2.36,1.06v2.74H45.3v4h4.93v3a1.37,1.37,0,0,0,2.36,1.06l4.55-4.93A1.54,1.54,0,0,0,57.14,37.2Z"/>
|
||||
<path d="M18.09,36.07H10a1.78,1.78,0,0,0-1.69,1.34,2,2,0,0,0,1.62,2.66h8.18Z"/>
|
||||
<path d="M43.41,14.88H20.11a2,2,0,0,0-2,2V28.09h2.3V21.28a.27.27,0,0,1,.3-.3h22.1a.27.27,0,0,1,.3.3v6.81h2.3V16.88A2,2,0,0,0,43.41,14.88Zm-8.5,4.7h-6.1a.47.47,0,0,1-.5-.5c0-.3.1-.5.4-.5h6.1a.47.47,0,0,1,.5.5A.46.46,0,0,1,34.91,19.58Z"/>
|
||||
<path d="M43.11,47.38v6.9a.27.27,0,0,1-.3.3H20.71a.27.27,0,0,1-.3-.3v-6.9h-2.3v10.2a2,2,0,0,0,2,2h23.3a2,2,0,0,0,2-2V47.38Zm-11.3,11.1a1.5,1.5,0,1,1,1.5-1.5A1.47,1.47,0,0,1,31.81,58.48Z"/>
|
||||
</g>
|
||||
<g style="isolation: isolate">
|
||||
<path d="M23.61,37,25,36.93a2.3,2.3,0,0,0,.5,1,1.48,1.48,0,0,0,1,.33,1.85,1.85,0,0,0,1-.27.87.87,0,0,0,.33-.66.62.62,0,0,0-.17-.44,1.88,1.88,0,0,0-.49-.33l-1.15-.33a5,5,0,0,1-1.59-.71,1.84,1.84,0,0,1-.66-1.43,1.51,1.51,0,0,1,.33-1,1.87,1.87,0,0,1,.88-.71,3.31,3.31,0,0,1,1.37-.22,3.23,3.23,0,0,1,2,.6,2.06,2.06,0,0,1,.71,1.53l-1.43.06a1.27,1.27,0,0,0-.38-.77,1.42,1.42,0,0,0-.88-.22,1.7,1.7,0,0,0-1,.28.48.48,0,0,0-.22.43.48.48,0,0,0,.22.44,3.13,3.13,0,0,0,1.31.5,6.81,6.81,0,0,1,1.54.49,1.87,1.87,0,0,1,.76.71,2.33,2.33,0,0,1,.28,1.15A2.27,2.27,0,0,1,29,38.57a2.13,2.13,0,0,1-.93.77,4.37,4.37,0,0,1-1.54.27,3.32,3.32,0,0,1-2.08-.6A3.15,3.15,0,0,1,23.61,37Z" fill="#fff"/>
|
||||
<path d="M30.41,39.34V32.27h2.14l1.26,4.82,1.26-4.82H37.2v7.07H35.89V33.81l-1.37,5.53H33.15l-1.37-5.53v5.53Z" fill="#fff"/>
|
||||
<path d="M38.19,37l1.37-.11a2.4,2.4,0,0,0,.49,1,1.57,1.57,0,0,0,1.05.27,1.87,1.87,0,0,0,1-.27.85.85,0,0,0,.33-.66.64.64,0,0,0-.17-.44,1.66,1.66,0,0,0-.49-.32c-.17-.06-.55-.17-1.15-.33a3.27,3.27,0,0,1-1.59-.72,1.82,1.82,0,0,1-.66-1.42,1.55,1.55,0,0,1,.33-1,1.83,1.83,0,0,1,.88-.71A3.48,3.48,0,0,1,41,32.16a3.2,3.2,0,0,1,2,.6,2.07,2.07,0,0,1,.72,1.54l-1.43.05a1.24,1.24,0,0,0-.38-.76,1.37,1.37,0,0,0-.88-.22,1.81,1.81,0,0,0-1,.27.5.5,0,0,0-.21.44.51.51,0,0,0,.21.44,3.3,3.3,0,0,0,1.32.49,7.64,7.64,0,0,1,1.53.49,1.92,1.92,0,0,1,.77.72,2.32,2.32,0,0,1-.05,2.3,1.89,1.89,0,0,1-.93.77,4.42,4.42,0,0,1-1.54.27,3.36,3.36,0,0,1-2.08-.6A3,3,0,0,1,38.19,37Z" fill="#fff"/>
|
||||
<path d="M59.16,35.18l-4.54-4.93a1.37,1.37,0,0,0-2.36,1.06V34H47.32v4h4.94v3a1.37,1.37,0,0,0,2.36,1.06l4.54-4.93A1.54,1.54,0,0,0,59.16,35.18Z" fill="#fff"/>
|
||||
<path d="M20.11,34H12a1.79,1.79,0,0,0-1.7,1.35A2,2,0,0,0,11.94,38h8.17Z" fill="#fff"/>
|
||||
<path d="M45.43,12.86H22.13a2,2,0,0,0-2,2V26.07h2.3V19.26a.27.27,0,0,1,.3-.3h22.1a.27.27,0,0,1,.3.3v6.81h2.3V14.86A2,2,0,0,0,45.43,12.86Zm-8.5,4.7h-6.1a.47.47,0,0,1-.5-.5c0-.3.1-.5.4-.5h6.1a.47.47,0,0,1,.5.5A.46.46,0,0,1,36.93,17.56Z" fill="#fff"/>
|
||||
<path d="M45.13,45.35v6.91a.27.27,0,0,1-.3.3H22.73a.27.27,0,0,1-.3-.3V45.35h-2.3V55.56a2,2,0,0,0,2,2h23.3a2,2,0,0,0,2-2V45.35ZM33.83,56.46a1.5,1.5,0,1,1,1.5-1.5A1.47,1.47,0,0,1,33.83,56.46Z" fill="#fff"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
1
odoo-bringout-oca-ocb-sms/sms/static/img/sms_failure.svg
Normal file
1
odoo-bringout-oca-ocb-sms/sms/static/img/sms_failure.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512"><defs><path id="a" d="M91.974 123.535v-17.07c0-.839-.283-1.542-.851-2.111-.568-.57-1.24-.854-2.017-.854H71.894c-.777 0-1.449.285-2.017.854-.568.569-.851 1.272-.851 2.11v17.071c0 .839.283 1.542.851 2.111.568.57 1.24.854 2.017.854h17.212c.777 0 1.449-.285 2.017-.854.568-.569.851-1.272.851-2.11zm-.179-33.601l1.614-41.239c0-.718-.3-1.287-.897-1.707-.777-.659-1.494-.988-2.151-.988H70.639c-.657 0-1.374.33-2.151.988-.598.42-.897 1.048-.897 1.887l1.524 41.059c0 .599.3 1.093.897 1.482.597.39 1.314.584 2.151.584h16.584c.837 0 1.54-.195 2.107-.584.568-.39.881-.883.941-1.482zM90.54 6.02l68.846 126.5c2.092 3.773 2.032 7.546-.179 11.32a11.276 11.276 0 0 1-4.168 4.133 11.192 11.192 0 0 1-5.693 1.527H11.654c-2.032 0-3.93-.51-5.693-1.527a11.276 11.276 0 0 1-4.168-4.133c-2.211-3.774-2.271-7.547-.18-11.32L70.46 6.02a11.462 11.462 0 0 1 4.213-4.403A11.105 11.105 0 0 1 80.5 0c2.092 0 4.034.54 5.827 1.617A11.462 11.462 0 0 1 90.54 6.02z"/><path id="c" d="M91.974 123.535v-17.07c0-.839-.283-1.542-.851-2.111-.568-.57-1.24-.854-2.017-.854H71.894c-.777 0-1.449.285-2.017.854-.568.569-.851 1.272-.851 2.11v17.071c0 .839.283 1.542.851 2.111.568.57 1.24.854 2.017.854h17.212c.777 0 1.449-.285 2.017-.854.568-.569.851-1.272.851-2.11zm-.179-33.601l1.614-41.239c0-.718-.3-1.287-.897-1.707-.777-.659-1.494-.988-2.151-.988H70.639c-.657 0-1.374.33-2.151.988-.598.42-.897 1.048-.897 1.887l1.524 41.059c0 .599.3 1.093.897 1.482.597.39 1.314.584 2.151.584h16.584c.837 0 1.54-.195 2.107-.584.568-.39.881-.883.941-1.482zM90.54 6.02l68.846 126.5c2.092 3.773 2.032 7.546-.179 11.32a11.276 11.276 0 0 1-4.168 4.133 11.192 11.192 0 0 1-5.693 1.527H11.654c-2.032 0-3.93-.51-5.693-1.527a11.276 11.276 0 0 1-4.168-4.133c-2.211-3.774-2.271-7.547-.18-11.32L70.46 6.02a11.462 11.462 0 0 1 4.213-4.403A11.105 11.105 0 0 1 80.5 0c2.092 0 4.034.54 5.827 1.617A11.462 11.462 0 0 1 90.54 6.02z"/></defs><g fill="none" fill-rule="evenodd"><circle cx="256" cy="253" r="256" fill="#FDA20C"/><path fill="#000" fill-opacity=".3" fill-rule="nonzero" d="M361 98.292C361 90.982 353.978 85 345.396 85H163.604C155.022 85 148 90.981 148 98.292v296.416c0 7.31 7.022 13.292 15.604 13.292h181.792c8.582 0 15.604-5.981 15.604-13.292v-64.636c-5.462 3.323-11.703 6.646-17.945 9.305v33.399c0 1.329-.78 1.994-2.34 1.994h-172.43c-1.56 0-2.34-.665-2.34-1.994V126.87c0-1.329.78-1.993 2.34-1.993h172.43c1.56 0 2.34.664 2.34 1.993v63.113c3.901 0 8.582-.664 12.483-.664H361V98.292zM254.5 380c5.067 0 9.5 4.433 9.5 9.5s-4.433 9.5-9.5 9.5-9.5-4.433-9.5-9.5 4.433-9.5 9.5-9.5zm24.577-266.907h-47.593c-2.341 0-3.902-1.56-3.902-3.9s1.56-3.899 3.902-3.899h47.593c2.34 0 3.901 1.56 3.901 3.9 0 2.339-2.34 3.899-3.901 3.899z"/><path fill="#FFF" fill-rule="nonzero" d="M355.538 321.966c-3.9 0-8.582 0-12.483-.665v45.438c0 1.33-.78 1.996-2.34 1.996h-172.43c-1.56 0-2.34-.665-2.34-1.996V120.58c0-1.33.78-1.996 2.34-1.996h172.43c1.56 0 2.34.665 2.34 1.996v63.81c3.901 0 8.582-.666 12.483-.666H361V91.306C361 83.988 353.978 78 345.396 78H163.604C155.022 78 148 83.988 148 91.306v297.388c0 7.318 7.022 13.306 15.604 13.306h181.792c8.582 0 15.604-5.988 15.604-13.306v-66.728h-5.462zM230.703 98.871h47.594c2.34 0 3.9 1.56 3.9 3.901 0 2.34-1.56 3.902-3.12 3.902h-47.593c-2.341 0-3.902-1.561-3.902-3.902 0-2.34.78-3.901 3.121-3.901zM254.5 394c-5.067 0-9.5-4.433-9.5-9.5s4.433-9.5 9.5-9.5 9.5 4.433 9.5 9.5c0 5.7-4.433 9.5-9.5 9.5z"/><g opacity=".437" transform="translate(217 160)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g fill="#2F3136" mask="url(#b)"><path d="M0 0H161V161H0z"/></g></g><g transform="translate(217 149)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><g fill="#FFF" mask="url(#d)"><path d="M0 0H161V161H0z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-inherit="mail.NotificationGroup" t-inherit-mode="extension">
|
||||
<xpath expr="//*[hasclass('o_NotificationGroup_inlineText')]" position="inside">
|
||||
<t t-if="notificationGroupView.notificationGroup.notification_type === 'sms'">
|
||||
An error occurred when sending an SMS.
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { PhoneField } from "@web/views/fields/phone/phone_field";
|
||||
import { SendSMSButton } from '@sms/components/sms_button/sms_button';
|
||||
|
||||
patch(PhoneField, "sms.PhoneField", {
|
||||
components: {
|
||||
...PhoneField.components,
|
||||
SendSMSButton
|
||||
},
|
||||
defaultProps: {
|
||||
...PhoneField.defaultProps,
|
||||
enableButton: true,
|
||||
},
|
||||
props: {
|
||||
...PhoneField.props,
|
||||
enableButton: { type: Boolean, optional: true },
|
||||
},
|
||||
extractProps: ({ attrs }) => {
|
||||
return {
|
||||
enableButton: attrs.options.enable_sms,
|
||||
placeholder: attrs.placeholder,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-inherit="web.PhoneField" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o_phone_content')]//a" position="after">
|
||||
<t t-if="props.enableButton and props.value.length > 0">
|
||||
<SendSMSButton t-props="props" />
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-inherit="web.FormPhoneField" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o_phone_content')]" position="inside">
|
||||
<t t-if="props.enableButton and props.value.length > 0">
|
||||
<SendSMSButton t-props="props" />
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component , status } = owl;
|
||||
|
||||
export class SendSMSButton extends Component {
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
this.user = useService("user");
|
||||
this.title = this.env._t("Send SMS Text Message");
|
||||
}
|
||||
get phoneHref() {
|
||||
return "sms:" + this.props.value.replace(/\s+/g, "");
|
||||
}
|
||||
async onClick() {
|
||||
await this.props.record.save();
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
target: "new",
|
||||
name: this.title,
|
||||
res_model: "sms.composer",
|
||||
views: [[false, "form"]],
|
||||
context: {
|
||||
...this.user.context,
|
||||
default_res_model: this.props.record.resModel,
|
||||
default_res_id: this.props.record.resId,
|
||||
default_number_field_name: this.props.name,
|
||||
default_composition_mode: 'comment',
|
||||
}
|
||||
}, {
|
||||
onClose: () => {
|
||||
if (status(this) !== "destroyed") {
|
||||
this.props.record.load();
|
||||
this.props.record.model.notify();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
SendSMSButton.template = "sms.SendSMSButton";
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="sms.SendSMSButton" owl="1">
|
||||
<a
|
||||
t-att-title="title"
|
||||
t-att-href="phoneHref"
|
||||
t-on-click.prevent.stop="onClick"
|
||||
class="ms-3 d-inline-flex align-items-center o_field_phone_sms"
|
||||
><i class="fa fa-mobile"></i><small class="fw-bold ms-1">SMS</small></a>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import basic_fields from 'web.basic_fields';
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { EmojisTextField} from '@mail/views/fields/emojis_text_field/emojis_text_field';
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const DynamicPlaceholderFieldMixin = basic_fields.DynamicPlaceholderFieldMixin;
|
||||
/**
|
||||
* SmsWidget is a widget to display a textarea (the body) and a text representing
|
||||
* the number of SMS and the number of characters. This text is computed every
|
||||
* time the user changes the body.
|
||||
*/
|
||||
export class SmsWidget extends EmojisTextField {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.notification = useService('notification');
|
||||
}
|
||||
|
||||
get encoding() {
|
||||
return this._extractEncoding(this.props.value || '');
|
||||
}
|
||||
get nbrChar() {
|
||||
const content = this._getValueForSmsCounts(this.props.value || '');
|
||||
return content.length + (content.match(/\n/g) || []).length;
|
||||
}
|
||||
get nbrCharExplanation() {
|
||||
return '';
|
||||
}
|
||||
get nbrSMS() {
|
||||
return this._countSMS(this.nbrChar, this.encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a Model Field Selector in order to select fields
|
||||
* and create a dynamic placeholder string with or without
|
||||
* a default text value.
|
||||
*
|
||||
* @public
|
||||
* @param {String} baseModel
|
||||
* @param {Array} chain
|
||||
*
|
||||
*/
|
||||
async openDynamicPlaceholder(baseModel, chain = []) {
|
||||
const modelSelector = await this._openNewModelSelector(baseModel, chain);
|
||||
modelSelector.$el.css('margin-top', 4);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private: SMS
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Count the number of SMS of the content
|
||||
* @private
|
||||
* @returns {integer} Number of SMS
|
||||
*/
|
||||
_countSMS(nbrChar, encoding) {
|
||||
if (nbrChar === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (encoding === 'UNICODE') {
|
||||
if (nbrChar <= 70) {
|
||||
return 1;
|
||||
}
|
||||
return Math.ceil(nbrChar / 67);
|
||||
}
|
||||
if (nbrChar <= 160) {
|
||||
return 1;
|
||||
}
|
||||
return Math.ceil(nbrChar / 153);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the encoding depending on the characters in the content
|
||||
* @private
|
||||
* @param {String} content Content of the SMS
|
||||
* @returns {String} Encoding of the content (GSM7 or UNICODE)
|
||||
*/
|
||||
_extractEncoding(content) {
|
||||
if (String(content).match(RegExp("^[@£$¥èéùìòÇ\\nØø\\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\\\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà]*$"))) {
|
||||
return 'GSM7';
|
||||
}
|
||||
return 'UNICODE';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement if more characters are going to be sent then those appearing in
|
||||
* value, if that value is processed before being sent.
|
||||
* E.g., links are converted to trackers in mass_mailing_sms.
|
||||
*
|
||||
* Note: goes with an explanation in nbrCharExplanation
|
||||
*
|
||||
* @param {String} value content to be parsed for counting extra characters
|
||||
* @return string length-corrected value placeholder for the post-processed
|
||||
* state
|
||||
*/
|
||||
_getValueForSmsCounts(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
async onBlur() {
|
||||
var content = this.props.value || '';
|
||||
if( !content.trim().length && content.length > 0) {
|
||||
this.notification.add(
|
||||
this.env._t("Your SMS Text Message must include at least one non-whitespace character"),
|
||||
{ type: 'danger' },
|
||||
)
|
||||
await this.props.update(content.trim());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
async onInput(ev) {
|
||||
await this.props.update(this.targetEditElement.el.value);
|
||||
super.onInput(...arguments);
|
||||
const key = ev.originalEvent ? ev.originalEvent.data : '';
|
||||
if (this.props.dynamicPlaceholder && key === this.DYNAMIC_PLACEHOLDER_TRIGGER_KEY) {
|
||||
const baseModel = this.recordData && this.recordData.mailing_model_real ? this.recordData.mailing_model_real : undefined;
|
||||
if (baseModel) {
|
||||
this.openDynamicPlaceholder(baseModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
SmsWidget.template = 'sms.SmsWidget';
|
||||
SmsWidget.additionalClasses = [...(EmojisTextField.additionalClasses || []), 'o_field_text'];
|
||||
patch(SmsWidget.prototype, 'sms_widget_dynamic_placeholder_field_mixin', DynamicPlaceholderFieldMixin);
|
||||
registry.category("fields").add("sms_widget", SmsWidget);
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="sms.SmsWidget" t-inherit="mail.EmojisTextField" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//textarea[1]" position="attributes">
|
||||
<attribute name="t-on-blur">onBlur</attribute>
|
||||
</xpath>
|
||||
<xpath expr="/*[last()]/*[last()]" position="after">
|
||||
<div class="o_sms_container">
|
||||
<span class="text-muted o_sms_count">
|
||||
<t t-out="nbrChar"/> characters<t t-out="nbrCharExplanation"/>, fits in <t t-out="nbrSMS"/> SMS (<t t-out="encoding"/>)
|
||||
<a href="https://iap-services.odoo.com/iap/sms/pricing" target="_blank"
|
||||
title="SMS Pricing" aria-label="SMS Pricing" class="fa fa-lg fa-info-circle"/>
|
||||
</span>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
odoo.define('sms.fields', function (require) {
|
||||
"use strict";
|
||||
|
||||
var basic_fields = require('web.basic_fields');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
/**
|
||||
* Override of FieldPhone to add a button calling SMS composer if option activated (default)
|
||||
*/
|
||||
|
||||
var Phone = basic_fields.FieldPhone;
|
||||
Phone.include({
|
||||
/**
|
||||
* By default, enable_sms is activated
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
init() {
|
||||
this._super.apply(this, arguments);
|
||||
this.enableSMS = 'enable_sms' in this.attrs.options ? this.attrs.options.enable_sms : true;
|
||||
// reinject in nodeOptions (and thus in this.attrs) to signal the property
|
||||
this.attrs.options.enable_sms = this.enableSMS;
|
||||
},
|
||||
/**
|
||||
* When the send SMS button is displayed, $el becomes a div wrapping
|
||||
* the original links.
|
||||
* This method makes sure we always focus the phone number
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
getFocusableElement() {
|
||||
if (this.enableSMS && this.mode === 'readonly') {
|
||||
return this.$el.filter('.' + this.className).find('a');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Open SMS composer wizard
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onClickSMS: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
var context = session.user_context;
|
||||
context = _.extend({}, context, {
|
||||
default_res_model: this.model,
|
||||
default_res_id: parseInt(this.res_id),
|
||||
default_number_field_name: this.name,
|
||||
default_composition_mode: 'comment',
|
||||
});
|
||||
var self = this;
|
||||
return this.do_action({
|
||||
title: _t('Send SMS Text Message'),
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'sms.composer',
|
||||
target: 'new',
|
||||
views: [[false, 'form']],
|
||||
context: context,
|
||||
}, {
|
||||
on_close: function () {
|
||||
self.trigger_up('reload');
|
||||
}});
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a button to call the composer wizard
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_renderReadonly: function () {
|
||||
var def = this._super.apply(this, arguments);
|
||||
if (this.enableSMS && this.value) {
|
||||
var $composerButton = $('<a>', {
|
||||
title: _t('Send SMS Text Message'),
|
||||
href: '',
|
||||
class: 'ms-3 d-inline-flex align-items-center o_field_phone_sms',
|
||||
html: $('<small>', {class: 'fw-bold ms-1', html: 'SMS'}),
|
||||
});
|
||||
$composerButton.prepend($('<i>', {class: 'fa fa-mobile'}));
|
||||
$composerButton.on('click', this._onClickSMS.bind(this));
|
||||
this.$el = this.$el.add($composerButton);
|
||||
}
|
||||
return def;
|
||||
},
|
||||
});
|
||||
|
||||
return Phone;
|
||||
|
||||
});
|
||||
26
odoo-bringout-oca-ocb-sms/sms/static/src/models/message.js
Normal file
26
odoo-bringout-oca-ocb-sms/sms/static/src/models/message.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerPatch } from '@mail/model/model_core';
|
||||
|
||||
registerPatch({
|
||||
name: 'Message',
|
||||
recordMethods: {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
openResendAction() {
|
||||
if (this.message_type === 'sms') {
|
||||
this.env.services.action.doAction(
|
||||
'sms.sms_resend_action',
|
||||
{
|
||||
additionalContext: {
|
||||
default_mail_message_id: this.id,
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
this._super(...arguments);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerPatch } from '@mail/model/model_core';
|
||||
|
||||
registerPatch({
|
||||
name: 'MessageView',
|
||||
fields: {
|
||||
failureNotificationIconClassName: {
|
||||
compute() {
|
||||
if (this.message && this.message.message_type === 'sms') {
|
||||
return 'fa fa-mobile';
|
||||
}
|
||||
return this._super();
|
||||
},
|
||||
},
|
||||
failureNotificationIconLabel: {
|
||||
compute() {
|
||||
if (this.message && this.message.message_type === 'sms') {
|
||||
return this.env._t("SMS");
|
||||
}
|
||||
return this._super();
|
||||
},
|
||||
},
|
||||
notificationIconClassName: {
|
||||
compute() {
|
||||
if (this.message && this.message.message_type === 'sms') {
|
||||
return 'fa fa-mobile';
|
||||
}
|
||||
return this._super();
|
||||
},
|
||||
},
|
||||
notificationIconLabel: {
|
||||
compute() {
|
||||
if (this.message && this.message.message_type === 'sms') {
|
||||
return this.env._t("SMS");
|
||||
}
|
||||
return this._super();
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerPatch } from '@mail/model/model_core';
|
||||
|
||||
registerPatch({
|
||||
name: 'NotificationGroup',
|
||||
recordMethods: {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_openDocuments() {
|
||||
if (this.notification_type !== 'sms') {
|
||||
return this._super(...arguments);
|
||||
}
|
||||
this.env.services.action.doAction({
|
||||
name: this.env._t("SMS Failures"),
|
||||
type: 'ir.actions.act_window',
|
||||
view_mode: 'kanban,list,form',
|
||||
views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
|
||||
target: 'current',
|
||||
res_model: this.res_model,
|
||||
domain: [['message_has_sms_error', '=', true]],
|
||||
context: { create: false },
|
||||
});
|
||||
if (this.messaging.device.isSmall) {
|
||||
// messaging menu has a higher z-index than views so it must
|
||||
// be closed to ensure the visibility of the view
|
||||
this.messaging.messagingMenu.close();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerPatch } from '@mail/model/model_core';
|
||||
|
||||
registerPatch({
|
||||
name: 'NotificationGroupView',
|
||||
fields: {
|
||||
imageSrc: {
|
||||
compute() {
|
||||
if (this.notificationGroup.notification_type === 'sms') {
|
||||
return '/sms/static/img/sms_failure.svg';
|
||||
}
|
||||
return this._super();
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { makeDeferred } from '@mail/utils/deferred';
|
||||
import {
|
||||
afterNextRender,
|
||||
start,
|
||||
startServer,
|
||||
} from '@mail/../tests/helpers/test_utils';
|
||||
|
||||
import { patchWithCleanup } from '@web/../tests/helpers/utils';
|
||||
|
||||
QUnit.module('sms', {}, function () {
|
||||
QUnit.module('components', {}, function () {
|
||||
QUnit.module('message_tests.js');
|
||||
|
||||
QUnit.test('Notification Sent', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Someone", partner_share: true });
|
||||
const mailMessageId1 = pyEnv['mail.message'].create({
|
||||
body: 'not empty',
|
||||
message_type: 'sms',
|
||||
model: 'res.partner',
|
||||
res_id: resPartnerId1,
|
||||
});
|
||||
pyEnv['mail.notification'].create({
|
||||
mail_message_id: mailMessageId1,
|
||||
notification_status: 'sent',
|
||||
notification_type: 'sms',
|
||||
res_partner_id: resPartnerId1,
|
||||
});
|
||||
const { openFormView } = await start();
|
||||
await openFormView({
|
||||
res_id: resPartnerId1,
|
||||
res_model: 'res.partner',
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Message',
|
||||
"should display a message component"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Message_notificationIconClickable',
|
||||
"should display the notification icon container"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Message_notificationIcon',
|
||||
"should display the notification icon"
|
||||
);
|
||||
assert.hasClass(
|
||||
document.querySelector('.o_Message_notificationIcon'),
|
||||
'fa-mobile',
|
||||
"icon should represent sms"
|
||||
);
|
||||
|
||||
await afterNextRender(() => {
|
||||
document.querySelector('.o_Message_notificationIconClickable').click();
|
||||
});
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_MessageNotificationPopoverContent',
|
||||
"notification popover should be open"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_MessageNotificationPopoverContent_notificationIcon',
|
||||
"popover should have one icon"
|
||||
);
|
||||
assert.hasClass(
|
||||
document.querySelector('.o_MessageNotificationPopoverContent_notificationIcon'),
|
||||
'fa-check',
|
||||
"popover should have the sent icon"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_MessageNotificationPopoverContent_notificationPartnerName',
|
||||
"popover should have the partner name"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_MessageNotificationPopoverContent_notificationPartnerName').textContent.trim(),
|
||||
"Someone",
|
||||
"partner name should be correct"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('Notification Error', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const openResendActionDef = makeDeferred();
|
||||
const pyEnv = await startServer();
|
||||
const resPartnerId1 = pyEnv['res.partner'].create({ name: "Someone", partner_share: true });
|
||||
const mailMessageId1 = pyEnv['mail.message'].create({
|
||||
body: 'not empty',
|
||||
message_type: 'sms',
|
||||
model: 'res.partner',
|
||||
res_id: resPartnerId1,
|
||||
});
|
||||
pyEnv['mail.notification'].create({
|
||||
mail_message_id: mailMessageId1,
|
||||
notification_status: 'exception',
|
||||
notification_type: 'sms',
|
||||
res_partner_id: resPartnerId1,
|
||||
});
|
||||
const { env, openFormView } = await start();
|
||||
await openFormView({
|
||||
res_id: resPartnerId1,
|
||||
res_model: 'res.partner',
|
||||
});
|
||||
patchWithCleanup(env.services.action, {
|
||||
doAction(action, options) {
|
||||
assert.step('do_action');
|
||||
assert.strictEqual(
|
||||
action,
|
||||
'sms.sms_resend_action',
|
||||
"action should be the one to resend sms"
|
||||
);
|
||||
assert.strictEqual(
|
||||
options.additionalContext.default_mail_message_id,
|
||||
mailMessageId1,
|
||||
"action should have correct message id"
|
||||
);
|
||||
openResendActionDef.resolve();
|
||||
},
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Message',
|
||||
"should display a message component"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Message_notificationIconClickable',
|
||||
"should display the notification icon container"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_Message_notificationIcon',
|
||||
"should display the notification icon"
|
||||
);
|
||||
assert.hasClass(
|
||||
document.querySelector('.o_Message_notificationIcon'),
|
||||
'fa-mobile',
|
||||
"icon should represent sms"
|
||||
);
|
||||
document.querySelector('.o_Message_notificationIconClickable').click();
|
||||
await openResendActionDef;
|
||||
assert.verifySteps(
|
||||
['do_action'],
|
||||
"should do an action to display the resend sms dialog"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { start, startServer } from '@mail/../tests/helpers/test_utils';
|
||||
|
||||
import { patchWithCleanup } from '@web/../tests/helpers/utils';
|
||||
|
||||
QUnit.module('sms', {}, function () {
|
||||
QUnit.module('components', {}, function () {
|
||||
QUnit.module('notification_list', {}, function () {
|
||||
QUnit.module('notification_list_notification_group_tests.js');
|
||||
|
||||
QUnit.test('mark as read', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const mailChannelId1 = pyEnv['mail.channel'].create({});
|
||||
const mailMessageId1 = pyEnv['mail.message'].create(
|
||||
// message that is expected to have a failure
|
||||
{
|
||||
author_id: pyEnv.currentPartnerId,
|
||||
message_type: 'sms',
|
||||
model: 'mail.channel',
|
||||
res_id: mailChannelId1,
|
||||
}
|
||||
);
|
||||
pyEnv['mail.notification'].create(
|
||||
// failure that is expected to be used in the test
|
||||
{
|
||||
mail_message_id: mailMessageId1, // id of the related message
|
||||
notification_status: 'exception', // necessary value to have a failure
|
||||
notification_type: 'sms',
|
||||
}
|
||||
);
|
||||
const { afterNextRender, click } = await start();
|
||||
await click('.o_MessagingMenu_toggler');
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_NotificationGroup_markAsRead',
|
||||
"should have 1 mark as read button"
|
||||
);
|
||||
|
||||
await afterNextRender(() => {
|
||||
document.querySelector('.o_NotificationGroup_markAsRead').click();
|
||||
});
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_NotificationGroup',
|
||||
"should have no notification group"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('notifications grouped by notification_type', async function (assert) {
|
||||
assert.expect(11);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const resPartnerId1 = pyEnv['res.partner'].create({});
|
||||
const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([
|
||||
{
|
||||
message_type: 'sms', // different type from second message
|
||||
model: 'res.partner', // same model as second message (and not `mail.channel`)
|
||||
res_id: resPartnerId1, // same res_id as second message
|
||||
res_model_name: "Partner", // random related model name
|
||||
},
|
||||
{
|
||||
message_type: 'email', // different type from first message
|
||||
model: 'res.partner', // same model as first message (and not `mail.channel`)
|
||||
res_id: resPartnerId1, // same res_id as first message
|
||||
res_model_name: "Partner", // same related model name for consistency
|
||||
},
|
||||
]);
|
||||
pyEnv['mail.notification'].create([
|
||||
{
|
||||
mail_message_id: mailMessageId1, // id of the related first message
|
||||
notification_status: 'exception', // necessary value to have a failure
|
||||
notification_type: 'sms', // different type from second failure
|
||||
},
|
||||
{
|
||||
mail_message_id: mailMessageId1,
|
||||
notification_status: 'exception',
|
||||
notification_type: 'sms',
|
||||
},
|
||||
{
|
||||
mail_message_id: mailMessageId2, // id of the related second message
|
||||
notification_status: 'exception', // necessary value to have a failure
|
||||
notification_type: 'email', // different type from first failure
|
||||
},
|
||||
{
|
||||
mail_message_id: mailMessageId2,
|
||||
notification_status: 'exception',
|
||||
notification_type: 'email',
|
||||
},
|
||||
]);
|
||||
const { click } = await start();
|
||||
await click('.o_MessagingMenu_toggler');
|
||||
|
||||
assert.containsN(
|
||||
document.body,
|
||||
'.o_NotificationGroup',
|
||||
2,
|
||||
"should have 2 notifications group"
|
||||
);
|
||||
const groups = document.querySelectorAll('.o_NotificationGroup');
|
||||
assert.containsOnce(
|
||||
groups[0],
|
||||
'.o_NotificationGroup_name',
|
||||
"should have 1 group name in first group"
|
||||
);
|
||||
assert.strictEqual(
|
||||
groups[0].querySelector('.o_NotificationGroup_name').textContent,
|
||||
"Partner",
|
||||
"should have model name as group name"
|
||||
);
|
||||
assert.containsOnce(
|
||||
groups[0],
|
||||
'.o_NotificationGroup_counter',
|
||||
"should have 1 group counter in first group"
|
||||
);
|
||||
assert.strictEqual(
|
||||
groups[0].querySelector('.o_NotificationGroup_counter').textContent.trim(),
|
||||
"(2)",
|
||||
"should have 2 notifications in first group"
|
||||
);
|
||||
assert.strictEqual(
|
||||
groups[0].querySelector('.o_NotificationGroup_inlineText').textContent.trim(),
|
||||
"An error occurred when sending an email.",
|
||||
"should have the group text corresponding to email"
|
||||
);
|
||||
assert.containsOnce(
|
||||
groups[1],
|
||||
'.o_NotificationGroup_name',
|
||||
"should have 1 group name in second group"
|
||||
);
|
||||
assert.strictEqual(
|
||||
groups[1].querySelector('.o_NotificationGroup_name').textContent,
|
||||
"Partner",
|
||||
"should have second model name as group name"
|
||||
);
|
||||
assert.containsOnce(
|
||||
groups[1],
|
||||
'.o_NotificationGroup_counter',
|
||||
"should have 1 group counter in second group"
|
||||
);
|
||||
assert.strictEqual(
|
||||
groups[1].querySelector('.o_NotificationGroup_counter').textContent.trim(),
|
||||
"(2)",
|
||||
"should have 2 notifications in second group"
|
||||
);
|
||||
assert.strictEqual(
|
||||
groups[1].querySelector('.o_NotificationGroup_inlineText').textContent.trim(),
|
||||
"An error occurred when sending an SMS.",
|
||||
"should have the group text corresponding to sms"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('grouped notifications by document model', async function (assert) {
|
||||
// If all failures linked to a document model refers to different documents,
|
||||
// a single notification should group all failures that are linked to this
|
||||
// document model.
|
||||
assert.expect(12);
|
||||
|
||||
const pyEnv = await startServer();
|
||||
const [mailMessageId1, mailMessageId2] = pyEnv['mail.message'].create([
|
||||
// first message that is expected to have a failure
|
||||
{
|
||||
message_type: 'sms', // message must be sms (goal of the test)
|
||||
model: 'res.partner', // same model as second message (and not `mail.channel`)
|
||||
res_id: 31, // different res_id from second message
|
||||
res_model_name: "Partner", // random related model name
|
||||
},
|
||||
// second message that is expected to have a failure
|
||||
{
|
||||
message_type: 'sms', // message must be sms (goal of the test)
|
||||
model: 'res.partner', // same model as first message (and not `mail.channel`)
|
||||
res_id: 32, // different res_id from first message
|
||||
res_model_name: "Partner", // same related model name for consistency
|
||||
},
|
||||
]);
|
||||
pyEnv['mail.notification'].create([
|
||||
// first failure that is expected to be used in the test
|
||||
{
|
||||
mail_message_id: mailMessageId1, // id of the related first message
|
||||
notification_status: 'exception', // necessary value to have a failure
|
||||
notification_type: 'sms', // expected failure type for sms message
|
||||
},
|
||||
// second failure that is expected to be used in the test
|
||||
{
|
||||
mail_message_id: mailMessageId2, // id of the related second message
|
||||
notification_status: 'exception', // necessary value to have a failure
|
||||
notification_type: 'sms', // expected failure type for sms message
|
||||
},
|
||||
]);
|
||||
const { click, env } = await start();
|
||||
patchWithCleanup(env.services.action, {
|
||||
doAction(action) {
|
||||
assert.step('do_action');
|
||||
assert.strictEqual(
|
||||
action.name,
|
||||
"SMS Failures",
|
||||
"action should have 'SMS Failures' as name",
|
||||
);
|
||||
assert.strictEqual(
|
||||
action.type,
|
||||
'ir.actions.act_window',
|
||||
"action should have the type act_window"
|
||||
);
|
||||
assert.strictEqual(
|
||||
action.view_mode,
|
||||
'kanban,list,form',
|
||||
"action should have 'kanban,list,form' as view_mode"
|
||||
);
|
||||
assert.strictEqual(
|
||||
JSON.stringify(action.views),
|
||||
JSON.stringify([[false, 'kanban'], [false, 'list'], [false, 'form']]),
|
||||
"action should have correct views"
|
||||
);
|
||||
assert.strictEqual(
|
||||
action.target,
|
||||
'current',
|
||||
"action should have 'current' as target"
|
||||
);
|
||||
assert.strictEqual(
|
||||
action.res_model,
|
||||
'res.partner',
|
||||
"action should have the group model as res_model"
|
||||
);
|
||||
assert.strictEqual(
|
||||
JSON.stringify(action.domain),
|
||||
JSON.stringify([['message_has_sms_error', '=', true]]),
|
||||
"action should have 'message_has_sms_error' as domain"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
await click('.o_MessagingMenu_toggler');
|
||||
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_NotificationGroup',
|
||||
"should have 1 notification group"
|
||||
);
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_NotificationGroup_counter',
|
||||
"should have 1 group counter"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector('.o_NotificationGroup_counter').textContent.trim(),
|
||||
"(2)",
|
||||
"should have 2 notifications in the group"
|
||||
);
|
||||
|
||||
document.querySelector('.o_NotificationGroup').click();
|
||||
assert.verifySteps(
|
||||
['do_action'],
|
||||
"should do an action to display the related records"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { click, editInput, getFixture, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module(
|
||||
"fields",
|
||||
{
|
||||
beforeEach: function () {
|
||||
serverData = {
|
||||
models: {
|
||||
partner: {
|
||||
fields: {
|
||||
message: { string: "message", type: "text" },
|
||||
foo: { string: "Foo", type: "char", default: "My little Foo Value" },
|
||||
mobile: { string: "mobile", type: "text" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
message: "",
|
||||
foo: "yop",
|
||||
mobile: "+32494444444",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
message: "",
|
||||
foo: "bayou",
|
||||
},
|
||||
],
|
||||
},
|
||||
visitor: {
|
||||
fields: {
|
||||
mobile: { string: "mobile", type: "text" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
mobile: "+32494444444",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
target = getFixture();
|
||||
},
|
||||
},
|
||||
function () {
|
||||
QUnit.module("SmsButton");
|
||||
|
||||
QUnit.test("Sms button in form view", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "visitor",
|
||||
resId: 1,
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="mobile" widget="phone"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(
|
||||
target.querySelector(".o_field_phone"),
|
||||
".o_field_phone_sms",
|
||||
"the button is present"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Sms button with option enable_sms set as False", async function (assert) {
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "visitor",
|
||||
resId: 1,
|
||||
serverData,
|
||||
mode: "readonly",
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="mobile" widget="phone" options="{'enable_sms': false}"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
assert.containsNone(
|
||||
target.querySelector(".o_field_phone"),
|
||||
".o_field_phone_sms",
|
||||
"the button is not present"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"click on the sms button while creating a new record in a FormView",
|
||||
async function (assert) {
|
||||
const form = await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="foo"/>
|
||||
<field name="mobile" widget="phone"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
patchWithCleanup(form.env.services.action, {
|
||||
doAction: (action, options) => {
|
||||
assert.strictEqual(action.type, "ir.actions.act_window");
|
||||
assert.strictEqual(action.res_model, "sms.composer");
|
||||
options.onClose();
|
||||
},
|
||||
});
|
||||
await editInput(target, "[name='foo'] input", "John");
|
||||
await editInput(target, "[name='mobile'] input", "+32494444411");
|
||||
|
||||
await click(target, ".o_field_phone_sms", true);
|
||||
assert.strictEqual(target.querySelector("[name='foo'] input").value, "John");
|
||||
assert.strictEqual(
|
||||
target.querySelector("[name='mobile'] input").value,
|
||||
"+32494444411"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"click on the sms button in a FormViewDialog has no effect on the main form view",
|
||||
async function (assert) {
|
||||
serverData.models.partner.fields.partner_ids = {
|
||||
string: "one2many partners field",
|
||||
type: "one2many",
|
||||
relation: "partner",
|
||||
};
|
||||
|
||||
const form = await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData,
|
||||
arch: /* xml */ `
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="foo"/>
|
||||
<field name="mobile" widget="phone"/>
|
||||
<field name="partner_ids">
|
||||
<kanban>
|
||||
<field name="display_name"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div><t t-esc="record.display_name"/></div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
patchWithCleanup(form.env.services.action, {
|
||||
doAction: (action, options) => {
|
||||
assert.strictEqual(action.type, "ir.actions.act_window");
|
||||
assert.strictEqual(action.res_model, "sms.composer");
|
||||
options.onClose();
|
||||
},
|
||||
});
|
||||
await editInput(target, "[name='foo'] input", "John");
|
||||
await editInput(target, "[name='mobile'] input", "+32494444411");
|
||||
|
||||
await click(target, "[name='partner_ids'] .o-kanban-button-new");
|
||||
assert.containsOnce(target, ".modal");
|
||||
|
||||
const modal = target.querySelector(".modal");
|
||||
await editInput(modal, "[name='foo'] input", "Max");
|
||||
await editInput(modal, "[name='mobile'] input", "+324955555");
|
||||
|
||||
await click(modal, ".o_field_phone_sms", true);
|
||||
assert.strictEqual(modal.querySelector("[name='foo'] input").value, "Max");
|
||||
assert.strictEqual(
|
||||
modal.querySelector("[name='mobile'] input").value,
|
||||
"+324955555"
|
||||
);
|
||||
|
||||
await click(modal, ".o_form_button_cancel");
|
||||
assert.strictEqual(target.querySelector("[name='foo'] input").value, "John");
|
||||
assert.strictEqual(
|
||||
target.querySelector("[name='mobile'] input").value,
|
||||
"+32494444411"
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
225
odoo-bringout-oca-ocb-sms/sms/static/tests/sms_widget_test.js
Normal file
225
odoo-bringout-oca-ocb-sms/sms/static/tests/sms_widget_test.js
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import FormView from 'web.FormView';
|
||||
import ListView from 'web.ListView';
|
||||
import testUtils from 'web.test_utils';
|
||||
import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
|
||||
import { getFixture } from "@web/../tests/helpers/utils";
|
||||
|
||||
const createView = testUtils.createView;
|
||||
|
||||
QUnit.module('fields', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
partner: {
|
||||
fields: {
|
||||
message: {string: "message", type: "text"},
|
||||
foo: {string: "Foo", type: "char", default: "My little Foo Value"},
|
||||
mobile: {string: "mobile", type: "text"},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
message: "",
|
||||
foo: 'yop',
|
||||
mobile: "+32494444444",
|
||||
}, {
|
||||
id: 2,
|
||||
message: "",
|
||||
foo: 'bayou',
|
||||
}]
|
||||
},
|
||||
visitor: {
|
||||
fields: {
|
||||
mobile: {string: "mobile", type: "text"},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
mobile: "+32494444444",
|
||||
}]
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
this.target = getFixture();
|
||||
}
|
||||
}, function () {
|
||||
|
||||
QUnit.module('SmsWidget');
|
||||
|
||||
QUnit.test('Sms widgets are correctly rendered', async function (assert) {
|
||||
assert.expect(9);
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
serverData: { models: this.data },
|
||||
arch: /* xml */ `<form><sheet><field name="message" widget="sms_widget"/></sheet></form>`,
|
||||
});
|
||||
|
||||
assert.containsOnce(this.target, '.o_sms_count', "Should have a sms counter");
|
||||
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '0 characters, fits in 0 SMS (GSM7) ',
|
||||
'Should be "0 characters, fits in 0 SMS (GSM7) " by default');
|
||||
// GSM-7
|
||||
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), "Hello from Odoo", 'input');
|
||||
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '15 characters, fits in 1 SMS (GSM7) ',
|
||||
'Should be "15 characters, fits in 1 SMS (GSM7) " for "Hello from Odoo"');
|
||||
// GSM-7 with \n => this one count as 2 characters
|
||||
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), "Hello from Odoo\n", 'input');
|
||||
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '17 characters, fits in 1 SMS (GSM7) ',
|
||||
'Should be "17 characters, fits in 1 SMS (GSM7) " for "Hello from Odoo\\n"');
|
||||
// Unicode => ê
|
||||
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), "Hêllo from Odoo", 'input');
|
||||
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '15 characters, fits in 1 SMS (UNICODE) ',
|
||||
'Should be "15 characters, fits in 1 SMS (UNICODE) " for "Hêllo from Odoo"');
|
||||
// GSM-7 with 160c
|
||||
var text = Array(161).join('a');
|
||||
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), text, 'input');
|
||||
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '160 characters, fits in 1 SMS (GSM7) ',
|
||||
'Should be "160 characters, fits in 1 SMS (GSM7) " for 160 x "a"');
|
||||
// GSM-7 with 161c
|
||||
text = Array(162).join('a');
|
||||
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), text, 'input');
|
||||
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '161 characters, fits in 2 SMS (GSM7) ',
|
||||
'Should be "161 characters, fits in 2 SMS (GSM7) " for 161 x "a"');
|
||||
// Unicode with 70c
|
||||
text = Array(71).join('ê');
|
||||
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), text, 'input');
|
||||
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '70 characters, fits in 1 SMS (UNICODE) ',
|
||||
'Should be "70 characters, fits in 1 SMS (UNICODE) " for 70 x "ê"');
|
||||
// Unicode with 71c
|
||||
text = Array(72).join('ê');
|
||||
await testUtils.fields.editAndTrigger(this.target.querySelector('.o_input'), text, 'input');
|
||||
assert.strictEqual(this.target.querySelector('.o_sms_count').textContent, '71 characters, fits in 2 SMS (UNICODE) ',
|
||||
'Should be "71 characters, fits in 2 SMS (UNICODE) " for 71 x "ê"');
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('Sms widgets with non-empty initial value', async function (assert) {
|
||||
assert.expect(1);
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "visitor",
|
||||
resId: 1,
|
||||
serverData: { models: this.data },
|
||||
arch: /* xml */ `<form><sheet><field name="mobile" widget="sms_widget" readonly="true"/></sheet></form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(this.target.querySelector('.o_field_text span').textContent, '+32494444444',
|
||||
'Should have the initial value');
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('Sms widgets with empty initial value', async function (assert) {
|
||||
assert.expect(1);
|
||||
await makeView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: 1,
|
||||
serverData: { models: this.data },
|
||||
arch: /* xml */ `<form><sheet><field name="message" widget="sms_widget" readonly="true"/></sheet></form>`,
|
||||
});
|
||||
|
||||
assert.strictEqual(this.target.querySelector('.o_field_text span').textContent, '',
|
||||
'Should have the empty initial value');
|
||||
|
||||
});
|
||||
|
||||
QUnit.module('PhoneWidget');
|
||||
|
||||
QUnit.test('phone field in editable list view on normal screens', async function (assert) {
|
||||
assert.expect(11);
|
||||
var doActionCount = 0;
|
||||
|
||||
var list = await createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
debug:true,
|
||||
arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>',
|
||||
intercepts: {
|
||||
do_action(ev) {
|
||||
assert.equal(ev.data.action.res_model, 'sms.composer',
|
||||
'The action to send an SMS should have been executed');
|
||||
doActionCount += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.containsN(list, 'tbody td:not(.o_list_record_selector)', 4);
|
||||
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'yopSMS',
|
||||
"value should be displayed properly with a link to send SMS");
|
||||
|
||||
assert.containsN(list, 'div.o_field_widget.o_form_uri.o_field_phone > a', 2,
|
||||
"should have the correct classnames");
|
||||
|
||||
// Edit a line and check the result
|
||||
var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
|
||||
await testUtils.dom.click($cell);
|
||||
assert.hasClass($cell.parent(),'o_selected_row', 'should be set as edit mode');
|
||||
assert.strictEqual($cell.find('input').val(), 'yop',
|
||||
'should have the corect value in internal input');
|
||||
await testUtils.fields.editInput($cell.find('input'), 'new');
|
||||
|
||||
// save
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
|
||||
$cell = list.$('tbody td:not(.o_list_record_selector)').first();
|
||||
assert.doesNotHaveClass($cell.parent(), 'o_selected_row', 'should not be in edit mode anymore');
|
||||
assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'newSMS',
|
||||
"value should be properly updated");
|
||||
assert.containsN(list, 'div.o_field_widget.o_form_uri.o_field_phone > a', 2,
|
||||
"should still have links with correct classes");
|
||||
|
||||
await testUtils.dom.click(list.$('tbody td:not(.o_list_record_selector) .o_field_phone_sms').first());
|
||||
assert.equal(doActionCount, 1, 'Only one action should have been executed');
|
||||
assert.containsNone(list, '.o_selected_row',
|
||||
'None of the list element should have been activated');
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('readonly sms phone field is properly rerendered after been changed by onchange', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const NEW_PHONE = '+32595555555';
|
||||
|
||||
const form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="foo" on_change="1"/>' + // onchange to update mobile in readonly mode directly
|
||||
'<field name="mobile" widget="phone" readonly="1"/>' + // readonly only, we don't want to go through write mode
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {mode: 'edit'},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'onchange') {
|
||||
return Promise.resolve({
|
||||
value: {
|
||||
mobile: NEW_PHONE, // onchange to update mobile in readonly mode directly
|
||||
},
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
// check initial rendering
|
||||
assert.strictEqual(form.$('.o_field_phone').text(), "+32494444444",
|
||||
'Initial Phone text should be set');
|
||||
assert.strictEqual(form.$('.o_field_phone_sms').text(), 'SMS',
|
||||
'SMS button label should be rendered');
|
||||
|
||||
// trigger the onchange to update phone field, but still in readonly mode
|
||||
await testUtils.fields.editInput($('input[name="foo"]'), 'someOtherFoo');
|
||||
|
||||
// check rendering after changes
|
||||
assert.strictEqual(form.$('.o_field_phone').text(), NEW_PHONE,
|
||||
'Phone text should be updated');
|
||||
assert.strictEqual(form.$('.o_field_phone_sms').text(), 'SMS',
|
||||
'SMS button label should not be changed');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
6
odoo-bringout-oca-ocb-sms/sms/tests/__init__.py
Normal file
6
odoo-bringout-oca-ocb-sms/sms/tests/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
from . import test_sms_composer
|
||||
from . import test_sms_template
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue