Fix allow_mail_archived_partner causing system notifications to send emails

This commit is contained in:
Kristof Bernaert
2026-01-26 00:44:43 +01:00
commit f64fbb5e06
25 changed files with 453 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
from . import models
+10
View File
@@ -0,0 +1,10 @@
{
'name': 'Allow Emails to Archived Contacts',
'version': '1.0',
'category': 'Tools',
'summary': 'Allow sending emails to archived contacts for Sales and Invoices',
'depends': ['base', 'mail', 'sale', 'account'],
'data': [],
'installable': True,
'application': False,
}
+6
View File
@@ -0,0 +1,6 @@
# models/__init__.py
from . import mail_thread
from . import res_partner
from . import mail_template
# from . import mail_compose_message
from . import account_move_send
+14
View File
@@ -0,0 +1,14 @@
from odoo import models, api
class AccountMoveSend(models.TransientModel):
_inherit = "account.move.send"
@api.model
def _get_default_mail_partner_ids(self, move, mail_template, mail_lang):
"""
The invoice 'Send by Email' wizard uses this to compute partner_ids (To:).
We must disable active_test here to include archived partners.
"""
wiz = self.with_context(active_test=False, include_archived_partners=True)
return super(AccountMoveSend, wiz)._get_default_mail_partner_ids(move, mail_template, mail_lang)
+48
View File
@@ -0,0 +1,48 @@
from odoo import models
class MailComposeMessage(models.TransientModel):
_inherit = "mail.compose.message"
def _get_default_recipients(self):
"""
This is the ONLY place that fills the 'To' field in Odoo 16.
We explicitly allow archived partners here for user-initiated sends.
"""
recipients = super()._get_default_recipients()
model = self.env.context.get("active_model")
active_ids = self.env.context.get("active_ids") or []
if model == "account.move" and active_ids:
moves = self.env["account.move"].with_context(
active_test=False
).browse(active_ids)
partners = moves.mapped("partner_id").filtered(
lambda p: p.email
)
if partners:
recipients["partner_ids"] = [(6, 0, partners.ids)]
recipients["email_to"] = ", ".join(partners.mapped("email"))
return recipients
def _prepare_mail_values(self, res_ids):
"""
Explicit user send → allow archived partners internally.
"""
model = self.env.context.get("active_model") or self.model
if model in ["sale.order", "account.move"]:
return super(
MailComposeMessage,
self.with_context(
active_test=False,
include_archived_partners=True,
mail_notify_force=True,
)
)._prepare_mail_values(res_ids)
return super()._prepare_mail_values(res_ids)
+27
View File
@@ -0,0 +1,27 @@
import logging
_logger = logging.getLogger(__name__)
from odoo import models
class MailTemplate(models.Model):
_inherit = "mail.template"
def generate_email(self, res_ids, fields=None):
"""
Only allow archived partners when *explicitly* sending through a flow
that sets:
context['mail_notify_force'] = True
This prevents system notifications from inheriting your relaxed rules.
"""
if self.model in ["sale.order", "account.move"] and self.env.context.get("mail_notify_force"):
return super(
MailTemplate,
self.with_context(
active_test=False,
include_archived_partners=True
)
).generate_email(res_ids, fields)
return super().generate_email(res_ids, fields)
+50
View File
@@ -0,0 +1,50 @@
import logging
_logger = logging.getLogger(__name__)
from odoo import models
class MailThread(models.AbstractModel):
_inherit = "mail.thread"
def _notify_thread(self, message, msg_vals=False, **kwargs):
"""
Do NOT force notifications, do NOT set mail_notification=True,
do NOT globally disable active_test here.
The parent method decides whether a message should notify.
"""
return super()._notify_thread(message, msg_vals=msg_vals, **kwargs)
def _notify_get_recipients(self, message, msg_vals, **kwargs):
"""
CRITICAL RULES:
- Never turn system notifications into outgoing emails.
- Never fabricate recipients when Odoo returns none.
- Never force notif='email'.
If archived partners should be allowed, that must be opt-in
via context['include_archived_partners'] and only for explicit user sends,
not tracking/system messages.
"""
# 1) Never email system notifications (tracking, auto messages, etc.)
# These are the ones that caused "Re: Draft Bill ..." and odoobot@example.com.
if getattr(message, "message_type", None) == "notification":
return super()._notify_get_recipients(message, msg_vals, **kwargs)
# 2) Never auto-notify supplier bills. Vendor bills created from email/EDI
# must not trigger replies to vendors via chatter notifications.
# (If you DO want it for customer invoices, leave those alone.)
if self._name == "account.move":
# self can be multi; if any are supplier bills, just don't customize at all
# (safer than partial overriding recipient lists)
if any(m.move_type == "in_invoice" for m in self):
return super()._notify_get_recipients(message, msg_vals, **kwargs)
# 3) Default behavior first
recipients = super()._notify_get_recipients(message, msg_vals, **kwargs)
# 4) Do NOT fabricate recipients if empty.
# If you need archived partner support, handle it in explicit sending flows
# (compose + template), not here.
return recipients
+48
View File
@@ -0,0 +1,48 @@
import logging
_logger = logging.getLogger(__name__)
from odoo import models, api
class ResPartner(models.Model):
_inherit = "res.partner"
@api.model
def _search(
self,
args,
offset=0,
limit=None,
order=None,
count=False,
access_rights_uid=None
):
"""
Only include archived partners when the caller explicitly opts in via:
context['include_archived_partners'] = True
Never key off fragile string matches like 'mail'/'notify'/'message'.
"""
if self.env.context.get("include_archived_partners"):
# Remove explicit active=True filters only (do not touch other domains)
args = [
arg for arg in args
if not (
isinstance(arg, (list, tuple))
and len(arg) == 3
and arg[0] == "active"
and arg[1] == "="
and arg[2] is True
)
]
# Also ensure active_test is off so ORM won't auto-filter archived partners
self = self.with_context(active_test=False)
return super()._search(
args,
offset=offset,
limit=limit,
order=order,
count=count,
access_rights_uid=access_rights_uid
)
+1
View File
@@ -0,0 +1 @@
from . import models
+18
View File
@@ -0,0 +1,18 @@
{
"name": "Archived Contact Warning",
"version": "1.0",
"category": "Sales",
"summary": "Show warning when using archived contacts",
"depends": ["sale", "account"],
"data": [
"views/sale_order_views.xml",
"views/account_move_views.xml",
],
"assets": {
"web.assets_backend": [
"archived_partner_show_warning/static/src/css/style.css",
],
},
"installable": True,
"application": False,
}
+50
View File
@@ -0,0 +1,50 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * archived_partner_show_warning
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-15 18:10+0000\n"
"PO-Revision-Date: 2026-01-15 19:15+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.6\n"
#. module: archived_partner_show_warning
#: model_terms:ir.ui.view,arch_db:archived_partner_show_warning.view_move_form_archived_warning
#: model_terms:ir.ui.view,arch_db:archived_partner_show_warning.view_order_form_archived_warning
msgid "<span>This customer is archived.</span>"
msgstr ""
"<span>De klant is gearchiveerd (geïmporteerd van vorige Odoo). Ga na of dit "
"aangepast moet worden naar een nieuwe klantenfiche.</span>"
#. module: archived_partner_show_warning
#: model_terms:ir.ui.view,arch_db:archived_partner_show_warning.view_move_form_archived_warning
#: model_terms:ir.ui.view,arch_db:archived_partner_show_warning.view_order_form_archived_warning
msgid "<strong>Warning: </strong>"
msgstr "<strong>Opgelet: </strong>"
#. module: archived_partner_show_warning
#: model:ir.model,name:archived_partner_show_warning.model_account_move
msgid "Journal Entry"
msgstr "Boeking"
#. module: archived_partner_show_warning
#: model:ir.model.fields,field_description:archived_partner_show_warning.field_account_bank_statement_line__partner_is_archived
#: model:ir.model.fields,field_description:archived_partner_show_warning.field_account_move__partner_is_archived
#: model:ir.model.fields,field_description:archived_partner_show_warning.field_account_payment__partner_is_archived
#: model:ir.model.fields,field_description:archived_partner_show_warning.field_sale_order__partner_is_archived
msgid "Partner Archived"
msgstr ""
#. module: archived_partner_show_warning
#: model:ir.model,name:archived_partner_show_warning.model_sale_order
msgid "Sales Order"
msgstr "Verkooporder"
+2
View File
@@ -0,0 +1,2 @@
from . import sale_order
from . import account_move
+15
View File
@@ -0,0 +1,15 @@
from odoo import models, fields, api
class AccountMove(models.Model):
_inherit = 'account.move'
partner_is_archived = fields.Boolean(
string='Partner Archived',
compute='_compute_partner_is_archived',
store=False
)
@api.depends('partner_id', 'partner_id.active')
def _compute_partner_is_archived(self):
for invoice in self:
invoice.partner_is_archived = invoice.partner_id and not invoice.partner_id.active
+15
View File
@@ -0,0 +1,15 @@
from odoo import models, fields, api
class SaleOrder(models.Model):
_inherit = 'sale.order'
partner_is_archived = fields.Boolean(
string='Partner Archived',
compute='_compute_partner_is_archived',
store=False
)
@api.depends('partner_id', 'partner_id.active')
def _compute_partner_is_archived(self):
for order in self:
order.partner_is_archived = order.partner_id and not order.partner_id.active
+18
View File
@@ -0,0 +1,18 @@
/* Custom styling for archived partner warning */
.archived-warning-custom {
margin: -20px 00 5px 0;
padding-top: 13px;
border-left: 4px solid #f0ad4e;
background: linear-gradient(135deg, #fcf8e3 0%, #faf2cc 100%);
}
.archived-warning-custom i {
font-size: 18px;
margin-right: 5px;
}
.archived-warning-custom strong {
font-size: 14px;
}
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_move_form_archived_warning" model="ir.ui.view">
<field name="name">account.move.form.archived.warning</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<!-- Add field somewhere in the view first -->
<xpath expr="//field[@name='partner_id']" position="before">
<field name="partner_is_archived" invisible="1"/>
</xpath>
<!-- Warning after oe_title -->
<xpath expr="//div[@class='oe_title']" position="after">
<div class="row" attrs="{'invisible': [('partner_is_archived', '=', False)]}" >
<div class="col-12">
<div class="alert alert-warning archived-warning-custom">
<i class="fa fa-exclamation-triangle mr-2"></i>
<!-- TRANSLATORS: Warning label for archived customers -->
<strong>Warning: </strong>
<!-- TRANSLATORS: Warning message for archived customers -->
<span>This customer is archived.</span>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_order_form_archived_warning" model="ir.ui.view">
<field name="name">sale.order.form.archived.warning</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<!-- Add field somewhere in the view first -->
<xpath expr="//field[@name='partner_id']" position="before">
<field name="partner_is_archived" invisible="1"/>
</xpath>
<!-- Warning after oe_title -->
<xpath expr="//div[@class='oe_title']" position="after">
<div class="row" attrs="{'invisible': [('partner_is_archived', '=', False)]}" >
<div class="col-12">
<div class="alert alert-warning archived-warning-custom">
<i class="fa fa-exclamation-triangle mr-2"></i>
<!-- TRANSLATORS: Warning label for archived customers -->
<strong>Warning: </strong>
<!-- TRANSLATORS: Warning message for archived customers -->
<span>This customer is archived.</span>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
+1
View File
@@ -0,0 +1 @@
from . import models
+17
View File
@@ -0,0 +1,17 @@
{
"name": "Partner Hierarchical Name",
"version": "16.0.1.0.0",
"summary": "Show full partner hierarchy in dropdowns (Company / Delivery / Contact)",
"category": "Contacts",
"author": "Van Bernaert (Developer), Domus La Vila (Distributor)",
"license": "LGPL-3",
"depends": ["base"],
"installable": True,
"application": False,
"assets": {
"web.assets_backend": [
"partner_hierarchical_name/static/src/css/many2one_dropdown.css",
],
},
}
+1
View File
@@ -0,0 +1 @@
from . import res_partner
+25
View File
@@ -0,0 +1,25 @@
from odoo import models
class ResPartner(models.Model):
_inherit = "res.partner"
def name_get(self):
result = []
for partner in self:
names = []
current = partner
# Walk up the hierarchy
while current:
if current.name:
names.append(current.name)
current = current.parent_id
# Reverse to show top-down (L1 / L2 / L3)
full_name = " / ".join(reversed(names))
result.append((partner.id, full_name))
return result
@@ -0,0 +1,24 @@
/* jQuery UI autocomplete dropdown (authoritative override) */
.ui-autocomplete {
min-width: 650px !important;
max-width: 900px !important;
/* Float above form & right column */
z-index: 1100 !important;
/* Allow multi-line hierarchy */
white-space: normal !important;
}
/* Ensure individual items wrap */
.ui-autocomplete .ui-menu-item-wrapper {
white-space: normal !important;
word-break: break-word;
}
/* Prevent clipping by form containers */
.o_form_view,
.o_form_sheet,
.o_form_sheet_bg {
overflow: visible !important;
}