From fe62bfeacb38e03cb94010f1f75ae301951ac9d1 Mon Sep 17 00:00:00 2001 From: Nishant Jain Date: Thu, 24 Aug 2023 15:45:10 +0200 Subject: [PATCH 1/4] [IMP] web: add DynamicModelFieldSelectorChar widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this commit we create a DynamicModelFieldSelectorChar widget. Purpose is to use it in WhatsApp to select fields when trying to make a valid path to find information on a given model. Example It is based on char widget, using only_searchable options. Task-2377154 (Add WhatsApp Support) Part-of: odoo/odoo#134377 Co-authored-by: Akshat Trivedi Co-authored-by: Aurélien Warnon Co-authored-by: Gitashri Mantha Co-authored-by: Jigar Vaghela Co-authored-by: Kashyap Patel Co-authored-by: Khushi Vakil Co-authored-by: Nishant Jain Co-authored-by: Prakash Prajapati Co-authored-by: Priyanshi patel Co-authored-by: Rahul Prajapati Co-authored-by: Renaud Thiry Co-authored-by: Shreya Patel Co-authored-by: Stéphane Debauche Co-authored-by: Zeel Patel --- .../dynamic_model_field_selector.js | 12 +++ .../dynamic_model_field_selector_char.js | 74 +++++++++++++++++++ .../dynamic_model_field_selector_char.xml | 14 ++++ addons/web/tooling/_eslintignore | 6 ++ addons/web/tooling/_jsconfig.json | 4 +- 5 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 addons/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector.js create mode 100644 addons/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector_char.js create mode 100644 addons/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector_char.xml diff --git a/addons/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector.js b/addons/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector.js new file mode 100644 index 00000000000..082528e6783 --- /dev/null +++ b/addons/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector.js @@ -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 }, + }; +} diff --git a/addons/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector_char.js b/addons/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector_char.js new file mode 100644 index 00000000000..e65ac0023d3 --- /dev/null +++ b/addons/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector_char.js @@ -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); diff --git a/addons/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector_char.xml b/addons/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector_char.xml new file mode 100644 index 00000000000..91269be4533 --- /dev/null +++ b/addons/web/static/src/views/fields/dynamic_widget/dynamic_model_field_selector_char.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/addons/web/tooling/_eslintignore b/addons/web/tooling/_eslintignore index 81b309be399..ae2b35a0dac 100644 --- a/addons/web/tooling/_eslintignore +++ b/addons/web/tooling/_eslintignore @@ -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 diff --git a/addons/web/tooling/_jsconfig.json b/addons/web/tooling/_jsconfig.json index 5749dbfab2a..fd14f58da6b 100644 --- a/addons/web/tooling/_jsconfig.json +++ b/addons/web/tooling/_jsconfig.json @@ -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"], From cdae935731555d4b2a46bc9da23cbf9af3048378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 11 Aug 2023 14:27:59 +0200 Subject: [PATCH 2/4] [IMP] mail: allow to link partners to logs In this commit we allow to give 'partner_ids' when using '_message_log'. This allows to link a message to recipients but those are not notified in any way. Main purpose is to link a message which has a side effect to the main record customer. First usage will be in WhatsApp implementation to link a sent WhatsApp to a recipient, easing finding it back when receiving replies. Task-2377154 (Add WhatsApp Support) Part-of: odoo/odoo#134377 --- addons/mail/models/mail_thread.py | 10 +++++-- addons/test_mail/tests/test_message_post.py | 33 +++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 2e424380c37..145065894bd 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -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 diff --git a/addons/test_mail/tests/test_message_post.py b/addons/test_mail/tests/test_message_post.py index 2d0ead3f7f3..81661445e69 100644 --- a/addons/test_mail/tests/test_message_post.py +++ b/addons/test_mail/tests/test_message_post.py @@ -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('

Test _message_log_batch

') + 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': '

Test _message_log_batch

', + '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'), From 628f3a54e49a7d1c984db18aa835284ad8abf766 Mon Sep 17 00:00:00 2001 From: Nishant Jain Date: Thu, 24 Aug 2023 15:42:52 +0200 Subject: [PATCH 3/4] [IMP] mail: prepare inclusion of whatsapp channels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prepare 'discuss.channel' model as well as discuss code to include 'whatsapp' channels and WhatsApp flow inclusion. Those channels are specifically used in whatsapp-based flows and have some specific behavior, hence using a specific channel_type for them. Also add a setting in 'mail' to install 'whatsapp' module. Task-2377154 (Add WhatsApp Support) Part-of: odoo/odoo#134377 Co-authored-by: Akshat Trivedi Co-authored-by: Aurélien Warnon Co-authored-by: Gitashri Mantha Co-authored-by: Jigar Vaghela Co-authored-by: Kashyap Patel Co-authored-by: Khushi Vakil Co-authored-by: Nishant Jain Co-authored-by: Prakash Prajapati Co-authored-by: Priyanshi patel Co-authored-by: Rahul Prajapati Co-authored-by: Renaud Thiry Co-authored-by: Shreya Patel Co-authored-by: Stéphane Debauche Co-authored-by: Zeel Patel --- addons/mail/models/discuss/discuss_channel.py | 5 +++-- addons/mail/models/discuss/res_partner.py | 4 ++-- .../mail/static/src/core/common/composer.js | 21 +++++++++---------- addons/mail/static/src/core/web/chatter.xml | 2 +- .../mock_server/models/discuss_channel.js | 11 ++++++++++ 5 files changed, 27 insertions(+), 16 deletions(-) diff --git a/addons/mail/models/discuss/discuss_channel.py b/addons/mail/models/discuss/discuss_channel.py index 7097ba61d04..645db447d50 100644 --- a/addons/mail/models/discuss/discuss_channel.py +++ b/addons/mail/models/discuss/discuss_channel.py @@ -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) diff --git a/addons/mail/models/discuss/res_partner.py b/addons/mail/models/discuss/res_partner.py index ea5c086be42..61b77b5caeb 100644 --- a/addons/mail/models/discuss/res_partner.py +++ b/addons/mail/models/discuss/res_partner.py @@ -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", diff --git a/addons/mail/static/src/core/common/composer.js b/addons/mail/static/src/core/common/composer.js index 8cf6ddd3281..b8a80bdb854 100644 --- a/addons/mail/static/src/core/common/composer.js +++ b/addons/mail/static/src/core/common/composer.js @@ -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 */ diff --git a/addons/mail/static/src/core/web/chatter.xml b/addons/mail/static/src/core/web/chatter.xml index ea644c09d7e..2a56b5141ed 100644 --- a/addons/mail/static/src/core/web/chatter.xml +++ b/addons/mail/static/src/core/web/chatter.xml @@ -21,7 +21,7 @@ Log note
- diff --git a/addons/mail/static/tests/helpers/mock_server/models/discuss_channel.js b/addons/mail/static/tests/helpers/mock_server/models/discuss_channel.js index 60ba2b75c6b..99aa24e87fa 100644 --- a/addons/mail/static/tests/helpers/mock_server/models/discuss_channel.js +++ b/addons/mail/static/tests/helpers/mock_server/models/discuss_channel.js @@ -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 = `
invited ${partner.name} to the channel
`; 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 = `
${selfPartner.name} joined the channel
`; + 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], From b71e5eea64d63b881ae93319c954345749b330ba Mon Sep 17 00:00:00 2001 From: Renaud Thiry Date: Thu, 7 Sep 2023 08:56:39 +0200 Subject: [PATCH 4/4] [FIX] mail: voice do not trigger change on unmount 'unmounting' key was not passed in when cleaning up the voice recorder for an unmount. In whatsapp this made the composer difficult to use as the voice recoder is unmounted each time a message is sent. The missing key made it impossible to send multiple messages as the composer would be in the 'recording message' state, disabling the send button. Task-2377154 Part-of: odoo/odoo#134377 --- .../static/src/discuss/voice_message/common/voice_recorder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/mail/static/src/discuss/voice_message/common/voice_recorder.js b/addons/mail/static/src/discuss/voice_message/common/voice_recorder.js index 6a527d5b8e3..d1ea10f8adb 100644 --- a/addons/mail/static/src/discuss/voice_message/common/voice_recorder.js +++ b/addons/mail/static/src/discuss/voice_message/common/voice_recorder.js @@ -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}); } }); }