diff --git a/mail_composer_cc_bcc/README.rst b/mail_composer_cc_bcc/README.rst new file mode 100644 index 00000000..6a55e52f --- /dev/null +++ b/mail_composer_cc_bcc/README.rst @@ -0,0 +1,147 @@ +================ +Email CC and BCC +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:74b49ed221c9dcabf5962c9efa43cc1b8dff0b08074da5a2ec9b685adbb5868f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmail-lightgray.png?logo=github + :target: https://github.com/OCA/mail/tree/18.0/mail_composer_cc_bcc + :alt: OCA/mail +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/mail-18-0/mail-18-0-mail_composer_cc_bcc + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/mail&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Odoo native does not support defining a Cc field in the Mail Composer by +default; instead, it only has a unique Recipients fields, which is +confusing for a lot of end users. + +This module allows to properly separate To:, Cc:, and Bcc: fields in the +Mail Composer. + +From Odoo 17.0, this module sends one mail per recipient and keeps same +all headers (To, Cc, Bcc) in all emails + +Features +-------- + +- Add Cc and Bcc fields to company form to use them as default in mail + composer form. +- Add Bcc field to mail template form. Use Cc and Bcc fields to lookup + partners by email then add them to corresponding fields in mail + composer form. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +In company form there are two fields to set default cc and bcc partners. + + |res_company_form_default_cc_bcc| + +In template form there are two fields to set cc and bcc emails. + + |email_template_form_cc_bcc| + +.. |res_company_form_default_cc_bcc| image:: https://raw.githubusercontent.com/OCA/mail/18.0/mail_composer_cc_bcc/static/img/res_company_form_default_cc_bcc.png +.. |email_template_form_cc_bcc| image:: https://raw.githubusercontent.com/OCA/mail/18.0/mail_composer_cc_bcc/static/img/email_template_form_cc_bcc.png + +Usage +===== + +The partners cc and bcc from company form will be used to fill in mail +composer form. + + |image| + +When select a template that has cc and/or bcc emails, the emails will be +used to lookup partners then found partners will be added to +corresponding mail composer's fields. + + |image1| + +.. |image| image:: https://raw.githubusercontent.com/OCA/mail/18.0/mail_composer_cc_bcc/static/img/mail_compose_message_default_cc_bcc.png +.. |image1| image:: https://raw.githubusercontent.com/OCA/mail/18.0/mail_composer_cc_bcc/static/img/mail_compose_message_template_cc_bcc.png + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Camptocamp + +Contributors +------------ + +- `Trobz `__: + + - Hai N. Le + - Son Ho + - Tri Doan + +Other credits +------------- + +The creation and migration of this module from 16.0 to 17.0, and then +17.0 to 18.0 were financially supported by Camptocamp. + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-trisdoan| image:: https://github.com/trisdoan.png?size=40px + :target: https://github.com/trisdoan + :alt: trisdoan + +Current `maintainer `__: + +|maintainer-trisdoan| + +This module is part of the `OCA/mail `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mail_composer_cc_bcc/__init__.py b/mail_composer_cc_bcc/__init__.py new file mode 100644 index 00000000..aee8895e --- /dev/null +++ b/mail_composer_cc_bcc/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/mail_composer_cc_bcc/__manifest__.py b/mail_composer_cc_bcc/__manifest__.py new file mode 100644 index 00000000..44344d92 --- /dev/null +++ b/mail_composer_cc_bcc/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Email CC and BCC", + "summary": "This module enables sending mail to CC and BCC partners in mail composer form.", # noqa: E501 + "version": "18.0.1.0.0", + "development_status": "Alpha", + "category": "Social", + "website": "https://github.com/OCA/mail", + "author": "Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["trisdoan"], + "license": "AGPL-3", + "application": False, + "installable": True, + "preloadable": True, + "depends": [ + "mail", + ], + "data": [ + "views/res_company_views.xml", + "views/mail_mail_views.xml", + "views/mail_message_views.xml", + "views/mail_template_views.xml", + "wizards/mail_compose_message_view.xml", + "wizards/mail_template_preview_view.xml", + ], +} diff --git a/mail_composer_cc_bcc/i18n/es.po b/mail_composer_cc_bcc/i18n/es.po new file mode 100644 index 00000000..346cffc8 --- /dev/null +++ b/mail_composer_cc_bcc/i18n/es.po @@ -0,0 +1,125 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_composer_cc_bcc +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-11-08 13:02+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_compose_message__partner_bcc_ids +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_mail__email_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_template__email_bcc +msgid "Bcc" +msgstr "Bcc" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_mail__recipient_bcc_ids +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_message__recipient_bcc_ids +msgid "Bcc (Partners)" +msgstr "Bcc (Socios)" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,help:mail_composer_cc_bcc.field_mail_mail__email_bcc +msgid "Blind Cc message recipients" +msgstr "Destinatarios de mensajes Cc ciegos" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,help:mail_composer_cc_bcc.field_mail_template__email_bcc +msgid "Blind cc recipients (placeholders may be used here)" +msgstr "Destinatarios cc ciegos (pueden utilizarse marcadores de posición)" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_compose_message__partner_cc_ids +msgid "Cc" +msgstr "Con copia (cc)" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_mail__recipient_cc_ids +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_message__recipient_cc_ids +msgid "Cc (Partners)" +msgstr "Cc (Socios)" + +#. module: mail_composer_cc_bcc +#: model_terms:ir.ui.view,arch_db:mail_composer_cc_bcc.email_template_form_inherit +#: model_terms:ir.ui.view,arch_db:mail_composer_cc_bcc.view_mail_form_inherit +msgid "Comma-separated blind carbon copy recipients addresses" +msgstr "" +"Direcciones de los destinatarios de la copia oculta separadas por comas" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_res_company +msgid "Companies" +msgstr "Compañías" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_res_company__default_partner_bcc_ids +msgid "Default Bcc" +msgstr "Bcc por defecto" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_res_company__default_partner_cc_ids +msgid "Default Cc" +msgstr "Cc por defecto" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_template +msgid "Email Templates" +msgstr "Plantillas Correo Electrónico" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_thread +msgid "Email Thread" +msgstr "Hilo de Correo Electrónico" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_compose_message +msgid "Email composition wizard" +msgstr "Asistente de redacción de correo electrónico" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_ir_mail_server +msgid "Mail Server" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_message +msgid "Message" +msgstr "Mensaje" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_mail +msgid "Outgoing Mails" +msgstr "Correos Salientes" + +#. module: mail_composer_cc_bcc +#: model_terms:ir.ui.view,arch_db:mail_composer_cc_bcc.view_mail_form_inherit +msgid "Partners" +msgstr "Socios" + +#, python-format +#~ msgid "" +#~ "Error without exception. Probably due do concurrent access update of " +#~ "notification records. Please see with an administrator." +#~ msgstr "" +#~ "Error sin excepción. Probablemente debido a la actualización de acceso " +#~ "concurrente de los registros de notificación. Por favor, consulte con un " +#~ "administrador." + +#, python-format +#~ msgid "" +#~ "Error without exception. Probably due do sending an email without " +#~ "computed recipients." +#~ msgstr "" +#~ "Error sin excepción. Probablemente debido a enviar un email sin " +#~ "destinatarios computados." diff --git a/mail_composer_cc_bcc/i18n/it.po b/mail_composer_cc_bcc/i18n/it.po new file mode 100644 index 00000000..cd0da7fb --- /dev/null +++ b/mail_composer_cc_bcc/i18n/it.po @@ -0,0 +1,123 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_composer_cc_bcc +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-07-23 09:58+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_compose_message__partner_bcc_ids +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_mail__email_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_template__email_bcc +msgid "Bcc" +msgstr "Bcc" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_mail__recipient_bcc_ids +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_message__recipient_bcc_ids +msgid "Bcc (Partners)" +msgstr "Bcc (partner)" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,help:mail_composer_cc_bcc.field_mail_mail__email_bcc +msgid "Blind Cc message recipients" +msgstr "Nascondi destinatari in CC meggaggio" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,help:mail_composer_cc_bcc.field_mail_template__email_bcc +msgid "Blind cc recipients (placeholders may be used here)" +msgstr "Nascondi destinatari in CC (qui si possono usare segnaposto)" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_compose_message__partner_cc_ids +msgid "Cc" +msgstr "Cc" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_mail__recipient_cc_ids +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_message__recipient_cc_ids +msgid "Cc (Partners)" +msgstr "Cc (partner)" + +#. module: mail_composer_cc_bcc +#: model_terms:ir.ui.view,arch_db:mail_composer_cc_bcc.email_template_form_inherit +#: model_terms:ir.ui.view,arch_db:mail_composer_cc_bcc.view_mail_form_inherit +msgid "Comma-separated blind carbon copy recipients addresses" +msgstr "Indrizzi destinatari in copia carbone nascosta separati da virgola" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_res_company +msgid "Companies" +msgstr "Aziende" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_res_company__default_partner_bcc_ids +msgid "Default Bcc" +msgstr "Bcc predefinito" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_res_company__default_partner_cc_ids +msgid "Default Cc" +msgstr "Cc predefinito" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_template +msgid "Email Templates" +msgstr "Modelli e-mail" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_thread +msgid "Email Thread" +msgstr "Discussione e-mail" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_compose_message +msgid "Email composition wizard" +msgstr "Procedura guidata creazione e-mail" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_ir_mail_server +msgid "Mail Server" +msgstr "Server di posta" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_message +msgid "Message" +msgstr "Messaggio" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_mail +msgid "Outgoing Mails" +msgstr "E-mail in uscita" + +#. module: mail_composer_cc_bcc +#: model_terms:ir.ui.view,arch_db:mail_composer_cc_bcc.view_mail_form_inherit +msgid "Partners" +msgstr "Partner" + +#, python-format +#~ msgid "" +#~ "Error without exception. Probably due do concurrent access update of " +#~ "notification records. Please see with an administrator." +#~ msgstr "" +#~ "Errore senza eccezione. Probabilmente per un accesso concorrente per " +#~ "aggiornamento record notifiche. Verificare con un amministratore." + +#, python-format +#~ msgid "" +#~ "Error without exception. Probably due do sending an email without " +#~ "computed recipients." +#~ msgstr "" +#~ "Errore senza eccezione. Probabilmente dovto all'invio di una e-mail con " +#~ "destinatari mancanti." diff --git a/mail_composer_cc_bcc/i18n/mail_composer_cc_bcc.pot b/mail_composer_cc_bcc/i18n/mail_composer_cc_bcc.pot new file mode 100644 index 00000000..18f1ba18 --- /dev/null +++ b/mail_composer_cc_bcc/i18n/mail_composer_cc_bcc.pot @@ -0,0 +1,104 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_composer_cc_bcc +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_compose_message__partner_bcc_ids +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_mail__email_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_template__email_bcc +msgid "Bcc" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_mail__recipient_bcc_ids +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_message__recipient_bcc_ids +msgid "Bcc (Partners)" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,help:mail_composer_cc_bcc.field_mail_mail__email_bcc +msgid "Blind Cc message recipients" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,help:mail_composer_cc_bcc.field_mail_template__email_bcc +msgid "Blind cc recipients (placeholders may be used here)" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_compose_message__partner_cc_ids +msgid "Cc" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_mail__recipient_cc_ids +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_mail_message__recipient_cc_ids +msgid "Cc (Partners)" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model_terms:ir.ui.view,arch_db:mail_composer_cc_bcc.email_template_form_inherit +#: model_terms:ir.ui.view,arch_db:mail_composer_cc_bcc.view_mail_form_inherit +msgid "Comma-separated blind carbon copy recipients addresses" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_res_company +msgid "Companies" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_res_company__default_partner_bcc_ids +msgid "Default Bcc" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model.fields,field_description:mail_composer_cc_bcc.field_res_company__default_partner_cc_ids +msgid "Default Cc" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_template +msgid "Email Templates" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_thread +msgid "Email Thread" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_compose_message +msgid "Email composition wizard" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_ir_mail_server +msgid "Mail Server" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_message +msgid "Message" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model:ir.model,name:mail_composer_cc_bcc.model_mail_mail +msgid "Outgoing Mails" +msgstr "" + +#. module: mail_composer_cc_bcc +#: model_terms:ir.ui.view,arch_db:mail_composer_cc_bcc.view_mail_form_inherit +msgid "Partners" +msgstr "" diff --git a/mail_composer_cc_bcc/models/__init__.py b/mail_composer_cc_bcc/models/__init__.py new file mode 100644 index 00000000..3e2600ae --- /dev/null +++ b/mail_composer_cc_bcc/models/__init__.py @@ -0,0 +1,8 @@ +# These modules are sorted by calling sequence, i.e. mail_thread calls +# mail_message, etc. +from . import res_company +from . import mail_template +from . import mail_thread +from . import mail_message +from . import ir_mail_server +from . import mail_mail diff --git a/mail_composer_cc_bcc/models/ir_mail_server.py b/mail_composer_cc_bcc/models/ir_mail_server.py new file mode 100644 index 00000000..df6b1af3 --- /dev/null +++ b/mail_composer_cc_bcc/models/ir_mail_server.py @@ -0,0 +1,35 @@ +# Copyright 2024 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import models + +_logger = logging.getLogger(__name__) + + +class IrMailServer(models.Model): + _inherit = "ir.mail_server" + + def _prepare_email_message(self, message, smtp_session): + """ + Define smtp_to based on context instead of To+Cc+Bcc + """ + x_odoo_bcc_value = next( + (value for key, value in message._headers if key == "X-Odoo-Bcc"), None + ) + # Add Bcc field inside message to pass validation + if x_odoo_bcc_value: + message["Bcc"] = x_odoo_bcc_value + + smtp_from, smtp_to_list, message = super()._prepare_email_message( + message, smtp_session + ) + + is_from_composer = self.env.context.get("is_from_composer", False) + if is_from_composer and self.env.context.get("recipients", False): + smtp_to = self.env.context["recipients"].pop(0) + _logger.debug("smtp_to: %s", smtp_to) + smtp_to_list = [smtp_to] + + return smtp_from, smtp_to_list, message diff --git a/mail_composer_cc_bcc/models/mail_mail.py b/mail_composer_cc_bcc/models/mail_mail.py new file mode 100644 index 00000000..a43830f9 --- /dev/null +++ b/mail_composer_cc_bcc/models/mail_mail.py @@ -0,0 +1,85 @@ +# Copyright 2023 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import fields, models, tools + +from odoo.addons.base.models.ir_mail_server import extract_rfc2822_addresses + + +def format_emails(partners): + emails = [tools.formataddr((p.name or "", p.email)) for p in partners if p.email] + return ", ".join(emails) + + +def format_emails_raw(partners): + emails = [p.email for p in partners if p.email] + return ", ".join(emails) + + +class MailMail(models.Model): + _inherit = "mail.mail" + + email_bcc = fields.Char("Bcc", help="Blind Cc message recipients") + + def _prepare_outgoing_list( + self, mail_server=False, recipients_follower_status=None + ): + # First, return if we're not coming from the Mail Composer + res = super()._prepare_outgoing_list( + mail_server=mail_server, + recipients_follower_status=recipients_follower_status, + ) + is_out_of_scope = len(self.ids) > 1 + is_from_composer = self.env.context.get("is_from_composer", False) + + if is_out_of_scope or not is_from_composer: + return res + + # Prepare values for To, Cc headers + partners_cc_bcc = self.recipient_cc_ids + self.recipient_bcc_ids + partner_to_ids = [r.id for r in self.recipient_ids if r not in partners_cc_bcc] + partner_to = self.env["res.partner"].browse(partner_to_ids) + email_to = format_emails(partner_to) + email_to_raw = format_emails_raw(partner_to) + email_cc = format_emails(self.recipient_cc_ids) + email_bcc = [r.email for r in self.recipient_bcc_ids if r.email] + + # Collect recipients (RCPT TO) and update all emails + # with the same To, Cc headers (to be shown by email client as users expect) + recipients = set() + for m in res: + rcpt_to = None + if m["email_to"]: + rcpt_to = extract_rfc2822_addresses(m["email_to"][0])[0] + + # If the recipient is a Bcc, we had an explicit header X-Odoo-Bcc + # - It won't be shown by the email client, but can be useful for a recipient # noqa: E501 + # to understand why he received a given email + # - Also note that in python3, the smtp.send_message method does not + # transmit the Bcc field of a Message object + if rcpt_to in email_bcc: + m["headers"].update({"X-Odoo-Bcc": m["email_to"][0]}) + + # in the absence of self.email_to, Odoo creates one special mail for CC + # see https://github.com/odoo/odoo/commit/46bad8f0 + elif m["email_cc"]: + rcpt_to = extract_rfc2822_addresses(m["email_cc"][0])[0] + + if rcpt_to: + recipients.add(rcpt_to) + + m.update( + { + "email_to": email_to, + "email_to_raw": email_to_raw, + "email_cc": email_cc, + } + ) + + self.env.context = {**self.env.context, "recipients": list(recipients)} + + if len(res) > len(recipients): + res.pop() + + return res diff --git a/mail_composer_cc_bcc/models/mail_message.py b/mail_composer_cc_bcc/models/mail_message.py new file mode 100644 index 00000000..d4740f0a --- /dev/null +++ b/mail_composer_cc_bcc/models/mail_message.py @@ -0,0 +1,25 @@ +# Copyright 2023 Camptocamp +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class MailMessage(models.Model): + _inherit = "mail.message" + + recipient_cc_ids = fields.Many2many( + "res.partner", + "mail_message_res_partner_cc_rel", + "mail_message_id", + "parent_id", + string="Cc (Partners)", + context={"active_test": False}, + ) + recipient_bcc_ids = fields.Many2many( + "res.partner", + "mail_message_res_partner_bcc_rel", + "mail_message_id", + "parent_id", + string="Bcc (Partners)", + context={"active_test": False}, + ) diff --git a/mail_composer_cc_bcc/models/mail_template.py b/mail_composer_cc_bcc/models/mail_template.py new file mode 100644 index 00000000..fb128abb --- /dev/null +++ b/mail_composer_cc_bcc/models/mail_template.py @@ -0,0 +1,13 @@ +# Copyright 2023 Camptocamp +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import fields, models + + +class MailTemplate(models.Model): + _inherit = "mail.template" + + email_bcc = fields.Char( + "Bcc", help="Blind cc recipients (placeholders may be used here)" + ) diff --git a/mail_composer_cc_bcc/models/mail_thread.py b/mail_composer_cc_bcc/models/mail_thread.py new file mode 100644 index 00000000..02b60e32 --- /dev/null +++ b/mail_composer_cc_bcc/models/mail_thread.py @@ -0,0 +1,129 @@ +# Copyright 2023 Camptocamp +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + +from .mail_mail import format_emails + + +class MailThread(models.AbstractModel): + _inherit = "mail.thread" + + # ------------------------------------------------------------ + # MAIL.MESSAGE HELPERS + # ------------------------------------------------------------ + + def _get_message_create_valid_field_names(self): + """ + add cc and bcc field to create record in mail.mail + """ + field_names = super()._get_message_create_valid_field_names() + field_names.update({"recipient_cc_ids", "recipient_bcc_ids"}) + return field_names + + # ------------------------------------------------------ + # NOTIFICATION API + # ------------------------------------------------------ + + def _notify_by_email_get_base_mail_values(self, message, additional_values=None): + """ + This is to add cc, bcc addresses to mail.mail objects so that email + can be sent to those addresses. + """ + res = super()._notify_by_email_get_base_mail_values( + message, additional_values=additional_values + ) + context = self.env.context + skip_adding_cc_bcc = context.get("skip_adding_cc_bcc", False) + if skip_adding_cc_bcc: + return res + + partners_cc = context.get("partner_cc_ids", None) + if partners_cc: + res["email_cc"] = format_emails(partners_cc) + + partners_bcc = context.get("partner_bcc_ids", None) + if partners_bcc: + res["email_bcc"] = format_emails(partners_bcc) + + return res + + def _notify_get_recipients(self, message, msg_vals, **kwargs): + """ + This is to add cc, bcc recipients so that they can be grouped with + other recipients. + """ + ResPartner = self.env["res.partner"] + MailFollowers = self.env["mail.followers"] + rdata = super()._notify_get_recipients(message, msg_vals, **kwargs) + context = self.env.context + is_from_composer = context.get("is_from_composer", False) + skip_adding_cc_bcc = context.get("skip_adding_cc_bcc", False) + if not is_from_composer or skip_adding_cc_bcc: + return rdata + for pdata in rdata: + pdata["type"] = "customer" + partners_cc_bcc = context.get("partner_cc_ids", ResPartner) + partners_cc_bcc += context.get("partner_bcc_ids", ResPartner) + msg_sudo = message.sudo() + message_type = ( + msg_vals.get("message_type") if msg_vals else msg_sudo.message_type + ) + subtype_id = msg_vals.get("subtype_id") if msg_vals else msg_sudo.subtype_id.id + recipients_cc_bcc = MailFollowers._get_recipient_data( + None, message_type, subtype_id, partners_cc_bcc.ids + ) + partners_already_marked_as_recipient = [r.get("id", False) for r in rdata] + for _, value in recipients_cc_bcc.items(): + for _, data in value.items(): + if ( + not data.get("id") + or data.get("id") in partners_already_marked_as_recipient + ): + continue + if not data.get( + "notif" + ): # notif is False, has no user, is therefore customer + notif = "email" + msg_type = "customer" + pdata = { + "id": data.get("id"), + "active": data.get("active"), + "share": data.get("share"), + "notif": data.get("notif") and data.get("notif") or notif, + "type": msg_type, + "is_follower": data.get("is_follower"), + "uid": False, + } + rdata.append(pdata) + return rdata + + def _notify_get_recipients_classify( + self, message, recipients_data, model_description, msg_vals=None + ): + res = super()._notify_get_recipients_classify( + message, recipients_data, model_description, msg_vals=msg_vals + ) + is_from_composer = self.env.context.get("is_from_composer", False) + skip_adding_cc_bcc = self.env.context.get("skip_adding_cc_bcc", False) + if not is_from_composer or skip_adding_cc_bcc: + return res + ids = [] + customer_data = None + for rcpt_data in res: + if rcpt_data["notification_group_name"] == "customer": + customer_data = rcpt_data + else: + ids += rcpt_data["recipients"] + if not customer_data: + customer_data = res[0] + customer_data["notification_group_name"] = "customer" + customer_data["recipients"] = ids + else: + customer_data["recipients"] += ids + return [customer_data] + + def _notify_thread(self, message, msg_vals=False, **kwargs): + if message.message_type == "notification": + self = self.with_context(skip_adding_cc_bcc=True) + return super()._notify_thread(message, msg_vals, **kwargs) diff --git a/mail_composer_cc_bcc/models/res_company.py b/mail_composer_cc_bcc/models/res_company.py new file mode 100644 index 00000000..47ea9e7e --- /dev/null +++ b/mail_composer_cc_bcc/models/res_company.py @@ -0,0 +1,23 @@ +# Copyright 2023 Camptocamp +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class Company(models.Model): + _inherit = "res.company" + + default_partner_cc_ids = fields.Many2many( + "res.partner", + "res_company_res_partner_cc_rel", + "company_id", + "partner_id", + string="Default Cc", + ) + default_partner_bcc_ids = fields.Many2many( + "res.partner", + "res_company_res_partner_bcc_rel", + "company_id", + "partner_id", + string="Default Bcc", + ) diff --git a/mail_composer_cc_bcc/pyproject.toml b/mail_composer_cc_bcc/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/mail_composer_cc_bcc/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/mail_composer_cc_bcc/readme/CONFIGURE.md b/mail_composer_cc_bcc/readme/CONFIGURE.md new file mode 100644 index 00000000..8d9cddde --- /dev/null +++ b/mail_composer_cc_bcc/readme/CONFIGURE.md @@ -0,0 +1,7 @@ +In company form there are two fields to set default cc and bcc partners. + +> ![res_company_form_default_cc_bcc](../static/img/res_company_form_default_cc_bcc.png) + +In template form there are two fields to set cc and bcc emails. + +> ![email_template_form_cc_bcc](../static/img/email_template_form_cc_bcc.png) diff --git a/mail_composer_cc_bcc/readme/CONTRIBUTORS.md b/mail_composer_cc_bcc/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..a8be9c13 --- /dev/null +++ b/mail_composer_cc_bcc/readme/CONTRIBUTORS.md @@ -0,0 +1,5 @@ +- [Trobz](https://www.trobz.com): + + > - Hai N. Le \<\> + > - Son Ho \<\> + > - Tri Doan \<\> \ No newline at end of file diff --git a/mail_composer_cc_bcc/readme/CREDITS.md b/mail_composer_cc_bcc/readme/CREDITS.md new file mode 100644 index 00000000..53026c98 --- /dev/null +++ b/mail_composer_cc_bcc/readme/CREDITS.md @@ -0,0 +1 @@ +The creation and migration of this module from 16.0 to 17.0, and then 17.0 to 18.0 were financially supported by Camptocamp. diff --git a/mail_composer_cc_bcc/readme/DESCRIPTION.md b/mail_composer_cc_bcc/readme/DESCRIPTION.md new file mode 100644 index 00000000..b2fa1d3d --- /dev/null +++ b/mail_composer_cc_bcc/readme/DESCRIPTION.md @@ -0,0 +1,16 @@ +Odoo native does not support defining a Cc field in the Mail Composer by +default; instead, it only has a unique Recipients fields, which is +confusing for a lot of end users. + +This module allows to properly separate To:, Cc:, and Bcc: fields in the +Mail Composer. + +From Odoo 17.0, this module sends one mail per recipient and keeps same all headers (To, Cc, Bcc) in all emails + +## Features + +- Add Cc and Bcc fields to company form to use them as default in mail + composer form. +- Add Bcc field to mail template form. Use Cc and Bcc fields to lookup + partners by email then add them to corresponding fields in mail + composer form. diff --git a/mail_composer_cc_bcc/readme/USAGE.md b/mail_composer_cc_bcc/readme/USAGE.md new file mode 100644 index 00000000..dd4c9cd7 --- /dev/null +++ b/mail_composer_cc_bcc/readme/USAGE.md @@ -0,0 +1,10 @@ +The partners cc and bcc from company form will be used to fill in mail +composer form. + +> ![image](../static/img/mail_compose_message_default_cc_bcc.png) + +When select a template that has cc and/or bcc emails, the emails will be +used to lookup partners then found partners will be added to +corresponding mail composer's fields. + +> ![image](../static/img/mail_compose_message_template_cc_bcc.png) diff --git a/mail_composer_cc_bcc/static/description/icon.png b/mail_composer_cc_bcc/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/mail_composer_cc_bcc/static/description/icon.png differ diff --git a/mail_composer_cc_bcc/static/description/index.html b/mail_composer_cc_bcc/static/description/index.html new file mode 100644 index 00000000..3572c948 --- /dev/null +++ b/mail_composer_cc_bcc/static/description/index.html @@ -0,0 +1,478 @@ + + + + + +Email CC and BCC + + + +
+

Email CC and BCC

+ + +

Alpha License: AGPL-3 OCA/mail Translate me on Weblate Try me on Runboat

+

Odoo native does not support defining a Cc field in the Mail Composer by +default; instead, it only has a unique Recipients fields, which is +confusing for a lot of end users.

+

This module allows to properly separate To:, Cc:, and Bcc: fields in the +Mail Composer.

+

From Odoo 17.0, this module sends one mail per recipient and keeps same +all headers (To, Cc, Bcc) in all emails

+
+

Features

+
    +
  • Add Cc and Bcc fields to company form to use them as default in mail +composer form.
  • +
  • Add Bcc field to mail template form. Use Cc and Bcc fields to lookup +partners by email then add them to corresponding fields in mail +composer form.
  • +
+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Configuration

+

In company form there are two fields to set default cc and bcc partners.

+
+res_company_form_default_cc_bcc
+

In template form there are two fields to set cc and bcc emails.

+
+email_template_form_cc_bcc
+
+
+

Usage

+

The partners cc and bcc from company form will be used to fill in mail +composer form.

+
+image
+

When select a template that has cc and/or bcc emails, the emails will be +used to lookup partners then found partners will be added to +corresponding mail composer’s fields.

+
+image1
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+ +
+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The creation and migration of this module from 16.0 to 17.0, and then +17.0 to 18.0 were financially supported by Camptocamp.

+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

trisdoan

+

This module is part of the OCA/mail project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+ + diff --git a/mail_composer_cc_bcc/static/img/email_template_form_cc_bcc.png b/mail_composer_cc_bcc/static/img/email_template_form_cc_bcc.png new file mode 100644 index 00000000..336e4c1a Binary files /dev/null and b/mail_composer_cc_bcc/static/img/email_template_form_cc_bcc.png differ diff --git a/mail_composer_cc_bcc/static/img/mail_compose_message_default_cc_bcc.png b/mail_composer_cc_bcc/static/img/mail_compose_message_default_cc_bcc.png new file mode 100644 index 00000000..7e1b0706 Binary files /dev/null and b/mail_composer_cc_bcc/static/img/mail_compose_message_default_cc_bcc.png differ diff --git a/mail_composer_cc_bcc/static/img/mail_compose_message_template_cc_bcc.png b/mail_composer_cc_bcc/static/img/mail_compose_message_template_cc_bcc.png new file mode 100644 index 00000000..f1de0f5e Binary files /dev/null and b/mail_composer_cc_bcc/static/img/mail_compose_message_template_cc_bcc.png differ diff --git a/mail_composer_cc_bcc/static/img/res_company_form_default_cc_bcc.png b/mail_composer_cc_bcc/static/img/res_company_form_default_cc_bcc.png new file mode 100644 index 00000000..8393e346 Binary files /dev/null and b/mail_composer_cc_bcc/static/img/res_company_form_default_cc_bcc.png differ diff --git a/mail_composer_cc_bcc/tests/__init__.py b/mail_composer_cc_bcc/tests/__init__.py new file mode 100644 index 00000000..7a8ae217 --- /dev/null +++ b/mail_composer_cc_bcc/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mail_cc_bcc diff --git a/mail_composer_cc_bcc/tests/test_mail_cc_bcc.py b/mail_composer_cc_bcc/tests/test_mail_cc_bcc.py new file mode 100644 index 00000000..f0d76b98 --- /dev/null +++ b/mail_composer_cc_bcc/tests/test_mail_cc_bcc.py @@ -0,0 +1,280 @@ +# Copyright 2023 Camptocamp +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import hashlib +import inspect + +from odoo import tools +from odoo.tests import Form, tagged +from odoo.tests.common import TransactionCase + +from odoo.addons.mail.tests.common import MailCase +from odoo.addons.mail.tests.test_mail_composer import TestMailComposerForm +from odoo.addons.mail.wizard.mail_compose_message import ( + MailComposer as MailComposer_upstream, +) + +VALID_HASHES = { + "mail.composer:_compute_partner_ids": ["813ef112e3948fe625b9a89428f2518d"], +} + + +class TestMailCcBcc(TestMailComposerForm): + @classmethod + def setUpClass(cls): + super().setUpClass() + env = cls.env + cls.partner = env.ref("base.res_partner_address_31") + cls.partner_cc = env.ref("base.partner_demo") + cls.partner_cc2 = env.ref("base.partner_demo_portal") + cls.partner_cc3 = env.ref("base.res_partner_main1") + cls.partner_bcc = env.ref("base.res_partner_main2") + + def open_mail_composer_form(self): + # Use form to populate data + test_record = self.test_record.with_env(self.env) + ctx = { + "default_partner_ids": test_record.ids, + "default_model": test_record._name, + "default_res_ids": test_record.ids, + # to ensure consistent test results even when mail_post_defer is installed + "mail_notify_force_send": True, + } + form = Form(self.env["mail.compose.message"].with_context(**ctx)) + form.body = "

Hello

" + return form + + def test_MailComposer_upstream_file_hash(self): + """Test that copied upstream function hasn't received fixes""" + _compute_partner_ids = inspect.getsource( + MailComposer_upstream._compute_partner_ids + ).encode() + func_hash = hashlib.md5(_compute_partner_ids).hexdigest() + self.assertIn(func_hash, VALID_HASHES.get("mail.composer:_compute_partner_ids")) + + def test_email_cc_bcc(self): + self.test_record.email = "test@example.com" + form = self.open_mail_composer_form() + composer = form.save() + # Use object to update Many2many fields (form can't do like this) + composer.partner_cc_ids = self.partner_cc + composer.partner_cc_ids |= self.partner_cc2 + composer.partner_cc_ids |= self.partner_cc3 + composer.partner_bcc_ids = self.partner_bcc + + with self.mock_mail_gateway(): + composer._action_send_mail() + + self.assertEqual(len(self._mails), 5) + + # Verify recipients of mail.message + message = self.test_record.message_ids[0] + + # only keep 1 email to avoid clutting db + # but actually send 1 mail per recipients + self.assertEqual(len(message.mail_ids), 1) + self.assertEqual(len(message.recipient_cc_ids), 3) + self.assertEqual(len(message.recipient_bcc_ids), 1) + # Verify notification + for_message = [ + ("mail_message_id", "=", message.id), + ("notification_type", "=", "email"), + ] + notif = self.env["mail.notification"].search(for_message) + self.assertEqual(len(notif), 5) + + # Verify data of mail.mail + mail = message.mail_ids + expecting = ", ".join( + [ + '"Marc Demo" ', + '"Joel Willis" ', + '"Chester Reed" ', + ] + ) + self.assertEqual(mail.email_cc, expecting) + expecting = '"Dwayne Newman" ' + self.assertEqual(mail.email_bcc, expecting) + + def test_template_cc_bcc(self): + env = self.env + # Company default values + env.company.default_partner_cc_ids = self.partner_cc3 + env.company.default_partner_bcc_ids = self.partner_cc2 + # Partner template values + tmpl_model = env["ir.model"].search([("model", "=", "res.partner")]) + partner_cc = self.partner_cc + partner_bcc = self.partner_bcc + vals = { + "name": "Product Template: Re: [E-COM11] Cabinet with Doors", + "model_id": tmpl_model.id, + "subject": "Re: [E-COM11] Cabinet with Doors", + "body_html": """

+Test Template

""", + "email_cc": tools.formataddr( + (partner_cc.name or "False", partner_cc.email or "False") + ), + "email_bcc": tools.formataddr( + (partner_bcc.name or "False", partner_bcc.email or "False") + ), + } + partner_tmpl = env["mail.template"].create(vals) + + # Open mail composer form and check for default values from company + form = self.open_mail_composer_form() + composer = form.save() + + self.assertEqual(composer.partner_cc_ids, self.partner_cc3) + self.assertEqual(composer.partner_bcc_ids, self.partner_cc2) + + # Change email template and check for values from it + form.template_id = partner_tmpl + composer = form.save() + + # Beside existing Cc and Bcc, add template's ones + form = Form(composer) + form.template_id = partner_tmpl + composer = form.save() + expecting = self.partner_cc3 + self.partner_cc + + self.assertEqual(composer.partner_cc_ids, expecting) + expecting = self.partner_cc2 + self.partner_bcc + self.assertEqual(composer.partner_bcc_ids, expecting) + # But not add Marc Demo from cc field to partner_ids field + self.assertEqual(len(composer.partner_ids), 1) + self.assertEqual(composer.partner_ids.display_name, "Test") + + # Selecting the template again doesn't add as the partners already + # in the list + form = Form(composer) + form.template_id = env["mail.template"] + form.save() + self.assertFalse(form.template_id) # no template + + form.template_id = partner_tmpl + composer = form.save() + + expecting = self.partner_cc3 + self.partner_cc + self.assertEqual(composer.partner_cc_ids, expecting) + + expecting = self.partner_cc2 + self.partner_bcc + self.assertEqual(composer.partner_bcc_ids, expecting) + + def set_company(self): + company = self.env.company + # Company default values + company.default_partner_cc_ids = self.partner_cc3 + company.default_partner_bcc_ids = self.partner_cc2 + + def test_recipient_ids_and_cc_bcc(self): + self.set_company() + form = self.open_mail_composer_form() + composer = form.save() + composer.partner_ids = self.partner + self.partner_cc + + with self.mock_mail_gateway(): + composer._action_send_mail() + + message = self.test_record.message_ids[0] + self.assertEqual(len(message.mail_ids), 1) + + # Only 4 partners notified + self.assertEqual(len(message.notified_partner_ids), 4) + self.assertEqual(len(message.notification_ids), 4) + + def test_mail_without_cc_bcc(self): + self.set_company() + form = self.open_mail_composer_form() + subject = "Testing without cc/bcc single mail" + form.subject = subject + composer = form.save() + composer.partner_cc_ids = None + composer.partner_bcc_ids = None + composer.partner_ids = self.partner + self.partner_cc # 2 emails are sent + ctx = {"mail_notify_force_send": True} + ctx.update(composer.env.context) + composer = composer.with_context(**ctx) + with self.mock_mail_gateway(): + composer._action_send_mail() + sent_mails = 0 + for mail in self._mails: + if subject == mail.get("subject"): + sent_mails += 1 + self.assertEqual(sent_mails, 2, "There should be 2 mails sent") + + +@tagged("-at_install", "post_install") +class TestMailComposerCcBccWithTracking(TransactionCase, MailCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env.ref("base.res_partner_address_31") + cls.partner_cc = cls.env.ref("base.partner_demo") + cls.partner_bcc = cls.env.ref("base.res_partner_main2") + cls.admin_user = cls.env.ref("base.user_admin") + + if "purchase.order" in cls.env: + cls.new_po = ( + cls.env["purchase.order"] + .create( + { + "partner_id": cls.partner.id, + } + ) + .with_context(mail_notrack=False) + ) + + def test_tracking_mail_without_cc_bcc(self): + if "purchase.order" in self.env: + self.cr.precommit.clear() + # create a PO + # user subscribe to tracking status of PO + self.new_po.message_subscribe( + partner_ids=self.admin_user.partner_id.ids, + subtype_ids=( + ( + self.env.ref("purchase.mt_rfq_sent") + | self.env.ref("purchase.mt_rfq_confirmed") + ).ids + ), + ) + + composer_ctx = self.new_po.action_rfq_send() + # send RFQ with cc/bcc + form = Form( + self.env["mail.compose.message"].with_context(**composer_ctx["context"]) + ) + composer = form.save() + composer.partner_ids = self.partner + composer.partner_cc_ids = self.partner_cc + composer.partner_bcc_ids = self.partner_bcc + + with self.mock_mail_gateway(), self.mock_mail_app(): + composer._action_send_mail() + self.flush_tracking() + self.assertEqual( + len(self._new_msgs), + 2, + "Expected a tracking message and a RFQ message", + ) + self.assertEqual( + self.ref("purchase.mt_rfq_sent"), + self._new_msgs[1].subtype_id.id, + "Expected a tracking message", + ) + + # RFQ email should include cc/bcc + rfq_message = self._new_msgs.filtered(lambda x: x.message_type == "comment") + self.assertEqual(len(rfq_message.notified_partner_ids), 3) + self.assertEqual(len(rfq_message.notification_ids), 3) + rfq_mail = rfq_message.mail_ids + self.assertEqual(len(rfq_mail.recipient_ids), 3) + + # tracking email should not include cc/bcc + tracking_message = self._new_msgs.filtered( + lambda x: x.message_type == "notification" + ) + tracking_field_mail = tracking_message.mail_ids + self.assertEqual(len(tracking_field_mail.recipient_ids), 1) + self.assertEqual( + tracking_field_mail.recipient_ids, self.admin_user.partner_id + ) diff --git a/mail_composer_cc_bcc/views/mail_mail_views.xml b/mail_composer_cc_bcc/views/mail_mail_views.xml new file mode 100644 index 00000000..6da62131 --- /dev/null +++ b/mail_composer_cc_bcc/views/mail_mail_views.xml @@ -0,0 +1,19 @@ + + + + email.mail.form.inherit + mail.mail + + + + + + + Partners + + + + diff --git a/mail_composer_cc_bcc/views/mail_message_views.xml b/mail_composer_cc_bcc/views/mail_message_views.xml new file mode 100644 index 00000000..13a2a273 --- /dev/null +++ b/mail_composer_cc_bcc/views/mail_message_views.xml @@ -0,0 +1,22 @@ + + + + mail.message.form.inherit + mail.message + + + + + + + + + diff --git a/mail_composer_cc_bcc/views/mail_template_views.xml b/mail_composer_cc_bcc/views/mail_template_views.xml new file mode 100644 index 00000000..e916996a --- /dev/null +++ b/mail_composer_cc_bcc/views/mail_template_views.xml @@ -0,0 +1,17 @@ + + + + email.template.form.inherit + mail.template + + + + + + + + diff --git a/mail_composer_cc_bcc/views/res_company_views.xml b/mail_composer_cc_bcc/views/res_company_views.xml new file mode 100644 index 00000000..a1f6555d --- /dev/null +++ b/mail_composer_cc_bcc/views/res_company_views.xml @@ -0,0 +1,22 @@ + + + + res.company.form.inherit + res.company + + + + + + + + + diff --git a/mail_composer_cc_bcc/wizards/__init__.py b/mail_composer_cc_bcc/wizards/__init__.py new file mode 100644 index 00000000..2a2ad24b --- /dev/null +++ b/mail_composer_cc_bcc/wizards/__init__.py @@ -0,0 +1,2 @@ +from . import mail_compose_message +from . import mail_template_preview diff --git a/mail_composer_cc_bcc/wizards/mail_compose_message.py b/mail_composer_cc_bcc/wizards/mail_compose_message.py new file mode 100644 index 00000000..741f06c8 --- /dev/null +++ b/mail_composer_cc_bcc/wizards/mail_compose_message.py @@ -0,0 +1,149 @@ +# Copyright 2023 Camptocamp +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import itertools + +from odoo import Command, api, fields, models, tools + + +class MailComposeMessage(models.TransientModel): + _inherit = "mail.compose.message" + + partner_cc_ids = fields.Many2many( + "res.partner", + "mail_compose_message_res_partner_cc_rel", + "wizard_id", + "partner_id", + string="Cc", + compute="_compute_partner_cc_bcc_ids", + readonly=False, + store=True, + ) + partner_bcc_ids = fields.Many2many( + "res.partner", + "mail_compose_message_res_partner_bcc_rel", + "wizard_id", + "partner_id", + string="Bcc", + compute="_compute_partner_cc_bcc_ids", + readonly=False, + store=True, + ) + + # ------------------------------------------------------------ + # SET DEFAULT VALUES FOR CC, BCC + # ------------------------------------------------------------ + + @api.model + def default_get(self, fields_list): + company = self.env.company + res = super().default_get(fields_list) + partner_cc = company.default_partner_cc_ids + if partner_cc: + res["partner_cc_ids"] = [Command.set(partner_cc.ids)] + partner_bcc = company.default_partner_bcc_ids + if partner_bcc: + res["partner_bcc_ids"] = [Command.set(partner_bcc.ids)] + return res + + @api.depends( + "composition_mode", "model", "parent_id", "res_domain", "res_ids", "template_id" + ) + def _compute_partner_cc_bcc_ids(self): + for composer in self: + if ( + composer.template_id + and composer.composition_mode == "comment" + and not composer.composition_batch + ): + composer._set_partner_ids_from_mails( + composer.template_id.email_cc, "partner_cc_ids" + ) + composer._set_partner_ids_from_mails( + composer.template_id.email_bcc, "partner_bcc_ids" + ) + elif composer.parent_id and composer.composition_mode == "comment": + composer.partner_cc_ids = composer.parent_id.partner_cc_ids + composer.partner_bcc_ids = composer.parent_id.partner_bcc_ids + elif not composer.template_id: + composer.partner_cc_ids = self.env.company.default_partner_cc_ids + composer.partner_bcc_ids = self.env.company.default_partner_bcc_ids + + @api.depends( + "composition_mode", "model", "parent_id", "res_domain", "res_ids", "template_id" + ) + def _compute_partner_ids(self): + """ + Change: dont add email_cc to partner_ids + + return: field Recipients filled with value from 'email_to', 'partner_ids' + """ + for composer in self: + if ( + composer.template_id + and composer.composition_mode == "comment" + and not composer.composition_batch + ): + res_ids = composer._evaluate_res_ids() or [0] + rendered_values = composer._generate_template_for_composer( + res_ids, + # DIFFERENT FROM ODOO NATIVE: + {"email_to", "partner_ids"}, + find_or_create_partners=True, + )[res_ids[0]] + if rendered_values.get("partner_ids"): + composer.partner_ids = rendered_values["partner_ids"] + elif composer.parent_id and composer.composition_mode == "comment": + composer.partner_ids = composer.parent_id.partner_ids + elif not composer.template_id: + composer.partner_ids = False + + def _set_partner_ids_from_mails(self, email_field, partner_field): + if email_field: + mails = tools.email_split(email_field) + partner_ids = self.env["res.partner"]._find_or_create_from_emails( + mails, + additional_values={ + email: { + "company_id": self.record_company_id.id, + } + for email in itertools.chain(mails, [False]) + }, + ) + if not isinstance(partner_ids, list): + partner_ids = [partner_ids] + for partner_id in partner_ids: + setattr(self, partner_field, [(4, partner_id.id)]) + + # ------------------------------------------------------------ + # RENDERING / VALUES GENERATION + # ------------------------------------------------------------ + + def _prepare_mail_values_rendered(self, res_ids): + """ + add cc and bcc when send to mail.message + """ + mail_values = super()._prepare_mail_values_rendered(res_ids) + + for res_id in mail_values: + mail_values[res_id].update( + { + "recipient_cc_ids": self.partner_cc_ids.ids, + "recipient_bcc_ids": self.partner_bcc_ids.ids, + } + ) + return mail_values + + # ------------------------------------------------------------ + # ACTIONS + # ------------------------------------------------------------ + + def _action_send_mail_comment(self, res_ids): + """Add context is_from_composer""" + self.ensure_one() + context = { + "is_from_composer": True, + "partner_cc_ids": self.partner_cc_ids, + "partner_bcc_ids": self.partner_bcc_ids, + } + self = self.with_context(**context) + return super()._action_send_mail_comment(res_ids) diff --git a/mail_composer_cc_bcc/wizards/mail_compose_message_view.xml b/mail_composer_cc_bcc/wizards/mail_compose_message_view.xml new file mode 100644 index 00000000..3ebf1fda --- /dev/null +++ b/mail_composer_cc_bcc/wizards/mail_compose_message_view.xml @@ -0,0 +1,24 @@ + + + + mail.compose.message.form.inherit + mail.compose.message + + + + + + + + + diff --git a/mail_composer_cc_bcc/wizards/mail_template_preview.py b/mail_composer_cc_bcc/wizards/mail_template_preview.py new file mode 100644 index 00000000..9d8d1a64 --- /dev/null +++ b/mail_composer_cc_bcc/wizards/mail_template_preview.py @@ -0,0 +1,18 @@ +# Copyright 2024 Camptocamp +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + +from odoo.addons.mail.wizard.mail_template_preview import MailTemplatePreview + + +class MailTemplatePreview(models.TransientModel): + _inherit = "mail.template.preview" + + _MAIL_TEMPLATE_FIELDS = MailTemplatePreview._MAIL_TEMPLATE_FIELDS + ["email_bcc"] + + email_bcc = fields.Char( + "Bcc", + compute="_compute_mail_template_fields", + help="Blind Carbon copy recipients", + ) diff --git a/mail_composer_cc_bcc/wizards/mail_template_preview_view.xml b/mail_composer_cc_bcc/wizards/mail_template_preview_view.xml new file mode 100644 index 00000000..6935e31a --- /dev/null +++ b/mail_composer_cc_bcc/wizards/mail_template_preview_view.xml @@ -0,0 +1,12 @@ + + + + mail.template.preview + + + + + + + +