Initial commit: Accounting packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:47 +02:00
commit 4ef34c2317
2661 changed files with 1709616 additions and 0 deletions

View file

@ -0,0 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_account_payment
from . import test_payment_flows

View file

@ -0,0 +1,94 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from unittest.mock import patch
from contextlib import contextmanager
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
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.
super().setUpClass()
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.invoice = cls.env['account.move'].create({
'move_type': 'entry',
'date': '2019-01-01',
'currency_id': cls.currency_euro.id,
'partner_id': cls.partner.id,
'line_ids': [
(0, 0, {
'account_id': cls.account.id,
'debit': 100.0,
'credit': 0.0,
'amount_currency': 200.0,
}),
(0, 0, {
'account_id': cls.account.id,
'debit': 0.0,
'credit': 100.0,
'amount_currency': -200.0,
}),
],
})
def setUp(self):
self.enable_reconcile_after_done_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

View file

@ -0,0 +1,205 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
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
@tagged('-at_install', 'post_install')
class TestAccountPayment(AccountPaymentCommon):
def test_no_amount_available_for_refund_when_not_supported(self):
self.provider.support_refund = False
tx = self._create_transaction('redirect', state='done')
tx._reconcile_after_done() # Create the payment
self.assertEqual(
tx.payment_id.amount_available_for_refund,
0,
msg="The value of `amount_available_for_refund` should be 0 when the provider doesn't "
"support refunds."
)
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
self.assertAlmostEqual(
tx.payment_id.amount_available_for_refund,
tx.amount,
places=2,
msg="The value of `amount_available_for_refund` should be that of `total` when there "
"are no linked refunds."
)
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
})
tx = self._create_transaction('redirect', state='done')
tx._reconcile_after_done() # Create the payment
for reference_index, state in enumerate(('draft', 'pending', 'authorized')):
self._create_transaction(
'dummy',
amount=-tx.amount,
reference=f'R-{tx.reference}-{reference_index + 1}',
state=state,
operation='refund', # Override the computed flow
source_transaction_id=tx.id,
)
self.assertAlmostEqual(
tx.payment_id.amount_available_for_refund,
tx.payment_id.amount,
places=2,
msg="The value of `amount_available_for_refund` should be that of `total` when all the "
"linked refunds are pending (not in the state 'done')."
)
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
self._create_transaction(
'dummy',
amount=-tx.amount,
reference=f'R-{tx.reference}',
state='done',
operation='refund', # Override the computed flow
source_transaction_id=tx.id,
)._reconcile_after_done()
self.assertEqual(
tx.payment_id.amount_available_for_refund,
0,
msg="The value of `amount_available_for_refund` should be 0 when there is a linked "
"refund of the full amount that is confirmed (state 'done')."
)
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
self._create_transaction(
'dummy',
amount=-(tx.amount / 10),
reference=f'R-{tx.reference}',
state='done',
operation='refund', # Override the computed flow
source_transaction_id=tx.id,
)._reconcile_after_done()
self.assertAlmostEqual(
tx.payment_id.amount_available_for_refund,
tx.payment_id.amount - (tx.amount / 10),
places=2,
msg="The value of `amount_available_for_refund` should be equal to the total amount "
"minus the sum of the absolute amount of the refunds that are confirmed (state "
"'done')."
)
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
for reference_index, operation in enumerate(
('online_redirect', 'online_direct', 'online_token', 'validation', 'refund')
):
self._create_transaction(
'dummy',
reference=f'R-{tx.reference}-{reference_index + 1}',
state='done',
operation=operation, # Override the computed flow
source_transaction_id=tx.id,
)._reconcile_after_done()
self.assertEqual(
tx.payment_id.refunds_count,
1,
msg="The refunds count should only consider transactions with operation 'refund'."
)
def test_action_post_calls_send_payment_request_only_once(self):
payment_token = self._create_token()
payment_without_token = self.env['account.payment'].create({
'payment_type': 'inbound',
'partner_type': 'customer',
'amount': 2000.0,
'date': '2019-01-01',
'currency_id': self.currency.id,
'partner_id': self.partner.id,
'journal_id': self.provider.journal_id.id,
'payment_method_line_id': self.inbound_payment_method_line.id,
})
payment_with_token = payment_without_token.copy()
payment_with_token.payment_token_id = payment_token.id
with patch(
'odoo.addons.payment.models.payment_transaction.PaymentTransaction'
'._send_payment_request'
) as patched:
payment_without_token.action_post()
patched.assert_not_called()
payment_with_token.action_post()
patched.assert_called_once()
def test_no_payment_for_validations(self):
tx = self._create_transaction(flow='dummy', operation='validation') # Overwrite the flow
tx._reconcile_after_done()
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_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.
"""
self.assertEqual(self.dummy_provider.state, 'test')
with self.assertRaises(UserError):
self.dummy_provider.journal_id.inbound_payment_method_line_ids.unlink()
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),
])
with self.mocked_get_payment_method_information(), self.mocked_get_default_payment_method_id():
journal = self.company_data['default_journal_bank']
provider = self.provider
self.assertRecordValues(provider, [{'journal_id': journal.id}])
# Test changing the journal.
copy_journal = journal.copy()
payment_method_line = get_payment_method_line(provider)
provider.journal_id = copy_journal
self.assertRecordValues(provider, [{'journal_id': copy_journal.id}])
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
copy_provider = self.provider.copy()
self.assertRecordValues(copy_provider, [{'journal_id': False}])
copy_provider.state = 'test'
self.assertRecordValues(copy_provider, [{'journal_id': journal.id}])
self.assertRecordValues(get_payment_method_line(copy_provider), [{
'journal_id': journal.id,
'payment_account_id': payment_method_line.payment_account_id.id,
}])
# We are able to have both on the same journal...
with self.assertRaises(ValidationError):
# ...but not having both with the same name.
provider.journal_id = journal
method_line = get_payment_method_line(copy_provider)
method_line.name = "dummy (copy)"
provider.journal_id = journal
# You can't have twice the same acquirer on the same journal.
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})]

View file

@ -0,0 +1,47 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.addons.payment.tests.http_common import PaymentHttpCommon
from odoo.addons.account_payment.tests.common import AccountPaymentCommon
@tagged('post_install', '-at_install')
class TestFlows(AccountPaymentCommon, PaymentHttpCommon):
def test_invoice_payment_flow(self):
"""Test the payment of an invoice through the payment/pay route"""
# 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)
# 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({
'flow': 'direct',
'payment_option_id': self.provider.id,
'tokenization_requested': False,
})
with mute_logger('odoo.addons.payment.models.payment_transaction'):
processing_values = self._get_processing_values(**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)