mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-21 01:42:09 +02:00
19.0 vanilla
This commit is contained in:
parent
5df8c07b59
commit
daa394e8b0
2114 changed files with 564841 additions and 299642 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 1.6 KiB |
|
|
@ -1,41 +1 @@
|
|||
<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>
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M13 8a4 4 0 0 1 4-4h16a4 4 0 0 1 4 4v34a4 4 0 0 1-4 4H17a4 4 0 0 1-4-4V8Z" fill="#1AD3BB"/><path d="M37 19v18a9 9 0 1 1 0-18Z" fill="#1A6F66"/><path d="M13 31V13a9 9 0 1 1 0 18Z" fill="#005E7A"/><path d="M13 13H4v9a9 9 0 0 0 9 9V13Z" fill="#985184"/><path d="M37 37h9v-9a9 9 0 0 0-9-9v18Z" fill="#FC868B"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 405 B |
BIN
odoo-bringout-oca-ocb-sms/sms/static/description/icon_hi.png
Normal file
BIN
odoo-bringout-oca-ocb-sms/sms/static/description/icon_hi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
|
|
@ -1,12 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { PhoneField } from "@web/views/fields/phone/phone_field";
|
||||
import { PhoneField, phoneField, formPhoneField } from "@web/views/fields/phone/phone_field";
|
||||
import { SendSMSButton } from '@sms/components/sms_button/sms_button';
|
||||
|
||||
patch(PhoneField, "sms.PhoneField", {
|
||||
patch(PhoneField, {
|
||||
components: {
|
||||
...PhoneField.components,
|
||||
SendSMSButton
|
||||
|
|
@ -17,10 +16,21 @@ patch(PhoneField, "sms.PhoneField", {
|
|||
...PhoneField.props,
|
||||
enableButton: { type: Boolean, optional: true },
|
||||
},
|
||||
extractProps: ({ attrs }) => {
|
||||
return {
|
||||
enableButton: attrs.options.enable_sms,
|
||||
placeholder: attrs.placeholder,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const patchDescr = () => ({
|
||||
extractProps({ options }) {
|
||||
const props = super.extractProps(...arguments);
|
||||
props.enableButton = options.enable_sms;
|
||||
return props;
|
||||
},
|
||||
supportedOptions: [{
|
||||
label: _t("Enable SMS"),
|
||||
name: "enable_sms",
|
||||
type: "boolean",
|
||||
default: true,
|
||||
}],
|
||||
});
|
||||
|
||||
patch(phoneField, patchDescr());
|
||||
patch(formPhoneField, patchDescr());
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<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">
|
||||
<t t-if="props.enableButton and props.record.data[props.name].length > 0">
|
||||
<SendSMSButton t-props="props" />
|
||||
</t>
|
||||
</xpath>
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<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">
|
||||
<t t-if="props.enableButton and props.record.data[props.name].length > 0">
|
||||
<SendSMSButton t-props="props" />
|
||||
</t>
|
||||
</xpath>
|
||||
|
|
|
|||
|
|
@ -1,41 +1,44 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { user } from "@web/core/user";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component , status } = owl;
|
||||
import { Component, status } from "@odoo/owl";
|
||||
|
||||
export class SendSMSButton extends Component {
|
||||
static template = "sms.SendSMSButton";
|
||||
static props = ["*"];
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
this.user = useService("user");
|
||||
this.title = this.env._t("Send SMS Text Message");
|
||||
this.title = _t("Send SMS");
|
||||
}
|
||||
get phoneHref() {
|
||||
return "sms:" + this.props.value.replace(/\s+/g, "");
|
||||
return "sms:" + this.props.record.data[this.props.name].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();
|
||||
}
|
||||
this.action.doAction(
|
||||
{
|
||||
type: "ir.actions.act_window",
|
||||
target: "new",
|
||||
name: this.title,
|
||||
res_model: "sms.composer",
|
||||
views: [[false, "form"]],
|
||||
context: {
|
||||
...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",
|
||||
dialog_size: "medium",
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
onClose: () => {
|
||||
if (status(this) === "destroyed") {
|
||||
return;
|
||||
}
|
||||
this.props.record.load();
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
SendSMSButton.template = "sms.SendSMSButton";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="sms.SendSMSButton" owl="1">
|
||||
<t t-name="sms.SendSMSButton">
|
||||
<a
|
||||
t-att-title="title"
|
||||
t-att-href="phoneHref"
|
||||
|
|
|
|||
|
|
@ -1,52 +1,38 @@
|
|||
/** @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 { _t } from "@web/core/l10n/translation";
|
||||
import {
|
||||
EmojisTextField,
|
||||
emojisTextField,
|
||||
} from "@mail/views/web/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 {
|
||||
static template = "sms.SmsWidget";
|
||||
setup() {
|
||||
super.setup();
|
||||
this._emojiAdded = () => this.props.record.update({ [this.props.name]: this.targetEditElement.el.value });
|
||||
this.notification = useService('notification');
|
||||
}
|
||||
|
||||
get encoding() {
|
||||
return this._extractEncoding(this.props.value || '');
|
||||
return this._extractEncoding(this.props.record.data[this.props.name] || '');
|
||||
}
|
||||
get nbrChar() {
|
||||
const content = this._getValueForSmsCounts(this.props.value || '');
|
||||
const content = this._getValueForSmsCounts(this.props.record.data[this.props.name] || "");
|
||||
return content.length + (content.match(/\n/g) || []).length;
|
||||
}
|
||||
get nbrCharExplanation() {
|
||||
return '';
|
||||
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
|
||||
//--------------------------------------------------------------------------
|
||||
|
|
@ -109,13 +95,14 @@ export class SmsWidget extends EmojisTextField {
|
|||
* @private
|
||||
*/
|
||||
async onBlur() {
|
||||
var content = this.props.value || '';
|
||||
await super.onBlur();
|
||||
var content = this.props.record.data[this.props.name] || '';
|
||||
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"),
|
||||
_t("Your SMS Text Message must include at least one non-whitespace character"),
|
||||
{ type: 'danger' },
|
||||
)
|
||||
await this.props.update(content.trim());
|
||||
await this.props.record.update({ [this.props.name]: content.trim() });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -124,18 +111,19 @@ export class SmsWidget extends EmojisTextField {
|
|||
* @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);
|
||||
}
|
||||
}
|
||||
await this.props.record.update({ [this.props.name]: this.targetEditElement.el.value });
|
||||
}
|
||||
}
|
||||
|
||||
export const smsWidget = {
|
||||
...emojisTextField,
|
||||
component: SmsWidget,
|
||||
additionalClasses: [
|
||||
...(emojisTextField.additionalClasses || []),
|
||||
"o_field_text",
|
||||
"o_field_text_emojis",
|
||||
],
|
||||
};
|
||||
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);
|
||||
|
||||
registry.category("fields").add("sms_widget", smsWidget);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
<?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>
|
||||
<t t-name="sms.SmsWidget" t-inherit="mail.EmojisTextField" t-inherit-mode="primary">
|
||||
<xpath expr="//div[hasclass('o_field_input_buttons')]/button[hasclass('fa-magic')]" position="replace"/>
|
||||
<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"/>)
|
||||
<div class="o_sms_container mt-3">
|
||||
<span class="text-muted o_sms_count me-1">
|
||||
<t t-out="nbrChar"/>/<t t-out="160 * nbrSMS" /> <t t-out="nbrCharExplanation" /> | <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"/>
|
||||
title="SMS Pricing" aria-label="SMS Pricing" class="fa fa-lg fa-info-circle align-middle"/>
|
||||
</span>
|
||||
<button t-if="props.dynamicPlaceholder"
|
||||
class="btn btn-link py-0 border-0"
|
||||
title="Insert Field"
|
||||
t-on-click="onDynamicPlaceholderOpen"
|
||||
><span class="fa fa-magic me-1"/><span>Insert Field</span></button>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { Failure } from "@mail/core/common/failure_model";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(Failure.prototype, {
|
||||
get iconSrc() {
|
||||
if (this.type === "sms") {
|
||||
return "/sms/static/img/sms_failure.svg";
|
||||
}
|
||||
return super.iconSrc;
|
||||
},
|
||||
get body() {
|
||||
if (this.type === "sms") {
|
||||
if (this.notifications.length === 1 && this.lastMessage?.thread) {
|
||||
return _t("An error occurred when sending an SMS on “%(record_name)s”", {
|
||||
record_name: this.lastMessage.thread.display_name,
|
||||
});
|
||||
}
|
||||
return _t("An error occurred when sending an SMS");
|
||||
}
|
||||
return super.body;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { Notification } from "@mail/core/common/notification_model";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
/** @type {import("models").Notification} */
|
||||
const notificationPatch = {
|
||||
get failureMessage() {
|
||||
switch (this.failure_type) {
|
||||
case "sms_number_missing":
|
||||
return _t("Missing Number");
|
||||
case "sms_number_format":
|
||||
return _t("Wrong Number Format");
|
||||
case "sms_credit":
|
||||
return _t("Insufficient Credit");
|
||||
case "sms_country_not_supported":
|
||||
return _t("Country Not Supported");
|
||||
case "sms_registration_needed":
|
||||
return _t("Country-specific Registration Required");
|
||||
case "sms_server":
|
||||
return _t("Server Error");
|
||||
case "sms_acc":
|
||||
return _t("Unregistered Account");
|
||||
case "sms_expired":
|
||||
return _t("Expired");
|
||||
case "sms_invalid_destination":
|
||||
return _t("Invalid Destination");
|
||||
case "sms_not_allowed":
|
||||
return _t("Not Allowed");
|
||||
case "sms_not_delivered":
|
||||
return _t("Not Delivered");
|
||||
case "sms_rejected":
|
||||
return _t("Rejected");
|
||||
default:
|
||||
return super.failureMessage;
|
||||
}
|
||||
},
|
||||
get icon() {
|
||||
if (this.notification_type === "sms") {
|
||||
return "fa fa-mobile";
|
||||
}
|
||||
return super.icon;
|
||||
},
|
||||
get label() {
|
||||
if (this.notification_type === "sms") {
|
||||
return _t("SMS");
|
||||
}
|
||||
return super.label;
|
||||
},
|
||||
};
|
||||
patch(Notification.prototype, notificationPatch);
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
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;
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { MessagingMenu } from "@mail/core/public_web/messaging_menu";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(MessagingMenu.prototype, {
|
||||
openFailureView(failure) {
|
||||
if (failure.type === "email") {
|
||||
return super.openFailureView(failure);
|
||||
}
|
||||
this.env.services.action.doAction({
|
||||
name: _t("SMS Failures"),
|
||||
type: "ir.actions.act_window",
|
||||
view_mode: "kanban,list,form",
|
||||
views: [
|
||||
[false, "kanban"],
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
target: "current",
|
||||
res_model: failure.resModel,
|
||||
domain: [["message_has_sms_error", "=", true]],
|
||||
context: { create: false },
|
||||
});
|
||||
this.dropdown.close();
|
||||
},
|
||||
getFailureNotificationName(failure) {
|
||||
if (failure.type === "sms") {
|
||||
return _t("SMS Failure: %(modelName)s", { modelName: failure.modelName });
|
||||
}
|
||||
return super.getFailureNotificationName(...arguments);
|
||||
},
|
||||
});
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/** @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);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
/** @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();
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/** @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();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
/** @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,36 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { user } from "@web/core/user";
|
||||
import { Message } from "@mail/core/common/message";
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(Message.prototype, {
|
||||
async onClickNotification(ev) {
|
||||
const hasAccountFailure = this.message.notification_ids.some(
|
||||
(notification) => notification.isFailure && notification.failure_type === "sms_acc"
|
||||
);
|
||||
if (
|
||||
this.message.message_type === "sms" &&
|
||||
hasAccountFailure &&
|
||||
(await user.hasGroup("base.group_system"))
|
||||
) {
|
||||
const [accountId] = await this.env.services.orm.call("iap.account", "get", [], {
|
||||
service_name: "sms",
|
||||
force_create: false,
|
||||
});
|
||||
if (accountId) {
|
||||
this.env.services.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: _t("SMS Account"),
|
||||
target: "current",
|
||||
res_model: "iap.account",
|
||||
res_id: accountId,
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
super.onClickNotification(ev);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
start,
|
||||
startServer,
|
||||
triggerEvents,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, expect, test } from "@odoo/hoot";
|
||||
import { defineSMSModels } from "@sms/../tests/sms_test_helpers";
|
||||
import { asyncStep, mockService, serverState, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineSMSModels();
|
||||
|
||||
test("mark as read", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
message_type: "sms",
|
||||
model: "res.partner",
|
||||
res_id: serverState.partnerId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: "exception",
|
||||
notification_type: "sms",
|
||||
});
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await contains(".o-mail-NotificationItem");
|
||||
await triggerEvents(".o-mail-NotificationItem", ["mouseenter"], { text: "" });
|
||||
await contains(".o-mail-NotificationItem [title='Mark As Read']");
|
||||
await contains(".o-mail-NotificationItem-text", {
|
||||
text: "An error occurred when sending an SMS on “Mitchell Admin”",
|
||||
});
|
||||
await click(".o-mail-NotificationItem [title='Mark As Read']");
|
||||
await contains(".o-mail-NotificationItem", { count: 0 });
|
||||
});
|
||||
|
||||
test("notifications grouped by notification_type", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
const [messageId_1, messageId_2] = pyEnv["mail.message"].create([
|
||||
{
|
||||
message_type: "sms",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
},
|
||||
{
|
||||
message_type: "email",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
},
|
||||
]);
|
||||
pyEnv["mail.notification"].create([
|
||||
{
|
||||
mail_message_id: messageId_1,
|
||||
notification_status: "exception",
|
||||
notification_type: "sms",
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId_1,
|
||||
notification_status: "exception",
|
||||
notification_type: "sms",
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId_2,
|
||||
notification_status: "exception",
|
||||
notification_type: "email",
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId_2,
|
||||
notification_status: "exception",
|
||||
notification_type: "email",
|
||||
},
|
||||
]);
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await contains(".o-mail-NotificationItem", { count: 2 });
|
||||
await contains(":nth-child(1 of .o-mail-NotificationItem)", {
|
||||
contains: [
|
||||
[".o-mail-NotificationItem-name", { text: "Email Failure: Contact" }],
|
||||
[".o-mail-NotificationItem-counter", { text: "2" }],
|
||||
[".o-mail-NotificationItem-text", { text: "An error occurred when sending an email" }],
|
||||
],
|
||||
});
|
||||
await contains(":nth-child(2 of .o-mail-NotificationItem)", {
|
||||
contains: [
|
||||
[".o-mail-NotificationItem-name", { text: "SMS Failure: Contact" }],
|
||||
[".o-mail-NotificationItem-counter", { text: "2" }],
|
||||
[".o-mail-NotificationItem-text", { text: "An error occurred when sending an SMS" }],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("grouped notifications by document model", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([{}, {}]);
|
||||
const [messageId_1, messageId_2] = pyEnv["mail.message"].create([
|
||||
{
|
||||
message_type: "sms",
|
||||
model: "res.partner",
|
||||
res_id: partnerId_1,
|
||||
},
|
||||
{
|
||||
message_type: "sms",
|
||||
model: "res.partner",
|
||||
res_id: partnerId_2,
|
||||
},
|
||||
]);
|
||||
pyEnv["mail.notification"].create([
|
||||
{
|
||||
mail_message_id: messageId_1,
|
||||
notification_status: "exception",
|
||||
notification_type: "sms",
|
||||
},
|
||||
{
|
||||
mail_message_id: messageId_2,
|
||||
notification_status: "exception",
|
||||
notification_type: "sms",
|
||||
},
|
||||
]);
|
||||
mockService("action", {
|
||||
doAction(action) {
|
||||
asyncStep("do_action");
|
||||
expect(action.name).toBe("SMS Failures");
|
||||
expect(action.type).toBe("ir.actions.act_window");
|
||||
expect(action.view_mode).toBe("kanban,list,form");
|
||||
expect(action.views).toEqual([
|
||||
[false, "kanban"],
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
]);
|
||||
expect(action.target).toBe("current");
|
||||
expect(action.res_model).toBe("res.partner");
|
||||
expect(action.domain).toEqual([["message_has_sms_error", "=", true]]);
|
||||
},
|
||||
});
|
||||
await start();
|
||||
await click(".o_menu_systray i[aria-label='Messages']");
|
||||
await click(".o-mail-NotificationItem", {
|
||||
text: "SMS Failure: Contact",
|
||||
contains: [".badge", { text: "2" }],
|
||||
});
|
||||
await waitForSteps(["do_action"]);
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { fields, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class Partner extends models.Model {
|
||||
_name = "partner";
|
||||
|
||||
message = fields.Char();
|
||||
foo = fields.Char();
|
||||
mobile = fields.Char();
|
||||
partner_ids = fields.One2many({ relation: "partner" });
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { fields, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export class Visitor extends models.Model {
|
||||
_name = "visitor";
|
||||
|
||||
mobile = fields.Char();
|
||||
}
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
/** @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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -1,261 +0,0 @@
|
|||
/** @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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
/** @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"
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { mailModels } from "@mail/../tests/mail_test_helpers";
|
||||
import { Partner } from "@sms/../tests/mock_server/mock_models/partner";
|
||||
import { Visitor } from "@sms/../tests/mock_server/mock_models/visitor";
|
||||
import { defineModels } from "@web/../tests/web_test_helpers";
|
||||
|
||||
export function defineSMSModels() {
|
||||
return defineModels(smsModels);
|
||||
}
|
||||
|
||||
export const smsModels = { ...mailModels, Partner, Visitor };
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
/** @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();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
openFormView,
|
||||
start,
|
||||
startServer
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { describe, test } from "@odoo/hoot";
|
||||
import { defineSMSModels } from "@sms/../tests/sms_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineSMSModels();
|
||||
|
||||
test("Notification Processing", async () => {
|
||||
const { partnerId } = await _prepareSmsNotification("process");
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await _assertContainsSmsNotification();
|
||||
await _assertContainsPopoverWithIcon("fa-hourglass-half");
|
||||
});
|
||||
|
||||
test("Notification Pending", async () => {
|
||||
const { partnerId } = await _prepareSmsNotification("pending");
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await _assertContainsSmsNotification();
|
||||
await _assertContainsPopoverWithIcon("fa-paper-plane-o");
|
||||
});
|
||||
|
||||
test("Notification Sent", async () => {
|
||||
const { partnerId } = await _prepareSmsNotification("sent");
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await _assertContainsPopoverWithIcon("fa-check");
|
||||
});
|
||||
|
||||
test("Notification Error", async () => {
|
||||
const { partnerId } = await _prepareSmsNotification("exception");
|
||||
await start();
|
||||
await openFormView("res.partner", partnerId);
|
||||
await _assertContainsSmsNotification();
|
||||
await click(".o-mail-Message-notification");
|
||||
await contains(".o-mail-MessageNotificationPopover");
|
||||
await contains(".o-mail-MessageNotificationPopover [title='Error']");
|
||||
});
|
||||
|
||||
const _prepareSmsNotification = async (notification_status) => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Someone", partner_share: true });
|
||||
const messageId = pyEnv["mail.message"].create({
|
||||
body: "not empty",
|
||||
message_type: "sms",
|
||||
model: "res.partner",
|
||||
res_id: partnerId,
|
||||
});
|
||||
pyEnv["mail.notification"].create({
|
||||
mail_message_id: messageId,
|
||||
notification_status: notification_status,
|
||||
notification_type: "sms",
|
||||
res_partner_id: partnerId,
|
||||
});
|
||||
return { partnerId, messageId };
|
||||
};
|
||||
|
||||
const _assertContainsSmsNotification = async () => {
|
||||
await contains(".o-mail-Message");
|
||||
await contains(".o-mail-Message-notification");
|
||||
await contains(".o-mail-Message-notification i");
|
||||
await contains(".o-mail-Message-notification i.fa-mobile");
|
||||
};
|
||||
|
||||
const _assertContainsPopoverWithIcon = async (iconClassName) => {
|
||||
await click(".o-mail-Message-notification");
|
||||
await contains(".o-mail-MessageNotificationPopover");
|
||||
await contains(".o-mail-MessageNotificationPopover i");
|
||||
await contains(`.o-mail-MessageNotificationPopover i.${iconClassName}`);
|
||||
await contains(".o-mail-MessageNotificationPopover", { text: "Someone" });
|
||||
};
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
import {
|
||||
click,
|
||||
contains,
|
||||
editInput,
|
||||
startServer
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { beforeEach, describe, expect, test } from "@odoo/hoot";
|
||||
import { defineSMSModels } from "@sms/../tests/sms_test_helpers";
|
||||
import { MockServer, asyncStep, mockService, mountView, waitForSteps } from "@web/../tests/web_test_helpers";
|
||||
|
||||
describe.current.tags("desktop");
|
||||
defineSMSModels();
|
||||
|
||||
beforeEach(async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["partner"].create([
|
||||
{ message: "", foo: "yop", mobile: "+32494444444"},
|
||||
{ message: "", foo: "bayou"},
|
||||
]);
|
||||
pyEnv["visitor"].create([
|
||||
{ mobile: "+32494444444" },
|
||||
]);
|
||||
})
|
||||
|
||||
test("Sms button in form view", async () => {
|
||||
const visitorId = MockServer.env["visitor"].search([["mobile","=","+32494444444"]])[0];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "visitor",
|
||||
resId: visitorId,
|
||||
readonly: true,
|
||||
arch:
|
||||
`<form>
|
||||
<sheet>
|
||||
<field name="mobile" widget="phone"/>
|
||||
</sheet>
|
||||
</form>`
|
||||
});
|
||||
await contains(".o_field_phone");
|
||||
await contains(".o_field_phone a.o_field_phone_sms", { count: 1 });
|
||||
});
|
||||
|
||||
test("Sms button with option enable_sms set as False", async () => {
|
||||
const visitorId = MockServer.env["visitor"].search([["mobile","=","+32494444444"]])[0];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "visitor",
|
||||
resId: visitorId,
|
||||
readonly: true,
|
||||
arch:
|
||||
`<form>
|
||||
<sheet>
|
||||
<field name="mobile" widget="phone" options="{'enable_sms': false}"/>
|
||||
</sheet>
|
||||
</form>`
|
||||
});
|
||||
await contains(".o_field_phone");
|
||||
await contains(".o_field_phone a.o_field_phone_sms", { count: 0 });
|
||||
});
|
||||
|
||||
test("click on the sms button while creating a new record in a FormView", async () => {
|
||||
mockService("action", {
|
||||
doAction(action, options) {
|
||||
asyncStep("do_action");
|
||||
expect(action.type).toBe("ir.actions.act_window");
|
||||
expect(action.res_model).toBe("sms.composer");
|
||||
options.onClose();
|
||||
},
|
||||
});
|
||||
const partnerId = MockServer.env["partner"].search([["foo", "=", "yop"]])[0];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: partnerId,
|
||||
arch:
|
||||
`<form>
|
||||
<sheet>
|
||||
<field name="foo"/>
|
||||
<field name="mobile" widget="phone"/>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
await editInput(document.body, "[name='foo'] input", "John");
|
||||
await editInput(document.body, "[name='mobile'] input", "+32494444411");
|
||||
await click(".o_field_phone_sms");
|
||||
expect("[name='foo'] input:first").toHaveValue("John");
|
||||
expect("[name='mobile'] input:first").toHaveValue("+32494444411");
|
||||
await waitForSteps(["do_action"]);
|
||||
});
|
||||
|
||||
|
||||
test(
|
||||
"click on the sms button in a FormViewDialog has no effect on the main form view",
|
||||
async () => {
|
||||
mockService("action", {
|
||||
doAction(action, options){
|
||||
asyncStep("do_action");
|
||||
expect(action.type).toBe("ir.actions.act_window");
|
||||
expect(action.res_model).toBe("sms.composer");
|
||||
options.onClose();
|
||||
},
|
||||
});
|
||||
const partnerId = MockServer.env["partner"].search([["foo", "=", "yop"]])[0];
|
||||
await mountView({
|
||||
type: "form",
|
||||
resModel: "partner",
|
||||
resId: partnerId,
|
||||
arch:
|
||||
`<form>
|
||||
<sheet>
|
||||
<field name="foo"/>
|
||||
<field name="mobile" widget="phone"/>
|
||||
<field name="partner_ids">
|
||||
<kanban>
|
||||
<templates>
|
||||
<t t-name="card">
|
||||
<field name="display_name"/>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</sheet>
|
||||
</form>`,
|
||||
});
|
||||
|
||||
await editInput(document.body, "[name='foo'] input", "John");
|
||||
await editInput(document.body, "[name='mobile'] input", "+32494444411");
|
||||
await click(".o-kanban-button-new");
|
||||
await contains(".modal");
|
||||
|
||||
await editInput(document.body, ".modal .o_field_char[name='foo'] input", "Max");
|
||||
await editInput(document.body, ".modal .o_field_phone[name='mobile'] input", "+324955555");
|
||||
await click(":nth-child(1 of .modal) .o_field_phone_sms");
|
||||
expect(".modal [name='foo'] input:first").toHaveValue("Max");
|
||||
expect(".modal [name='mobile'] input:first").toHaveValue("+324955555");
|
||||
|
||||
await click(":nth-child(1 of .modal) .o_form_button_cancel");
|
||||
expect("[name='foo'] input:first").toHaveValue("John");
|
||||
expect("[name='mobile'] input:first").toHaveValue("+32494444411");
|
||||
await waitForSteps(["do_action"]);
|
||||
}
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue