diff --git a/mail_layout_force/README.rst b/mail_layout_force/README.rst
index ae6fb6b92..d7aead85f 100644
--- a/mail_layout_force/README.rst
+++ b/mail_layout_force/README.rst
@@ -37,11 +37,14 @@ the company logo, and a small footer saying "Powered by Odoo".
There are notably two main layouts used in Odoo, and the user can't control when
they're used, as it's hardcoded into the different applications.
+* ``mail.message_notification_email``
* ``mail.mail_notification_light``
* ``mail.mail_notification_paynow``
This module allows to force a specific layout for a given ``email.template``,
-effectively overwritting the one hardcoded by Odoo.
+effectively overwritting the one hardcoded by Odoo. Additionally, it enables
+forcing a custom layout for emails that do not use an existing ``email.template``
+record (e.g., when sending an email from the chatter).
This allows you to fully customize the way Odoo emails are rendered and sent
to your customers.
@@ -54,9 +57,9 @@ to your customers.
Configuration
=============
-# Go to Configuration > Technical > Emails > Templates
-# Open the desired ``email.template`` record.
-# In Advanced Parameters tab, find the Force Layout field.
+#. Go to Settings > Technical > Emails > Templates
+#. Open the desired ``email.template`` record.
+#. In Advanced Parameters tab, find the Force Layout field.
You can leave it empty to use the default email layout (chosen by Odoo).
You can force a custom email layout of your own.
@@ -70,6 +73,16 @@ You can see how the existing layouts are defined for details or inspiration:
* ``mail.mail_notification_paynow``
* ``mail.mail_notification_borders``
+To force a new custom layout for emails that do not use an existing ``email.template``
+record (e.g., emails sent from the chatter):
+
+#. Go to Settings > Technical > User Interface > Views.
+#. Copy the current layout (e.g., mail.message_notification_email) to create a new one, and remove any parts you don’t need.
+#. Open the layout that you want to swap with a substitute. Then, under the Layout Mapping tab:
+ * Set ``Substitute Layout`` to the new custom layout you created.
+ * Set ``Models`` if you want to apply the replacement only to specific models. If left empty,
+ the email layout will be replaced for all models.
+
Bug Tracker
===========
@@ -95,6 +108,10 @@ Contributors
* Iván Todorovich
* Abraham Anes
+* `Quartile `_
+
+ * Aung Ko Ko Lin
+ * Yoshi Tashiro
Maintainers
~~~~~~~~~~~
diff --git a/mail_layout_force/__manifest__.py b/mail_layout_force/__manifest__.py
index 6152eaa26..d5b1438c3 100644
--- a/mail_layout_force/__manifest__.py
+++ b/mail_layout_force/__manifest__.py
@@ -13,5 +13,10 @@
"category": "Marketing",
"depends": ["mail"],
"demo": ["demo/mail_layout.xml"],
- "data": ["data/mail_layout.xml", "views/mail_template.xml"],
+ "data": [
+ "security/ir.model.access.csv",
+ "data/mail_layout.xml",
+ "views/ir_ui_views.xml",
+ "views/mail_template.xml",
+ ],
}
diff --git a/mail_layout_force/models/__init__.py b/mail_layout_force/models/__init__.py
index 89e090b24..e37fa21ee 100644
--- a/mail_layout_force/models/__init__.py
+++ b/mail_layout_force/models/__init__.py
@@ -1,2 +1,4 @@
+from . import email_layout_mapping
+from . import ir_ui_view
from . import mail_template
from . import mail_thread
diff --git a/mail_layout_force/models/email_layout_mapping.py b/mail_layout_force/models/email_layout_mapping.py
new file mode 100644
index 000000000..b8e162e43
--- /dev/null
+++ b/mail_layout_force/models/email_layout_mapping.py
@@ -0,0 +1,20 @@
+# Copyright 2025 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class EmailLayoutMapping(models.Model):
+ _name = "email.layout.mapping"
+ _description = "Email Layout Mapping"
+
+ layout_id = fields.Many2one("ir.ui.view", ondelete="cascade")
+ substitute_layout_id = fields.Many2one(
+ "ir.ui.view",
+ domain=[("type", "=", "qweb")],
+ required=True,
+ help="Select a target layout.",
+ )
+ model_ids = fields.Many2many(
+ "ir.model", string="Models", help="Select models that the swapping applies to."
+ )
diff --git a/mail_layout_force/models/ir_ui_view.py b/mail_layout_force/models/ir_ui_view.py
new file mode 100644
index 000000000..ae9b7215a
--- /dev/null
+++ b/mail_layout_force/models/ir_ui_view.py
@@ -0,0 +1,10 @@
+# Copyright 2025 Quartile (https://www.quartile.co)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class IrUiView(models.Model):
+ _inherit = "ir.ui.view"
+
+ layout_mapping_line_ids = fields.One2many("email.layout.mapping", "layout_id")
diff --git a/mail_layout_force/models/mail_thread.py b/mail_layout_force/models/mail_thread.py
index 25be8bdda..7428ddd57 100644
--- a/mail_layout_force/models/mail_thread.py
+++ b/mail_layout_force/models/mail_thread.py
@@ -18,3 +18,58 @@ class MailThread(models.AbstractModel):
return super().message_post_with_template(
template_id, email_layout_xmlid=email_layout_xmlid, **kwargs
)
+
+ def _notify_thread_by_email(
+ self,
+ message,
+ recipients_data,
+ msg_vals=False,
+ mail_auto_delete=True,
+ model_description=False,
+ force_email_company=False,
+ force_email_lang=False,
+ resend_existing=False,
+ force_send=True,
+ send_after_commit=True,
+ subtitles=None,
+ **kwargs
+ ):
+ msg_vals = msg_vals or {}
+ layout_xmlid = (
+ msg_vals.get("email_layout_xmlid")
+ or message.email_layout_xmlid
+ or "mail.mail_notification_layout"
+ )
+ layout = self.env.ref(layout_xmlid, raise_if_not_found=True)
+ res_model = (
+ self.env["ir.model"].sudo().search([("model", "=", self._name)], limit=1)
+ )
+ mapping = self.env["email.layout.mapping"].search(
+ [("layout_id", "=", layout.id), ("model_ids", "in", res_model.ids)],
+ limit=1,
+ )
+ if not mapping:
+ mapping = self.env["email.layout.mapping"].search(
+ [("layout_id", "=", layout.id), ("model_ids", "=", False)], limit=1
+ )
+ if mapping:
+ substitute_layout = mapping.substitute_layout_id
+ if not substitute_layout.xml_id:
+ substitute_layout._export_rows([["id"]])
+ # Refresh cache to get xml_id assigned by _export_rows
+ substitute_layout.invalidate_recordset()
+ msg_vals["email_layout_xmlid"] = mapping.substitute_layout_id.xml_id
+ return super()._notify_thread_by_email(
+ message,
+ recipients_data,
+ msg_vals=msg_vals,
+ mail_auto_delete=mail_auto_delete,
+ model_description=model_description,
+ force_email_company=force_email_company,
+ force_email_lang=force_email_lang,
+ resend_existing=resend_existing,
+ force_send=force_send,
+ send_after_commit=send_after_commit,
+ subtitles=subtitles,
+ **kwargs
+ )
diff --git a/mail_layout_force/readme/CONFIGURE.rst b/mail_layout_force/readme/CONFIGURE.rst
index 6e42c41e0..22a2dae54 100644
--- a/mail_layout_force/readme/CONFIGURE.rst
+++ b/mail_layout_force/readme/CONFIGURE.rst
@@ -1,6 +1,6 @@
-# Go to Configuration > Technical > Emails > Templates
-# Open the desired ``email.template`` record.
-# In Advanced Parameters tab, find the Force Layout field.
+#. Go to Settings > Technical > Emails > Templates
+#. Open the desired ``email.template`` record.
+#. In Advanced Parameters tab, find the Force Layout field.
You can leave it empty to use the default email layout (chosen by Odoo).
You can force a custom email layout of your own.
@@ -13,3 +13,13 @@ You can see how the existing layouts are defined for details or inspiration:
* ``mail.mail_notification_light``
* ``mail.mail_notification_paynow``
* ``mail.mail_notification_borders``
+
+To force a new custom layout for emails that do not use an existing ``email.template``
+record (e.g., emails sent from the chatter):
+
+#. Go to Settings > Technical > User Interface > Views.
+#. Copy the current layout (e.g., mail.message_notification_email) to create a new one, and remove any parts you don’t need.
+#. Open the layout that you want to swap with a substitute. Then, under the Layout Mapping tab:
+ * Set ``Substitute Layout`` to the new custom layout you created.
+ * Set ``Models`` if you want to apply the replacement only to specific models. If left empty,
+ the email layout will be replaced for all models.
diff --git a/mail_layout_force/readme/CONTRIBUTORS.rst b/mail_layout_force/readme/CONTRIBUTORS.rst
index c0de70411..211db2b9a 100644
--- a/mail_layout_force/readme/CONTRIBUTORS.rst
+++ b/mail_layout_force/readme/CONTRIBUTORS.rst
@@ -2,3 +2,7 @@
* Iván Todorovich
* Abraham Anes
+* `Quartile `_
+
+ * Aung Ko Ko Lin
+ * Yoshi Tashiro
diff --git a/mail_layout_force/readme/DESCRIPTION.rst b/mail_layout_force/readme/DESCRIPTION.rst
index e00348fe7..798c3b486 100644
--- a/mail_layout_force/readme/DESCRIPTION.rst
+++ b/mail_layout_force/readme/DESCRIPTION.rst
@@ -7,11 +7,14 @@ the company logo, and a small footer saying "Powered by Odoo".
There are notably two main layouts used in Odoo, and the user can't control when
they're used, as it's hardcoded into the different applications.
+* ``mail.message_notification_email``
* ``mail.mail_notification_light``
* ``mail.mail_notification_paynow``
This module allows to force a specific layout for a given ``email.template``,
-effectively overwritting the one hardcoded by Odoo.
+effectively overwritting the one hardcoded by Odoo. Additionally, it enables
+forcing a custom layout for emails that do not use an existing ``email.template``
+record (e.g., when sending an email from the chatter).
This allows you to fully customize the way Odoo emails are rendered and sent
to your customers.
diff --git a/mail_layout_force/security/ir.model.access.csv b/mail_layout_force/security/ir.model.access.csv
new file mode 100644
index 000000000..0e2f7c1c9
--- /dev/null
+++ b/mail_layout_force/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_email_layout_mapping_all,email.layout.mapping.all,model_email_layout_mapping,,1,0,0,0
+access_email_layout_mapping_admin,email.layout.mapping.admin,model_email_layout_mapping,base.group_system,1,1,1,1
diff --git a/mail_layout_force/static/description/index.html b/mail_layout_force/static/description/index.html
index c490ea2c7..ad70f48a6 100644
--- a/mail_layout_force/static/description/index.html
+++ b/mail_layout_force/static/description/index.html
@@ -376,11 +376,14 @@ the company logo, and a small footer saying “Powered by Odoo”.
There are notably two main layouts used in Odoo, and the user can’t control when
they’re used, as it’s hardcoded into the different applications.
+- mail.message_notification_email
- mail.mail_notification_light
- mail.mail_notification_paynow
This module allows to force a specific layout for a given email.template,
-effectively overwritting the one hardcoded by Odoo.
+effectively overwritting the one hardcoded by Odoo. Additionally, it enables
+forcing a custom layout for emails that do not use an existing email.template
+record (e.g., when sending an email from the chatter).
This allows you to fully customize the way Odoo emails are rendered and sent
to your customers.
Table of contents
@@ -398,9 +401,11 @@ to your customers.
-
# Go to Configuration > Technical > Emails > Templates
-# Open the desired email.template record.
-# In Advanced Parameters tab, find the Force Layout field.
+
+- Go to Settings > Technical > Emails > Templates
+- Open the desired email.template record.
+- In Advanced Parameters tab, find the Force Layout field.
+
You can leave it empty to use the default email layout (chosen by Odoo).
You can force a custom email layout of your own.
You can use the Mail: No-Layout notification template to prevent Odoo
@@ -412,6 +417,22 @@ You can see how the existing layouts are defined for details or inspiration:
mail.mail_notification_paynow
mail.mail_notification_borders
+
To force a new custom layout for emails that do not use an existing email.template
+record (e.g., emails sent from the chatter):
+
+- Go to Settings > Technical > User Interface > Views.
+- Copy the current layout (e.g., mail.message_notification_email) to create a new one, and remove any parts you don’t need.
+
+- Open the layout that you want to swap with a substitute. Then, under the Layout Mapping tab:
+
+- Set Substitute Layout to the new custom layout you created.
+- Set Models if you want to apply the replacement only to specific models. If left empty,
+the email layout will be replaced for all models.
+
+
+
+
+
@@ -441,6 +462,12 @@ If you spotted it first, help us to smash it by providing a detailed and welcome
Abraham Anes <abrahamanes@gmail.com>
+
Quartile
+
+- Aung Ko Ko Lin
+- Yoshi Tashiro
+
+
diff --git a/mail_layout_force/tests/test_mail_layout_force.py b/mail_layout_force/tests/test_mail_layout_force.py
index 8ef77222c..9f41370b6 100644
--- a/mail_layout_force/tests/test_mail_layout_force.py
+++ b/mail_layout_force/tests/test_mail_layout_force.py
@@ -2,6 +2,7 @@
# @author Iván Todorovich
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+from odoo import Command
from odoo.tests import TransactionCase
@@ -18,6 +19,20 @@ class TestMailLayoutForce(TransactionCase):
"arch": "",
}
)
+ cls.mail_notification_layout = cls.env.ref("mail.mail_notification_layout")
+ cls.layout_substitute = cls.env["ir.ui.view"].create(
+ {
+ "name": "Substitute Layout",
+ "type": "qweb",
+ "mode": "primary",
+ "arch": """
+
+
+
+ Substituted
+ """,
+ }
+ )
cls.template = cls.env["mail.template"].create(
{
"name": "Test Template",
@@ -71,3 +86,32 @@ class TestMailLayoutForce(TransactionCase):
composer._action_send_mail()
message = self.partner.message_ids[-1]
self.assertEqual(message.mail_ids.body_html.strip(), "Test
")
+
+ def test_chatter_message_uses_default_layout(self):
+ self.partner.message_post(
+ body="Test Message",
+ email_layout_xmlid=self.mail_notification_layout.xml_id,
+ message_type="comment",
+ subtype_xmlid="mail.mt_comment",
+ mail_auto_delete=False,
+ force_send=True,
+ )
+ message = self.partner.message_ids[-1]
+ self.assertNotIn("Substituted
", message.mail_ids.body_html)
+ self.assertIn("Test Message", message.mail_ids.body_html)
+
+ def test_chatter_message_uses_substituted_layout(self):
+ self.mail_notification_layout.layout_mapping_line_ids = [
+ Command.create({"substitute_layout_id": self.layout_substitute.id})
+ ]
+ self.partner.message_post(
+ body="Test Message",
+ email_layout_xmlid=self.mail_notification_layout.xml_id,
+ message_type="comment",
+ subtype_xmlid="mail.mt_comment",
+ mail_auto_delete=False,
+ force_send=True,
+ )
+ message = self.partner.message_ids[-1]
+ self.assertIn("Substituted
", message.mail_ids.body_html)
+ self.assertIn("Test Message", message.mail_ids.body_html)
diff --git a/mail_layout_force/views/ir_ui_views.xml b/mail_layout_force/views/ir_ui_views.xml
new file mode 100644
index 000000000..077c78a07
--- /dev/null
+++ b/mail_layout_force/views/ir_ui_views.xml
@@ -0,0 +1,24 @@
+
+
+
+ ir.ui.view.form.inherit
+ ir.ui.view
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+