Initial squashed commit

This commit is contained in:
hoangvv
2026-05-12 18:58:14 +07:00
commit 0b5c2ce91a
54896 changed files with 28050323 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 405 B

+1
View 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,38 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
import { PhoneField, phoneField, formPhoneField } from "@web/views/fields/phone/phone_field";
import { SendSMSButton } from '@sms/components/sms_button/sms_button';
patch(PhoneField, {
components: {
...PhoneField.components,
SendSMSButton
},
defaultProps: {
...PhoneField.defaultProps,
enableButton: true,
},
props: {
...PhoneField.props,
enableButton: { type: Boolean, optional: true },
},
});
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());
@@ -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.record.data[props.name].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.record.data[props.name].length > 0">
<SendSMSButton t-props="props" />
</t>
</xpath>
</t>
</templates>
@@ -0,0 +1,45 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { user } from "@web/core/user";
import { useService } from "@web/core/utils/hooks";
import { Component, status } from "@odoo/owl";
export class SendSMSButton extends Component {
static template = "sms.SendSMSButton";
static props = ["*"];
setup() {
this.action = useService("action");
this.title = _t("Send SMS");
}
get phoneHref() {
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: {
...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") {
return;
}
this.props.record.load();
},
}
);
}
}
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="sms.SendSMSButton">
<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,131 @@
/** @odoo-module **/
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";
/**
* 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.record.data[this.props.name] || '');
}
get nbrChar() {
const content = this._getValueForSmsCounts(this.props.record.data[this.props.name] || "");
return content.length + (content.match(/\n/g) || []).length;
}
get nbrCharExplanation() {
return "";
}
get nbrSMS() {
return this._countSMS(this.nbrChar, this.encoding);
}
//--------------------------------------------------------------------------
// 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() {
await super.onBlur();
var content = this.props.record.data[this.props.name] || '';
if( !content.trim().length && content.length > 0) {
this.notification.add(
_t("Your SMS Text Message must include at least one non-whitespace character"),
{ type: 'danger' },
)
await this.props.record.update({ [this.props.name]: content.trim() });
}
}
/**
* @override
* @private
*/
async onInput(ev) {
super.onInput(...arguments);
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",
],
};
registry.category("fields").add("sms_widget", smsWidget);
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="sms.SmsWidget" t-inherit="mail.EmojisTextField" t-inherit-mode="primary">
<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,25 @@
/** @odoo-module */
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.name,
});
}
return _t("An error occurred when sending an SMS");
}
return super.body;
},
});
@@ -0,0 +1,20 @@
/** @odoo-module */
import { Notification } from "@mail/core/common/notification_model";
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
patch(Notification.prototype, {
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;
},
});
@@ -0,0 +1,34 @@
/** @odoo-module */
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);
},
});
@@ -0,0 +1,19 @@
/** @odoo-module */
import { Message } from "@mail/core/common/message";
import { patch } from "@web/core/utils/patch";
patch(Message.prototype, {
onClickFailure() {
if (this.message.message_type === "sms") {
this.env.services.action.doAction("sms.sms_resend_action", {
additionalContext: {
default_mail_message_id: this.message.id,
},
});
} else {
super.onClickFailure(...arguments);
}
},
});
@@ -0,0 +1,147 @@
import {
assertSteps,
click,
contains,
start,
startServer,
step,
triggerEvents,
} from "@mail/../tests/mail_test_helpers";
import { describe, expect, test } from "@odoo/hoot";
import { defineSMSModels } from "@sms/../tests/sms_test_helpers";
import { mockService, serverState } 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) {
step("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 assertSteps(["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();
}
@@ -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 };
@@ -0,0 +1,90 @@
import {
assertSteps,
click,
contains,
openFormView,
start,
startServer,
step,
} from "@mail/../tests/mail_test_helpers";
import { describe, expect, test } from "@odoo/hoot";
import { defineSMSModels } from "@sms/../tests/sms_test_helpers";
import { mockService } from "@web/../tests/web_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, messageId } = await _prepareSmsNotification("exception");
mockService("action", {
doAction(action, options) {
if (action?.res_model === "res.partner") {
return super.doAction(...arguments);
}
expect(action).toBe("sms.sms_resend_action");
expect(options.additionalContext.default_mail_message_id).toBe(messageId);
step("do_action");
},
});
await start();
await openFormView("res.partner", partnerId);
await _assertContainsSmsNotification();
await click(".o-mail-Message-notification");
await assertSteps(["do_action"]);
});
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,145 @@
import {
assertSteps,
click,
contains,
editInput,
startServer,
step,
} from "@mail/../tests/mail_test_helpers";
import { beforeEach, describe, expect, test } from "@odoo/hoot";
import { queryFirst } from "@odoo/hoot-dom";
import { defineSMSModels } from "@sms/../tests/sms_test_helpers";
import { mockService, mountView, MockServer } 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,
mode: "readonly",
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,
mode: "readonly",
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) {
step("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(queryFirst("[name='foo'] input")).toHaveValue("John");
expect(queryFirst("[name='mobile'] input")).toHaveValue("+32494444411");
await assertSteps(["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){
step("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(queryFirst(".modal [name='foo'] input")).toHaveValue("Max");
expect(queryFirst(".modal [name='mobile'] input")).toHaveValue("+324955555");
await click(":nth-child(1 of .modal) .o_form_button_cancel");
expect(queryFirst("[name='foo'] input")).toHaveValue("John");
expect(queryFirst("[name='mobile'] input")).toHaveValue("+32494444411");
await assertSteps(["do_action"]);
}
);