From d0abfa2f4f4c4547562391876427ff367de34fe0 Mon Sep 17 00:00:00 2001 From: Christopher Ormaza Date: Fri, 27 Sep 2024 11:58:45 -0500 Subject: [PATCH] [IMP] added mockup of avatax anwser --- account_avatax_oca/models/account_move.py | 44 ++- account_avatax_oca/tests/mock_avatax.py | 367 ++++++++++++++++++++++ account_avatax_oca/tests/test_avatax.py | 145 ++++++++- 3 files changed, 538 insertions(+), 18 deletions(-) create mode 100644 account_avatax_oca/tests/mock_avatax.py diff --git a/account_avatax_oca/models/account_move.py b/account_avatax_oca/models/account_move.py index 7e03a5b84..920969c04 100644 --- a/account_avatax_oca/models/account_move.py +++ b/account_avatax_oca/models/account_move.py @@ -2,7 +2,7 @@ from odoo import _, api, fields, models from odoo.exceptions import UserError -from odoo.tests.common import Form +from odoo.tools import float_compare _logger = logging.getLogger(__name__) @@ -236,9 +236,8 @@ def _avatax_compute_tax(self, commit=False): if self.state == "draft": Tax = self.env["account.tax"] tax_result_lines = {int(x["lineNumber"]): x for x in tax_result["lines"]} - taxes_to_set = [] - lines = self.invoice_line_ids - for index, line in enumerate(lines): + taxes_to_set = {} + for line in self.invoice_line_ids: tax_result_line = tax_result_lines.get(line.id) if tax_result_line: # rate = tax_result_line.get("rate", 0.0) @@ -252,29 +251,40 @@ def _avatax_compute_tax(self, commit=False): tax = Tax.get_avalara_tax(rate, doc_type) if tax and tax not in line.tax_ids: line_taxes = line.tax_ids.filtered(lambda x: not x.is_avatax) - taxes_to_set.append((index, line_taxes | tax)) + taxes_to_set[line.id] = line_taxes | tax line.avatax_amt_line = tax_result_line["tax"] self.with_context(check_move_validity=False).avatax_amount = tax_result[ "totalTax" ] container = {"records": self} - self.with_context( - avatax_invoice=self, check_move_validity=False - )._sync_dynamic_lines(container) - self.line_ids.mapped("move_id")._check_balanced(container) # Set Taxes on lines in a way that properly triggers onchanges # This same approach is also used by the official account_taxcloud connector - with Form(self) as move_form: - for index, taxes in taxes_to_set: - with move_form.invoice_line_ids.edit(index) as line_form: - line_form.tax_ids.clear() - for tax in taxes: - line_form.tax_ids.add(tax) - self = move_form.save() + with self.with_context( + avatax_invoice=self, check_move_validity=False + )._sync_dynamic_lines(container), self.line_ids.mapped( + "move_id" + )._check_balanced( + container + ): + for line_id in taxes_to_set.keys(): + line = self.invoice_line_ids.filtered(lambda x: x.id == line_id) + line.tax_ids.write({"tax_ids": [(6, 0, [])]}) + line.write({"tax_ids": taxes_to_set.get(line_id).ids}) # After taxes are changed is needed to force compute taxes again, in 16 version - # change of tax doesn't trigger compute of taxes on header + # change of tax doesn't trigger compute of taxes on header for unknown reason self._compute_amount() + if float_compare( + self.amount_untaxed + max(self.amount_tax, abs(self.avatax_amount)), + self.amount_residual, + precision_rounding=self.currency_id.rounding or 0.001, + ): + taxes_data = { + iline.id: iline.tax_ids for iline in self.invoice_line_ids + } + self.invoice_line_ids.write({"tax_ids": [(6, 0, [])]}) + for line in self.invoice_line_ids: + line.write({"tax_ids": taxes_data[line.id].ids}) return tax_result # Same as v13 diff --git a/account_avatax_oca/tests/mock_avatax.py b/account_avatax_oca/tests/mock_avatax.py new file mode 100644 index 000000000..883789c1a --- /dev/null +++ b/account_avatax_oca/tests/mock_avatax.py @@ -0,0 +1,367 @@ +def _mock_line(product_data): + subtotal = product_data.get("price_unit", 0.0) * product_data.get( + "quantity" + ) - product_data.get("discount_amount") + tax_amount = subtotal * product_data.get("rate_expected", 0.0) + res = { + "boundaryOverrideId": 0, + "businessIdentificationNo": "", + "costInsuranceFreight": 0.0, + "customerUsageType": "", + "description": product_data.get("product") + and product_data.get("product").display_name + or "No Name", + "destinationAddressId": 85600959974166, + "details": [ + { + "addressId": 85600959974167, + "chargedTo": "Buyer", + "country": "US", + "countyFIPS": "", + "exemptAmount": product_data.get("exemption_amount", 0.0), + "exemptReasonId": 3, + "exemptRuleId": 7455340, + "exemptUnits": product_data.get("exemption_amount", 0.0), + "id": 85600959974176, + "inState": True, + "isFee": False, + "isNonPassThru": False, + "jurisCode": "29", + "jurisName": "MISSOURI", + "jurisType": "STA", + "jurisdictionId": 2000001420, + "jurisdictionType": "State", + "liabilityType": "Seller", + "nonTaxableAmount": 0.0, + "nonTaxableRuleId": 0, + "nonTaxableType": "RateRule", + "nonTaxableUnits": 0.0, + "rate": product_data.get("rate_expected", 0.0), + "rateRuleId": 1065438, + "rateSourceId": 3, + "rateType": "General", + "rateTypeCode": "G", + "region": "MO", + "reportingExemptUnits": product_data.get("exemption_amount", 0.0), + "reportingNonTaxableUnits": 0.0, + "reportingTax": 0.0, + "reportingTaxCalculated": 0.0, + "reportingTaxableUnits": 0.0, + "serCode": "", + "signatureCode": "AXYM", + "sourcing": "Origin", + "stateAssignedNo": "", + "stateFIPS": "29", + "tax": tax_amount, + "taxAuthorityTypeId": 45, + "taxCalculated": tax_amount, + "taxName": "MO STATE TAX", + "taxOverride": 0.0, + "taxRegionId": 2078034, + "taxSubTypeId": "S", + "taxType": "Sales", + "taxTypeGroupId": "SalesAndUse", + "taxableAmount": subtotal, + "taxableUnits": subtotal, + "transactionId": 85600959974165, + "transactionLineId": 85600959974171, + "unitOfBasis": "PerCurrencyUnit", + }, + { + "addressId": 85600959974167, + "chargedTo": "Buyer", + "country": "US", + "countyFIPS": "", + "exemptAmount": product_data.get("exemption_amount", 0.0), + "exemptReasonId": 3, + "exemptRuleId": 7455340, + "exemptUnits": product_data.get("exemption_amount", 0.0), + "id": 85600959974177, + "inState": True, + "isFee": False, + "isNonPassThru": False, + "jurisCode": "037", + "jurisName": "CASS", + "jurisType": "CTY", + "jurisdictionId": 1527, + "jurisdictionType": "County", + "liabilityType": "Seller", + "nonTaxableAmount": 0.0, + "nonTaxableRuleId": 0, + "nonTaxableType": "RateRule", + "nonTaxableUnits": 0.0, + "rate": product_data.get("rate_expected", 0.0), + "rateRuleId": 1654198, + "rateSourceId": 3, + "rateType": "General", + "rateTypeCode": "G", + "region": "MO", + "reportingExemptUnits": product_data.get("exemption_amount", 0.0), + "reportingNonTaxableUnits": 0.0, + "reportingTax": 0.0, + "reportingTaxCalculated": 0.0, + "reportingTaxableUnits": 0.0, + "serCode": "", + "signatureCode": "AYFX", + "sourcing": "Origin", + "stateAssignedNo": "56756-037-000", + "stateFIPS": "29", + "tax": tax_amount, + "taxAuthorityTypeId": 45, + "taxCalculated": tax_amount, + "taxName": "MO COUNTY TAX", + "taxOverride": 0.0, + "taxRegionId": 2078034, + "taxSubTypeId": "S", + "taxType": "Sales", + "taxTypeGroupId": "SalesAndUse", + "taxableAmount": subtotal, + "taxableUnits": subtotal, + "transactionId": 85600959974165, + "transactionLineId": 85600959974171, + "unitOfBasis": "PerCurrencyUnit", + }, + { + "addressId": 85600959974167, + "chargedTo": "Buyer", + "country": "US", + "countyFIPS": "", + "exemptAmount": product_data.get("exemption_amount", 0.0), + "exemptReasonId": 3, + "exemptRuleId": 7455340, + "exemptUnits": product_data.get("exemption_amount", 0.0), + "id": 85600959974178, + "inState": True, + "isFee": False, + "isNonPassThru": False, + "jurisCode": "56756", + "jurisName": "PECULIAR", + "jurisType": "CIT", + "jurisdictionId": 85774, + "jurisdictionType": "City", + "liabilityType": "Seller", + "nonTaxableAmount": 0.0, + "nonTaxableRuleId": 0, + "nonTaxableType": "RateRule", + "nonTaxableUnits": 0.0, + "rate": product_data.get("rate_expected"), + "rateRuleId": 1391040, + "rateSourceId": 3, + "rateType": "General", + "rateTypeCode": "G", + "region": "MO", + "reportingExemptUnits": product_data.get("exemption_amount", 0.0), + "reportingNonTaxableUnits": 0.0, + "reportingTax": 0.0, + "reportingTaxCalculated": 0.0, + "reportingTaxableUnits": 0.0, + "serCode": "", + "signatureCode": "AYGM", + "sourcing": "Origin", + "stateAssignedNo": "56756-037-000", + "stateFIPS": "29", + "tax": tax_amount, + "taxAuthorityTypeId": 45, + "taxCalculated": tax_amount, + "taxName": "MO CITY TAX", + "taxOverride": 0.0, + "taxRegionId": 2078034, + "taxSubTypeId": "S", + "taxType": "Sales", + "taxTypeGroupId": "SalesAndUse", + "taxableAmount": subtotal, + "taxableUnits": subtotal, + "transactionId": 85600959974165, + "transactionLineId": 85600959974171, + "unitOfBasis": "PerCurrencyUnit", + }, + ], + "discountAmount": product_data.get("discount_amount"), + "discountTypeId": 0, + "entityUseCode": "", + "exemptAmount": product_data.get("exemption_amount", 0.0), + "exemptCertId": 90867213, + "exemptNo": "", + "hsCode": "", + "id": 85600959974171, + "isItemTaxable": False, + "isSSTP": False, + "itemCode": "MPC", + "lineAmount": subtotal, + "lineLocationTypes": [ + { + "documentAddressId": 85600959974167, + "documentLineId": 85600959974171, + "documentLineLocationTypeId": 85600959974174, + "locationTypeCode": "ShipFrom", + }, + { + "documentAddressId": 85600959974166, + "documentLineId": 85600959974171, + "documentLineLocationTypeId": 85600959974175, + "locationTypeCode": "ShipTo", + }, + ], + "lineNumber": f"{product_data.get('line_id')}", + "nonPassthroughDetails": [], + "originAddressId": 85600959974167, + "quantity": product_data.get("quantity"), + "ref1": "", + "ref2": "", + "reportingDate": "2024-09-17", + "revAccount": "", + "sourcing": "Origin", + "tax": tax_amount, + "taxCalculated": tax_amount, + "taxCode": "PA020122", + "taxCodeId": 71096, + "taxDate": "2024-09-17", + "taxEngine": "", + "taxIncluded": False, + "taxOverrideAmount": 0.0, + "taxOverrideReason": "", + "taxOverrideType": "None", + "taxableAmount": subtotal, + "transactionId": 85600959974165, + "vatCode": "", + "vatNumberTypeId": 0, + } + return subtotal, tax_amount, res + + +def mock_response(product_data_list): + """ + Mock to simulate avalara answer, it's only a standard compute + Keyword arguments: + product_data_list -- List of dict with: + - product (browse record) + - quantity + - price_unit + - discount_amount + - exemption_amount + - rate_expected + - line_id (invoice line id) + Return: + Dict with mocked response + """ + lines_data = [_mock_line(product_data) for product_data in product_data_list] + subtotal = sum(line[0] for line in lines_data) + tax_amount = sum(line[1] for line in lines_data) + lines_data = [line[2] for line in lines_data] + res = { + "addresses": [ + { + "boundaryLevel": "Zip5", + "city": "Hale", + "country": "US", + "id": 85600941773548, + "line1": "0000 E State Rd", + "line2": "", + "line3": "", + "postalCode": "00000-0000", + "region": "MI", + "taxRegionId": 1056912, + "transactionId": 85600941773547, + }, + { + "boundaryLevel": "Address", + "city": "Blairsville", + "country": "US", + "id": 85600941773549, + "latitude": "0.000000", + "line1": "000 Kendall Rd", + "line2": "", + "line3": "", + "longitude": "0.000000", + "postalCode": "00000-0000", + "region": "PA", + "taxRegionId": 4012044, + "transactionId": 85600941773547, + }, + ], + "adjustmentDescription": "", + "adjustmentReason": "NotAdjusted", + "apStatus": None, + "apStatusCode": None, + "batchCode": "", + "businessIdentificationNo": "", + "code": "INV/2024/09/1482", + "companyId": 951445, + "country": "US", + "currencyCode": "USD", + "customerCode": "CC-000000:0", + "customerUsageType": "", + "customerVendorCode": "CC-000000:0", + "date": "2024-09-17", + "description": "INV/2024/09/1482", + "destinationAddressId": 85600941773548, + "email": "", + "entityUseCode": "", + "exchangeRate": 1.0, + "exchangeRateCurrencyCode": "USD", + "exchangeRateEffectiveDate": "2024-09-17", + "exemptNo": "", + "id": 85600941773547, + "lines": lines_data, + "locationCode": "", + "locationTypes": [ + { + "documentAddressId": 85600941773549, + "documentId": 85600941773547, + "documentLocationTypeId": 85600941773551, + "locationTypeCode": "ShipFrom", + }, + { + "documentAddressId": 85600941773548, + "documentId": 85600941773547, + "documentLocationTypeId": 85600941773552, + "locationTypeCode": "ShipTo", + }, + ], + "locked": False, + "modifiedDate": "2024-09-17T20:56:39.7321524Z", + "modifiedUserId": 1094294, + "originAddressId": 85600941773549, + "purchaseOrderNo": "", + "reconciled": False, + "referenceCode": "", + "region": "MI", + "reportingLocationCode": "", + "salespersonCode": "Jimmy Dunmire", + "softwareVersion": "24.8.0.0", + "status": "Committed", + "summary": [ + { + "country": "US", + "exemption": 0.0, + "jurisCode": "26", + "jurisName": "MICHIGAN", + "jurisType": "State", + "nonTaxable": 0.0, + "rate": 0.06, + "rateType": "General", + "region": "MI", + "stateAssignedNo": "", + "tax": tax_amount, + "taxAuthorityType": 45, + "taxCalculated": tax_amount, + "taxName": "MI STATE TAX", + "taxSubType": "S", + "taxType": "Sales", + "taxable": subtotal, + } + ], + "taxDate": "2024-09-17", + "taxOverrideAmount": 0.0, + "taxOverrideReason": "", + "taxOverrideType": "None", + "totalAmount": subtotal, + "totalDiscount": 0.0, + "totalExempt": 0.0, + "totalTax": tax_amount, + "totalTaxCalculated": tax_amount, + "totalTaxable": subtotal, + "type": "SalesInvoice", + "version": 1, + } + return res diff --git a/account_avatax_oca/tests/test_avatax.py b/account_avatax_oca/tests/test_avatax.py index 723181ad3..03448b319 100644 --- a/account_avatax_oca/tests/test_avatax.py +++ b/account_avatax_oca/tests/test_avatax.py @@ -2,16 +2,27 @@ # License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html -from odoo.tests import common +from unittest.mock import patch + +from odoo.tests import Form, common + +from .mock_avatax import mock_response class TestAvatax(common.TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() + cls.fiscal_position = cls.env["account.fiscal.position"].create( + { + "name": "Avatax Demo", + "is_avatax": True, + } + ) cls.customer = cls.env["res.partner"].create( { "name": "Customer", + "property_account_position_id": cls.fiscal_position.id, "property_tax_exempt": True, "property_exemption_number": "12321", "property_exemption_code_id": cls.env.ref( @@ -19,6 +30,7 @@ def setUpClass(cls): ), } ) + cls.invoice = cls.env["account.move"].create( { "move_type": "out_invoice", @@ -34,3 +46,134 @@ def test_100_onchange_customer_exempt(self): self.assertEqual( self.invoice.exemption_code, self.customer.property_exemption_number ) + + @patch( + "odoo.addons.account_avatax_oca.models.res_company.Company.get_avatax_config_company" + ) + @patch( + "odoo.addons.account_avatax_oca.models.avalara_salestax.AvalaraSalestax.create_transaction" # noqa: B950 + ) + def test_avatax_compute_tax( + self, mock_create_transaction, mock_get_avatax_config_company + ): + avatax_config = self.env["avalara.salestax"].create( + { + "account_number": "123456", + "license_key": "123456", + "company_code": "DEFAULT", + "disable_tax_calculation": False, + "invoice_calculate_tax": False, + } + ) + mock_get_avatax_config_company.return_value = avatax_config + + # Force empty taxes to check only avatax taxes + self.invoice.invoice_line_ids.write( + { + "tax_ids": [(6, 0, [])], + } + ) + + invoice_line_data = [ + { + "product_id": self.env["product.product"].create({"name": "Product 1"}), + "quantity": 5, + "price_unit": 102.5, + "rate": 0.06448, + }, + { + "product_id": self.env["product.product"].create({"name": "Product 2"}), + "quantity": 4, + "price_unit": 25.5, + "rate": 0.03448, + }, + ] + + self.invoice.invoice_line_ids.unlink() + invoice_form = Form(self.invoice) + + for line_data in invoice_line_data: + with invoice_form.invoice_line_ids.new() as line: + line.product_id = line_data.get("product_id") + line.quantity = line_data.get("quantity") + line.price_unit = line_data.get("price_unit") + line.tax_ids.clear() + self.assertFalse(invoice_form.calculate_tax_on_save) + self.invoice = invoice_form.save() + self.assertFalse(self.invoice.calculate_tax_on_save) + mock_create_transaction.return_value = mock_response( + [ + { + "product": line.product_id, + "quantity": line.quantity, + "price_unit": line.price_unit, + "discount_amount": line.price_subtotal + - ((line.price_unit * line.quantity) * (1 - line.discount * 100.0)), + "rate_expected": line_data.get("rate"), + "line_id": line.id, + } + for line, line_data in zip( + self.invoice.invoice_line_ids, invoice_line_data + ) + ] + ) + + self.invoice.invalidate_model(["invoice_line_ids"]) + for line in self.invoice.invoice_line_ids: + self.assertFalse(bool(line.tax_ids)) + self.invoice.action_post() + + for line in self.invoice.invoice_line_ids: + self.assertTrue(bool(line.tax_ids)) + + self.assertEqual( + self.invoice.amount_tax + self.invoice.amount_untaxed, + self.invoice.amount_residual, + ) + mock_get_avatax_config_company.assert_called() + mock_create_transaction.assert_called() + + self.invoice.button_draft() + + avatax_config.write( + { + "invoice_calculate_tax": True, + } + ) + + self.invoice.invoice_line_ids.unlink() + + invoice_form = Form(self.invoice) + for line_data in invoice_line_data: + with invoice_form.invoice_line_ids.new() as line: + line.product_id = line_data.get("product_id") + line.quantity = line_data.get("quantity") + line.price_unit = line_data.get("price_unit") + line.tax_ids.clear() + self.assertTrue(invoice_form.calculate_tax_on_save) + self.invoice = invoice_form.save() + mock_create_transaction.return_value = mock_response( + [ + { + "product": line.product_id, + "quantity": line.quantity, + "price_unit": line.price_unit, + "discount_amount": line.price_subtotal + - ((line.price_unit * line.quantity) * (1 - line.discount * 100.0)), + "rate_expected": line_data.get("rate"), + "line_id": line.id, + } + for line, line_data in zip( + self.invoice.invoice_line_ids, invoice_line_data + ) + ] + ) + self.assertFalse(self.invoice.calculate_tax_on_save) + self.invoice.action_post() + for line in self.invoice.invoice_line_ids: + self.assertTrue(bool(line.tax_ids)) + + self.assertEqual( + self.invoice.amount_tax + self.invoice.amount_untaxed, + self.invoice.amount_residual, + )