[FW][MERGE][IMP] mail, web: prepare support of WhatsApp
RATIONALE Enterprise is adding support of WhatsApp as a mean to communicate with customers. This require some update in community, notably by using a new widget and preparing discuss channels to support whatsapp channels. See sub commits for more details. Task-2377154 (Add WhatsApp Support) closes odoo/odoo#134377 Forward-port-of: odoo/odoo#133973 Forward-port-of: odoo/odoo#131688 Related: odoo/documentation#5730 Related: odoo/enterprise#46942 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com> Co-authored-by: Akshat Trivedi <aktr@odoo.com> Co-authored-by: Aurélien Warnon <awa@odoo.com> Co-authored-by: Gitashri Mantha <gman@odoo.com> Co-authored-by: Jigar Vaghela <jva@odoo.com> Co-authored-by: Kashyap Patel <kasp@odoo.com> Co-authored-by: Khushi Vakil <khva@odoo.com> Co-authored-by: Nishant Jain <niai@odoo.com> Co-authored-by: Prakash Prajapati <ppr@odoo.com> Co-authored-by: Priyanshi patel <prpa@odoo.com> Co-authored-by: Rahul Prajapati <rapr@odoo.com> Co-authored-by: Renaud Thiry <reth@odoo.com> Co-authored-by: Shreya Patel <shpa@odoo.com> Co-authored-by: Stéphane Debauche <std@odoo.com> Co-authored-by: Thibault Delavallee <tde@odoo.com> Co-authored-by: Zeel Patel <zepa@odoo.com>
This commit is contained in:
committed by
Odoo Online
co-authored by
Akshat Trivedi
Aurélien Warnon
Gitashri Mantha
Jigar Vaghela
Kashyap Patel
Khushi Vakil
Nishant Jain
Prakash Prajapati
Priyanshi patel
Rahul Prajapati
Renaud Thiry
Shreya Patel
Stéphane Debauche
Thibault Delavallee
Zeel Patel
commit
15ffe0b838
@@ -271,7 +271,7 @@ class Channel(models.Model):
|
||||
if 'channel_type' in vals:
|
||||
failing_channels = self.sudo().filtered(lambda channel: channel.channel_type != vals.get('channel_type'))
|
||||
if failing_channels:
|
||||
raise UserError(_('Cannot change the channel type of: %(channel_names)s'), channel_names=', '.join(failing_channels.mapped('name')))
|
||||
raise UserError(_('Cannot change the channel type of: %(channel_names)s', channel_names=', '.join(failing_channels.mapped('name'))))
|
||||
result = super().write(vals)
|
||||
if vals.get('group_ids'):
|
||||
self._subscribe_users_automatically()
|
||||
@@ -1033,7 +1033,8 @@ class Channel(models.Model):
|
||||
for channel in self:
|
||||
if not channel.message_ids.ids:
|
||||
return
|
||||
if channel.channel_type != 'chat':
|
||||
# a bit not-modular but helps understanding code
|
||||
if channel.channel_type not in {'chat', 'whatsapp'}:
|
||||
return
|
||||
last_message_id = channel.message_ids.ids[0] # zero is the index of the last message
|
||||
member = self.env['discuss.channel.member'].search([('channel_id', '=', channel.id), ('partner_id', '=', self.env.user.partner_id.id)], limit=1)
|
||||
|
||||
@@ -27,10 +27,10 @@ class ResPartner(models.Model):
|
||||
("channel_partner_ids", "in", [self.id]),
|
||||
]
|
||||
)
|
||||
# get the pinned direct messages
|
||||
# get the pinned direct messages (directly include whatsapp to avoid overrides)
|
||||
channels |= self.env["discuss.channel"].search(
|
||||
[
|
||||
("channel_type", "=", "chat"),
|
||||
("channel_type", "in", ["chat", "whatsapp"]),
|
||||
(
|
||||
"channel_member_ids",
|
||||
"in",
|
||||
|
||||
@@ -2570,6 +2570,7 @@ class MailThread(models.AbstractModel):
|
||||
body='', subject=False,
|
||||
author_id=None, email_from=None,
|
||||
message_type='notification',
|
||||
partner_ids=False,
|
||||
attachment_ids=False, tracking_value_ids=False):
|
||||
""" Shortcut allowing to post note on a document. See ``_message_log_batch``
|
||||
for more details. """
|
||||
@@ -2579,12 +2580,14 @@ class MailThread(models.AbstractModel):
|
||||
{self.id: body}, subject=subject,
|
||||
author_id=author_id, email_from=email_from,
|
||||
message_type=message_type,
|
||||
partner_ids=partner_ids,
|
||||
attachment_ids=attachment_ids, tracking_value_ids=tracking_value_ids
|
||||
)
|
||||
|
||||
def _message_log_batch(self, bodies, subject=False,
|
||||
author_id=None, email_from=None,
|
||||
message_type='notification',
|
||||
partner_ids=False,
|
||||
attachment_ids=False, tracking_value_ids=False):
|
||||
""" Shortcut allowing to post notes on a batch of documents. It does not
|
||||
perform any notification and pre-computes some values to have a short code
|
||||
@@ -2594,7 +2597,9 @@ class MailThread(models.AbstractModel):
|
||||
access rights are already granted to avoid privilege escalation.
|
||||
|
||||
:param bodies: dict {record_id: body}
|
||||
|
||||
:param list partner_ids: optional partners, not used in any notification
|
||||
mechanism. This is mainly used to link a log to a specific customer
|
||||
like SMS or WhatsApp log;
|
||||
:return: created messages (as sudo)
|
||||
"""
|
||||
# protect against side-effect prone usage
|
||||
@@ -2620,6 +2625,7 @@ class MailThread(models.AbstractModel):
|
||||
# recipients
|
||||
'email_add_signature': False, # False as no notification -> no need to compute signature
|
||||
'message_id': tools.generate_tracking_message_id('message-notify'), # why? this is all but a notify
|
||||
'partner_ids': partner_ids,
|
||||
'reply_to': self.env['mail.thread']._notify_get_reply_to(default=email_from)[False],
|
||||
}
|
||||
|
||||
@@ -2713,7 +2719,7 @@ class MailThread(models.AbstractModel):
|
||||
# Avoid warnings about non-existing fields
|
||||
for x in ('from', 'to', 'cc', 'canned_response_ids'):
|
||||
create_values.pop(x, None)
|
||||
create_values['partner_ids'] = [Command.link(pid) for pid in create_values.get('partner_ids', [])]
|
||||
create_values['partner_ids'] = [Command.link(pid) for pid in (create_values.get('partner_ids') or [])]
|
||||
create_values_list.append(create_values)
|
||||
|
||||
# remove context, notably for default keys, as this thread method is not
|
||||
|
||||
@@ -122,18 +122,9 @@ export class Composer extends Component {
|
||||
});
|
||||
this.suggestion = this.store.user ? useSuggestion() : undefined;
|
||||
this.markEventHandled = markEventHandled;
|
||||
this.onDropFile = this.onDropFile.bind(this);
|
||||
if (this.props.dropzoneRef && this.allowUpload) {
|
||||
useDropzone(
|
||||
this.props.dropzoneRef,
|
||||
(ev) => {
|
||||
if (isDragSourceExternalFile(ev.dataTransfer)) {
|
||||
for (const file of ev.dataTransfer.files) {
|
||||
this.attachmentUploader.uploadFile(file);
|
||||
}
|
||||
}
|
||||
},
|
||||
"o-mail-Composer-dropzone"
|
||||
);
|
||||
useDropzone(this.props.dropzoneRef, this.onDropFile, "o-mail-Composer-dropzone");
|
||||
}
|
||||
if (this.props.messageEdition) {
|
||||
this.props.messageEdition.composerOfThread = this;
|
||||
@@ -354,6 +345,14 @@ export class Composer extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onDropFile(ev) {
|
||||
if (isDragSourceExternalFile(ev.dataTransfer)) {
|
||||
for (const file of ev.dataTransfer.files) {
|
||||
this.attachmentUploader.uploadFile(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This doesn't work on firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1699743
|
||||
*/
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
Log note
|
||||
</button>
|
||||
<div class="flex-grow-1 d-flex">
|
||||
<button t-if="props.hasActivities" class="btn btn-secondary text-nowrap" t-att-class="{ 'my-2': !props.compactHeight }" data-hotkey="shift+a" t-on-click="scheduleActivity">
|
||||
<button t-if="props.hasActivities" class="o-mail-Chatter-activity btn btn-secondary text-nowrap" t-att-class="{ 'my-2': !props.compactHeight }" data-hotkey="shift+a" t-on-click="scheduleActivity">
|
||||
<i class="fa fa-clock-o me-1"/>
|
||||
<span>Activities</span>
|
||||
</button>
|
||||
|
||||
@@ -57,7 +57,7 @@ export class VoiceRecorder extends Component {
|
||||
this.notification.add(_t("Voice recording stopped"), { type: "warning" });
|
||||
this.stopRecording();
|
||||
} else {
|
||||
this.cleanUp();
|
||||
this.cleanUp({unmounting: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -264,6 +264,9 @@ patch(MockServer.prototype, {
|
||||
const [channel] = this.getRecords("discuss.channel", [["id", "in", ids]]);
|
||||
const partners = this.getRecords("res.partner", [["id", "in", partner_ids]]);
|
||||
for (const partner of partners) {
|
||||
if (partner.id === this.pyEnv.currentPartnerId) {
|
||||
continue; // adding 'yourself' to the conversation is handled below
|
||||
}
|
||||
const body = `<div class="o_mail_notification">invited ${partner.name} to the channel</div>`;
|
||||
const message_type = "notification";
|
||||
const subtype_xmlid = "mail.mt_comment";
|
||||
@@ -283,6 +286,14 @@ patch(MockServer.prototype, {
|
||||
invited_by_user_id: this.pyEnv.currentUserId,
|
||||
});
|
||||
}
|
||||
const selfPartner = partners.find((partner) => partner.id === this.pyEnv.currentPartnerId);
|
||||
if (selfPartner) {
|
||||
// needs to be done after adding 'self' as a member
|
||||
const body = `<div class="o_mail_notification">${selfPartner.name} joined the channel</div>`;
|
||||
const message_type = "notification";
|
||||
const subtype_xmlid = "mail.mt_comment";
|
||||
this._mockDiscussChannelMessagePost(channel.id, { body, message_type, subtype_xmlid });
|
||||
}
|
||||
const isSelfMember =
|
||||
this.pyEnv["discuss.channel.member"].searchCount([
|
||||
["partner_id", "=", this.pyEnv.currentPartnerId],
|
||||
|
||||
@@ -546,6 +546,7 @@ class TestMessageLog(TestMessagePostCommon):
|
||||
'message_type': 'notification',
|
||||
'model': test_record._name,
|
||||
'notified_partner_ids': self.env['res.partner'],
|
||||
'partner_ids': self.env['res.partner'],
|
||||
'reply_to': formataddr((self.company_admin.name, '%s@%s' % (self.alias_catchall, self.alias_domain))),
|
||||
'res_id': test_record.id,
|
||||
'subtype_id': self.env.ref('mail.mt_note'),
|
||||
@@ -573,6 +574,38 @@ class TestMessageLog(TestMessagePostCommon):
|
||||
'message_type': 'notification',
|
||||
'model': test_record._name,
|
||||
'notified_partner_ids': self.env['res.partner'],
|
||||
'partner_ids': self.env['res.partner'],
|
||||
'reply_to': formataddr((self.company_admin.name, '%s@%s' % (self.alias_catchall, self.alias_domain))),
|
||||
'res_id': test_record.id,
|
||||
'subtype_id': self.env.ref('mail.mt_note'),
|
||||
}
|
||||
)
|
||||
|
||||
@users('employee')
|
||||
def test_message_log_batch_with_partners(self):
|
||||
""" Partners can be given to log, but this should not generate any
|
||||
notification. """
|
||||
test_records = self.test_records.with_env(self.env)
|
||||
test_records.message_subscribe(self.partner_employee_2.ids)
|
||||
|
||||
new_notes = test_records._message_log_batch(
|
||||
bodies={
|
||||
test_record.id: Markup('<p>Test _message_log_batch</p>')
|
||||
for test_record in test_records
|
||||
},
|
||||
partner_ids=self.test_partners[:5].ids,
|
||||
)
|
||||
for test_record, new_note in zip(test_records, new_notes):
|
||||
self.assertMessageFields(
|
||||
new_note,
|
||||
{'author_id': self.partner_employee,
|
||||
'body': '<p>Test _message_log_batch</p>',
|
||||
'email_from': formataddr((self.partner_employee.name, self.partner_employee.email_normalized)),
|
||||
'is_internal': True,
|
||||
'message_type': 'notification',
|
||||
'model': test_record._name,
|
||||
'notified_partner_ids': self.env['res.partner'],
|
||||
'partner_ids': self.test_partners[:5],
|
||||
'reply_to': formataddr((self.company_admin.name, '%s@%s' % (self.alias_catchall, self.alias_domain))),
|
||||
'res_id': test_record.id,
|
||||
'subtype_id': self.env.ref('mail.mt_note'),
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { ModelFieldSelector } from "@web/core/model_field_selector/model_field_selector";
|
||||
|
||||
export class DynamicModelFieldSelector extends ModelFieldSelector {
|
||||
|
||||
static props = {
|
||||
...ModelFieldSelector.props,
|
||||
record: { type: Object, optional: true },
|
||||
recordProps: { type: Object, optional: true },
|
||||
};
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { CharField, charField } from "@web/views/fields/char/char_field";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { archParseBoolean } from "@web/views/utils";
|
||||
|
||||
import { DynamicModelFieldSelector } from "./dynamic_model_field_selector";
|
||||
|
||||
export class DynamicModelFieldSelectorChar extends CharField {
|
||||
static components = {
|
||||
...CharField.components,
|
||||
DynamicModelFieldSelector
|
||||
};
|
||||
|
||||
static props = {
|
||||
...CharField.props,
|
||||
resModel : { type: String, optional: true },
|
||||
onlySearchable: { type: Boolean, optional: true },
|
||||
followRelations: { type: Boolean, optional: true },
|
||||
}
|
||||
|
||||
/**
|
||||
* Update record
|
||||
*
|
||||
* @param {string} value
|
||||
* @private
|
||||
*/
|
||||
async _onRecordUpdate(value) {
|
||||
await this.props.record.update({[this.props.name]: value});
|
||||
}
|
||||
|
||||
//---- Getters ----
|
||||
get getSelectorProps() {
|
||||
return {
|
||||
path: this.props.record.data[this.props.name],
|
||||
resModel: this.getResModel(),
|
||||
readonly: this.props.readonly,
|
||||
record: this.props.record,
|
||||
recordProps: this.props,
|
||||
update: this._onRecordUpdate.bind(this),
|
||||
isDebugMode: !!this.env.debug,
|
||||
filter: this.filter.bind(this),
|
||||
followRelations: this.props.followRelations,
|
||||
};
|
||||
}
|
||||
|
||||
filter(fieldDef) {
|
||||
return !this.props.onlySearchable || fieldDef.searchable;
|
||||
}
|
||||
|
||||
getResModel(props = this.props) {
|
||||
const resModel = props.record.data[props.resModel];
|
||||
if (!resModel) {
|
||||
return props.record.resModel;
|
||||
}
|
||||
return resModel;
|
||||
}
|
||||
}
|
||||
|
||||
DynamicModelFieldSelectorChar.template = "web.DynamicModelFieldSelectorChar";
|
||||
|
||||
export const dynamicModelFieldSelectorChar = {
|
||||
...charField,
|
||||
component: DynamicModelFieldSelectorChar,
|
||||
extractProps({ options }, dynamicInfo) {
|
||||
return {
|
||||
followRelations: options.follow_relations ?? true,
|
||||
onlySearchable: archParseBoolean(options.only_searchable),
|
||||
resModel: options.model,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("DynamicModelFieldSelectorChar", dynamicModelFieldSelectorChar);
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="web.DynamicModelFieldSelectorChar" t-inherit="web.CharField" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//input" position="attributes">
|
||||
<attribute name="class" add="d-none" separator=" "/>
|
||||
</xpath>
|
||||
<xpath expr="//span" position="attributes">
|
||||
<attribute name="class" add="d-none" separator=" "/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-if='props.readonly']" position="before">
|
||||
<DynamicModelFieldSelector t-props="getSelectorProps" />
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -184,6 +184,10 @@ addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js
|
||||
!addons/website_livechat/
|
||||
!addons/website_livechat/**/*
|
||||
|
||||
# Whitelist whatsapp
|
||||
!whatsapp
|
||||
!whatsapp/**/*
|
||||
|
||||
# Whitelist point_of_sale
|
||||
!addons/point_of_sale/
|
||||
!addons/point_of_sale/**/*
|
||||
@@ -243,6 +247,8 @@ addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js
|
||||
!pos_restaurant_iot/**/*
|
||||
!pos_settle_due
|
||||
!pos_settle_due/**/*
|
||||
!whatsapp_pos
|
||||
!whatsapp_pos/**/*
|
||||
|
||||
# Whitelist misc enterprise modules
|
||||
!sign
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@bus/*": ["addons/bus/static/src/*"],
|
||||
"@mail/*": ["addons/mail/static/src/*"],
|
||||
"@im_livechat/*": ["addons/im_livechat/static/src/*"],
|
||||
"@whatsapp/*": ["addons/whatsapp/static/src/*"],
|
||||
"@point_of_sale/*": ["addons/point_of_sale/static/src/*"],
|
||||
|
||||
"@l10n_co_pos/*": ["addons/l10n_co_pos/static/src/*"],
|
||||
@@ -42,7 +43,8 @@
|
||||
"@pos_sale_product_configurator/*": ["addons/pos_sale_product_configurator/static/src/*"],
|
||||
"@pos_six/*": ["addons/pos_six/static/src/*"],
|
||||
"@pos_stripe/*": ["addons/pos_stripe/static/src/*"],
|
||||
"@test_mail/*": ["addons/test_mail/static/src/*"]
|
||||
"@test_mail/*": ["addons/test_mail/static/src/*"],
|
||||
"@whatsapp_pos/*": ["addons/whatsapp_pos/static/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.js", "**/*.ts"],
|
||||
|
||||
Reference in New Issue
Block a user