diff --git a/account_payment_receivable_email/__manifest__.py b/account_payment_receivable_email/__manifest__.py
index 29b40e79..137fe881 100644
--- a/account_payment_receivable_email/__manifest__.py
+++ b/account_payment_receivable_email/__manifest__.py
@@ -3,7 +3,7 @@
{
'name': 'Account Payment Receivable Email',
- 'version': '14.0.1.3.0',
+ 'version': '14.0.2.0.0',
'category': 'Accounting',
'author': 'Numigi',
"maintainer": "Numigi",
diff --git a/account_payment_receivable_email/i18n/fr.po b/account_payment_receivable_email/i18n/fr.po
index 589f4a69..15f8d375 100644
--- a/account_payment_receivable_email/i18n/fr.po
+++ b/account_payment_receivable_email/i18n/fr.po
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 14.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-02-11 15:14+0000\n"
-"PO-Revision-Date: 2026-02-11 15:14+0000\n"
+"POT-Creation-Date: 2026-04-08 15:14+0000\n"
+"PO-Revision-Date: 2026-04-08 15:14+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: fr\n"
@@ -16,12 +16,6 @@ msgstr ""
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
-#. module: account_payment_receivable_email
-#: code:addons/account_payment_receivable_email/models/res_partner.py:0
-#, python-format
-msgid "Automatically created from the Receivable Accounts Email field."
-msgstr "Créé automatiquement à partir du champ Email Comptes Recevables."
-
#. module: account_payment_receivable_email
#: model:ir.model,name:account_payment_receivable_email.model_res_partner
msgid "Contact"
@@ -34,12 +28,6 @@ msgstr "Contact"
msgid "Display Name"
msgstr "Nom affiché"
-#. module: account_payment_receivable_email
-#: model:ir.model.fields,help:account_payment_receivable_email.field_res_partner__payment_email
-#: model:ir.model.fields,help:account_payment_receivable_email.field_res_users__payment_email
-msgid "Email address to send payment notifications and receipts"
-msgstr "Adresse email pour envoyer les notifications de paiement et les reçus"
-
#. module: account_payment_receivable_email
#: model:ir.model,name:account_payment_receivable_email.model_mail_compose_message
msgid "Email composition wizard"
@@ -65,14 +53,19 @@ msgid "Payments"
msgstr "Paiements"
#. module: account_payment_receivable_email
-#: code:addons/account_payment_receivable_email/models/res_partner.py:0
-#: code:addons/account_payment_receivable_email/models/res_partner.py:0
-#, python-format
-msgid "Receivable Accounts"
-msgstr "Comptes Recevables"
+#: model:ir.model.fields,field_description:account_payment_receivable_email.field_res_partner__is_receivable_account
+#: model:ir.model.fields,field_description:account_payment_receivable_email.field_res_users__is_receivable_account
+msgid "Is Receivable Account"
+msgstr "Est un Compte Recevable"
+
+#. module: account_payment_receivable_email
+#: model:ir.model.fields,field_description:account_payment_receivable_email.field_res_partner__payment_email_id
+#: model:ir.model.fields,field_description:account_payment_receivable_email.field_res_users__payment_email_id
+msgid "Receivable Accounts Contact"
+msgstr "Contact Comptes Recevables"
#. module: account_payment_receivable_email
-#: model:ir.model.fields,field_description:account_payment_receivable_email.field_res_partner__payment_email
-#: model:ir.model.fields,field_description:account_payment_receivable_email.field_res_users__payment_email
-msgid "Receivable Accounts Email"
-msgstr "Email Comptes Recevables"
\ No newline at end of file
+#: model:ir.model.fields,help:account_payment_receivable_email.field_res_partner__payment_email_id
+#: model:ir.model.fields,help:account_payment_receivable_email.field_res_users__payment_email_id
+msgid "Select the specific contact to receive payment notifications."
+msgstr "Sélectionnez le contact spécifique qui recevra les notifications de paiement et les reçus."
\ No newline at end of file
diff --git a/account_payment_receivable_email/models/__init__.py b/account_payment_receivable_email/models/__init__.py
index e1a879ef..51123e68 100644
--- a/account_payment_receivable_email/models/__init__.py
+++ b/account_payment_receivable_email/models/__init__.py
@@ -3,5 +3,4 @@
from . import account_payment
from . import mail_compose_message
-from . import mail_mail
from . import res_partner
diff --git a/account_payment_receivable_email/models/account_payment.py b/account_payment_receivable_email/models/account_payment.py
index 674523ef..f8f2373c 100644
--- a/account_payment_receivable_email/models/account_payment.py
+++ b/account_payment_receivable_email/models/account_payment.py
@@ -8,7 +8,6 @@ class AccountPayment(models.Model):
_inherit = "account.payment"
def _notify_get_recipients(self, message, groups):
- """ Voir logique ci-dessus pour account.move """
- if self.partner_id.payment_email:
+ if self.partner_id.payment_email_id:
return []
return super(AccountPayment, self)._notify_get_recipients(message, groups)
diff --git a/account_payment_receivable_email/models/mail_compose_message.py b/account_payment_receivable_email/models/mail_compose_message.py
index 73324b31..2a4eb98d 100644
--- a/account_payment_receivable_email/models/mail_compose_message.py
+++ b/account_payment_receivable_email/models/mail_compose_message.py
@@ -17,36 +17,22 @@ def onchange_template_id(self, template_id, composition_mode, model, res_id):
res = super(MailComposer, self).onchange_template_id(
template_id, composition_mode, model, res_id)
- # Check model: we target Payment and Invoices
if model not in ['account.payment', 'account.move'] or not res_id:
return res
record = self.env[model].browse(res_id)
partner = getattr(record, 'partner_id', False)
- if partner and partner.payment_email:
- child = self.env['res.partner'].search([
- ('parent_id', '=', partner.id),
- ('is_receivable_account', '=', True)
- ], limit=1)
- if child:
- if 'value' not in res:
- res['value'] = {}
- res['value']['partner_ids'] = [(6, 0, [child.id])]
+ if partner and partner.payment_email_id:
+ if 'value' not in res:
+ res['value'] = {}
+ res['value']['partner_ids'] = [(6, 0, [partner.payment_email_id.id])]
+
return res
def get_mail_values(self, res_ids):
- """
- Override to inject the specific recipient at sending time.
- COMPATIBLE WITH:
- - Standard Send (Comment mode)
- - Mass Mailing (Batch Send)
- - Canada Bank Transfer (EFT Wizards)
- """
self.ensure_one()
results = super(MailComposer, self).get_mail_values(res_ids)
-
- # Safety check on model
if self.model not in ["account.payment", "account.move"]:
return results
@@ -54,17 +40,13 @@ def get_mail_values(self, res_ids):
record = self.env[self.model].browse(res_id)
partner = getattr(record, 'partner_id', False)
- if partner and partner.payment_email:
- child = self.env['res.partner'].search([
- ('parent_id', '=', partner.id),
- ('is_receivable_account', '=', True)
- ], limit=1)
+ if partner and partner.payment_email_id:
+ target_id = partner.payment_email_id.id
- if child:
- if 'partner_ids' in mail_values:
- mail_values['partner_ids'] = [child.id]
+ if 'partner_ids' in mail_values:
+ mail_values['partner_ids'] = [target_id]
- if 'recipient_ids' in mail_values:
- mail_values['recipient_ids'] = [(5, 0, 0), (4, child.id)]
+ if 'recipient_ids' in mail_values:
+ mail_values['recipient_ids'] = [(5, 0, 0), (4, target_id)]
return results
diff --git a/account_payment_receivable_email/models/mail_mail.py b/account_payment_receivable_email/models/mail_mail.py
deleted file mode 100644
index 26e71b8d..00000000
--- a/account_payment_receivable_email/models/mail_mail.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# © Numigi (tm) and all its contributors (https://numigi.com/r/home)
-# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-
-from odoo import models, api
-from odoo.tools import formataddr # Import nécessaire pour formater le nom
-
-
-class MailMail(models.Model):
- _inherit = 'mail.mail'
-
- @api.model_create_multi
- def create(self, values_list):
- for values in values_list:
- model = values.get('model')
- res_id = values.get('res_id')
-
- if not model and values.get('mail_message_id'):
- message = self.env['mail.message'].browse(values['mail_message_id'])
- model = message.model
- res_id = message.res_id
-
- if model in ['account.payment', 'account.move'] and res_id:
- record = self.env[model].browse(res_id)
- partner = getattr(record, 'partner_id', False)
-
- if partner and partner.payment_email:
- # On formate proprement le nom pour le destinataire
- formatted_name = f"{partner.name}, Comptes recevables"
- values['email_to'] = formataddr((formatted_name, partner.payment_email))
-
- # ON COUPE LE LIEN AVEC LE CONTACT ENFANT (qui a la fausse adresse)
- if 'recipient_ids' in values:
- values['recipient_ids'] = [(5, 0, 0)]
-
- return super(MailMail, self).create(values_list)
-
- def _postprocess_sent_message(self, success_pids, failure_reason=False, failure_type=None):
- mails_to_process = []
- for mail in self:
- if mail.model in ['account.payment', 'account.move'] and mail.mail_message_id:
- mails_to_process.append({
- 'message_id': mail.mail_message_id,
- 'model': mail.model,
- 'res_id': mail.res_id,
- })
-
- res = super(MailMail, self)._postprocess_sent_message(
- success_pids, failure_reason=failure_reason, failure_type=failure_type)
-
- for mail_data in mails_to_process:
- if (mail_data['model'] in ['account.payment', 'account.move']
- and mail_data['message_id']):
- record = self.env[mail_data['model']].browse(mail_data['res_id'])
- partner = getattr(record, 'partner_id', False)
-
- # Force le succès de la notification
- if partner and partner.payment_email and not failure_reason:
- notifications = mail_data['message_id'].notification_ids.filtered(
- lambda n: n.notification_status in ['ready', 'exception']
- )
- if notifications:
- notifications.write({
- 'notification_status': 'sent',
- 'failure_type': False,
- 'failure_reason': False,
- })
- return res
diff --git a/account_payment_receivable_email/models/res_partner.py b/account_payment_receivable_email/models/res_partner.py
index 104a8e55..3024e365 100644
--- a/account_payment_receivable_email/models/res_partner.py
+++ b/account_payment_receivable_email/models/res_partner.py
@@ -1,7 +1,7 @@
# © Numigi (tm) and all its contributors (https://numigi.com/r/home)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-from odoo import fields, models, _
+from odoo import fields, models
class ResPartner(models.Model):
@@ -13,80 +13,9 @@ class ResPartner(models.Model):
copy=False,
)
- payment_email = fields.Char(
- string="Receivable Accounts Email",
- help="Email address to send payment notifications and receipts",
- inverse="_inverse_payment_email"
+ payment_email_id = fields.Many2one(
+ 'res.partner',
+ string="Receivable Accounts Contact",
+ domain="[('is_receivable_account', '=', True)]",
+ help="Select the specific contact to receive payment notifications."
)
-
- def _inverse_payment_email(self):
- """
- Sync from Parent Field -> Child Contact.
- Uses the 'is_receivable_account' boolean to identify the target.
- """
- for partner in self:
- # --- ANTI- UNLIMITED LOOP ---
- if partner.is_receivable_account:
- continue
-
- if partner.payment_email:
- child_partner = self.env['res.partner'].with_context(active_test=False).search([
- ('parent_id', '=', partner.id),
- ('is_receivable_account', '=', True)
- ], limit=1)
-
- unique_fake_email = f"{partner.payment_email}.{partner.id}"
-
- if child_partner:
- # Update existing contact
- if child_partner.payment_email != partner.payment_email:
- child_partner.payment_email = partner.payment_email
- child_partner.email = unique_fake_email
-
- # Reactivate if it was archived
- if not child_partner.active:
- child_partner.active = True
- else:
- # Create NEW contact with the Boolean set to True
- self.env['res.partner'].create({
- 'name': _('Receivable Accounts'),
- 'parent_id': partner.id,
- 'type': 'other',
- 'company_type': 'person',
- 'email': unique_fake_email,
- 'payment_email': partner.payment_email,
- 'is_receivable_account': True,
- 'comment': _('Automatically created from the '
- 'Receivable Accounts Email field.'),
- })
-
- else:
- # Field Cleared -> Find the boolean contact and Archive it
- child_to_archive = self.env['res.partner'].search([
- ('parent_id', '=', partner.id),
- ('is_receivable_account', '=', True)
- ], limit=1)
-
- if child_to_archive:
- child_to_archive.active = False
-
- def write(self, vals):
- """
- Sync from Child Contact -> Parent Field.
- We strictly listen to contacts marked with 'is_receivable_account'.
- """
- records_to_sync = self.env['res.partner']
- if 'payment_email' in vals:
- for record in self:
- if record.parent_id and record.is_receivable_account:
- records_to_sync += record
-
- # Perform Standard Write
- res = super(ResPartner, self).write(vals)
-
- # Propagate to parents
- for record in records_to_sync:
- if record.parent_id.payment_email != vals['payment_email']:
- record.parent_id.payment_email = vals['payment_email']
-
- return res
diff --git a/account_payment_receivable_email/tests/test_receivable_email.py b/account_payment_receivable_email/tests/test_receivable_email.py
index 6108cf38..201a4b6d 100644
--- a/account_payment_receivable_email/tests/test_receivable_email.py
+++ b/account_payment_receivable_email/tests/test_receivable_email.py
@@ -7,13 +7,17 @@ class TestReceivableEmail(common.TransactionCase):
def setUp(self):
super(TestReceivableEmail, self).setUp()
- # Create a test partner
self.partner = self.env['res.partner'].create({
'name': 'Test Client',
'email': 'client@test.com',
})
- # Minimal configuration for a payment (Journal + Method)
+ self.receivable_contact = self.env['res.partner'].create({
+ 'name': 'Contact Compta',
+ 'email': 'compta@test.com',
+ 'is_receivable_account': True,
+ })
+
self.journal = self.env['account.journal'].create({
'name': 'Test Bank',
'type': 'bank',
@@ -22,79 +26,15 @@ def setUp(self):
})
self.payment_method = self.env.ref('account.account_payment_method_manual_in')
- def test_01_sync_parent_to_child(self):
- """ Test: Parent (payment_email) -> Child (Create/Update/Archive) """
-
- # 1. Set a payment email
- email_a = "accounting@client.com"
- self.partner.payment_email = email_a
-
- # Verify child contact creation
- child = self.env['res.partner'].search([
- ('parent_id', '=', self.partner.id),
- ('is_receivable_account', '=', True)
- ])
- self.assertTrue(child, "The child contact should be created.")
-
- # VERIFY FAKE EMAIL LOGIC & TYPES
- fake_email_a = f"{email_a}.{self.partner.id}"
- self.assertEqual(child.payment_email, email_a,
- "The child payment_email must match the parent payment email.")
- self.assertEqual(child.email, fake_email_a,
- "The standard email must be the falsified unique email.")
-
- # Vérification des types
- self.assertEqual(child.type, 'other', "The contact type must be 'other'.")
- self.assertEqual(child.company_type, 'person', "The company_type must be 'person'.")
-
- # 2. Update parent email
- email_b = "billing@client.com"
- self.partner.payment_email = email_b
-
- # Verify that the SAME contact is updated with new fake email
- fake_email_b = f"{email_b}.{self.partner.id}"
- self.assertEqual(child.payment_email, email_b,
- "The child payment_email should be updated.")
- self.assertEqual(child.email, fake_email_b, "The child fake email should be updated.")
- self.assertEqual(len(self.partner.child_ids), 1, "There should be no duplicates.")
+ def test_01_assign_receivable_contact(self):
+ """ Test: Assign a receivable accounts contact """
+ self.partner.payment_email_id = self.receivable_contact.id
+ self.assertEqual(self.partner.payment_email_id.email, "compta@test.com")
- # 3. Clear the field (Archive)
- self.partner.payment_email = False
- self.assertFalse(child.active,
- "The child contact should be archived if the field is cleared.")
+ def test_02_mail_composer_injection(self):
+ """ Test: The wizard injects the ID of the selected contact """
+ self.partner.payment_email_id = self.receivable_contact.id
- # 4. Set an email again (Reactivate)
- self.partner.payment_email = email_a
- self.assertTrue(child.active, "The child contact should be reactivated.")
- self.assertEqual(child.payment_email, email_a)
-
- def test_02_sync_child_to_parent(self):
- """ Test: Child (write payment_email) -> Parent (payment_email) """
-
- # Initialization
- self.partner.payment_email = "init@test.com"
- child = self.env['res.partner'].search([
- ('parent_id', '=', self.partner.id),
- ('is_receivable_account', '=', True)
- ])
-
- # Modify payment_email directly on the child contact
- # Note: We now listen to changes on 'payment_email', not 'email'
- new_email = "new_child@test.com"
- child.payment_email = new_email
-
- # Verify sync back to parent
- self.assertEqual(self.partner.payment_email, new_email,
- "Changes on the child's payment_email must update the parent field.")
-
- def test_03_mail_composer_injection(self):
- """ Test: The wizard injects the specific contact in UI and values """
-
- # Setup: Partner with specific config
- target_email = "specific@test.com"
- self.partner.payment_email = target_email
-
- # Create a payment linked to this partner
payment = self.env['account.payment'].create({
'payment_type': 'inbound',
'partner_type': 'customer',
@@ -104,13 +44,6 @@ def test_03_mail_composer_injection(self):
'payment_method_id': self.payment_method.id,
})
- # Fetch the automatically created child contact
- child = self.env['res.partner'].search([
- ('parent_id', '=', self.partner.id),
- ('is_receivable_account', '=', True)
- ], limit=1)
-
- # Instantiate the Mail Composer wizard
composer = self.env['mail.compose.message'].with_context(
default_model='account.payment',
default_res_id=payment.id,
@@ -123,20 +56,16 @@ def test_03_mail_composer_injection(self):
False, 'comment', 'account.payment', payment.id
)
- # Verify that partner_ids visually injects the child contact
if onchange_res and 'value' in onchange_res:
- self.assertEqual(onchange_res['value'].get('partner_ids'), [(6, 0, [child.id])],
- "The interface must visually inject the child recipient.")
+ self.assertEqual(onchange_res['value'].get('partner_ids'),
+ [(6, 0, [self.receivable_contact.id])])
# 2. Test Get Mail Values (Sending Logic)
mail_values = composer.get_mail_values([payment.id])[payment.id]
- # A. Verify Comment Mode (partner_ids)
if 'partner_ids' in mail_values:
- self.assertEqual(mail_values['partner_ids'], [child.id],
- "partner_ids must contain the child so Odoo sends an email.")
+ self.assertEqual(mail_values['partner_ids'], [self.receivable_contact.id])
- # B. Verify Mass Mail Mode (recipient_ids)
if 'recipient_ids' in mail_values:
- self.assertEqual(mail_values['recipient_ids'], [(5, 0, 0), (4, child.id)],
- "recipient_ids must be swapped to the child.")
+ self.assertEqual(mail_values['recipient_ids'],
+ [(5, 0, 0), (4, self.receivable_contact.id)])
diff --git a/account_payment_receivable_email/views/res_partner.xml b/account_payment_receivable_email/views/res_partner.xml
index 8767a234..7c33e0c4 100644
--- a/account_payment_receivable_email/views/res_partner.xml
+++ b/account_payment_receivable_email/views/res_partner.xml
@@ -5,7 +5,24 @@
-
+
+
+
+
+
+
+ res.partner.select.receivable
+ res.partner
+
+
+
+
+
+
+
+
+