From 88782d3a76fb3941b9ed7d998ec0610aa26ff611 Mon Sep 17 00:00:00 2001 From: "kdeb@odoo.com" Date: Fri, 16 Feb 2024 11:52:52 +0100 Subject: [PATCH] [FIX] payment: allow duplicate acquirer per company The payment acquirer must always be linked to a single journal, one per company. It's currently an hard constraint we have on the journal. So after the duplication, both are always linked to the same journal but only one appear on it. This "fix" is improving that allowing the duplication of an existing acquirer and to be able to link them to different journals or on the same one. opw-3704407 closes odoo/odoo#154351 Signed-off-by: John Laterre (jol) --- addons/account/models/account_journal.py | 248 ++++++++++++------ .../account/models/account_payment_method.py | 15 +- addons/payment/models/account_journal.py | 30 --- .../payment/models/account_payment_method.py | 44 ++-- addons/payment/models/payment_acquirer.py | 82 ++++-- addons/payment/tests/common.py | 60 +++-- addons/payment/tests/test_payments.py | 37 ++- .../payment/views/account_journal_views.xml | 3 +- .../models/account_payment_method.py | 2 +- .../models/account_payment_method.py | 2 +- .../models/account_payment_method.py | 2 +- .../models/account_payment_method.py | 2 +- .../models/account_payment_method.py | 2 +- .../models/account_payment_method.py | 2 +- .../models/account_payment_method.py | 2 +- .../models/account_payment_method.py | 2 +- .../models/account_payment_method.py | 2 +- .../models/account_payment_method.py | 2 +- .../models/account_payment_method.py | 2 +- 19 files changed, 342 insertions(+), 199 deletions(-) diff --git a/addons/account/models/account_journal.py b/addons/account/models/account_journal.py index fe649474ac271..f0e18b6c326b7 100644 --- a/addons/account/models/account_journal.py +++ b/addons/account/models/account_journal.py @@ -198,71 +198,123 @@ def _default_invoice_reference_model(self): ('code_company_uniq', 'unique (code, company_id)', 'Journal codes must be unique per company.'), ] + def _get_journals_payment_method_information(self): + method_information = self.env['account.payment.method']._get_payment_method_information() + unique_electronic_ids = set() + electronic_names = set() + pay_methods = self.env['account.payment.method'].sudo().search([('code', 'in', list(method_information.keys()))]) + manage_acquirers = 'payment_acquirer_id' in self.env['account.payment.method.line']._fields + + # Split the payment method information per id. + method_information_mapping = {} + for pay_method in pay_methods: + code = pay_method.code + values = method_information_mapping[pay_method.id] = { + **method_information[code], + 'payment_method': pay_method, + 'company_journals': {}, + } + if values['mode'] == 'unique': + unique_electronic_ids.add(pay_method.id) + elif manage_acquirers and values['mode'] == 'electronic': + unique_electronic_ids.add(pay_method.id) + electronic_names.add(pay_method.code) + + # Load the acquirer to manage 'electronic' payment methods. + acquirers_per_code = {} + if manage_acquirers: + acquirers = self.env['payment.acquirer'].sudo().search([ + ('company_id', 'in', self.company_id.ids), + ('provider', 'in', tuple(electronic_names)), + ]) + for acquirer in acquirers: + acquirers_per_code.setdefault(acquirer.company_id.id, {}).setdefault(acquirer.provider, set()).add(acquirer.id) + + # Collect the existing unique/electronic payment method lines. + if unique_electronic_ids: + self._cr.execute( + f''' + SELECT + apm.id, + journal.company_id, + journal.id, + {'apml.payment_acquirer_id' if manage_acquirers else 'NULL'} + FROM account_payment_method_line apml + JOIN account_journal journal ON journal.id = apml.journal_id + JOIN account_payment_method apm ON apm.id = apml.payment_method_id + WHERE apm.id IN %s + ''', + [tuple(unique_electronic_ids)], + ) + for pay_method_id, company_id, journal_id, acquirer_id in self._cr.fetchall(): + values = method_information_mapping[pay_method_id] + is_electronic = manage_acquirers and values['mode'] == 'electronic' + if is_electronic: + journal_ids = values['company_journals'].setdefault(company_id, {}).setdefault(acquirer_id, set()) + else: + journal_ids = values['company_journals'].setdefault(company_id, set()) + journal_ids.add(journal_id) + return { + 'pay_methods': pay_methods, + 'manage_acquirers': manage_acquirers, + 'method_information_mapping': method_information_mapping, + 'acquirers_per_code': acquirers_per_code, + } + @api.depends('outbound_payment_method_line_ids', 'inbound_payment_method_line_ids') def _compute_available_payment_method_ids(self): """ Compute the available payment methods id by respecting the following rules: - Methods of mode 'unique' cannot be used twice on the same company - Methods of mode 'multi' cannot be used twice on the same journal + Methods of mode 'unique' cannot be used twice on the same company. + Methods of mode 'electronic' cannot be used twice on the same company for the same 'payment_acquirer_id'. + Methods of mode 'multi' can be duplicated on the same journal. """ - method_information = self.env['account.payment.method']._get_payment_method_information() - pay_methods = self.env['account.payment.method'].search([('code', 'in', list(method_information.keys()))]) - pay_method_by_code = {x.code + x.payment_type: x for x in pay_methods} - unique_pay_methods = [k for k, v in method_information.items() if v['mode'] == 'unique'] - - pay_methods_by_company = {} - pay_methods_by_journal = {} - if unique_pay_methods: - self._cr.execute(''' - SELECT - journal.id, - journal.company_id, - ARRAY_AGG(DISTINCT apm.id) - FROM account_payment_method_line apml - JOIN account_journal journal ON journal.id = apml.journal_id - JOIN account_payment_method apm ON apm.id = apml.payment_method_id - WHERE apm.code IN %s - GROUP BY - journal.id, - journal.company_id - ''', [tuple(unique_pay_methods)]) - for journal_id, company_id, payment_method_ids in self._cr.fetchall(): - pay_methods_by_company[company_id] = set(payment_method_ids) - pay_methods_by_journal[journal_id] = set(payment_method_ids) - - pay_method_ids_commands_x_journal = {j: [Command.clear()] for j in self} - for payment_type in ('inbound', 'outbound'): - for code, vals in method_information.items(): - payment_method = pay_method_by_code.get(code + payment_type) - - if not payment_method: - continue - - # Get the domain of the journals on which the current method is usable. - method_domain = payment_method._get_payment_method_domain() + results = self._get_journals_payment_method_information() + pay_methods = results['pay_methods'] + manage_acquirers = results['manage_acquirers'] + method_information_mapping = results['method_information_mapping'] + acquirers_per_code = results['acquirers_per_code'] - for journal in self.filtered_domain(method_domain): - protected_pay_method_ids = pay_methods_by_company.get(journal.company_id._origin.id, set()) \ - - pay_methods_by_journal.get(journal._origin.id, set()) + # Compute the candidates for each journal. + for journal in self: + commands = [Command.clear()] + company = journal.company_id - if payment_type == 'inbound': - lines = journal.inbound_payment_method_line_ids - else: - lines = journal.outbound_payment_method_line_ids + # Exclude the 'unique' / 'electronic' values that are already set on the journal. + protected_acquirer_ids = set() + protected_payment_method_ids = set() + for payment_type in ('inbound', 'outbound'): + lines = journal[f'{payment_type}_payment_method_line_ids'] + for line in lines: + if line.payment_method_id: + protected_payment_method_ids.add(line.payment_method_id.id) + if manage_acquirers and method_information_mapping[line.payment_method_id.id]['mode'] == 'electronic': + protected_acquirer_ids.add(line.payment_acquirer_id.id) - already_used = payment_method in lines.payment_method_id - is_protected = payment_method.id in protected_pay_method_ids - if vals['mode'] == 'unique' and (already_used or is_protected): - continue + for pay_method in pay_methods: + values = method_information_mapping[pay_method.id] - # Only the manual payment method can be used multiple time on a single journal. - if payment_method.code != "manual" and already_used: - continue - - pay_method_ids_commands_x_journal[journal].append(Command.link(payment_method.id)) + # Get the domain of the journals on which the current method is usable. + method_domain = pay_method._get_payment_method_domain() + if not journal.filtered_domain(method_domain): + continue - for journal, pay_method_ids_commands in pay_method_ids_commands_x_journal.items(): - journal.available_payment_method_ids = pay_method_ids_commands + if values['mode'] == 'unique': + # 'unique' are linked to a single journal per company. + already_linked_journal_ids = values['company_journals'].get(company.id, set()) - {journal._origin.id} + if not already_linked_journal_ids and pay_method.id not in protected_payment_method_ids: + commands.append(Command.link(pay_method.id)) + elif manage_acquirers and values['mode'] == 'electronic': + # 'electronic' are linked to a single journal per company per acquirer. + for acquirer_id in acquirers_per_code.get(company.id, {}).get(pay_method.code, set()): + already_linked_journal_ids = values['company_journals'].get(company.id, {}).get(acquirer_id, set()) - {journal._origin.id} + if not already_linked_journal_ids and acquirer_id not in protected_acquirer_ids: + commands.append(Command.link(pay_method.id)) + elif values['mode'] == 'multi': + # 'multi' are unlimited. + commands.append(Command.link(pay_method.id)) + + journal.available_payment_method_ids = commands @api.depends('type') def _compute_default_account_type(self): @@ -409,35 +461,69 @@ def _check_payment_method_line_ids_multiplicity(self): """ Check and ensure that the payment method lines multiplicity is respected. """ - method_info = self.env['account.payment.method']._get_payment_method_information() - unique_codes = tuple(code for code, info in method_info.items() if info.get('mode') == 'unique') - - if not unique_codes: - return - self.flush(['inbound_payment_method_line_ids', 'outbound_payment_method_line_ids', 'company_id']) - self.env['account.payment.method.line'].flush(['payment_method_id', 'journal_id']) + self.env['account.payment.method.line'].flush(['payment_method_id', 'journal_id', 'name']) self.env['account.payment.method'].flush(['code']) - if unique_codes: - self._cr.execute(''' - SELECT apm.id - FROM account_payment_method apm - JOIN account_payment_method_line apml on apm.id = apml.payment_method_id - JOIN account_journal journal on journal.id = apml.journal_id - JOIN res_company company on journal.company_id = company.id - WHERE apm.code in %s - GROUP BY - company.id, - apm.id - HAVING array_length(array_agg(journal.id), 1) > 1; - ''', [unique_codes]) - - method_ids = [res[0] for res in self._cr.fetchall()] - if method_ids: - methods = self.env['account.payment.method'].browse(method_ids) - raise ValidationError(_("Some payment methods supposed to be unique already exists somewhere else.\n" - "(%s)", ', '.join([method.display_name for method in methods]))) + results = self._get_journals_payment_method_information() + pay_methods = results['pay_methods'] + manage_acquirers = results['manage_acquirers'] + method_information_mapping = results['method_information_mapping'] + acquirers_per_code = results['acquirers_per_code'] + + failing_unicity_payment_methods = self.env['account.payment.method'] + for journal in self: + company = journal.company_id + + # Exclude the 'unique' / 'electronic' values that are already set on the journal. + protected_acquirer_ids = set() + protected_payment_method_ids = set() + for payment_type in ('inbound', 'outbound'): + lines = journal[f'{payment_type}_payment_method_line_ids'] + + # Ensure you don't have the same payment_method/name combination twice on the same journal. + counter = {} + for line in lines: + if method_information_mapping[line.payment_method_id.id]['mode'] not in ('electronic', 'unique'): + continue + + key = line.payment_method_id.id, line.name + counter.setdefault(key, 0) + counter[key] += 1 + if counter[key] > 1: + raise ValidationError(_( + "You can't have two payment method lines of the same payment type (%s) " + "and with the same name (%s) on a single journal.", + payment_type, + line.name, + )) + + for line in lines: + if line.payment_method_id.id in method_information_mapping: + protected_payment_method_ids.add(line.payment_method_id.id) + if manage_acquirers and method_information_mapping[line.payment_method_id.id]['mode'] == 'electronic': + protected_acquirer_ids.add(line.payment_acquirer_id.id) + + for pay_method in pay_methods: + values = method_information_mapping[pay_method.id] + + if values['mode'] == 'unique': + # 'unique' are linked to a single journal per company. + already_linked_journal_ids = values['company_journals'].get(company.id, set()) - {journal._origin.id} + if len(already_linked_journal_ids) > 1: + failing_unicity_payment_methods |= pay_method + elif manage_acquirers and values['mode'] == 'electronic': + # 'electronic' are linked to a single journal per company per acquirer. + for acquirer_id in acquirers_per_code.get(company.id, {}).get(pay_method.code, set()): + already_linked_journal_ids = values['company_journals'].get(company.id, {}).get(acquirer_id, set()) - {journal._origin.id} + if len(already_linked_journal_ids) > 1: + failing_unicity_payment_methods |= pay_method + + if failing_unicity_payment_methods: + raise ValidationError(_( + "Some payment methods supposed to be unique already exists somewhere else.\n(%s)", + ', '.join(failing_unicity_payment_methods.mapped('display_name')), + )) @api.constrains('active') def _check_auto_post_draft_entries(self): diff --git a/addons/account/models/account_payment_method.py b/addons/account/models/account_payment_method.py index d8f5c439427f0..6d2f1d29efdff 100644 --- a/addons/account/models/account_payment_method.py +++ b/addons/account/models/account_payment_method.py @@ -127,20 +127,7 @@ def _compute_name(self): @api.constrains('name') def _ensure_unique_name_for_journal(self): - self.flush(['name']) - self._cr.execute(''' - SELECT apml.name, apm.payment_type - FROM account_payment_method_line apml - JOIN account_payment_method apm ON apml.payment_method_id = apm.id - WHERE apml.journal_id IS NOT NULL - GROUP BY apml.name, journal_id, apm.payment_type - HAVING count(apml.id) > 1 - ''') - res = self._cr.fetchall() - if res: - (name, payment_type) = res[0] - raise UserError(_("You can't have two payment method lines of the same payment type (%s) " - "and with the same name (%s) on a single journal.", payment_type, name)) + self.journal_id._check_payment_method_line_ids_multiplicity() def unlink(self): """ diff --git a/addons/payment/models/account_journal.py b/addons/payment/models/account_journal.py index 3b6258c14cdee..1a909f868670c 100644 --- a/addons/payment/models/account_journal.py +++ b/addons/payment/models/account_journal.py @@ -13,36 +13,6 @@ def _get_available_payment_method_lines(self, payment_type): return lines.filtered(lambda l: l.payment_acquirer_state != 'disabled') - @api.depends('outbound_payment_method_line_ids', 'inbound_payment_method_line_ids') - def _compute_available_payment_method_ids(self): - super()._compute_available_payment_method_ids() - - installed_acquirers = self.env['payment.acquirer'].sudo().search([]) - method_information = self.env['account.payment.method']._get_payment_method_information() - pay_methods = self.env['account.payment.method'].search([('code', 'in', list(method_information.keys()))]) - pay_method_by_code = {x.code + x.payment_type: x for x in pay_methods} - - # On top of the basic filtering, filter to hide unavailable acquirers. - # This avoid allowing payment method lines linked to an acquirer that has no record. - for code, vals in method_information.items(): - payment_method = pay_method_by_code.get(code + 'inbound') - - if not payment_method: - continue - - for journal in self: - to_remove = [] - - available_providers = installed_acquirers.filtered( - lambda a: a.company_id == journal.company_id - ).mapped('provider') - available = payment_method.code in available_providers - - if vals['mode'] == 'unique' and not available: - to_remove.append(payment_method.id) - - journal.available_payment_method_ids = [Command.unlink(payment_method) for payment_method in to_remove] - @api.ondelete(at_uninstall=False) def _unlink_except_linked_to_payment_acquirer(self): linked_acquirers = self.env['payment.acquirer'].sudo().search([]).filtered( diff --git a/addons/payment/models/account_payment_method.py b/addons/payment/models/account_payment_method.py index 24624ded05fba..231284f1f6b2a 100644 --- a/addons/payment/models/account_payment_method.py +++ b/addons/payment/models/account_payment_method.py @@ -12,7 +12,9 @@ class AccountPaymentMethodLine(models.Model): payment_acquirer_id = fields.Many2one( comodel_name='payment.acquirer', compute='_compute_payment_acquirer_id', - store=True + store=True, + readonly=False, + domain="[('provider', '=', code)]", ) payment_acquirer_state = fields.Selection( related='payment_acquirer_id.state' @@ -20,24 +22,34 @@ class AccountPaymentMethodLine(models.Model): @api.depends('payment_method_id') def _compute_payment_acquirer_id(self): - acquirers = self.env['payment.acquirer'].sudo().search([ - ('provider', 'in', self.mapped('code')), - ('company_id', 'in', self.journal_id.company_id.ids), - ]) + results = self.journal_id._get_journals_payment_method_information() + manage_acquirers = results['manage_acquirers'] + method_information_mapping = results['method_information_mapping'] + acquirers_per_code = results['acquirers_per_code'] - # Make sure to pick the active acquirer, if any. - acquirers_map = dict() - for acquirer in acquirers: - current_value = acquirers_map.get((acquirer.provider, acquirer.company_id), False) - if current_value and current_value.state != 'disabled': - continue + for line in self: + journal = line.journal_id + company = journal.company_id + if ( + company + and line.payment_method_id + and manage_acquirers + and method_information_mapping[line.payment_method_id.id]['mode'] == 'electronic' + ): + acquirer_ids = acquirers_per_code.get(company.id, {}).get(line.code, set()) - acquirers_map[(acquirer.provider, acquirer.company_id)] = acquirer + # Exclude the 'unique' / 'electronic' values that are already set on the journal. + protected_acquirer_ids = set() + for payment_type in ('inbound', 'outbound'): + lines = journal[f'{payment_type}_payment_method_line_ids'] + for journal_line in lines: + if journal_line.payment_method_id: + if manage_acquirers and method_information_mapping[journal_line.payment_method_id.id]['mode'] == 'electronic': + protected_acquirer_ids.add(journal_line.payment_acquirer_id.id) - for line in self: - code = line.payment_method_id.code - company = line.journal_id.company_id - line.payment_acquirer_id = acquirers_map.get((code, company), False) + candidates_acquirer_ids = acquirer_ids - protected_acquirer_ids + if candidates_acquirer_ids: + line.payment_acquirer_id = list(candidates_acquirer_ids)[0] def _get_payment_method_domain(self): # OVERRIDE diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index 27cc48ea275e7..8b37f02e2d95a 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -168,39 +168,65 @@ def _compute_view_configuration_fields(self): 'show_cancel_msg': True, }) + def _ensure_payment_method_line(self, allow_create=True): + self.ensure_one() + if not self.id: + return + + pay_method_line = self.env['account.payment.method.line'].search( + [('code', '=', self.provider), ('payment_acquirer_id', '=', self.id)], + limit=1, + ) + + if not self.journal_id: + if pay_method_line: + pay_method_line.unlink() + return + + if not pay_method_line: + pay_method_line = self.env['account.payment.method.line'].search( + [ + ('journal_id.company_id', '=', self.company_id.id), + ('code', '=', self.provider), + ('payment_acquirer_id', '=', False), + ], + limit=1, + ) + if pay_method_line: + pay_method_line.payment_acquirer_id = self + pay_method_line.journal_id = self.journal_id + elif allow_create: + default_payment_method_id = self._get_default_payment_method_id() + self.env['account.payment.method.line'].create({ + 'payment_method_id': default_payment_method_id, + 'journal_id': self.journal_id.id, + 'payment_acquirer_id': self.id, + }) + + @api.depends('provider', 'state', 'company_id') def _compute_journal_id(self): for acquirer in self: - payment_method = self.env['account.payment.method.line'].search([ - ('journal_id.company_id', '=', acquirer.company_id.id), - ('code', '=', acquirer.provider) - ], limit=1) - if payment_method: - acquirer.journal_id = payment_method.journal_id - else: - acquirer.journal_id = False + pay_method_line = self.env['account.payment.method.line'].search( + [('code', '=', acquirer.provider), ('payment_acquirer_id', '=', acquirer._origin.id)], + limit=1, + ) + + if pay_method_line: + acquirer.journal_id = pay_method_line.journal_id + elif acquirer.state in ('enabled', 'test'): + acquirer.journal_id = self.env['account.journal'].search( + [ + ('company_id', '=', acquirer.company_id.id), + ('type', '=', 'bank'), + ], + limit=1, + ) + if acquirer.id: + acquirer._ensure_payment_method_line() def _inverse_journal_id(self): for acquirer in self: - payment_method_line = self.env['account.payment.method.line'].search([ - ('journal_id.company_id', '=', acquirer.company_id.id), - ('code', '=', acquirer.provider) - ], limit=1) - if acquirer.journal_id: - if not payment_method_line: - default_payment_method_id = acquirer._get_default_payment_method_id() - existing_payment_method_line = self.env['account.payment.method.line'].search([ - ('payment_method_id', '=', default_payment_method_id), - ('journal_id', '=', acquirer.journal_id.id) - ], limit=1) - if not existing_payment_method_line: - self.env['account.payment.method.line'].create({ - 'payment_method_id': default_payment_method_id, - 'journal_id': acquirer.journal_id.id, - }) - else: - payment_method_line.journal_id = acquirer.journal_id - elif payment_method_line: - payment_method_line.unlink() + acquirer._ensure_payment_method_line() def _get_default_payment_method_id(self): self.ensure_one() diff --git a/addons/payment/tests/common.py b/addons/payment/tests/common.py index d67bc5e446ae4..b0290a5510b3b 100644 --- a/addons/payment/tests/common.py +++ b/addons/payment/tests/common.py @@ -1,6 +1,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging +from contextlib import contextmanager from unittest.mock import patch from odoo.addons.account.models.account_payment_method import AccountPaymentMethod from odoo.fields import Command @@ -16,13 +17,6 @@ class PaymentCommon(PaymentTestUtils): def setUpClass(cls, chart_template_ref=None): super().setUpClass(chart_template_ref=chart_template_ref) - Method_get_payment_method_information = AccountPaymentMethod._get_payment_method_information - - def _get_payment_method_information(self): - res = Method_get_payment_method_information(self) - res['none'] = {'mode': 'multi', 'domain': [('type', '=', 'bank')]} - return res - cls.currency_euro = cls._prepare_currency('EUR') cls.currency_usd = cls._prepare_currency('USD') @@ -77,20 +71,25 @@ def _get_payment_method_information(self): 'arch': arch, }) - with patch.object(AccountPaymentMethod, '_get_payment_method_information', _get_payment_method_information): - cls.env['account.payment.method'].sudo().create({ + with cls.mocked_get_payment_method_information(cls): + cls.dummy_acquirer_method = cls.env['account.payment.method'].sudo().create({ 'name': 'Dummy method', 'code': 'none', 'payment_type': 'inbound' }) - cls.dummy_acquirer = cls.env['payment.acquirer'].create({ - 'name': "Dummy Acquirer", - 'provider': 'none', - 'state': 'test', - 'allow_tokenization': True, - 'redirect_form_view_id': redirect_form.id, - 'journal_id': cls.company_data['default_journal_bank'].id, - }) + with cls.mocked_get_default_payment_method_id(cls): + cls.dummy_acquirer = cls.env['payment.acquirer'].create({ + 'name': "Dummy Acquirer", + 'provider': 'none', + 'state': 'test', + 'allow_tokenization': True, + 'redirect_form_view_id': redirect_form.id, + 'journal_id': cls.company_data['default_journal_bank'].id, + }) + + # The bank journal has been updated. + # Trigger the constraints with the 'flush' to have them evaluated with the mocked payment method. + cls.env['account.journal'].flush() cls.acquirer = cls.dummy_acquirer cls.amount = 1111.11 @@ -122,6 +121,27 @@ def _get_payment_method_information(self): #=== Utils ===# + @contextmanager + def mocked_get_payment_method_information(self): + Method_get_payment_method_information = AccountPaymentMethod._get_payment_method_information + + def _get_payment_method_information(record): + res = Method_get_payment_method_information(record) + res['none'] = {'mode': 'electronic', 'domain': [('type', '=', 'bank')]} + return res + + with patch.object(AccountPaymentMethod, '_get_payment_method_information', _get_payment_method_information): + yield + + @contextmanager + def mocked_get_default_payment_method_id(self): + + def _get_default_payment_method_id(record): + return self.dummy_acquirer_method.id + + with patch.object(self.env.registry['payment.acquirer'], '_get_default_payment_method_id', _get_default_payment_method_id): + yield + @classmethod def _prepare_currency(cls, currency_code): currency = cls.env['res.currency'].with_context(active_test=False).search( @@ -167,6 +187,12 @@ def _prepare_acquirer(cls, provider='none', company=None, update_values=None): ('type', '=', 'bank') ], limit=1) acquirer.state = 'test' + + with cls.mocked_get_payment_method_information(cls): + # The bank journal has been updated and the payment lines accordingly. + # Trigger the constraints with the 'flush' to have them evaluated with the mocked payment method. + cls.env['account.journal'].flush() + return acquirer def create_transaction(self, flow, sudo=True, **values): diff --git a/addons/payment/tests/test_payments.py b/addons/payment/tests/test_payments.py index c341986e90c1b..ce1cb8e20b8f3 100644 --- a/addons/payment/tests/test_payments.py +++ b/addons/payment/tests/test_payments.py @@ -2,8 +2,8 @@ from unittest.mock import patch +from odoo.exceptions import ValidationError from odoo.tests import tagged - from odoo.addons.payment.tests.common import PaymentCommon @@ -141,3 +141,38 @@ def test_action_post_calls_send_payment_request_only_once(self): patched.assert_not_called() payment_with_token.action_post() patched.assert_called_once() + + def test_acquirer_journal_assignation(self): + """ Test the computation of the 'journal_id' field and so, the link with the accounting side. """ + def get_payment_method_line(acquirer): + return self.env['account.payment.method.line'].search([ + ('code', '=', acquirer.provider), + ('payment_acquirer_id', '=', acquirer.id), + ]) + + with self.mocked_get_payment_method_information(), self.mocked_get_default_payment_method_id(): + journal = self.company_data['default_journal_bank'] + acquirer = self.acquirer + self.assertRecordValues(acquirer, [{'journal_id': journal.id}]) + + # Test changing the journal. + copy_journal = journal.copy() + acquirer.journal_id = copy_journal + self.assertRecordValues(acquirer, [{'journal_id': copy_journal.id}]) + self.assertRecordValues(get_payment_method_line(acquirer), [{'journal_id': copy_journal.id}]) + + # Test duplication of the acquirer. + copy_acquirer = self.acquirer.copy() + self.assertRecordValues(copy_acquirer, [{'journal_id': False}]) + copy_acquirer.state = 'test' + self.assertRecordValues(copy_acquirer, [{'journal_id': journal.id}]) + + # We are able to have both on the same journal... + with self.assertRaises(ValidationError): + # ...but not having both with the same name. + acquirer.journal_id = journal + journal._check_payment_method_line_ids_multiplicity() + + method_line = get_payment_method_line(copy_acquirer) + method_line.name = "dummy (copy)" + acquirer.journal_id = journal diff --git a/addons/payment/views/account_journal_views.xml b/addons/payment/views/account_journal_views.xml index 6d1c3b5970faf..46479922fbafb 100644 --- a/addons/payment/views/account_journal_views.xml +++ b/addons/payment/views/account_journal_views.xml @@ -7,8 +7,9 @@ - + +