Fix allow_mail_archived_partner causing system notifications to send emails

This commit is contained in:
Kristof Bernaert
2026-01-26 04:33:59 +01:00
parent c94fba0f02
commit c175e33c03
5 changed files with 233 additions and 52 deletions
@@ -1,3 +1,6 @@
import logging
_logger = logging.getLogger(__name__)
from odoo import models, api, fields from odoo import models, api, fields
@@ -15,9 +18,8 @@ class AccountInvoiceSend(models.TransientModel):
@api.model @api.model
def default_get(self, fields): def default_get(self, fields):
""" _logger.error("🔥 ACCOUNT.INVOICE.SEND default_get")
Pre-fill invoice email wizard with archived partners.
"""
res = super().default_get(fields) res = super().default_get(fields)
active_ids = self.env.context.get('active_ids', []) active_ids = self.env.context.get('active_ids', [])
@@ -29,13 +31,16 @@ class AccountInvoiceSend(models.TransientModel):
partners = moves.mapped('partner_id').filtered(lambda p: p.email) partners = moves.mapped('partner_id').filtered(lambda p: p.email)
if partners: if partners:
res['partner_ids'] = [(6, 0, partners.ids)] res['partner_ids'] = [(6, 0, partners.ids)]
_logger.error(f"🔥 Set partner_ids: {res['partner_ids']}")
return res return res
def action_send_and_print(self): def action_send_and_print(self):
""" _logger.error("🔥🔥🔥 ACCOUNT.INVOICE.SEND action_send_and_print CALLED 🔥🔥🔥")
Pass context to allow archived partners during email sending. _logger.error(f"🔥 Wizard ID: {self.id}")
""" _logger.error(f"🔥 Partner IDs: {self.partner_ids.ids}") # This returns a list of IDs
# Call parent with context AND ensure partner_ids are passed
return super( return super(
AccountInvoiceSend, AccountInvoiceSend,
self.with_context( self.with_context(
@@ -44,13 +49,15 @@ class AccountInvoiceSend(models.TransientModel):
mail_notify_force=True, mail_notify_force=True,
force_email=True, force_email=True,
mark_invoice_as_sent=True, mark_invoice_as_sent=True,
# Pass simple list of IDs, NOT ORM tuple format
invoice_partner_ids=self.partner_ids.ids if self.partner_ids else [],
) )
).action_send_and_print() ).action_send_and_print()
def _get_composer_values(self, res_ids, template): def _get_composer_values(self, res_ids, template):
""" _logger.error("🔥 ACCOUNT.INVOICE.SEND _get_composer_values")
Pass context to mail composer to allow archived partners.
""" # Also pass context to composer
return super( return super(
AccountInvoiceSend, AccountInvoiceSend,
self.with_context( self.with_context(
@@ -1,3 +1,6 @@
import logging
_logger = logging.getLogger(__name__)
from odoo import models, api from odoo import models, api
@@ -10,6 +13,8 @@ class MailComposeMessage(models.TransientModel):
Pre-fill email wizard with archived partners for sales orders. Pre-fill email wizard with archived partners for sales orders.
For invoices, this is handled by account.invoice.send. For invoices, this is handled by account.invoice.send.
""" """
_logger.debug("=== MAIL.COMPOSE.MESSAGE DEFAULT_GET ===")
res = super().default_get(fields) res = super().default_get(fields)
model = self.env.context.get("active_model") model = self.env.context.get("active_model")
@@ -17,6 +22,8 @@ class MailComposeMessage(models.TransientModel):
# Only handle sales orders here - invoices use account.invoice.send # Only handle sales orders here - invoices use account.invoice.send
if model == "sale.order" and res_id: if model == "sale.order" and res_id:
_logger.debug(f"Processing sales order {res_id}")
# Find order with archived partners allowed # Find order with archived partners allowed
order = self.env["sale.order"].with_context( order = self.env["sale.order"].with_context(
active_test=False active_test=False
@@ -24,6 +31,7 @@ class MailComposeMessage(models.TransientModel):
if order.exists() and order.partner_id: if order.exists() and order.partner_id:
res["partner_ids"] = [(6, 0, [order.partner_id.id])] res["partner_ids"] = [(6, 0, [order.partner_id.id])]
_logger.debug(f"Pre-filled archived partner {order.partner_id.id} for sales order")
return res return res
@@ -32,11 +40,14 @@ class MailComposeMessage(models.TransientModel):
Set context flags for manual email sends. Set context flags for manual email sends.
This ensures archived partners are allowed during email sending. This ensures archived partners are allowed during email sending.
""" """
_logger.debug("=== _prepare_mail_values ===")
model = self.env.context.get("active_model") or self.model model = self.env.context.get("active_model") or self.model
# Only for sales orders and account moves (invoices) # Only for sales orders and account moves (invoices)
# Note: account.invoice.send should handle invoices, but keep this as fallback # Note: account.invoice.send should handle invoices, but keep this as fallback
if model in ["sale.order", "account.move"]: if model in ["sale.order", "account.move"]:
_logger.debug(f"Setting context for {model} to allow archived partners")
return super( return super(
MailComposeMessage, MailComposeMessage,
self.with_context( self.with_context(
@@ -63,6 +74,7 @@ class MailComposeMessage(models.TransientModel):
# If it's a manual send and partner is archived, include them # If it's a manual send and partner is archived, include them
if is_manual_send and partner and not partner.active: if is_manual_send and partner and not partner.active:
_logger.debug(f"Including archived partner {partner.name} in email")
return { return {
"partner_id": partner.id, "partner_id": partner.id,
"email": partner.email, "email": partner.email,
@@ -72,3 +84,20 @@ class MailComposeMessage(models.TransientModel):
# Default behavior for active partners or non-manual sends # Default behavior for active partners or non-manual sends
return super()._prepare_recipient_values(partner) return super()._prepare_recipient_values(partner)
def action_send_mail(self):
"""
Log when email is actually sent.
"""
_logger.info("🔥 MAIL.COMPOSE.MESSAGE action_send_mail")
_logger.info(f"Partner IDs: {self.partner_ids.ids}")
_logger.info(f"Context: {dict(self.env.context)}")
return super().action_send_mail()
def _action_send_mail(self, auto_commit=False):
"""
Another possible send method.
"""
_logger.info("🔥 MAIL.COMPOSE.MESSAGE _action_send_mail")
return super()._action_send_mail(auto_commit=auto_commit)
@@ -1,6 +1,25 @@
import logging
_logger = logging.getLogger(__name__)
from odoo import models 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,from odoo import models
class MailTemplate(models.Model): class MailTemplate(models.Model):
_inherit = "mail.template" _inherit = "mail.template"
@@ -21,3 +40,10 @@ class MailTemplate(models.Model):
).generate_email(res_ids, fields) ).generate_email(res_ids, fields)
return super().generate_email(res_ids, fields) return super().generate_email(res_ids, fields)
self.with_context(
active_test=False,
include_archived_partners=True
)
).generate_email(res_ids, fields)
return super().generate_email(res_ids, fields)
+126 -24
View File
@@ -1,50 +1,152 @@
import logging
_logger = logging.getLogger(__name__)
from odoo import models from odoo import models
class MailThread(models.AbstractModel): class MailThread(models.AbstractModel):
_inherit = "mail.thread" _inherit = "mail.thread"
_logger.info("=== MAILTHREAD CLASS LOADED (allow_mail_archived_partner) ===")
def _notify_thread(self, message, msg_vals=False, **kwargs):
"""
Log notification thread calls.
"""
_logger.error("🔥🔥🔥 MailThread._notify_thread CALLED 🔥🔥🔥")
_logger.error(f"🔥 Model: {self._name}")
_logger.error(f"🔥 Message type: {getattr(message, 'message_type', 'Unknown')}")
_logger.error(f"🔥 Message subject: {getattr(message, 'subject', 'No subject')}")
_logger.error(f"🔥 Context: {dict(self.env.context)}")
# Check message attributes
_logger.error(f"🔥 Message author_id: {getattr(message, 'author_id', None)}")
_logger.error(f"🔥 Message partner_ids: {getattr(message, 'partner_ids', None)}")
_logger.error(f"🔥 Message record: {message.model if hasattr(message, 'model') else 'N/A'} {message.res_id if hasattr(message, 'res_id') else 'N/A'}")
return super()._notify_thread(message, msg_vals=msg_vals, **kwargs)
def _notify_get_recipients(self, message, msg_vals, **kwargs): def _notify_get_recipients(self, message, msg_vals, **kwargs):
""" """
Ensure archived recipients are included for manual email sends. Force inclusion of archived partners for manual sends.
""" """
# Get normal recipients first _logger.error("🔥🔥🔥🔥🔥 MailThread._notify_get_recipients CALLED 🔥🔥🔥🔥🔥")
recipients = super()._notify_get_recipients(message, msg_vals, **kwargs)
# Check if this is a manual email send (invoice or sales order) # Check if this is a manual send
is_manual_email = ( is_manual_send = (
self.env.context.get("force_email") or # Invoice wizard self.env.context.get("mail_notify_force") or
self.env.context.get("mark_invoice_as_sent") or # Invoice wizard self.env.context.get("include_archived_partners") or
self.env.context.get("mail_notify_force") or # Manual compose self.env.context.get("force_email") or
self.env.context.get("default_composition_mode") == "comment" # Email composer self.env.context.get("mark_invoice_as_sent")
) )
# If manual email AND message has partner_ids, ensure they're in recipients _logger.error(f"🔥 Is manual send? {is_manual_send}")
if is_manual_email and hasattr(message, 'partner_ids') and message.partner_ids: _logger.error(f"🔥 Message partner_ids: {getattr(message, 'partner_ids', None)}")
for partner in message.partner_ids:
# Check if partner is already in recipients
partner_found = False
for recipient in recipients:
if recipient.get('partner_id') == partner.id:
partner_found = True
# Ensure email notification is enabled
if recipient.get('notif') != 'email':
recipient['notif'] = 'email'
break
# If partner not found, add them (even if archived!) # For manual sends, we need to handle archived partners
if not partner_found and partner.email: if is_manual_send and hasattr(message, 'partner_ids') and message.partner_ids:
_logger.error("🔥🔥🔥 MANUAL SEND WITH PARTNERS - Handling archived partners 🔥🔥🔥")
# Get the partner IDs from the message
partner_ids = message.partner_ids.ids
_logger.error(f"🔥 Message has partners: {partner_ids}")
# Force the lookup of THESE partners (the recipients), not the author
# We need to ensure these partners are found even if archived
# Call parent with context that allows archived partners
recipients = super(
MailThread,
self.with_context(
active_test=False,
include_archived_partners=True,
mail_notify_force=True,
# Add the partner IDs we want to look up
force_notification_partner_ids=partner_ids,
)
)._notify_get_recipients(message, msg_vals, **kwargs)
_logger.error(f"🔥 Number of recipients found: {len(recipients)}")
# If no recipients found, create them manually
if len(recipients) == 0 and partner_ids:
_logger.error("🔥 No recipients found, creating manually")
recipients = []
for partner_id in partner_ids:
partner = self.env['res.partner'].with_context(
active_test=False
).browse(partner_id)
if partner.exists() and partner.email:
recipients.append({ recipients.append({
'id': partner.id, 'id': partner.id,
'partner_id': partner.id, 'partner_id': partner.id,
'email': partner.email, 'email': partner.email,
'name': partner.name, 'name': partner.name,
'notif': 'email', # Force email 'notif': 'email', # Force email notification
'lang': partner.lang or 'en_US', 'lang': partner.lang or 'en_US',
'type': 'customer', 'type': 'customer',
'is_follower': False, 'is_follower': False,
'groups': [], 'groups': [],
'notifications': [], 'notifications': [],
}) })
_logger.error(f"🔥 Created recipient for archived partner: {partner.id} - {partner.email}")
for i, recipient in enumerate(recipients):
_logger.error(f"🔥 Recipient {i}: partner_id={recipient.get('partner_id')}, "
f"notif={recipient.get('notif')}, email={recipient.get('email')}")
return recipients return recipients
def _message_post(self, **kwargs):
"""
Log message post calls to trace email flow.
"""
_logger.error("🔥🔥🔥 MailThread._message_post CALLED 🔥🔥🔥")
_logger.error(f"🔥 Model: {self._name}")
_logger.error(f"🔥 Kwargs keys: {kwargs.keys()}")
_logger.error(f"🔥 Context: {dict(self.env.context)}")
return super()._message_post(**kwargs)
def message_post(self, **kwargs):
"""
Override to ensure partner_ids are passed correctly.
"""
_logger.error("🔥🔥🔥 MailThread.message_post CALLED 🔥🔥🔥")
_logger.error(f"🔥 Model: {self._name}")
_logger.error(f"🔥 Kwargs keys: {kwargs.keys()}")
_logger.error(f"🔥 Context: {dict(self.env.context)}")
_logger.error(f"🔥 Partner IDs in kwargs: {kwargs.get('partner_ids', [])}")
# Check if we have partner_ids from context (passed from account.invoice.send)
context_partner_ids = self.env.context.get('invoice_partner_ids', [])
if context_partner_ids and not kwargs.get('partner_ids'):
_logger.error(f"🔥 Using partner_ids from context: {context_partner_ids}")
# Convert to simple list of IDs, NOT ORM tuple format
kwargs['partner_ids'] = context_partner_ids
# Also check for other sources of partner_ids
if not kwargs.get('partner_ids'):
# Try to get from the record itself
if self and hasattr(self, 'partner_id') and self.partner_id:
_logger.error(f"🔥 Getting partner_id from record: {self.partner_id.id}")
kwargs['partner_ids'] = [self.partner_id.id]
# Convert ORM tuple format to simple list if needed
if kwargs.get('partner_ids') and isinstance(kwargs['partner_ids'], list):
# Check if it's in ORM format [(6, 0, [id1, id2])]
if (len(kwargs['partner_ids']) == 1 and
isinstance(kwargs['partner_ids'][0], (list, tuple)) and
len(kwargs['partner_ids'][0]) == 3 and
kwargs['partner_ids'][0][0] == 6):
# Extract IDs from ORM format
partner_ids = kwargs['partner_ids'][0][2]
_logger.error(f"🔥 Converting ORM format to simple list: {kwargs['partner_ids']} -> {partner_ids}")
kwargs['partner_ids'] = partner_ids
_logger.error(f"🔥 Final partner_ids being passed: {kwargs.get('partner_ids', [])}")
return super().message_post(**kwargs)
@@ -1,3 +1,6 @@
import logging
_logger = logging.getLogger(__name__)
from odoo import models, api from odoo import models, api
@@ -7,9 +10,13 @@ class ResPartner(models.Model):
@api.model @api.model
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
""" """
Remove active filters when context indicates manual email send SIMPLE: Just remove active filters when context says to include archived.
(to allow finding archived partners).
""" """
_logger.error("🔥 RES.PARTNER._search - Checking for archived context")
_logger.error(f"🔥 Context force_email: {self.env.context.get('force_email')}")
_logger.error(f"🔥 Context mark_invoice_as_sent: {self.env.context.get('mark_invoice_as_sent')}")
_logger.error(f"🔥 Context mail_notify_force: {self.env.context.get('mail_notify_force')}")
# Check for manual email context # Check for manual email context
include_archived = ( include_archived = (
self.env.context.get("force_email") or self.env.context.get("force_email") or
@@ -17,8 +24,11 @@ class ResPartner(models.Model):
self.env.context.get("mail_notify_force") self.env.context.get("mail_notify_force")
) )
_logger.error(f"🔥 Should include archived? {include_archived}")
if include_archived: if include_archived:
# Remove active filters to include archived partners _logger.error("🔥 Removing active filters for archived partners")
# Simple: remove active filters
args = [ args = [
arg for arg in args arg for arg in args
if not (isinstance(arg, (list, tuple)) and if not (isinstance(arg, (list, tuple)) and
@@ -26,5 +36,12 @@ class ResPartner(models.Model):
arg[0] == "active") arg[0] == "active")
] ]
self = self.with_context(active_test=False) self = self.with_context(active_test=False)
_logger.error(f"🔥 Search args after: {args}")
return super()._search(args, offset, limit, order, count, access_rights_uid) result = super()._search(args, offset, limit, order, count, access_rights_uid)
if not count:
result_ids = list(result)
_logger.error(f"🔥 Search returned {len(result_ids)} results")
return result