mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-23 11:22:01 +02:00
19.0 vanilla
This commit is contained in:
parent
ba20ce7443
commit
768b70e05e
2357 changed files with 1057103 additions and 712486 deletions
|
|
@ -2,3 +2,4 @@
|
|||
|
||||
from . import test_account_payment
|
||||
from . import test_payment_flows
|
||||
from . import test_payment_provider
|
||||
|
|
|
|||
|
|
@ -10,19 +10,14 @@ from odoo.addons.payment.tests.common import PaymentCommon
|
|||
class AccountPaymentCommon(PaymentCommon, AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, *kw):
|
||||
# chart_template_ref is dropped on purpose because not needed for account_payment tests.
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.dummy_provider_method = cls._create_dummy_payment_method_for_provider(
|
||||
provider=cls.dummy_provider,
|
||||
journal=cls.company_data['default_journal_bank'],
|
||||
)
|
||||
|
||||
with cls.mocked_get_payment_method_information(cls):
|
||||
cls.dummy_provider_method = cls.env['account.payment.method'].sudo().create({
|
||||
'name': 'Dummy method',
|
||||
'code': 'none',
|
||||
'payment_type': 'inbound'
|
||||
})
|
||||
cls.dummy_provider.journal_id = cls.company_data['default_journal_bank']
|
||||
|
||||
cls.account = cls.company.account_journal_payment_credit_account_id
|
||||
cls.account = cls.outbound_payment_method_line.payment_account_id
|
||||
cls.invoice = cls.env['account.move'].create({
|
||||
'move_type': 'entry',
|
||||
'date': '2019-01-01',
|
||||
|
|
@ -44,51 +39,8 @@ class AccountPaymentCommon(PaymentCommon, AccountTestInvoicingCommon):
|
|||
],
|
||||
})
|
||||
|
||||
cls.provider.journal_id.inbound_payment_method_line_ids.filtered(lambda l: l.payment_provider_id == cls.provider).payment_account_id = cls.inbound_payment_method_line.payment_account_id
|
||||
|
||||
def setUp(self):
|
||||
self.enable_reconcile_after_done_patcher = False
|
||||
self.enable_post_process_patcher = False
|
||||
super().setUp()
|
||||
|
||||
#=== Utils ===#
|
||||
|
||||
@contextmanager
|
||||
def mocked_get_payment_method_information(self):
|
||||
Method_get_payment_method_information = self.env['account.payment.method']._get_payment_method_information
|
||||
|
||||
def _get_payment_method_information(*args, **kwargs):
|
||||
res = Method_get_payment_method_information()
|
||||
res['none'] = {'mode': 'electronic', 'domain': [('type', '=', 'bank')]}
|
||||
return res
|
||||
|
||||
with patch.object(self.env.registry['account.payment.method'], '_get_payment_method_information', _get_payment_method_information):
|
||||
yield
|
||||
|
||||
@contextmanager
|
||||
def mocked_get_default_payment_method_id(self):
|
||||
|
||||
def _get_default_payment_method_id(*args, **kwargs):
|
||||
return self.dummy_provider_method.id
|
||||
|
||||
with patch.object(self.env.registry['payment.provider'], '_get_default_payment_method_id', _get_default_payment_method_id):
|
||||
yield
|
||||
|
||||
@classmethod
|
||||
def _prepare_provider(cls, provider_code='none', company=None, update_values=None):
|
||||
""" Override of `payment` to prepare and return the first provider matching the given
|
||||
provider and company.
|
||||
|
||||
If no provider is found in the given company, we duplicate the one from the base company.
|
||||
All other providers belonging to the same company are disabled to avoid any interferences.
|
||||
|
||||
:param str provider_code: The code of the provider to prepare.
|
||||
:param recordset company: The company of the provider to prepare, as a `res.company` record.
|
||||
:param dict update_values: The values used to update the provider.
|
||||
:return: The provider to prepare, if found.
|
||||
:rtype: recordset of `payment.provider`
|
||||
"""
|
||||
provider = super()._prepare_provider(provider_code, company, update_values)
|
||||
if not provider.journal_id:
|
||||
provider.journal_id = cls.env['account.journal'].search(
|
||||
[('company_id', '=', provider.company_id.id), ('type', '=', 'bank')],
|
||||
limit=1,
|
||||
)
|
||||
return provider
|
||||
|
|
|
|||
|
|
@ -4,17 +4,28 @@ from unittest.mock import patch
|
|||
|
||||
from odoo import Command
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.addons.account_payment.tests.common import AccountPaymentCommon
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.account_payment.tests.common import AccountPaymentCommon
|
||||
from odoo.addons.base.models.ir_qweb import QWebError
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestAccountPayment(AccountPaymentCommon):
|
||||
|
||||
def test_no_amount_available_for_refund_when_no_tx(self):
|
||||
payment = self.env['account.payment'].create({'amount': 10})
|
||||
self.assertEqual(
|
||||
payment.amount_available_for_refund,
|
||||
0,
|
||||
msg="The value of `amount_available_for_refund` should be 0 when the payment was not"
|
||||
" created by a transaction."
|
||||
)
|
||||
|
||||
def test_no_amount_available_for_refund_when_not_supported(self):
|
||||
self.provider.support_refund = False
|
||||
self.provider.support_refund = 'none'
|
||||
tx = self._create_transaction('redirect', state='done')
|
||||
tx._reconcile_after_done() # Create the payment
|
||||
tx._post_process() # Create the payment
|
||||
self.assertEqual(
|
||||
tx.payment_id.amount_available_for_refund,
|
||||
0,
|
||||
|
|
@ -25,7 +36,7 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
def test_full_amount_available_for_refund_when_not_yet_refunded(self):
|
||||
self.provider.support_refund = 'full_only' # Should simply not be False
|
||||
tx = self._create_transaction('redirect', state='done')
|
||||
tx._reconcile_after_done() # Create the payment
|
||||
tx._post_process() # Create the payment
|
||||
self.assertAlmostEqual(
|
||||
tx.payment_id.amount_available_for_refund,
|
||||
tx.amount,
|
||||
|
|
@ -37,10 +48,10 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
def test_full_amount_available_for_refund_when_refunds_are_pending(self):
|
||||
self.provider.write({
|
||||
'support_refund': 'full_only', # Should simply not be False
|
||||
'support_manual_capture': True, # To create transaction in the 'authorized' state
|
||||
'support_manual_capture': 'partial', # To create transaction in the 'authorized' state
|
||||
})
|
||||
tx = self._create_transaction('redirect', state='done')
|
||||
tx._reconcile_after_done() # Create the payment
|
||||
tx._post_process() # Create the payment
|
||||
for reference_index, state in enumerate(('draft', 'pending', 'authorized')):
|
||||
self._create_transaction(
|
||||
'dummy',
|
||||
|
|
@ -61,7 +72,7 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
def test_no_amount_available_for_refund_when_fully_refunded(self):
|
||||
self.provider.support_refund = 'full_only' # Should simply not be False
|
||||
tx = self._create_transaction('redirect', state='done')
|
||||
tx._reconcile_after_done() # Create the payment
|
||||
tx._post_process() # Create the payment
|
||||
self._create_transaction(
|
||||
'dummy',
|
||||
amount=-tx.amount,
|
||||
|
|
@ -69,7 +80,7 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
state='done',
|
||||
operation='refund', # Override the computed flow
|
||||
source_transaction_id=tx.id,
|
||||
)._reconcile_after_done()
|
||||
)._post_process()
|
||||
self.assertEqual(
|
||||
tx.payment_id.amount_available_for_refund,
|
||||
0,
|
||||
|
|
@ -80,7 +91,7 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
def test_no_full_amount_available_for_refund_when_partially_refunded(self):
|
||||
self.provider.support_refund = 'partial'
|
||||
tx = self._create_transaction('redirect', state='done')
|
||||
tx._reconcile_after_done() # Create the payment
|
||||
tx._post_process() # Create the payment
|
||||
self._create_transaction(
|
||||
'dummy',
|
||||
amount=-(tx.amount / 10),
|
||||
|
|
@ -88,7 +99,7 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
state='done',
|
||||
operation='refund', # Override the computed flow
|
||||
source_transaction_id=tx.id,
|
||||
)._reconcile_after_done()
|
||||
)._post_process()
|
||||
self.assertAlmostEqual(
|
||||
tx.payment_id.amount_available_for_refund,
|
||||
tx.payment_id.amount - (tx.amount / 10),
|
||||
|
|
@ -101,7 +112,7 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
def test_refunds_count(self):
|
||||
self.provider.support_refund = 'full_only' # Should simply not be False
|
||||
tx = self._create_transaction('redirect', state='done')
|
||||
tx._reconcile_after_done() # Create the payment
|
||||
tx._post_process() # Create the payment
|
||||
for reference_index, operation in enumerate(
|
||||
('online_redirect', 'online_direct', 'online_token', 'validation', 'refund')
|
||||
):
|
||||
|
|
@ -111,7 +122,7 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
state='done',
|
||||
operation=operation, # Override the computed flow
|
||||
source_transaction_id=tx.id,
|
||||
)._reconcile_after_done()
|
||||
)._post_process()
|
||||
|
||||
self.assertEqual(
|
||||
tx.payment_id.refunds_count,
|
||||
|
|
@ -119,6 +130,21 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
msg="The refunds count should only consider transactions with operation 'refund'."
|
||||
)
|
||||
|
||||
def test_refund_message_author_is_logged_in_user(self):
|
||||
"""Ensure that the chatter message author is the user processing the refund."""
|
||||
self.provider.support_refund = 'full_only'
|
||||
|
||||
tx = self._create_transaction('redirect', state='done')
|
||||
tx._post_process()
|
||||
|
||||
with patch.object(
|
||||
self.env.registry['account.payment'], 'message_post', autospec=True
|
||||
) as message_post_mock:
|
||||
tx.action_refund()
|
||||
author_id = message_post_mock.call_args[1].get("author_id")
|
||||
|
||||
self.assertEqual(author_id, self.user.partner_id.id)
|
||||
|
||||
def test_action_post_calls_send_payment_request_only_once(self):
|
||||
payment_token = self._create_token()
|
||||
payment_without_token = self.env['account.payment'].create({
|
||||
|
|
@ -136,7 +162,7 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
|
||||
with patch(
|
||||
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
|
||||
'._send_payment_request'
|
||||
'._charge_with_token'
|
||||
) as patched:
|
||||
payment_without_token.action_post()
|
||||
patched.assert_not_called()
|
||||
|
|
@ -145,12 +171,39 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
|
||||
def test_no_payment_for_validations(self):
|
||||
tx = self._create_transaction(flow='dummy', operation='validation') # Overwrite the flow
|
||||
tx._reconcile_after_done()
|
||||
tx._post_process()
|
||||
payment_count = self.env['account.payment'].search_count(
|
||||
[('payment_transaction_id', '=', tx.id)]
|
||||
)
|
||||
self.assertEqual(payment_count, 0, msg="validation transactions should not create payments")
|
||||
|
||||
def test_payments_for_source_tx_with_children(self):
|
||||
self.provider.support_manual_capture = 'partial'
|
||||
source_tx = self._create_transaction(flow='direct', state='authorized')
|
||||
child_tx_1 = source_tx._create_child_transaction(100)
|
||||
child_tx_1._set_done()
|
||||
child_tx_2 = source_tx._create_child_transaction(source_tx.amount - 100)
|
||||
self.assertEqual(
|
||||
source_tx.state,
|
||||
'authorized',
|
||||
msg="The source transaction should be authorized when the total processed amount of its"
|
||||
" children is not equal to the source amount.",
|
||||
)
|
||||
child_tx_2._set_canceled()
|
||||
self.assertEqual(
|
||||
source_tx.state,
|
||||
'done',
|
||||
msg="The source transaction should be done when the total processed amount of its"
|
||||
" children is equal to the source amount.",
|
||||
)
|
||||
child_tx_1._post_process()
|
||||
self.assertTrue(child_tx_1.payment_id, msg="Child transactions should create payments.")
|
||||
source_tx._post_process()
|
||||
self.assertFalse(
|
||||
source_tx.payment_id,
|
||||
msg="source transactions with done or cancel children should not create payments.",
|
||||
)
|
||||
|
||||
def test_prevent_unlink_apml_with_active_provider(self):
|
||||
""" Deleting an account.payment.method.line that is related to a provider in 'test' or 'enabled' state
|
||||
should raise an error.
|
||||
|
|
@ -162,12 +215,9 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
def test_provider_journal_assignation(self):
|
||||
""" Test the computation of the 'journal_id' field and so, the link with the accounting side. """
|
||||
def get_payment_method_line(provider):
|
||||
return self.env['account.payment.method.line'].search([
|
||||
('code', '=', provider.code),
|
||||
('payment_provider_id', '=', provider.id),
|
||||
])
|
||||
return self.env['account.payment.method.line'].search([('payment_provider_id', '=', provider.id)])
|
||||
|
||||
with self.mocked_get_payment_method_information(), self.mocked_get_default_payment_method_id():
|
||||
with self.mocked_get_payment_method_information():
|
||||
journal = self.company_data['default_journal_bank']
|
||||
provider = self.provider
|
||||
self.assertRecordValues(provider, [{'journal_id': journal.id}])
|
||||
|
|
@ -180,7 +230,7 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
self.assertRecordValues(payment_method_line, [{'journal_id': copy_journal.id}])
|
||||
|
||||
# Test duplication of the provider.
|
||||
payment_method_line.payment_account_id = self.env.company.account_journal_payment_debit_account_id
|
||||
payment_method_line.payment_account_id = self.inbound_payment_method_line.payment_account_id
|
||||
copy_provider = self.provider.copy()
|
||||
self.assertRecordValues(copy_provider, [{'journal_id': False}])
|
||||
copy_provider.state = 'test'
|
||||
|
|
@ -203,3 +253,213 @@ class TestAccountPayment(AccountPaymentCommon):
|
|||
copy_provider_pml = get_payment_method_line(copy_provider)
|
||||
with self.assertRaises(ValidationError):
|
||||
journal.inbound_payment_method_line_ids = [Command.update(copy_provider_pml.id, {'payment_provider_id': provider.id})]
|
||||
|
||||
def test_generate_payment_link_with_no_invoice_line(self):
|
||||
invoice = self.invoice
|
||||
invoice.line_ids.unlink()
|
||||
payment_values = invoice._get_default_payment_link_values()
|
||||
|
||||
self.assertDictEqual(payment_values, {
|
||||
'currency_id': invoice.currency_id.id,
|
||||
'partner_id': invoice.partner_id.id,
|
||||
'open_installments': [],
|
||||
'amount': None,
|
||||
'amount_max': None,
|
||||
})
|
||||
|
||||
def test_payment_invoice_same_receivable(self):
|
||||
"""
|
||||
Test that when creating a payment transaction, the payment uses the same account_id as the related invoice
|
||||
and not the partner accound_id
|
||||
"""
|
||||
payment_term = self.env['account.payment.term'].create({
|
||||
'name': "early_payment_term",
|
||||
'company_id': self.company_data['company'].id,
|
||||
'discount_percentage': 10,
|
||||
'discount_days': 10,
|
||||
'early_discount': True,
|
||||
})
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner.id,
|
||||
'currency_id': self.currency.id,
|
||||
'invoice_payment_term_id': payment_term.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'test line',
|
||||
'price_unit': 100.0,
|
||||
'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)],
|
||||
}),
|
||||
Command.create({
|
||||
'name': 'test line 2',
|
||||
'price_unit': 100.0,
|
||||
'tax_ids': [Command.set(self.company_data['default_tax_sale'].ids)],
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
self.partner.property_account_receivable_id = self.env['account.account'].search([('name', '=', 'Account Payable')], limit=1)
|
||||
payment = self._create_transaction(
|
||||
reference='payment_3',
|
||||
flow='direct',
|
||||
state='done',
|
||||
amount=invoice.invoice_payment_term_id._get_amount_due_after_discount(
|
||||
total_amount=invoice.amount_residual,
|
||||
untaxed_amount=invoice.amount_tax,
|
||||
),
|
||||
invoice_ids=[invoice.id],
|
||||
partner_id=self.partner.id,
|
||||
)._create_payment()
|
||||
|
||||
self.assertNotEqual(self.partner.property_account_receivable_id, payment.destination_account_id)
|
||||
self.assertEqual(payment.destination_account_id, invoice.line_ids[-1].account_id)
|
||||
|
||||
def test_vendor_payment_name_remains_same_after_repost(self):
|
||||
"""
|
||||
Test that modifying and reposting a vendor payment does not change its name, except when the journal is changed.
|
||||
"""
|
||||
journal = self.company_data['default_journal_bank']
|
||||
|
||||
payment = self.env['account.payment'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'partner_type': 'supplier',
|
||||
'payment_type': 'outbound',
|
||||
'amount': 10,
|
||||
'journal_id': journal.id,
|
||||
'payment_method_line_id': journal.inbound_payment_method_line_ids[0].id,
|
||||
})
|
||||
payment.action_post()
|
||||
|
||||
original_name = payment.move_id.name
|
||||
|
||||
payment2 = self.env['account.payment'].create({
|
||||
'partner_id': self.partner.id,
|
||||
'partner_type': 'supplier',
|
||||
'payment_type': 'outbound',
|
||||
'amount': 20,
|
||||
'journal_id': journal.id,
|
||||
'payment_method_line_id': journal.inbound_payment_method_line_ids[0].id,
|
||||
})
|
||||
|
||||
payment2.action_post()
|
||||
payment.move_id.button_draft()
|
||||
payment.move_id.line_ids.unlink()
|
||||
payment.amount = 30
|
||||
payment.move_id._compute_name()
|
||||
payment.move_id._post()
|
||||
|
||||
self.assertEqual(
|
||||
payment.move_id.name,
|
||||
original_name,
|
||||
"Payment name should remain the same after reposting"
|
||||
)
|
||||
|
||||
# Now try to change the journal, and check if the name is now updated
|
||||
payment.move_id.button_draft()
|
||||
new_journal = journal.copy()
|
||||
new_payment_method_line = new_journal.outbound_payment_method_line_ids[0]
|
||||
new_payment_method_line.write({'payment_account_id': payment.payment_method_line_id.payment_account_id.id})
|
||||
payment.write({
|
||||
'journal_id': new_journal.id,
|
||||
'payment_method_line_id': new_payment_method_line.id,
|
||||
})
|
||||
payment.move_id.action_post()
|
||||
self.assertNotEqual(
|
||||
payment.move_id.name,
|
||||
original_name,
|
||||
"Payment name should be updated after changing the journal"
|
||||
)
|
||||
|
||||
def test_post_process_does_not_fail_on_cancelled_invoice(self):
|
||||
""" If the payment state is 'pending' and the invoice gets cancelled, and later the payment is confirmed,
|
||||
ensure that the _post_process() method does not raise an error.
|
||||
"""
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'test line',
|
||||
'price_unit': 100.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
tx = self._create_transaction(
|
||||
flow='direct',
|
||||
state='pending',
|
||||
invoice_ids=[invoice.id],
|
||||
)
|
||||
invoice.button_cancel()
|
||||
tx._set_done()
|
||||
# _post_process() shouldn't raise an error even though the invoice is cancelled
|
||||
tx._post_process()
|
||||
self.assertEqual(tx.payment_id.state, 'in_process')
|
||||
|
||||
def test_payment_token_for_invoice_partner_is_available(self):
|
||||
"""Test that the payment token of the invoice partner is available"""
|
||||
Wizard = self.env['account.payment.register'].with_context(active_model='account.move')
|
||||
with self.mocked_get_payment_method_information():
|
||||
bank_journal = self.company_data['default_journal_bank']
|
||||
payment_method_line = bank_journal.inbound_payment_method_line_ids\
|
||||
.filtered(lambda line: line.payment_provider_id == self.dummy_provider)
|
||||
self.assertTrue(payment_method_line)
|
||||
|
||||
def payment_register_wizard(invoices):
|
||||
return Wizard.with_context(active_ids=invoices.ids).create({
|
||||
'payment_method_line_id': payment_method_line.id,
|
||||
})
|
||||
|
||||
child_partner, other_child = self.env['res.partner'].create([{
|
||||
'name': name,
|
||||
'is_company': False,
|
||||
'parent_id': self.partner.id,
|
||||
} for name in ("child_partner", "other_child")])
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': child_partner.id,
|
||||
'invoice_line_ids': [
|
||||
Command.create({
|
||||
'name': 'test line',
|
||||
'price_unit': 100.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
invoice.action_post()
|
||||
payment_token = self._create_token(partner_id=child_partner.id)
|
||||
wizard = payment_register_wizard(invoice)
|
||||
self.assertRecordValues(wizard, [{
|
||||
'suitable_payment_token_ids': payment_token.ids,
|
||||
'payment_token_id': payment_token.id,
|
||||
}])
|
||||
|
||||
# Check that tokens assigned to the specific partner as well as their
|
||||
# commercial partner can be selected.
|
||||
parent_token = self._create_token(partner_id=self.partner.id)
|
||||
wizard = payment_register_wizard(invoice)
|
||||
self.assertEqual(wizard.suitable_payment_token_ids, payment_token + parent_token)
|
||||
|
||||
# Check that payments for multiple invoices with multiple partners
|
||||
# only retrieve tokens assigned to a common commercial partner.
|
||||
other_invoice = invoice.copy({'partner_id': other_child.id})
|
||||
other_invoice.action_post()
|
||||
wizard = payment_register_wizard(invoice + other_invoice)
|
||||
self.assertEqual(wizard.suitable_payment_token_ids, parent_token)
|
||||
|
||||
def test_generate_and_send_invoice_with_qr_code(self):
|
||||
"""Test generating & sending invoices with QR codes enabled."""
|
||||
self.env.company.link_qr_code = True
|
||||
move = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner.id,
|
||||
'invoice_line_ids': [Command.create({'name': "$100", 'price_unit': 100.0})],
|
||||
})
|
||||
move.action_post()
|
||||
|
||||
with patch.object(
|
||||
move.__class__, '_generate_portal_payment_qr', wraps=move._generate_portal_payment_qr,
|
||||
) as payment_qr_mock:
|
||||
self._assert_does_not_raise(
|
||||
QWebError,
|
||||
self.env['account.move.send']._generate_and_send_invoices(move),
|
||||
)
|
||||
self.assertTrue(payment_qr_mock.called)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import Command, fields
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.tests import tagged, JsonRpcException
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
|
||||
from odoo.addons.account_payment.controllers.payment import PaymentPortal
|
||||
from odoo.addons.account_payment.controllers.portal import PortalAccount
|
||||
from odoo.addons.account_payment.tests.common import AccountPaymentCommon
|
||||
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
|
||||
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
|
|
@ -16,32 +24,240 @@ class TestFlows(AccountPaymentCommon, PaymentHttpCommon):
|
|||
# Pay for this invoice (no impact even if amounts do not match)
|
||||
route_values = self._prepare_pay_values()
|
||||
route_values['invoice_id'] = self.invoice.id
|
||||
tx_context = self._get_tx_checkout_context(**route_values)
|
||||
self.assertEqual(tx_context['invoice_id'], self.invoice.id)
|
||||
tx_context = self._get_portal_pay_context(**route_values)
|
||||
|
||||
# payment/transaction
|
||||
route_values = {
|
||||
k: tx_context[k]
|
||||
for k in [
|
||||
'amount',
|
||||
'currency_id',
|
||||
'reference_prefix',
|
||||
'partner_id',
|
||||
'access_token',
|
||||
'landing_route',
|
||||
'invoice_id',
|
||||
]
|
||||
}
|
||||
route_values.update({
|
||||
# /invoice/transaction/<id>
|
||||
tx_route_values = {
|
||||
'provider_id': self.provider.id,
|
||||
'payment_method_id': self.payment_method_id,
|
||||
'token_id': None,
|
||||
'amount': tx_context['amount'],
|
||||
'flow': 'direct',
|
||||
'payment_option_id': self.provider.id,
|
||||
'tokenization_requested': False,
|
||||
})
|
||||
'landing_route': tx_context['landing_route'],
|
||||
'access_token': tx_context['access_token'],
|
||||
}
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = self._get_processing_values(**route_values)
|
||||
processing_values = self._get_processing_values(
|
||||
tx_route=tx_context['transaction_route'], **tx_route_values
|
||||
)
|
||||
tx_sudo = self._get_tx(processing_values['reference'])
|
||||
# Note: strangely, the check
|
||||
# self.assertEqual(tx_sudo.invoice_ids, invoice)
|
||||
# doesn't work, and cache invalidation doesn't work either.
|
||||
self.invoice.invalidate_recordset(['transaction_ids'])
|
||||
self.assertEqual(self.invoice.transaction_ids, tx_sudo)
|
||||
|
||||
def test_check_portal_access_token_before_rerouting_flow(self):
|
||||
""" Test that access to the provided invoice is checked against the portal access token
|
||||
before rerouting the payment flow. """
|
||||
payment_portal_controller = PaymentPortal()
|
||||
|
||||
with patch.object(CustomerPortal, '_document_check_access') as mock:
|
||||
payment_portal_controller._get_extra_payment_form_values()
|
||||
self.assertEqual(
|
||||
mock.call_count, 0, msg="No check should be made when invoice_id is not provided."
|
||||
)
|
||||
|
||||
mock.reset_mock()
|
||||
|
||||
payment_portal_controller._get_extra_payment_form_values(
|
||||
invoice_id=self.invoice.id, access_token='whatever'
|
||||
)
|
||||
self.assertEqual(
|
||||
mock.call_count, 1, msg="The check should be made as invoice_id is provided."
|
||||
)
|
||||
|
||||
def test_check_payment_access_token_before_rerouting_flow(self):
|
||||
""" Test that access to the provided invoice is checked against the payment access token
|
||||
before rerouting the payment flow. """
|
||||
payment_portal_controller = PaymentPortal()
|
||||
|
||||
def _document_check_access_mock(*_args, **_kwargs):
|
||||
raise AccessError('')
|
||||
|
||||
with patch.object(
|
||||
CustomerPortal, '_document_check_access', _document_check_access_mock
|
||||
), patch('odoo.addons.payment.utils.check_access_token') as check_payment_access_token_mock:
|
||||
try:
|
||||
payment_portal_controller._get_extra_payment_form_values(
|
||||
invoice_id=self.invoice.id, access_token='whatever'
|
||||
)
|
||||
except Exception:
|
||||
pass # We don't care if it runs or not; we only count the calls.
|
||||
self.assertEqual(
|
||||
check_payment_access_token_mock.call_count,
|
||||
1,
|
||||
msg="The access token should be checked again as a payment access token if the"
|
||||
" check as a portal access token failed.",
|
||||
)
|
||||
|
||||
@mute_logger('odoo.http')
|
||||
def test_transaction_route_rejects_unexpected_kwarg(self):
|
||||
url = self._build_url(f'/invoice/transaction/{self.invoice.id}/')
|
||||
route_kwargs = {
|
||||
'access_token': self.invoice._portal_ensure_token(),
|
||||
'partner_id': self.partner.id, # This should be rejected.
|
||||
}
|
||||
with self.assertRaises(JsonRpcException, msg='odoo.exceptions.ValidationError'):
|
||||
self.make_jsonrpc_request(url, route_kwargs)
|
||||
|
||||
def test_public_user_new_company(self):
|
||||
""" Test that the payment of an invoice is correctly processed when
|
||||
using public user with a new company. """
|
||||
self.amount = 1000.0
|
||||
|
||||
invoice = self.init_invoice(
|
||||
"out_invoice", self.partner, amounts=[self.amount], currency=self.currency,
|
||||
)
|
||||
invoice.action_post()
|
||||
self.assertEqual(invoice.payment_state, 'not_paid')
|
||||
|
||||
route_values = self._prepare_pay_values()
|
||||
route_values['invoice_id'] = invoice.id
|
||||
tx_context = self._get_portal_pay_context(**route_values)
|
||||
|
||||
tx_route_values = {
|
||||
'provider_id': self.provider.id,
|
||||
'payment_method_id': self.payment_method_id,
|
||||
'token_id': None,
|
||||
'amount': tx_context['amount'],
|
||||
'flow': 'direct',
|
||||
'tokenization_requested': False,
|
||||
'landing_route': tx_context['landing_route'],
|
||||
'access_token': tx_context['access_token'],
|
||||
}
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = self._get_processing_values(
|
||||
tx_route=tx_context['transaction_route'], **tx_route_values
|
||||
)
|
||||
tx_sudo = self._get_tx(processing_values['reference'])
|
||||
tx_sudo._set_done()
|
||||
|
||||
url = self._build_url('/payment/status/poll')
|
||||
resp = self.make_jsonrpc_request(url, {})
|
||||
self.assertTrue(tx_sudo.is_post_processed)
|
||||
|
||||
self.assertEqual(resp['state'], 'done')
|
||||
self.assertTrue(invoice.payment_state in ('in_payment', 'paid'))
|
||||
|
||||
def test_invoice_overdue_payment_flow(self):
|
||||
"""
|
||||
Test the overdue payment of an invoice is correctly processed
|
||||
with the invoice amount
|
||||
"""
|
||||
# Create an user and partner to create invoice and to authenticate while making an http request
|
||||
partner = self.env['res.partner'].create({'name': 'Alsh'})
|
||||
account_user = self.env['res.users'].create({
|
||||
'login': 'TestUser',
|
||||
'password': 'Odoo@123',
|
||||
'group_ids': [Command.set(self.env.ref('account.group_account_manager').ids)],
|
||||
'partner_id': partner.id
|
||||
})
|
||||
# Create an invoice with invoice due date must be in past with payment status to be not paid
|
||||
invoice = self.init_invoice(
|
||||
"out_invoice", partner, amounts=[1000.0], currency=self.currency,
|
||||
)
|
||||
invoice.write({
|
||||
'invoice_date_due':invoice.invoice_date - timedelta(days=10)
|
||||
})
|
||||
invoice.action_post()
|
||||
self.assertEqual(invoice.payment_state, 'not_paid')
|
||||
|
||||
# Must be authenticated before making an http resqest
|
||||
self.authenticate('TestUser', 'Odoo@123')
|
||||
overdue_url = self._build_url('/my/invoices/overdue')
|
||||
resp = self._make_http_get_request(overdue_url, {})
|
||||
|
||||
# Validate the response status code
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
tx_context = self._get_payment_context(resp)
|
||||
|
||||
# Validate the transaction context amount and payment_reference
|
||||
self.assertEqual(tx_context.get('amount'), invoice.amount_total)
|
||||
self.assertEqual(tx_context['payment_reference'], invoice.payment_reference)
|
||||
|
||||
# Prepare the transaction route values
|
||||
tx_route_values = {
|
||||
'provider_id': self.provider.id,
|
||||
'payment_method_id': self.payment_method_id,
|
||||
'token_id': None,
|
||||
'amount': tx_context.get('amount'),
|
||||
'flow': 'direct',
|
||||
'tokenization_requested': False,
|
||||
'landing_route': tx_context['landing_route'],
|
||||
'payment_reference': tx_context['payment_reference'],
|
||||
}
|
||||
with mute_logger('odoo.addons.payment.models.payment_transaction'):
|
||||
processing_values = self._get_processing_values(
|
||||
tx_route=tx_context['transaction_route'], **tx_route_values
|
||||
)
|
||||
tx_sudo = self._get_tx(processing_values['reference'])
|
||||
tx_sudo._set_done()
|
||||
|
||||
# Validate the transaction amount is equal to the invoice amount
|
||||
self.assertEqual(tx_sudo.amount, invoice.amount_total)
|
||||
|
||||
url = self._build_url('/payment/status/poll')
|
||||
resp = self.make_jsonrpc_request(url, {})
|
||||
self.assertTrue(tx_sudo.is_post_processed)
|
||||
|
||||
self.assertEqual(resp['state'], 'done')
|
||||
self.assertTrue(invoice.payment_state == invoice._get_invoice_in_payment_state())
|
||||
|
||||
def test_out_invoice_get_page_view_values(self):
|
||||
"""Test the invoice-specific portal page view values of an out invoice"""
|
||||
invoice = self.init_invoice(
|
||||
'out_invoice', partner=self.partner, amounts=[50.0], currency=self.currency,
|
||||
)
|
||||
|
||||
def mock_get_page_view_values(self, document, access_token, values, *args, **kwargs):
|
||||
return values
|
||||
|
||||
with patch.object(PortalAccount, '_get_page_view_values', mock_get_page_view_values):
|
||||
values = PortalAccount()._invoice_get_page_view_values(
|
||||
invoice, invoice.access_token, amount=26.0, payment=True,
|
||||
)
|
||||
|
||||
self.assertEqual(values['page_name'], 'invoice')
|
||||
self.assertEqual(values['invoice'], invoice)
|
||||
self.assertEqual(values['amount_paid'], 0.0)
|
||||
self.assertEqual(values['amount_due'], 50.0)
|
||||
self.assertEqual(values['next_amount_to_pay'], 26.0)
|
||||
self.assertEqual(values['payment_state'], 'not_paid')
|
||||
self.assertTrue(values['payment'])
|
||||
|
||||
def test_payment_link_wizard_defaults_from_invoice(self):
|
||||
"""
|
||||
Test that the payment link wizard opened from the QR code
|
||||
correctly uses default values from the invoice.
|
||||
"""
|
||||
payment_term = self.env['account.payment.term'].create({
|
||||
'name': '30% now, rest in 60 days',
|
||||
'line_ids': [
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 30.00,
|
||||
'delay_type': 'days_after',
|
||||
'nb_days': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'value': 'percent',
|
||||
'value_amount': 70.00,
|
||||
'delay_type': 'days_after',
|
||||
'nb_days': 60,
|
||||
}),
|
||||
],
|
||||
})
|
||||
invoice_date = fields.Date.today() - timedelta(days=1)
|
||||
invoice = self.init_invoice(
|
||||
'out_invoice', partner=self.partner, invoice_date=invoice_date, amounts=[1000.0]
|
||||
)
|
||||
invoice.invoice_payment_term_id = payment_term
|
||||
invoice.action_post()
|
||||
|
||||
# Simulate the opening of the payment link wizard from the invoice QR code.
|
||||
link = invoice._get_portal_payment_link()
|
||||
self.assertIsNotNone(link, "A payment link should be generated for the invoice.")
|
||||
self.assertIn('amount=300.0', link) # 30% of 1000 first installment
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.account_payment.tests.common import AccountPaymentCommon
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestPaymentProvider(AccountPaymentCommon):
|
||||
|
||||
def test_duplicate_provider_child_company_no_journal_id(self):
|
||||
"""
|
||||
When you duplicate a payment provider from a parent company and set it to a child company,
|
||||
if you don't set the journal (only possible if the provider is disabled), it should not raise an error when trying to reopen it.
|
||||
We want the journal to be set only if the company has a Bank journal defined in it.
|
||||
"""
|
||||
child_company = self.env['res.company'].create({
|
||||
'name': 'Child Company',
|
||||
'parent_id': self.env.company.id,
|
||||
})
|
||||
with self.mocked_get_payment_method_information():
|
||||
provider_duplicated = self.dummy_provider.copy(default={
|
||||
'name': 'Duplicated Provider',
|
||||
'company_id': child_company.id,
|
||||
'state': 'test',
|
||||
})
|
||||
self.assertFalse(provider_duplicated.journal_id)
|
||||
|
||||
bank_journal = self.env['account.journal'].create({
|
||||
'name': 'Bank Journal',
|
||||
'type': 'bank',
|
||||
'company_id': child_company.id,
|
||||
})
|
||||
provider_duplicated.invalidate_recordset(fnames=['journal_id'])
|
||||
self.assertEqual(provider_duplicated.journal_id, bank_journal)
|
||||
Loading…
Add table
Add a link
Reference in a new issue