1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/account_avatax/tests/test_avatax.py
2024-12-10 09:04:09 +07:00

470 lines
21 KiB
Python

from collections import defaultdict
from unittest.mock import patch
from odoo import Command
from odoo.exceptions import UserError, ValidationError, RedirectWarning
from odoo.tests.common import tagged
from odoo.modules.neutralize import get_neutralization_queries
from .common import TestAccountAvataxCommon
from .mocked_refund_1_response import generate_response as generate_response_refund_1
class TestAccountAvalaraInternalCommon(TestAccountAvataxCommon):
def assertInvoice(self, invoice, test_exact_response):
self.assertEqual(
len(invoice.invoice_line_ids.tax_ids),
0,
"There should be no tax rate on the line."
)
self.assertRecordValues(invoice, [{
'amount_total': 90.0,
'amount_untaxed': 90.0,
'amount_tax': 0.0,
}])
invoice.action_post()
if test_exact_response:
self.assertRecordValues(invoice, [{
'amount_total': 96.54,
'amount_untaxed': 90.0,
'amount_tax': 6.54,
}])
avatax_mapping = {avatax_line['lineNumber']: avatax_line for avatax_line in test_exact_response['lines']}
for line in invoice.invoice_line_ids:
line_number = f'account.move.line,{line.id}'
self.assertIn(line_number, avatax_mapping)
avatax_line = avatax_mapping[line_number]
self.assertEqual(
line.price_total,
avatax_line['tax'] + avatax_line['lineAmount'],
f"Tax-included price doesn't match tax returned by Avatax for line {line.id} (product: {line.product_id.display_name})."
)
self.assertEqual(
line.price_subtotal,
avatax_line['lineAmount'],
f"Wrong Avatax amount for {line.id} (product: {line.product_id.display_name}), there is probably a mismatch between the test SO and the mocked response."
)
else:
for line in invoice.invoice_line_ids:
product_name = line.product_id.display_name
self.assertGreater(len(line.tax_ids), 0, "Line with %s did not get any taxes set." % product_name)
self.assertGreater(invoice.amount_tax, 0.0, "Invoice has a tax_amount of 0.0.")
@tagged("-at_install", "post_install")
class TestAccountAvalaraInternal(TestAccountAvalaraInternalCommon):
def test_01_odoo_invoice(self):
invoice, response = self._create_invoice_01_and_expected_response()
with self._capture_request(return_value=response):
self.assertInvoice(invoice, test_exact_response=response)
# verify transactions are uncommitted
with patch('odoo.addons.account_avatax.models.account_external_tax_mixin.AccountExternalTaxMixin._uncommit_external_taxes') as mocked_uncommit:
invoice.button_draft()
mocked_uncommit.assert_called()
def test_02_odoo_invoice(self):
invoice, response = self._create_invoice_02_and_expected_response()
with self._capture_request(return_value=response):
self.assertInvoice(invoice, test_exact_response=response)
# verify transactions are uncommitted
with patch('odoo.addons.account_avatax.models.account_external_tax_mixin.AccountExternalTaxMixin._uncommit_external_taxes') as mocked_uncommit:
invoice.button_draft()
mocked_uncommit.assert_called()
def test_01_odoo_refund(self):
invoice, response = self._create_invoice_01_and_expected_response()
with self._capture_request(return_value=response):
invoice.action_post()
move_reversal = self.env['account.move.reversal'] \
.with_context(active_model='account.move', active_ids=invoice.ids) \
.create({'journal_id': invoice.journal_id.id})
refund = self.env['account.move'].browse(move_reversal.refund_moves()['res_id'])
# Amounts should be sent as negative for refunds:
# https://developer.avalara.com/erp-integration-guide/sales-tax-badge/transactions/test-refunds/
for line in refund._get_avatax_invoice_lines():
if 'Discount' in line['description']:
self.assertGreater(line['amount'], 0)
else:
self.assertLess(line['amount'], 0)
def test_02_odoo_refund(self):
refund = self.env['account.move'].create({
'move_type': 'out_refund',
'partner_id': self.partner.id,
'fiscal_position_id': self.fp_avatax.id,
'invoice_date': '2024-01-24',
'invoice_line_ids': [
(0, 0, {
'product_id': self.product_user.id,
'tax_ids': None,
'price_unit': self.product_user.list_price,
}),
]
})
response = generate_response_refund_1(refund.invoice_line_ids)
with self._capture_request(return_value=response):
refund.button_external_tax_calculation()
self.assertEqual(
refund.invoice_line_ids[0].price_subtotal,
self.product_user.list_price,
"Subtotal shouldn't have changed on this refund"
)
self.assertEqual(
refund.invoice_line_ids[0].price_total,
abs(response['lines'][0]['tax'] + response['lines'][0]['lineAmount']),
"Total amount should match the absolute value of what Avatax returned (which is negative for refunds)"
)
def test_unlink(self):
invoice, _ = self._create_invoice_01_and_expected_response()
mock_response = {'error': {'code': 'EntityNotFoundError',
'details': [{'code': 'EntityNotFoundError',
'description': "The Document with code 'Journal Entry "
"2180' was not found.",
'faultCode': 'Client',
'helpLink': 'http://developer.avalara.com/avatax/errors/EntityNotFoundError',
'message': 'Document not found.',
'number': 4,
'severity': 'Error'}],
'message': 'Document not found.',
'target': 'HttpRequest'}}
with self._capture_request(return_value=mock_response) as capture:
invoice.unlink()
self.assertEqual(capture.val['json']['code'], 'DocVoided', 'Should have tried to void without raising on EntityNotFoundError.')
def test_journal_entry(self):
entry, _ = self._create_invoice_01_and_expected_response()
entry.move_type = 'entry'
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
entry.action_post()
self.assertIsNone(capture.val, "Journal entries should not be sent to Avatax.")
def test_vendor_bill(self):
"""We shouldn't send any requests to Avatax for vendor bills."""
vendor_bill = self.env['account.move'].create({
'move_type': 'in_invoice',
'invoice_date': '2017-01-01',
'partner_id': self.partner.id,
'invoice_line_ids': [(0, 0, {'product_id': self.product_user.id, 'price_unit': 123.0, 'tax_ids': []})],
})
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
vendor_bill.action_post()
self.assertIsNone(capture.val, "Posting a vendor bill should not send anything to Avatax.")
vendor_bill.button_draft()
self.assertIsNone(capture.val, "Resetting a vendor bill to draft should not send anything to Avatax.")
vendor_bill.unlink()
self.assertIsNone(capture.val, "Deleting a vendor bill should not send anything to Avatax.")
def test_invoice_multi_company(self):
invoice, response = self._create_invoice_01_and_expected_response()
company_2 = self.company_data_2['company']
company_2.account_fiscal_country_id = self.env.ref('base.be')
self.env.user.company_id = company_2
with self._capture_request(return_value=response):
# ensure this doesn't raise:
# odoo.exceptions.ValidationError
# This entry contains some tax from an unallowed country. Please check its fiscal position and your tax configuration.
invoice.button_external_tax_calculation()
def test_invoice_branch_company(self):
branch = self.env['res.company'].create({
'name': "Branch A",
'parent_id': self.env.company.id,
})
child_branch = self.env['res.company'].create({
'name': "Branch B",
'parent_id': branch.id,
})
self.cr.precommit.run() # load the CoA
invoice = self._create_invoice(post=False, company_id=child_branch.id)
# Avalara configuration defined on parent company
# Should not raise RedirectWarning: ('Please add your AvaTax credentials')
with self._capture_request(return_value={'lines': [], 'summary': []}):
invoice.button_external_tax_calculation()
self.env.company.avalara_api_id = False
# No avalara configuration defined in the parent tree
with self.assertRaises(RedirectWarning, msg='Please add your AvaTax credentials'):
with self._capture_request(return_value={'lines': [], 'summary': []}):
invoice.button_external_tax_calculation()
child_branch.write({
'avalara_api_id': "AVALARA_LOGIN_ID",
'avalara_api_key': "AVALARA_API_KEY",
'avalara_environment': 'sandbox',
'avalara_commit': True,
})
# Avalara configuration defined on the child branch
with self._capture_request(return_value={'lines': [], 'summary': []}):
invoice.button_external_tax_calculation()
def test_posted_invoice(self):
invoice, _ = self._create_invoice_01_and_expected_response()
with self._capture_request(return_value={'lines': [], 'summary': []}):
invoice.action_post()
with self._capture_request(return_value={'lines': [], 'summary': []}) as capture:
invoice.button_external_tax_calculation()
self.assertIsNone(capture.val, "Should not update taxes of posted invoices.")
def test_check_address_constraint(self):
invoice, _ = self._create_invoice_01_and_expected_response()
partner_no_zip = self.env["res.partner"].create({
"name": "Test no zip",
"state_id": self.env.ref("base.state_us_5").id,
"country_id": self.env.ref("base.us").id,
"zip": False,
"property_account_position_id": self.fp_avatax.id,
})
with self.assertRaises(ValidationError):
invoice.partner_id = partner_no_zip
def test_negative_quantities(self):
""" The quantity field sent to Avatax should always be positive. From the Avatax documentation:
'Quantity of items in this line. This quantity value should always be a positive value representing the quantity
of product that changed hands, even when handling returns or refunds.'
"""
line_data = defaultdict(lambda: False)
line_data["product_id"] = self.product_accounting
line_data["qty"] = -1
res = self.env['account.external.tax.mixin']._get_avatax_invoice_line(line_data)
self.assertEqual(res['quantity'], 1, 'Quantities sent to Avatax should always be positive.')
def test_multi_currency_exempted_tax(self):
""" Test an invoice in another currency having 2 taxes computed from AvaTax whose one is exempted"""
# create an invoice of 100 in a currency with a rate of 2.0
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner.id,
'fiscal_position_id': self.fp_avatax.id,
'currency_id': self.currency_data['currency'].id,
'invoice_date': '2021-01-01',
'invoice_line_ids': [
(0, 0, {
'product_id': self.product_user.id,
'tax_ids': None,
'price_unit': 100.00,
}),
]
})
# Taxes from AvaTax:
# - "CA STATE TAX" (4%)
# - "CA COUNTY TAX" (6%) [exempted]
lines = [{
'details': [{
'jurisCode': '06',
'nonTaxableAmount': 0.0,
'rate': 0.04,
'taxableAmount': 100.0,
'taxName': 'CA STATE TAX',
}, {
'jurisCode': '075',
'nonTaxableAmount': 100.0,
'rate': 0.06,
'taxableAmount': 0.0,
'taxName': 'CA COUNTY TAX',
}],
'lineAmount': 100.0,
'lineNumber': 'account.move.line,' + str(invoice.invoice_line_ids.id),
'tax': 4.0,
}]
summary = [{
'jurisCode': '06',
'nonTaxable': 0.0,
'rate': 0.04,
'tax': 4.0,
'taxCalculated': 4.0,
'taxName': 'CA STATE TAX',
'taxable': 100.0,
}, {
'country': 'US',
'jurisCode': '075',
'nonTaxable': 100.0,
'rate': 0.06,
'tax': 0.0,
'taxCalculated': 0.0,
'taxName': 'CA COUNTY TAX',
'taxable': 0.0,
}]
with self._capture_request(return_value={'lines': lines, 'summary': summary}):
invoice.action_post()
self.assertRecordValues(invoice, [{'amount_tax': 4.0, 'amount_total': 104.0, 'amount_untaxed': 100.0}])
# The tax lines should be:
# ________________________________________________________________________________
# Label | Amount in Currency | Balance | Debit | Credit
# --------------------------------------------------------------------------------
# CA STATE TAX [06] (4.0000 %) | -4.0 | -2.0 | 0.0 | 2.0
# CA COUNTY TAX [075] (6.0000 %) | 0.0 | 0.0 | 0.0 | 0.0
tax_line = invoice.line_ids.filtered(lambda l: l.tax_line_id.name == 'CA STATE TAX [06] (4.0000 %)')
self.assertRecordValues(tax_line, [{'amount_currency': -4.0, 'balance': -2.0, 'debit': 0.0, 'credit': 2.0}])
exempted_tax_line = invoice.line_ids.filtered(lambda l: l.tax_line_id.name == 'CA COUNTY TAX [075] (6.0000 %)')
self.assertRecordValues(exempted_tax_line, [{'name': 'CA COUNTY TAX [075] (6.0000 %)', 'amount_currency': 0.0, 'balance': 0.0, 'debit': 0.0, 'credit': 0.0}])
def test_invoice_multi_taxline(self):
""" Test that multiple tax lines having the same tax are not an issue for avatax computation"""
self.env['account.fiscal.position'].search([('is_avatax', '=', True)]).write({
'avatax_invoice_account_id': False,
'avatax_refund_account_id': False,
})
default_plan = self.env['account.analytic.plan'].create({'name': 'Default'})
analytic_account_a = self.env['account.analytic.account'].create({
'name': 'analytic_account_a',
'plan_id': default_plan.id,
'company_id': False,
})
analytic_account_b = self.env['account.analytic.account'].create({
'name': 'analytic_account_b',
'plan_id': default_plan.id,
'company_id': False,
})
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner.id,
'fiscal_position_id': self.fp_avatax.id,
'invoice_date': '2021-01-01',
'invoice_line_ids': [
Command.create({
'product_id': self.product_accounting.id,
'tax_ids': None,
'price_unit': 295.00,
'analytic_distribution': {
analytic_account_a.id: 100,
},
}),
Command.create({
'product_id': self.product_accounting.id,
'tax_ids': None,
'price_unit': 295.00,
'analytic_distribution': {
analytic_account_b.id: 100,
},
}),
]
})
response = {
'lines': [{'details': [{'jurisCode': '06',
'rate': 0.06,
'taxName': 'CA STATE TAX'},
{'jurisCode': '075',
'rate': 0.0025,
'taxName': 'CA COUNTY TAX'},
{'jurisCode': 'EMAK0',
'rate': 0.03,
'taxName': 'CA SPECIAL TAX'},
{'jurisCode': 'EMTV0',
'rate': 0.01,
'taxName': 'CA SPECIAL TAX'}],
'lineAmount': 295.0,
'lineNumber': 'account.move.line,' + str(line.id),
'tax': 30.24} for line in invoice.invoice_line_ids],
'summary': [{'jurisCode': '06',
'nonTaxable': 0.0,
'rate': 0.06,
'tax': 35.4,
'taxCalculated': 35.4,
'taxName': 'CA STATE TAX',
'taxable': 590.0},
{'jurisCode': '075',
'nonTaxable': 0.0,
'rate': 0.0025,
'tax': 1.48,
'taxCalculated': 1.48,
'taxName': 'CA COUNTY TAX',
'taxable': 590.0}]}
with self._capture_request(return_value=response):
# ensure this doesn't raise:
# odoo.exceptions.ValidationError: Expected singleton:
invoice.button_external_tax_calculation()
tax_lines = invoice.line_ids.filtered(lambda l: l.tax_line_id.name == 'CA STATE TAX [06] (6.0000 %)')
self.assertEqual(len(tax_lines), 2, "Multiple tax lines should have been created")
self.assertRecordValues(invoice, [{'amount_tax': 60.48, 'amount_total': 650.48, 'amount_untaxed': 590.0}])
self.assertRecordValues(tax_lines, [{'amount_currency': -17.7}, {'amount_currency': -17.7}])
@tagged("external_l10n", "external", "-at_install", "post_install", "-standard")
class TestAccountAvalaraInternalIntegration(TestAccountAvalaraInternalCommon):
def test_integration_01_odoo_invoice(self):
with self._skip_no_credentials():
invoice, _ = self._create_invoice_01_and_expected_response()
self.assertInvoice(invoice, test_exact_response=False)
invoice.button_draft()
def test_integration_02_odoo_invoice(self):
with self._skip_no_credentials():
invoice, _ = self._create_invoice_02_and_expected_response()
self.assertInvoice(invoice, test_exact_response=False)
invoice.button_draft()
@tagged("-at_install", "post_install")
class TestAccountAvalaraSalesTaxAdministration(TestAccountAvataxCommon):
"""https://developer.avalara.com/certification/avatax/sales-tax-badge/"""
@classmethod
def setUpClass(cls, chart_template_ref=None):
res = super().setUpClass(chart_template_ref)
cls.config = cls.env['res.config.settings'].create({})
return res
def test_disable_document_recording(self):
"""In order for this connector to be used in conjunction with other integrations to AvaTax,
the user must be able to control which connector is used for recording documents to AvaTax.
From a technical standpoint, simply use DocType: 'SalesOrder' on all calls
and suppress any non-getTax calls (i.e. cancelTax, postTax).
"""
self.env.company.avalara_commit = False
invoice, response = self._create_invoice_01_and_expected_response()
with self._capture_request(return_value=response) as capture:
invoice.action_post()
self.assertFalse(capture.val['json']['createTransactionModel']['commit'], 'Should not have committed.')
def test_disable_avatax(self):
"""The user must have an option to turn on or off the AvaTax Calculation service
independent of any other Avalara product or service.
"""
self.fp_avatax.is_avatax = False
with patch('odoo.addons.account_avatax.lib.avatax_client.AvataxClient.request') as mocked_request:
self._create_invoice()
mocked_request.assert_not_called()
def test_disable_avatax_neutralize(self):
"""ORM's neutralization feature works."""
self.cr.execute(next(get_neutralization_queries(['account_avatax'])))
with patch('odoo.addons.account_avatax.lib.avatax_client.AvataxClient.request') as mocked_request:
self._create_invoice()
mocked_request.assert_not_called()
def test_integration_connect_button(self):
"""Test the connection to the AvaTax service and verify the AvaTax credentials."""
with self._skip_no_credentials(), self.assertRaisesRegex(UserError, "'version'"):
self.config.avatax_ping()