Fix allow_mail_archived_partner causing system notifications to send emails
This commit is contained in:
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
Executable
+10
@@ -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
@@ -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
@@ -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)
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
)
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
+18
@@ -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,
|
||||||
|
}
|
||||||
Executable
+50
@@ -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
@@ -0,0 +1,2 @@
|
|||||||
|
from . import sale_order
|
||||||
|
from . import account_move
|
||||||
+15
@@ -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
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
Executable
+17
@@ -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",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
Binary file not shown.
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
from . import res_partner
|
||||||
Binary file not shown.
Binary file not shown.
+25
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user