You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
333 lines
13 KiB
333 lines
13 KiB
# -*- 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()
|