mirror of
https://github.com/OCA/contract.git
synced 2025-02-13 17:57:24 +02:00
When a contrat had no payment token but the corresponding partner had one, the transaction was created without an acquirer, leading to an integrity error in postgres. This change makes sure the token used to test the ability to pay an invoice is passed along to the transaction creation call. Tests were also added to check the ability to use the contract token if present, but the partner's in the opposite case. This change fixes #165.
334 lines
13 KiB
Python
334 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2017 LasLabs Inc.
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
|
|
import mock
|
|
|
|
from contextlib import contextmanager
|
|
|
|
from odoo import fields
|
|
from odoo.tools import mute_logger
|
|
from odoo.tests import common
|
|
|
|
from ..models import account_analytic_account
|
|
|
|
|
|
@common.at_install(False)
|
|
@common.post_install(True)
|
|
class TestAccountAnalyticAccount(common.HttpCase):
|
|
def setUp(self):
|
|
super(TestAccountAnalyticAccount, self).setUp()
|
|
self.Model = self.env['account.analytic.account']
|
|
self.partner = self.env.ref('base.res_partner_2')
|
|
self.product = self.env.ref('product.product_product_2')
|
|
self.product.taxes_id += self.env['account.tax'].search(
|
|
[('type_tax_use', '=', 'sale')], limit=1)
|
|
self.product.description_sale = 'Test description sale'
|
|
self.template_vals = {
|
|
'recurring_rule_type': 'yearly',
|
|
'recurring_interval': 12345,
|
|
'name': 'Test Contract Template',
|
|
'is_auto_pay': True,
|
|
}
|
|
self.template = self.env['account.analytic.contract'].create(
|
|
self.template_vals,
|
|
)
|
|
self.acquirer = self.env['payment.acquirer'].create({
|
|
'name': 'Test Acquirer',
|
|
'provider': 'manual',
|
|
'view_template_id': self.env['ir.ui.view'].search([], limit=1).id,
|
|
})
|
|
self.payment_token = self.env['payment.token'].create({
|
|
'name': 'Test Token',
|
|
'partner_id': self.partner.id,
|
|
'active': True,
|
|
'acquirer_id': self.acquirer.id,
|
|
'acquirer_ref': 'Test',
|
|
})
|
|
self.other_payment_token = self.env['payment.token'].create({
|
|
'name': 'Test Other Token',
|
|
'partner_id': self.partner.id,
|
|
'active': True,
|
|
'acquirer_id': self.acquirer.id,
|
|
'acquirer_ref': 'OtherTest',
|
|
})
|
|
self.contract = self.Model.create({
|
|
'name': 'Test Contract',
|
|
'partner_id': self.partner.id,
|
|
'pricelist_id': self.partner.property_product_pricelist.id,
|
|
'recurring_invoices': True,
|
|
'date_start': '2016-02-15',
|
|
'recurring_next_date': fields.Datetime.now(),
|
|
'payment_token_id': self.payment_token.id,
|
|
})
|
|
self.contract_line = self.env['account.analytic.invoice.line'].create({
|
|
'analytic_account_id': self.contract.id,
|
|
'product_id': self.product.id,
|
|
'name': 'Services from #START# to #END#',
|
|
'quantity': 1,
|
|
'uom_id': self.product.uom_id.id,
|
|
'price_unit': 100,
|
|
'discount': 50,
|
|
})
|
|
|
|
def _validate_invoice(self, invoice):
|
|
self.assertEqual(len(invoice), 1)
|
|
self.assertEqual(invoice._name, 'account.invoice')
|
|
|
|
def _create_invoice(self, open=False, sent=False):
|
|
self.contract.is_auto_pay = False
|
|
invoice = self.contract._create_invoice()
|
|
if open or sent:
|
|
invoice.action_invoice_open()
|
|
if sent:
|
|
invoice.sent = True
|
|
self.contract.is_auto_pay = True
|
|
return invoice
|
|
|
|
@contextmanager
|
|
def _mock_transaction(self, state='authorized', s2s_side_effect=None):
|
|
|
|
Transactions = self.contract.env['payment.transaction']
|
|
TransactionsCreate = Transactions.create
|
|
|
|
if not callable(s2s_side_effect):
|
|
s2s_side_effect = [s2s_side_effect]
|
|
|
|
s2s = mock.MagicMock()
|
|
s2s.side_effect = s2s_side_effect
|
|
|
|
def create(vals):
|
|
record = TransactionsCreate(vals)
|
|
record.state = state
|
|
return record
|
|
|
|
model_create = mock.MagicMock()
|
|
model_create.side_effect = create
|
|
|
|
Transactions._patch_method('create', model_create)
|
|
Transactions._patch_method('s2s_do_transaction', s2s)
|
|
|
|
try:
|
|
yield
|
|
finally:
|
|
Transactions._revert_method('create')
|
|
Transactions._revert_method('s2s_do_transaction')
|
|
|
|
def test_onchange_partner_id_payment_token(self):
|
|
""" It should clear the payment token. """
|
|
self.assertTrue(self.contract.payment_token_id)
|
|
self.contract._onchange_partner_id_payment_token()
|
|
self.assertFalse(self.contract.payment_token_id)
|
|
|
|
def test_create_invoice_no_autopay(self):
|
|
""" It should return the new invoice without calling autopay. """
|
|
self.contract.is_auto_pay = False
|
|
with mock.patch.object(self.contract, '_do_auto_pay') as method:
|
|
invoice = self.contract._create_invoice()
|
|
self._validate_invoice(invoice)
|
|
method.assert_not_called()
|
|
|
|
def test_create_invoice_autopay(self):
|
|
""" It should return the new invoice after calling autopay. """
|
|
with mock.patch.object(self.contract, '_do_auto_pay') as method:
|
|
invoice = self.contract._create_invoice()
|
|
self._validate_invoice(invoice)
|
|
method.assert_called_once_with(invoice)
|
|
|
|
def test_do_auto_pay_ensure_one(self):
|
|
""" It should ensure_one on self. """
|
|
with self.assertRaises(ValueError):
|
|
self.env['account.analytic.account']._do_auto_pay(
|
|
self._create_invoice(),
|
|
)
|
|
|
|
def test_do_auto_pay_invoice_ensure_one(self):
|
|
""" It should ensure_one on the invoice. """
|
|
with self.assertRaises(ValueError):
|
|
self.contract._do_auto_pay(
|
|
self.env['account.invoice'],
|
|
)
|
|
|
|
def test_do_auto_pay_open_invoice(self):
|
|
""" It should open the invoice. """
|
|
invoice = self._create_invoice()
|
|
self.contract._do_auto_pay(invoice)
|
|
self.assertEqual(invoice.state, 'open')
|
|
|
|
def test_do_auto_pay_sends_message(self):
|
|
""" It should call the send message method with the invoice. """
|
|
with mock.patch.object(self.contract, '_send_invoice_message') as m:
|
|
invoice = self._create_invoice()
|
|
self.contract._do_auto_pay(invoice)
|
|
m.assert_called_once_with(invoice)
|
|
|
|
def test_do_auto_pay_does_pay(self):
|
|
""" It should try to pay the invoice. """
|
|
with mock.patch.object(self.contract, '_pay_invoice') as m:
|
|
invoice = self._create_invoice()
|
|
self.contract._do_auto_pay(invoice)
|
|
m.assert_called_once_with(invoice)
|
|
|
|
def test_pay_invoice_not_open(self):
|
|
""" It should return None if the invoice isn't open. """
|
|
invoice = self._create_invoice()
|
|
res = self.contract._pay_invoice(invoice)
|
|
self.assertIs(res, None)
|
|
|
|
def test_pay_invoice_no_residual(self):
|
|
""" It should return None if no residual on the invoice. """
|
|
invoice = self._create_invoice()
|
|
invoice.state = 'open'
|
|
res = self.contract._pay_invoice(invoice)
|
|
self.assertIs(res, None)
|
|
|
|
def test_pay_invoice_no_token(self):
|
|
""" It should return None if no payment token. """
|
|
self.contract.payment_token_id = False
|
|
invoice = self._create_invoice(True)
|
|
res = self.contract._pay_invoice(invoice)
|
|
self.assertIs(res, None)
|
|
|
|
def assert_successful_pay_invoice(self, expected_token=None):
|
|
with self._mock_transaction(s2s_side_effect=True):
|
|
invoice = self._create_invoice(True)
|
|
res = self.contract._pay_invoice(invoice)
|
|
self.assertTrue(res)
|
|
if expected_token is not None:
|
|
Transactions = self.contract.env['payment.transaction']
|
|
tx_vals = Transactions.create.call_args[0][0]
|
|
self.assertEqual(tx_vals.get('payment_token_id'),
|
|
expected_token.id)
|
|
|
|
def test_pay_invoice_success(self):
|
|
""" It should return True on success. """
|
|
self.assert_successful_pay_invoice()
|
|
|
|
def test_pay_invoice_with_contract_token(self):
|
|
""" When contract and partner have a token, contract's is used. """
|
|
self.partner.payment_token_id = self.other_payment_token
|
|
self.contract.payment_token_id = self.payment_token
|
|
self.assert_successful_pay_invoice(expected_token=self.payment_token)
|
|
|
|
def test_pay_invoice_with_partner_token_success(self):
|
|
""" When contract has no related token, it should use partner's. """
|
|
self.contract.payment_token_id = False
|
|
self.partner.payment_token_id = self.other_payment_token
|
|
self.assert_successful_pay_invoice(
|
|
expected_token=self.other_payment_token)
|
|
|
|
@mute_logger(account_analytic_account.__name__)
|
|
def test_pay_invoice_exception(self):
|
|
""" It should catch exceptions. """
|
|
with self._mock_transaction(s2s_side_effect=Exception):
|
|
invoice = self._create_invoice(True)
|
|
res = self.contract._pay_invoice(invoice)
|
|
self.assertIs(res, None)
|
|
|
|
def test_pay_invoice_invalid_state(self):
|
|
""" It should return None on invalid state. """
|
|
with self._mock_transaction(s2s_side_effect=True):
|
|
invoice = self._create_invoice(True)
|
|
invoice.state = 'draft'
|
|
res = self.contract._pay_invoice(invoice)
|
|
self.assertIs(res, None)
|
|
|
|
@mute_logger(account_analytic_account.__name__)
|
|
def test_pay_invoice_increments_retries(self):
|
|
""" It should increment invoice retries on failure. """
|
|
with self._mock_transaction(s2s_side_effect=False):
|
|
invoice = self._create_invoice(True)
|
|
self.assertFalse(invoice.auto_pay_attempts)
|
|
self.contract._pay_invoice(invoice)
|
|
self.assertTrue(invoice.auto_pay_attempts)
|
|
|
|
def test_pay_invoice_updates_fail_date(self):
|
|
""" It should update the invoice auto pay fail date on failure. """
|
|
with self._mock_transaction(s2s_side_effect=False):
|
|
invoice = self._create_invoice(True)
|
|
self.assertFalse(invoice.auto_pay_failed)
|
|
self.contract._pay_invoice(invoice)
|
|
self.assertTrue(invoice.auto_pay_failed)
|
|
|
|
def test_pay_invoice_too_many_attempts(self):
|
|
""" It should clear autopay after too many attempts. """
|
|
with self._mock_transaction(s2s_side_effect=False):
|
|
invoice = self._create_invoice(True)
|
|
invoice.auto_pay_attempts = self.contract.auto_pay_retries - 1
|
|
self.contract._pay_invoice(invoice)
|
|
self.assertFalse(self.contract.is_auto_pay)
|
|
self.assertFalse(self.contract.payment_token_id)
|
|
|
|
def test_pay_invoice_too_many_attempts_partner_token(self):
|
|
""" It should clear the partner token when attempts were on it. """
|
|
self.partner.payment_token_id = self.contract.payment_token_id
|
|
with self._mock_transaction(s2s_side_effect=False):
|
|
invoice = self._create_invoice(True)
|
|
invoice.auto_pay_attempts = self.contract.auto_pay_retries
|
|
self.contract._pay_invoice(invoice)
|
|
self.assertFalse(self.partner.payment_token_id)
|
|
|
|
def test_get_tx_vals(self):
|
|
""" It should return a dict. """
|
|
self.assertIsInstance(
|
|
self.contract._get_tx_vals(self._create_invoice(),
|
|
self.contract.payment_token_id),
|
|
dict,
|
|
)
|
|
|
|
def test_send_invoice_message_sent(self):
|
|
""" It should return None if the invoice has already been sent. """
|
|
invoice = self._create_invoice(sent=True)
|
|
res = self.contract._send_invoice_message(invoice)
|
|
self.assertIs(res, None)
|
|
|
|
def test_send_invoice_message_no_template(self):
|
|
""" It should return None if the invoice isn't sent. """
|
|
invoice = self._create_invoice(True)
|
|
self.contract.invoice_mail_template_id = False
|
|
res = self.contract._send_invoice_message(invoice)
|
|
self.assertIs(res, None)
|
|
|
|
def test_send_invoice_message_sets_invoice_state(self):
|
|
""" It should set the invoice to sent. """
|
|
invoice = self._create_invoice(True)
|
|
self.assertFalse(invoice.sent)
|
|
self.contract._send_invoice_message(invoice)
|
|
self.assertTrue(invoice.sent)
|
|
|
|
def test_send_invoice_message_returns_mail(self):
|
|
""" It should create and return the message. """
|
|
invoice = self._create_invoice(True)
|
|
res = self.contract._send_invoice_message(invoice)
|
|
self.assertEqual(res._name, 'mail.mail')
|
|
|
|
def test_cron_retry_auto_pay_needed(self):
|
|
""" It should auto-pay the correct invoice if needed. """
|
|
invoice = self._create_invoice(True)
|
|
invoice.write({
|
|
'auto_pay_attempts': 1,
|
|
'auto_pay_failed': '2015-01-01 00:00:00',
|
|
})
|
|
meth = mock.MagicMock()
|
|
self.contract._patch_method('_do_auto_pay', meth)
|
|
try:
|
|
self.contract.cron_retry_auto_pay()
|
|
finally:
|
|
self.contract._revert_method('_do_auto_pay')
|
|
meth.assert_called_once_with(invoice)
|
|
|
|
def test_cron_retry_auto_pay_skip(self):
|
|
""" It should skip invoices that don't need to be paid. """
|
|
invoice = self._create_invoice(True)
|
|
invoice.write({
|
|
'auto_pay_attempts': 1,
|
|
'auto_pay_failed': fields.Datetime.now(),
|
|
})
|
|
meth = mock.MagicMock()
|
|
self.contract._patch_method('_do_auto_pay', meth)
|
|
try:
|
|
self.contract.cron_retry_auto_pay()
|
|
finally:
|
|
self.contract._revert_method('_do_auto_pay')
|
|
meth.assert_not_called()
|