Files
contract/contract_payment_auto/tests/test_account_analytic_account.py
fcayre caec4cf6f8 [FIX] contract_payment_auto: transaction create must always get a token (#167)
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.
2018-05-28 17:46:32 +02:00

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()