Initial commit: Mail packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:51 +02:00
commit 4e53507711
1948 changed files with 751201 additions and 0 deletions

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.NotificationGroup" t-inherit-mode="extension">
<xpath expr="//*[hasclass('o_NotificationGroup_inlineText')]" position="inside">
<t t-if="notificationGroupView.notificationGroup.notification_type === 'sms'">
An error occurred when sending an SMS.
</t>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,26 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { PhoneField } from "@web/views/fields/phone/phone_field";
import { SendSMSButton } from '@sms/components/sms_button/sms_button';
patch(PhoneField, "sms.PhoneField", {
components: {
...PhoneField.components,
SendSMSButton
},
defaultProps: {
...PhoneField.defaultProps,
enableButton: true,
},
props: {
...PhoneField.props,
enableButton: { type: Boolean, optional: true },
},
extractProps: ({ attrs }) => {
return {
enableButton: attrs.options.enable_sms,
placeholder: attrs.placeholder,
};
},
});

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="web.PhoneField" t-inherit-mode="extension">
<xpath expr="//div[hasclass('o_phone_content')]//a" position="after">
<t t-if="props.enableButton and props.value.length > 0">
<SendSMSButton t-props="props" />
</t>
</xpath>
</t>
<t t-inherit="web.FormPhoneField" t-inherit-mode="extension">
<xpath expr="//div[hasclass('o_phone_content')]" position="inside">
<t t-if="props.enableButton and props.value.length > 0">
<SendSMSButton t-props="props" />
</t>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,41 @@
/** @odoo-module **/
import { useService } from "@web/core/utils/hooks";
const { Component , status } = owl;
export class SendSMSButton extends Component {
setup() {
this.action = useService("action");
this.user = useService("user");
this.title = this.env._t("Send SMS Text Message");
}
get phoneHref() {
return "sms:" + this.props.value.replace(/\s+/g, "");
}
async onClick() {
await this.props.record.save();
this.action.doAction({
type: "ir.actions.act_window",
target: "new",
name: this.title,
res_model: "sms.composer",
views: [[false, "form"]],
context: {
...this.user.context,
default_res_model: this.props.record.resModel,
default_res_id: this.props.record.resId,
default_number_field_name: this.props.name,
default_composition_mode: 'comment',
}
}, {
onClose: () => {
if (status(this) !== "destroyed") {
this.props.record.load();
this.props.record.model.notify();
}
},
});
}
};
SendSMSButton.template = "sms.SendSMSButton";

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="sms.SendSMSButton" owl="1">
<a
t-att-title="title"
t-att-href="phoneHref"
t-on-click.prevent.stop="onClick"
class="ms-3 d-inline-flex align-items-center o_field_phone_sms"
><i class="fa fa-mobile"></i><small class="fw-bold ms-1">SMS</small></a>
</t>
</templates>

View file

@ -0,0 +1,141 @@
/** @odoo-module **/
import basic_fields from 'web.basic_fields';
import { patch } from "@web/core/utils/patch";
import { EmojisTextField} from '@mail/views/fields/emojis_text_field/emojis_text_field';
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
const DynamicPlaceholderFieldMixin = basic_fields.DynamicPlaceholderFieldMixin;
/**
* SmsWidget is a widget to display a textarea (the body) and a text representing
* the number of SMS and the number of characters. This text is computed every
* time the user changes the body.
*/
export class SmsWidget extends EmojisTextField {
setup() {
super.setup();
this.notification = useService('notification');
}
get encoding() {
return this._extractEncoding(this.props.value || '');
}
get nbrChar() {
const content = this._getValueForSmsCounts(this.props.value || '');
return content.length + (content.match(/\n/g) || []).length;
}
get nbrCharExplanation() {
return '';
}
get nbrSMS() {
return this._countSMS(this.nbrChar, this.encoding);
}
/**
* Open a Model Field Selector in order to select fields
* and create a dynamic placeholder string with or without
* a default text value.
*
* @public
* @param {String} baseModel
* @param {Array} chain
*
*/
async openDynamicPlaceholder(baseModel, chain = []) {
const modelSelector = await this._openNewModelSelector(baseModel, chain);
modelSelector.$el.css('margin-top', 4);
}
//--------------------------------------------------------------------------
// Private: SMS
//--------------------------------------------------------------------------
/**
* Count the number of SMS of the content
* @private
* @returns {integer} Number of SMS
*/
_countSMS(nbrChar, encoding) {
if (nbrChar === 0) {
return 0;
}
if (encoding === 'UNICODE') {
if (nbrChar <= 70) {
return 1;
}
return Math.ceil(nbrChar / 67);
}
if (nbrChar <= 160) {
return 1;
}
return Math.ceil(nbrChar / 153);
}
/**
* Extract the encoding depending on the characters in the content
* @private
* @param {String} content Content of the SMS
* @returns {String} Encoding of the content (GSM7 or UNICODE)
*/
_extractEncoding(content) {
if (String(content).match(RegExp("^[@£$¥èéùìòÇ\\nØø\\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\\\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà]*$"))) {
return 'GSM7';
}
return 'UNICODE';
}
/**
* Implement if more characters are going to be sent then those appearing in
* value, if that value is processed before being sent.
* E.g., links are converted to trackers in mass_mailing_sms.
*
* Note: goes with an explanation in nbrCharExplanation
*
* @param {String} value content to be parsed for counting extra characters
* @return string length-corrected value placeholder for the post-processed
* state
*/
_getValueForSmsCounts(value) {
return value;
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @override
* @private
*/
async onBlur() {
var content = this.props.value || '';
if( !content.trim().length && content.length > 0) {
this.notification.add(
this.env._t("Your SMS Text Message must include at least one non-whitespace character"),
{ type: 'danger' },
)
await this.props.update(content.trim());
}
}
/**
* @override
* @private
*/
async onInput(ev) {
await this.props.update(this.targetEditElement.el.value);
super.onInput(...arguments);
const key = ev.originalEvent ? ev.originalEvent.data : '';
if (this.props.dynamicPlaceholder && key === this.DYNAMIC_PLACEHOLDER_TRIGGER_KEY) {
const baseModel = this.recordData && this.recordData.mailing_model_real ? this.recordData.mailing_model_real : undefined;
if (baseModel) {
this.openDynamicPlaceholder(baseModel);
}
}
}
};
SmsWidget.template = 'sms.SmsWidget';
SmsWidget.additionalClasses = [...(EmojisTextField.additionalClasses || []), 'o_field_text'];
patch(SmsWidget.prototype, 'sms_widget_dynamic_placeholder_field_mixin', DynamicPlaceholderFieldMixin);
registry.category("fields").add("sms_widget", SmsWidget);

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="sms.SmsWidget" t-inherit="mail.EmojisTextField" t-inherit-mode="primary" owl="1">
<xpath expr="//textarea[1]" position="attributes">
<attribute name="t-on-blur">onBlur</attribute>
</xpath>
<xpath expr="/*[last()]/*[last()]" position="after">
<div class="o_sms_container">
<span class="text-muted o_sms_count">
<t t-out="nbrChar"/> characters<t t-out="nbrCharExplanation"/>, fits in <t t-out="nbrSMS"/> SMS (<t t-out="encoding"/>)
<a href="https://iap-services.odoo.com/iap/sms/pricing" target="_blank"
title="SMS Pricing" aria-label="SMS Pricing" class="fa fa-lg fa-info-circle"/>
</span>
</div>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,99 @@
odoo.define('sms.fields', function (require) {
"use strict";
var basic_fields = require('web.basic_fields');
var core = require('web.core');
var session = require('web.session');
var _t = core._t;
/**
* Override of FieldPhone to add a button calling SMS composer if option activated (default)
*/
var Phone = basic_fields.FieldPhone;
Phone.include({
/**
* By default, enable_sms is activated
*
* @override
*/
init() {
this._super.apply(this, arguments);
this.enableSMS = 'enable_sms' in this.attrs.options ? this.attrs.options.enable_sms : true;
// reinject in nodeOptions (and thus in this.attrs) to signal the property
this.attrs.options.enable_sms = this.enableSMS;
},
/**
* When the send SMS button is displayed, $el becomes a div wrapping
* the original links.
* This method makes sure we always focus the phone number
*
* @override
*/
getFocusableElement() {
if (this.enableSMS && this.mode === 'readonly') {
return this.$el.filter('.' + this.className).find('a');
}
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Open SMS composer wizard
*
* @private
*/
_onClickSMS: function (ev) {
ev.preventDefault();
ev.stopPropagation();
var context = session.user_context;
context = _.extend({}, context, {
default_res_model: this.model,
default_res_id: parseInt(this.res_id),
default_number_field_name: this.name,
default_composition_mode: 'comment',
});
var self = this;
return this.do_action({
title: _t('Send SMS Text Message'),
type: 'ir.actions.act_window',
res_model: 'sms.composer',
target: 'new',
views: [[false, 'form']],
context: context,
}, {
on_close: function () {
self.trigger_up('reload');
}});
},
/**
* Add a button to call the composer wizard
*
* @override
* @private
*/
_renderReadonly: function () {
var def = this._super.apply(this, arguments);
if (this.enableSMS && this.value) {
var $composerButton = $('<a>', {
title: _t('Send SMS Text Message'),
href: '',
class: 'ms-3 d-inline-flex align-items-center o_field_phone_sms',
html: $('<small>', {class: 'fw-bold ms-1', html: 'SMS'}),
});
$composerButton.prepend($('<i>', {class: 'fa fa-mobile'}));
$composerButton.on('click', this._onClickSMS.bind(this));
this.$el = this.$el.add($composerButton);
}
return def;
},
});
return Phone;
});

View file

@ -0,0 +1,26 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
registerPatch({
name: 'Message',
recordMethods: {
/**
* @override
*/
openResendAction() {
if (this.message_type === 'sms') {
this.env.services.action.doAction(
'sms.sms_resend_action',
{
additionalContext: {
default_mail_message_id: this.id,
},
},
);
} else {
this._super(...arguments);
}
},
},
});

View file

@ -0,0 +1,41 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
registerPatch({
name: 'MessageView',
fields: {
failureNotificationIconClassName: {
compute() {
if (this.message && this.message.message_type === 'sms') {
return 'fa fa-mobile';
}
return this._super();
},
},
failureNotificationIconLabel: {
compute() {
if (this.message && this.message.message_type === 'sms') {
return this.env._t("SMS");
}
return this._super();
},
},
notificationIconClassName: {
compute() {
if (this.message && this.message.message_type === 'sms') {
return 'fa fa-mobile';
}
return this._super();
},
},
notificationIconLabel: {
compute() {
if (this.message && this.message.message_type === 'sms') {
return this.env._t("SMS");
}
return this._super();
},
},
},
});

View file

@ -0,0 +1,32 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
registerPatch({
name: 'NotificationGroup',
recordMethods: {
/**
* @override
*/
_openDocuments() {
if (this.notification_type !== 'sms') {
return this._super(...arguments);
}
this.env.services.action.doAction({
name: this.env._t("SMS Failures"),
type: 'ir.actions.act_window',
view_mode: 'kanban,list,form',
views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
target: 'current',
res_model: this.res_model,
domain: [['message_has_sms_error', '=', true]],
context: { create: false },
});
if (this.messaging.device.isSmall) {
// messaging menu has a higher z-index than views so it must
// be closed to ensure the visibility of the view
this.messaging.messagingMenu.close();
}
},
},
});

View file

@ -0,0 +1,17 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
registerPatch({
name: 'NotificationGroupView',
fields: {
imageSrc: {
compute() {
if (this.notificationGroup.notification_type === 'sms') {
return '/sms/static/img/sms_failure.svg';
}
return this._super();
},
},
},
});