Merge PR #1224 into 16.0

Signed-off-by simahawk
pull/1243/head
OCA-git-bot 2023-10-18 09:49:25 +00:00
commit 6109df89e6
34 changed files with 1757 additions and 0 deletions

View File

@ -0,0 +1,143 @@
================
Email CC and BCC
================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:5db429b9ae0f739ec6b4aa02e8b8e717c47c128c2643f218d54f7a3e1b0a673e
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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%2Fsocial-lightgray.png?logo=github
:target: https://github.com/OCA/social/tree/16.0/mail_composer_cc_bcc
:alt: OCA/social
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/social-16-0/social-16-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/social&target_branch=16.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.
Features
~~~~~~~~
* Add Cc and Bcc fields to mail composer form. Send only once to multiple email
addresses.
* 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 <https://odoo-community.org/page/development-status>`_
**Table of contents**
.. contents::
:local:
Configuration
=============
In company form there are two fields to set default cc and bcc
partners.
.. image:: https://raw.githubusercontent.com/OCA/social/16.0/mail_composer_cc_bcc/static/img/res_company_form_default_cc_bcc.png
In template form there are two fields to set cc and bcc emails.
.. image:: https://raw.githubusercontent.com/OCA/social/16.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:: https://raw.githubusercontent.com/OCA/social/16.0/mail_composer_cc_bcc/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:: https://raw.githubusercontent.com/OCA/social/16.0/mail_composer_cc_bcc/static/img/mail_compose_message_template_cc_bcc.png
Known issues / Roadmap
======================
* Extract `account` customization (`account.invoice.send` wizard) to a specific module `mail_composer_cc_bcc_account`
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/social/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 <https://github.com/OCA/social/issues/new?body=module:%20mail_composer_cc_bcc%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Camptocamp SA
Contributors
~~~~~~~~~~~~
* `Trobz <https://www.trobz.com>`_:
* Hai N. Le <hailn@trobz.com>
* Son Ho <sonhd@trobz.com>
Other credits
~~~~~~~~~~~~~
The creation and migration from 15.0 to 16.0 of this module 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-hailangvn2023| image:: https://github.com/hailangvn2023.png?size=40px
:target: https://github.com/hailangvn2023
:alt: hailangvn2023
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-hailangvn2023|
This module is part of the `OCA/social <https://github.com/OCA/social/tree/16.0/mail_composer_cc_bcc>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@ -0,0 +1,2 @@
from . import models
from . import wizards

View File

@ -0,0 +1,28 @@
# 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.",
"version": "16.0.1.0.0",
"development_status": "Alpha",
"category": "Social",
"website": "https://github.com/OCA/social",
"author": "Camptocamp SA, Odoo Community Association (OCA)",
"maintainers": ["hailangvn2023"],
"license": "AGPL-3",
"application": False,
"installable": True,
"preloadable": True,
"depends": [
"mail",
"account",
],
"data": [
"views/res_company_views.xml",
"views/mail_mail_views.xml",
"views/mail_message_views.xml",
"views/mail_template_views.xml",
"wizards/account_invoice_send_views.xml",
"wizards/mail_compose_message_view.xml",
],
}

View File

@ -0,0 +1,117 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mail_composer_cc_bcc
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.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_account_invoice_send__partner_bcc_ids
#: 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_account_invoice_send__partner_cc_ids
#: 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
#: code:addons/mail_composer_cc_bcc/models/mail_mail.py:0
#, python-format
msgid ""
"Error without exception. Probably due do concurrent access update of "
"notification records. Please see with an administrator."
msgstr ""
#. module: mail_composer_cc_bcc
#: code:addons/mail_composer_cc_bcc/models/mail_mail.py:0
#, python-format
msgid ""
"Error without exception. Probably due do sending an email without computed "
"recipients."
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 ""

View File

@ -0,0 +1,7 @@
# 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 mail_mail

View File

@ -0,0 +1,278 @@
# Copyright 2023 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import ast
import base64
import logging
import re
import smtplib
import psycopg2
from odoo import _, fields, models, tools
from odoo.addons.base.models.ir_mail_server import MailDeliveryException
_logger = logging.getLogger(__name__)
def format_emails(partners):
emails = [
tools.formataddr((p.name or "False", p.email or "False")) for p in partners
]
return ", ".join(emails)
class MailMail(models.Model):
_inherit = "mail.mail"
email_bcc = fields.Char("Bcc", help="Blind Cc message recipients")
def _send( # noqa: max-complexity: 4
self, auto_commit=False, raise_exception=False, smtp_session=None
):
env = self.env
IrMailServer = env["ir.mail_server"]
IrAttachment = env["ir.attachment"]
ICP = env["ir.config_parameter"].sudo()
# Mail composer only sends 1 mail at a time.
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 super()._send(
auto_commit=auto_commit,
raise_exception=raise_exception,
smtp_session=smtp_session,
)
mail = self
success_pids = []
failure_type = None
# ===== Same with native Odoo =====
# https://github.com/odoo/odoo/blob/0a3fc96cd51c0aab024207a4608f6ba32d49da36
# /addons/mail/models/mail_mail.py#L384
try:
if mail.state != "outgoing":
if mail.state != "exception" and mail.auto_delete:
mail.sudo().unlink()
return True
# remove attachments if user send the link with the access_token
body = mail.body_html or ""
attachments = mail.attachment_ids
for link in re.findall(r"/web/(?:content|image)/([0-9]+)", body):
attachments = attachments - IrAttachment.browse(int(link))
# load attachment binary data with a separate read(), as
# prefetching all `datas` (binary field) could bloat the browse
# cache, triggerring soft/hard mem limits with temporary data.
attachments = [
(a["name"], base64.b64decode(a["datas"]), a["mimetype"])
for a in attachments.sudo().read(["name", "datas", "mimetype"])
if a["datas"] is not False
]
# ===== Different than native Odoo =====
email = mail._send_prepare_values()
# ===== Same with native Odoo =====
# headers
headers = {}
bounce_alias = ICP.get_param("mail.bounce.alias")
catchall_domain = ICP.get_param("mail.catchall.domain")
if bounce_alias and catchall_domain:
headers["Return-Path"] = "%s@%s" % (bounce_alias, catchall_domain)
if mail.headers:
try:
headers.update(ast.literal_eval(mail.headers))
except Exception as e:
# ===== Different than native Odoo =====
_logger.warning("Error during update headers: %s" % e)
# ===== Same with native Odoo =====
# Writing on the mail object may fail (e.g. lock on user) which
# would trigger a rollback *after* actually sending the email.
# To avoid sending twice the same email, provoke the failure earlier
mail.write(
{
"state": "exception",
"failure_reason": _(
"Error without exception. Probably due do sending an email"
" without computed recipients."
),
}
)
# Update notification in a transient exception state to avoid concurrent
# update in case an email bounces while sending all emails related to current
# mail record.
notifs = self.env["mail.notification"].search(
[
("notification_type", "=", "email"),
("mail_mail_id", "in", mail.ids),
("notification_status", "not in", ("sent", "canceled")),
]
)
if notifs:
notif_msg = _(
"Error without exception. Probably due do concurrent access"
" update of notification records. Please see with an administrator."
)
notifs.sudo().write(
{
"notification_status": "exception",
"failure_type": "unknown",
"failure_reason": notif_msg,
}
)
# `test_mail_bounce_during_send`, force immediate update to obtain the lock.
# see rev. 56596e5240ef920df14d99087451ce6f06ac6d36
notifs.flush_recordset(
["notification_status", "failure_type", "failure_reason"]
)
# protect against ill-formatted email_from when formataddr was used on an already formatted email # noqa: B950
emails_from = tools.email_split_and_format(mail.email_from)
email_from = emails_from[0] if emails_from else mail.email_from
# build an RFC2822 email.message.Message object and send it without queuing
res = None
# TDE note: could be great to pre-detect missing to/cc and skip sending it
# to go directly to failed state update
# ===== Different than native Odoo =====
email["email_from"] = email_from
msg = self.build_email(
email,
attachments=attachments,
headers=headers,
)
try:
res = IrMailServer.send_email(
msg,
mail_server_id=mail.mail_server_id.id,
smtp_session=smtp_session,
)
success_pids += mail.recipient_ids.ids
# ===== Same with native Odoo =====
except AssertionError as error:
if str(error) == IrMailServer.NO_VALID_RECIPIENT:
# if we have a list of void emails for email_list
# -> email missing, otherwise generic email
# failure
if (
not email.get("email_to")
and failure_type != "mail_email_invalid"
):
failure_type = "mail_email_missing"
else:
failure_type = "mail_email_invalid"
# No valid recipient found for this particular
# mail item -> ignore error to avoid blocking
# delivery to next recipients, if any. If this is
# the only recipient, the mail will show as failed.
_logger.info(
"Ignoring invalid recipients for mail.mail %s: %s",
mail.message_id,
email.get("email_to"),
)
else:
raise
if res: # mail has been sent at least once, no major exception occurred
mail.write(
{"state": "sent", "message_id": res, "failure_reason": False}
)
_logger.info(
"Mail with ID %r and Message-Id %r successfully sent",
mail.id,
mail.message_id,
)
# /!\ can't use mail.state here, as mail.refresh() will cause an error
# see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
mail._postprocess_sent_message(
success_pids=success_pids, failure_type=failure_type
)
except MemoryError:
# prevent catching transient MemoryErrors, bubble up to
# notify user or abort cron job instead of marking the
# mail as failed
_logger.exception(
"MemoryError while processing mail with ID %r and Msg-Id %r."
" Consider raising the --limit-memory-hard startup option",
mail.id,
mail.message_id,
)
# mail status will stay on ongoing since transaction will be rollback
raise
except (psycopg2.Error, smtplib.SMTPServerDisconnected):
# If an error with the database or SMTP session occurs,
# chances are that the cursor or SMTP session are
# unusable, causing further errors when trying to save the
# state.
_logger.exception(
"Exception while processing mail with ID %r and Msg-Id %r.",
mail.id,
mail.message_id,
)
raise
except Exception as e:
failure_reason = tools.ustr(e)
_logger.exception(
"failed sending mail (id: %s) due to %s", mail.id, failure_reason
)
mail.write({"state": "exception", "failure_reason": failure_reason})
mail._postprocess_sent_message(
success_pids=success_pids,
failure_reason=failure_reason,
failure_type="unknown",
)
if raise_exception:
if isinstance(e, (AssertionError, UnicodeEncodeError)):
if isinstance(e, UnicodeEncodeError):
value = "Invalid text: %s" % e.object
else:
value = ". ".join(e.args)
raise MailDeliveryException(value) from e
raise
# ===== Different than native Odoo =====
# As we only send one email, auto_commit has no value
return True
def build_email(self, email, attachments=None, headers=None):
env = self.env
mail = self
email_from = email.get("email_from")
IrMailServer = env["ir.mail_server"]
# ===== Same with native Odoo =====
# https://github.com/odoo/odoo/blob/0a3fc96cd51c0aab024207a4608f6ba32d49da36
# /addons/mail/models/mail_mail.py#L458
msg = IrMailServer.build_email(
email_from=email_from,
email_to=email.get("email_to"),
subject=mail.subject,
body=email.get("body"),
body_alternative=email.get("body_alternative"),
# ===== Different than native Odoo =====
email_cc=mail.email_cc,
email_bcc=mail.email_bcc,
# ===== Same with native Odoo =====
reply_to=mail.reply_to,
attachments=attachments,
message_id=mail.message_id,
references=mail.references,
object_id=mail.res_id and ("%s-%s" % (mail.res_id, mail.model)),
subtype="html",
subtype_alternative="plain",
headers=headers,
)
return msg
def _send_prepare_values(self, partner=None):
res = super()._send_prepare_values(partner=partner)
is_from_composer = self.env.context.get("is_from_composer", False)
if not is_from_composer:
return res
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)
res["email_to"] = format_emails(partner_to)
res["email_cc"] = format_emails(self.recipient_cc_ids)
res["email_bcc"] = format_emails(self.recipient_bcc_ids)
return res

View File

@ -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},
)

View File

@ -0,0 +1,52 @@
# Copyright 2023 Camptocamp
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models, tools
from ..wizards.mail_compose_message import CC_BCC_FIELDS
class MailTemplate(models.Model):
_inherit = "mail.template"
email_bcc = fields.Char(
"Bcc", help="Blind cc recipients (placeholders may be used here)"
)
def generate_recipients(self, results, res_ids):
res = super().generate_recipients(results, res_ids)
is_from_composer = self.env.context.get("is_from_composer", False)
if not is_from_composer or not (self.email_cc or self.email_bcc):
return res
ctx = {"tpl_partners_only": False}
ctx.update(self.env.context)
tmpl_ctx = super().with_context(**ctx)
template_values = {}
tmpl_ctx._render_fields(res_ids, CC_BCC_FIELDS.keys(), template_values)
for res_id, values in template_values.items():
email_cc_bcc = tools.email_split(values["email_cc"])
email_cc_bcc += tools.email_split(values["email_bcc"])
for_emails = [("email", "in", email_cc_bcc)]
partner_cc_bcc_ids = self.env["res.partner"].search(for_emails).ids
if not partner_cc_bcc_ids:
continue
res[res_id]["partner_ids"] = [
_id
for _id in res[res_id]["partner_ids"]
if _id not in partner_cc_bcc_ids
]
return res
def _render_fields(self, res_ids, field_names, results):
template = self
template_res_ids = res_ids
for field in field_names:
generated_field_values = template._render_field(
field,
template_res_ids,
options={"render_safe": field == "subject"},
post_process=(field == "body_html"),
)
for res_id, field_value in generated_field_values.items():
results.setdefault(res_id, dict())[field] = field_value
return results

View File

@ -0,0 +1,128 @@
# 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"
def _message_create(self, values_list):
context = self.env.context
res = super()._message_create(values_list)
partners_cc = context.get("partner_cc_ids", None)
if partners_cc:
res.recipient_cc_ids = partners_cc
partners_bcc = context.get("partner_bcc_ids", None)
if partners_bcc:
res.recipient_bcc_ids = partners_bcc
return res
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.
"""
context = self.env.context
res = super()._notify_by_email_get_base_mail_values(
message, additional_values=additional_values
)
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)
if not is_from_composer:
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
)
for _, value in recipients_cc_bcc.items():
for _, data in value.items():
if not data.get("id"):
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"),
}
rdata.append(pdata)
return rdata
def _notify_by_email_get_final_mail_values(
self, recipient_ids, base_mail_values, additional_values=None
):
"""
This is to add cc, bcc recipients' ids to recipient_ids of mail.mail
"""
res = super()._notify_by_email_get_final_mail_values(
recipient_ids, base_mail_values, additional_values=additional_values
)
context = self.env.context
r_ids = list(recipient_ids)
partners_cc = context.get("partner_cc_ids", None)
if partners_cc:
r_ids += partners_cc.ids
partners_bcc = context.get("partner_bcc_ids", None)
if partners_bcc:
r_ids += partners_bcc.ids
if partners_cc or partners_bcc:
res["recipient_ids"] = tuple(set(r_ids))
return res
def _notify_get_recipients_classify(
self, recipient_data, model_name, msg_vals=None
):
res = super()._notify_get_recipients_classify(
recipient_data, model_name, msg_vals=msg_vals
)
is_from_composer = self.env.context.get("is_from_composer", False)
if not is_from_composer:
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]

View File

@ -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",
)

View File

@ -0,0 +1,8 @@
In company form there are two fields to set default cc and bcc
partners.
.. image:: ../static/img/res_company_form_default_cc_bcc.png
In template form there are two fields to set cc and bcc emails.
.. image:: ../static/img/email_template_form_cc_bcc.png

View File

@ -0,0 +1,4 @@
* `Trobz <https://www.trobz.com>`_:
* Hai N. Le <hailn@trobz.com>
* Son Ho <sonhd@trobz.com>

View File

@ -0,0 +1 @@
The creation and migration from 15.0 to 16.0 of this module were financially supported by Camptocamp.

View File

@ -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.
Features
~~~~~~~~
* Add Cc and Bcc fields to mail composer form. Send only once to multiple email
addresses.
* 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.

View File

@ -0,0 +1 @@
* Extract `account` customization (`account.invoice.send` wizard) to a specific module `mail_composer_cc_bcc_account`

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,485 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: http://docutils.sourceforge.net/" />
<title>Email CC and BCC</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="email-cc-and-bcc">
<h1 class="title">Email CC and BCC</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:5db429b9ae0f739ec6b4aa02e8b8e717c47c128c2643f218d54f7a3e1b0a673e
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/social/tree/16.0/mail_composer_cc_bcc"><img alt="OCA/social" src="https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/social-16-0/social-16-0-mail_composer_cc_bcc"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runboat.odoo-community.org/builds?repo=OCA/social&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>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.</p>
<p>This module allows to properly separate To:, Cc:, and Bcc: fields
in the Mail Composer.</p>
<div class="section" id="features">
<h1>Features</h1>
<ul class="simple">
<li>Add Cc and Bcc fields to mail composer form. Send only once to multiple email
addresses.</li>
<li>Add Cc and Bcc fields to company form to use them as default in mail composer
form.</li>
<li>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.</li>
</ul>
<div class="admonition important">
<p class="first admonition-title">Important</p>
<p class="last">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.
<a class="reference external" href="https://odoo-community.org/page/development-status">More details on development status</a></p>
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="id1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="id2">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id3">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id5">Credits</a></li>
</ul>
</div>
<div class="section" id="configuration">
<h2><a class="toc-backref" href="#id1">Configuration</a></h2>
<p>In company form there are two fields to set default cc and bcc
partners.</p>
<blockquote>
<img alt="https://raw.githubusercontent.com/OCA/social/16.0/mail_composer_cc_bcc/static/img/res_company_form_default_cc_bcc.png" src="https://raw.githubusercontent.com/OCA/social/16.0/mail_composer_cc_bcc/static/img/res_company_form_default_cc_bcc.png" />
</blockquote>
<p>In template form there are two fields to set cc and bcc emails.</p>
<blockquote>
<img alt="https://raw.githubusercontent.com/OCA/social/16.0/mail_composer_cc_bcc/static/img/email_template_form_cc_bcc.png" src="https://raw.githubusercontent.com/OCA/social/16.0/mail_composer_cc_bcc/static/img/email_template_form_cc_bcc.png" />
</blockquote>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#id2">Usage</a></h2>
<p>The partners cc and bcc from company form will be used to fill in mail composer
form.</p>
<blockquote>
<img alt="https://raw.githubusercontent.com/OCA/social/16.0/mail_composer_cc_bcc/static/img/mail_compose_message_default_cc_bcc.png" src="https://raw.githubusercontent.com/OCA/social/16.0/mail_composer_cc_bcc/static/img/mail_compose_message_default_cc_bcc.png" />
</blockquote>
<p>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
composers fields.</p>
<blockquote>
<img alt="https://raw.githubusercontent.com/OCA/social/16.0/mail_composer_cc_bcc/static/img/mail_compose_message_template_cc_bcc.png" src="https://raw.githubusercontent.com/OCA/social/16.0/mail_composer_cc_bcc/static/img/mail_compose_message_template_cc_bcc.png" />
</blockquote>
</div>
<div class="section" id="known-issues-roadmap">
<h2><a class="toc-backref" href="#id3">Known issues / Roadmap</a></h2>
<ul class="simple">
<li>Extract <cite>account</cite> customization (<cite>account.invoice.send</cite> wizard) to a specific module <cite>mail_composer_cc_bcc_account</cite></li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#id4">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/social/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/social/issues/new?body=module:%20mail_composer_cc_bcc%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#id5">Credits</a></h2>
</div>
</div>
<div class="section" id="authors">
<h1>Authors</h1>
<ul class="simple">
<li>Camptocamp SA</li>
</ul>
</div>
<div class="section" id="contributors">
<h1>Contributors</h1>
<ul>
<li><p class="first"><a class="reference external" href="https://www.trobz.com">Trobz</a>:</p>
<blockquote>
<ul class="simple">
<li>Hai N. Le &lt;<a class="reference external" href="mailto:hailn&#64;trobz.com">hailn&#64;trobz.com</a>&gt;</li>
<li>Son Ho &lt;<a class="reference external" href="mailto:sonhd&#64;trobz.com">sonhd&#64;trobz.com</a>&gt;</li>
</ul>
</blockquote>
</li>
</ul>
</div>
<div class="section" id="other-credits">
<h1>Other credits</h1>
<p>The creation and migration from 15.0 to 16.0 of this module were financially supported by Camptocamp.</p>
</div>
<div class="section" id="maintainers">
<h1>Maintainers</h1>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>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.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external" href="https://github.com/hailangvn2023"><img alt="hailangvn2023" src="https://github.com/hailangvn2023.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/social/tree/16.0/mail_composer_cc_bcc">OCA/social</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -0,0 +1 @@
from . import test_mail_cc_bcc

View File

@ -0,0 +1,206 @@
# 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
from odoo.addons.mail.models.mail_mail import MailMail as upstream
from odoo.addons.mail.tests.test_mail_composer import TestMailComposer
VALID_HASHES = [
"d52cb36b88b33abc9556f7be6718d93f",
]
class TestMailCcBcc(TestMailComposer):
@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_id": test_record.id,
# 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 = "<p>Hello</p>"
return form
def open_invoice_mail_composer_form(self):
# Use form to populate data
for_name = [("name", "=", "INV/2023/00003")]
self.test_invoice = test_record = self.env["account.move"].search(for_name)
ctx = {
"active_ids": test_record.ids,
"default_model": "account.move",
"default_res_id": test_record.id,
"mail_notify_force_send": True,
}
form = Form(self.env["account.invoice.send"].with_context(**ctx))
form.body = "<p>Hello</p>"
return form
def test_upstream_file_hash(self):
"""Test that copied upstream function hasn't received fixes"""
func = inspect.getsource(upstream._send).encode()
func_hash = hashlib.md5(func).hexdigest()
self.assertIn(func_hash, VALID_HASHES)
def test_email_cc_bcc(self):
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()
# Verify recipients of mail.message
message = self.test_record.message_ids[0]
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" <mark.brown23@example.com>',
'"Joel Willis" <joel.willis63@example.com>',
'"Chester Reed" <chester.reed79@example.com>',
]
)
self.assertEqual(mail.email_cc, expecting)
expecting = '"Dwayne Newman" <dwayne.newman28@example.com>'
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
# Product template values
tmpl_model = env["ir.model"].search([("model", "=", "product.template")])
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": """<p style="margin:0px 0 12px 0;box-sizing:border-box;">
Test Template<br></p>""",
"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")
),
}
prod_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 = prod_tmpl
composer = form.save()
# Beside existing Cc and Bcc, add template's ones
form = Form(composer)
form.template_id = prod_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)
form.template_id = prod_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_invoice_mail_cc_bcc(self):
self.set_company()
form = self.open_invoice_mail_composer_form()
form.subject = "Hello"
composer = form.save()
with self.mock_mail_gateway():
composer._send_email()
message = self.test_invoice.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):
# Test without any partner in cc/bcc -> only 1 mail should be sent
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
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, 1)

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="view_mail_form_inherit">
<field name="name">email.mail.form.inherit</field>
<field name="model">mail.mail</field>
<field name="inherit_id" ref="mail.view_mail_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='email_cc']" position="after">
<field
name="email_bcc"
placeholder="Comma-separated blind carbon copy recipients addresses"
/>
</xpath>
<xpath expr="//field[@name='recipient_ids']" position="attributes">
<attribute name="string">Partners</attribute>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="mail_message_view_form_inherit">
<field name="name">mail.message.form.inherit</field>
<field name="model">mail.message</field>
<field name="inherit_id" ref="mail.mail_message_view_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_ids']" position="after">
<field
name="recipient_cc_ids"
widget="many2many_tags_email"
context="{'force_email':True, 'show_email':True}"
/>
<field
name="recipient_bcc_ids"
widget="many2many_tags_email"
context="{'force_email':True, 'show_email':True}"
/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="email_template_form_inherit">
<field name="name">email.template.form.inherit</field>
<field name="model">mail.template</field>
<field name="inherit_id" ref="mail.email_template_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='email_cc']" position="after">
<field
name="email_bcc"
attrs="{'invisible': [('use_default_to', '=', True)]}"
placeholder="Comma-separated blind carbon copy recipients addresses"
/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="view_company_form_inherit">
<field name="name">res.company.form.inherit</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='email']" position="after">
<field
name="default_partner_cc_ids"
widget="many2many_tags_email"
context="{'force_email':True, 'show_email':True}"
/>
<field
name="default_partner_bcc_ids"
widget="many2many_tags_email"
context="{'force_email':True, 'show_email':True}"
/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1 @@
from . import mail_compose_message

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="account_invoice_send_wizard_form_inherit" model="ir.ui.view">
<field name="name">account.invoice.send.form.inherit</field>
<field name="model">account.invoice.send</field>
<field name="inherit_id" ref="account.account_invoice_send_wizard_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_ids']/.." position="after">
<field
name="partner_cc_ids"
widget="many2many_tags_email"
context="{'force_email':True, 'show_email':True}"
attrs="{'invisible': [('composition_mode', '!=', 'comment')]}"
/>
<field
name="partner_bcc_ids"
widget="many2many_tags_email"
context="{'force_email':True, 'show_email':True}"
attrs="{'invisible': [('composition_mode', '!=', 'comment')]}"
/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,84 @@
# Copyright 2023 Camptocamp
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import Command, api, fields, models, tools
CC_BCC_FIELDS = {
"email_cc": "partner_cc_ids",
"email_bcc": "partner_bcc_ids",
}
class MailComposeMessage(models.TransientModel):
_inherit = "mail.compose.message"
@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
partner_cc_ids = fields.Many2many(
"res.partner",
"mail_compose_message_res_partner_cc_rel",
"wizard_id",
"partner_id",
string="Cc",
)
partner_bcc_ids = fields.Many2many(
"res.partner",
"mail_compose_message_res_partner_bcc_rel",
"wizard_id",
"partner_id",
string="Bcc",
)
def _onchange_template_id(self, template_id, composition_mode, model, res_id):
if not template_id:
return {"value": {}}
ctx = {"is_from_composer": True}
ctx.update(self.env.context)
self_ctx = self.with_context(**ctx)
res = super(MailComposeMessage, self_ctx)._onchange_template_id(
template_id, composition_mode, model, res_id
)
res_ids = [res_id]
# tpl_partners_only need to be False for email_cc value
tmpl_ctx = self.env["mail.template"].with_context(tpl_partners_only=False)
mail_tmpl = tmpl_ctx.browse(template_id)
template_values = mail_tmpl.generate_email(res_ids, CC_BCC_FIELDS)
values = template_values[res_id]
for fname in CC_BCC_FIELDS:
value = values.get(fname, None)
if not value:
continue
self._set_partner_field(CC_BCC_FIELDS[fname], value)
return res
def _set_partner_field(self, field_name, email):
if field_name not in CC_BCC_FIELDS.values():
return
for_email = [("email", "in", tools.email_split(email))]
partner = self.env["res.partner"].search(for_email)
current_partners = getattr(self, field_name)
setattr(self, field_name, current_partners + partner)
def _action_send_mail(self, auto_commit=False):
# don't impact mass_mailing that also uses mail.compose.message
if self.composition_mode == "mass_mail":
return super()._action_send_mail(auto_commit)
context = {
"is_from_composer": True,
"partner_cc_ids": self.partner_cc_ids,
"partner_bcc_ids": self.partner_bcc_ids,
}
context.update(self.env.context)
self_super = super(MailComposeMessage, self.with_context(**context))
res = self_super._action_send_mail(auto_commit)
return res

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="email_compose_message_wizard_inherit_form">
<field name="name">mail.compose.message.form.inherit</field>
<field name="model">mail.compose.message</field>
<field name="inherit_id" ref="mail.email_compose_message_wizard_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_ids']/.." position="after">
<field
name="partner_cc_ids"
widget="many2many_tags_email"
context="{'force_email':True, 'show_email':True}"
attrs="{'invisible': ['|', ('is_log', '=', True), ('composition_mode', '!=', 'comment')]}"
/>
<field
name="partner_bcc_ids"
widget="many2many_tags_email"
context="{'force_email':True, 'show_email':True}"
attrs="{'invisible': ['|', ('is_log', '=', True), ('composition_mode', '!=', 'comment')]}"
/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1 @@
../../../../mail_composer_cc_bcc

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)