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 + + + + + + + + +