mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-19 23:42:08 +02:00
Initial commit: Mail packages
This commit is contained in:
commit
4e53507711
1948 changed files with 751201 additions and 0 deletions
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();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue