from odoo import fields from odoo.tests.common import tagged from odoo.tools.misc import formatLang from odoo.addons.account_avatax.tests.common import TestAccountAvataxCommon from .mocked_so_response import generate_response @tagged("-at_install", "post_install") class TestSaleAvalara(TestAccountAvataxCommon): @classmethod def setUpClass(cls, chart_template_ref=None): res = super().setUpClass(chart_template_ref) # This tax is deliberately wrong with an amount of 1. This is # used to make sure we use the tax values that Avatax returns # and not the tax values Odoo computes (these values would be # wrong if a user manually changes it or if they're partially # exempt). cls.tax_with_diff_amount = cls.env["account.tax"].create({ 'name': 'CA COUNTY TAX [075] (0.2500 %)', 'company_id': cls.env.user.company_id.id, 'amount': 1, 'amount_type': 'percent', }) cls.sales_user = cls.env['res.users'].create({ 'name': 'Sales user', 'login': 'sales', 'email': 'sale_user@test.com', 'groups_id': [(6, 0, [cls.env.ref('base.group_user').id, cls.env.ref('sales_team.group_sale_salesman').id])], }) cls.env = cls.env(user=cls.sales_user) cls.cr = cls.env.cr return res def assertOrder(self, order, mocked_response=None): if mocked_response: self.assertRecordValues(order, [{ 'amount_total': 97.68, 'amount_untaxed': 90.0, 'amount_tax': 7.68, }]) totals = order.tax_totals subtotal_group = totals['groups_by_subtotal']['Untaxed Amount'] self.assertEqual(len(subtotal_group), 1, 'There should only be one subtotal group (Untaxed Amount)') self.assertEqual(subtotal_group[0]['tax_group_amount'], order.amount_tax, 'The tax on tax_totals is different from amount_tax.') self.assertEqual(totals['amount_total'], order.amount_total) self.assertEqual(totals['formatted_amount_total'], formatLang(self.env, order.amount_total, currency_obj=order.currency_id)) for avatax_line in mocked_response['lines']: so_line = order.order_line.filtered(lambda l: str(l.id) == avatax_line['lineNumber'].split(',')[1]) self.assertRecordValues(so_line, [{ 'price_subtotal': avatax_line['taxableAmount'], 'price_tax': avatax_line['tax'], 'price_total': avatax_line['taxableAmount'] + avatax_line['tax'], }]) else: for line in order.order_line: product_name = line.product_id.display_name self.assertGreater(len(line.tax_id), 0, "Line with %s did not get any taxes set." % product_name) self.assertGreater(order.amount_tax, 0.0, "Invoice has a tax_amount of 0.0.") def _create_sale_order(self): return self.env['sale.order'].create({ 'user_id': self.sales_user.id, 'partner_id': self.partner.id, 'fiscal_position_id': self.fp_avatax.id, 'date_order': '2021-01-01', 'order_line': [ (0, 0, { 'product_id': self.product_user.id, 'tax_id': None, 'price_unit': self.product_user.list_price, }), (0, 0, { 'product_id': self.product_user_discound.id, 'tax_id': None, 'price_unit': self.product_user_discound.list_price, }), (0, 0, { 'product_id': self.product_accounting.id, 'tax_id': None, 'price_unit': self.product_accounting.list_price, }), (0, 0, { 'product_id': self.product_expenses.id, 'tax_id': None, 'price_unit': self.product_expenses.list_price, }), (0, 0, { 'product_id': self.product_invoicing.id, 'tax_id': None, 'price_unit': self.product_invoicing.list_price, }), ] }) def test_compute_on_send(self): order = self._create_sale_order() mocked_response = generate_response(order.order_line) with self._capture_request(return_value=mocked_response): order.action_quotation_send() self.assertOrder(order, mocked_response=mocked_response) def test_01_odoo_sale_order(self): order = self._create_sale_order() mocked_response = generate_response(order.order_line) with self._capture_request(return_value=mocked_response): order.button_external_tax_calculation() self.assertOrder(order, mocked_response=mocked_response) def test_integration_01_odoo_sale_order(self): with self._skip_no_credentials(): order = self._create_sale_order() order.button_external_tax_calculation() self.assertOrder(order) def test_tax_round_globally(self): """The total amount of sale orders elligible for Avatax should never be computed with the 'round_globally' option but should instead use the 'round_per_line' mechanism""" self.env.company.sudo().tax_calculation_rounding_method = 'round_globally' order = self.env['sale.order'].create({ 'user_id': self.sales_user.id, 'partner_id': self.partner.id, 'fiscal_position_id': self.fp_avatax.id, 'date_order': '2021-01-01', 'order_line': [ (0, 0, { 'product_id': self.product.id, 'product_uom_qty': 1, 'price_unit': 1.48, 'tax_id': self.tax_with_diff_amount.ids, }), (0, 0, { 'product_id': self.product.id, 'product_uom_qty': 1, 'price_unit': 1.48, 'tax_id': self.tax_with_diff_amount.ids, }), ], }) self.assertEqual(order.amount_total, 2.98) def test_sale_order_downpayment(self): """ Test the expected down payment flow. Down payments are not sent to Avalara. We invoice everything on the final "regular" invoice, as if the down payments never happened. """ order = self._create_sale_order() mocked_response = generate_response(order.order_line) with self._capture_request(return_value=mocked_response): order.action_confirm() downpayment_pct = 50 payment_ctx = { "active_model": "sale.order", "active_ids": [order.id], "active_id": order.id, } wizard = ( self.env["sale.advance.payment.inv"] .with_context(**payment_ctx) .create({ 'advance_payment_method': 'percentage', 'amount': downpayment_pct, }) ) wizard.product_id.taxes_id = self.env["account.tax"].sudo().create({"name": "downpayment tax", "amount": 20}) wizard.sudo().create_invoices() downpayment_invoice = order.invoice_ids with self._capture_request(return_value={'lines': [], 'summary': []}) as capture: downpayment_invoice.sudo().action_post() self.assertIsNone(capture.val, "Shouldn't call Avatax when posting a down payment invoice.") self.assertEqual(len(order.order_line.filtered(lambda line: not line.display_type)), 6, "Should have generated a new down payment line.") self.assertFalse(order.order_line.filtered('is_downpayment').tax_id, "Down payment lines on the quotation shouldn't have taxes.") self.assertAlmostEqual(downpayment_invoice.amount_total, order.amount_total * downpayment_pct / 100, msg="Down payment has the wrong amount.") self.assertEqual(downpayment_invoice.amount_tax, 0, "Down payment shouldn't have taxes.") wizard = ( self.env["sale.advance.payment.inv"] .with_context(**payment_ctx) .create({ 'advance_payment_method': 'delivered', }) ) with self._capture_request(return_value={'lines': [], 'summary': []}) as capture: wizard.sudo().create_invoices() sent_lines = capture.val['json']['createTransactionModel']['lines'] self.assertEqual(len(sent_lines), 5, "Should send only the regular lines.") @tagged("-at_install", "post_install") class TestAccountAvalaraSalesTaxItemsIntegration(TestAccountAvataxCommon): """https://developer.avalara.com/certification/avatax/sales-tax-badge/""" @classmethod def setUpClass(cls, chart_template_ref=None): res = super().setUpClass(chart_template_ref) shipping_partner = cls.env["res.partner"].create({ 'name': "Shipping Partner", 'street': "234 W 18th Ave", 'city': "Columbus", 'state_id': cls.env.ref("base.state_us_30").id, # Ohio 'country_id': cls.env.ref("base.us").id, 'zip': "43210", }) with cls._capture_request(return_value={'lines': [], 'summary': []}) as capture: cls.sale_order = cls.env['sale.order'].create({ 'partner_id': cls.partner.id, 'partner_shipping_id': shipping_partner.id, 'fiscal_position_id': cls.fp_avatax.id, 'date_order': '2021-01-01', 'order_line': [ (0, 0, { 'product_id': cls.product.id, 'tax_id': None, 'price_unit': cls.product.list_price, }), ] }) cls.sale_order.button_external_tax_calculation() cls.captured_arguments = capture.val['json']['createTransactionModel'] return res def test_item_code(self): """Identify customer code (number, ID) to pass to the AvaTax service.""" line_model, line_id = self.captured_arguments['lines'][0]['number'].split(',') self.assertEqual(self.sale_order.order_line, self.env[line_model].browse(int(line_id))) def test_item_description(self): """Identify item/service/charge description to pass to the AvaTax service with a human-readable description or item name. """ line_description = self.captured_arguments['lines'][0]['description'] self.assertEqual(self.sale_order.order_line.name, line_description) def test_tax_code_mapping(self): """Association of an item or item group to an AvaTax Tax Code to describe the taxability (e.g. Clothing-Shirts – B-to-C). """ tax_code = self.captured_arguments['lines'][0]['taxCode'] self.assertEqual(self.product.avatax_category_id.code, tax_code) def test_doc_code(self): """Values that can come across to AvaTax as the DocCode.""" code = self.captured_arguments['code'] sent_so = self.env['sale.order'].search([('avatax_unique_code', '=', code)]) self.assertEqual(self.sale_order, sent_so) def test_customer_code(self): """Values that can come across to AvaTax as the Customer Code.""" customer_code = self.captured_arguments['customerCode'] self.assertEqual(self.sale_order.partner_id.avalara_partner_code, customer_code) def test_doc_date(self): """Value that comes across to AvaTax as the DocDate.""" doc_date = self.captured_arguments['date'] # didn't find anything with "DocDate" self.assertEqual(self.sale_order.date_order.date(), fields.Date.to_date(doc_date)) def test_calculation_date(self): """Value that is used for Tax Calculation Date in AvaTax.""" tax_date = self.captured_arguments['taxOverride']['taxDate'] self.assertEqual(self.sale_order.date_order.date(), fields.Date.to_date(tax_date)) def test_doc_type(self): """DocType used for varying stages of the transaction life cycle.""" doc_type = self.captured_arguments['type'] self.assertEqual('SalesOrder', doc_type) def test_header_level_destination_address(self): """Value that is sent to AvaTax for Destination Address at the header level.""" destination_address = self.captured_arguments['addresses']['shipTo'] self.assertEqual(destination_address, { 'city': 'Columbus', 'country': 'US', 'line1': '234 W 18th Ave', 'postalCode': '43210', 'region': 'OH', }) def test_header_level_origin_address(self): """Value that is sent to AvaTax for Origin Address at the header level.""" origin_address = self.captured_arguments['addresses']['shipFrom'] self.assertEqual(origin_address, { 'city': 'San Francisco', 'country': 'US', 'line1': '250 Executive Park Blvd', 'postalCode': '94134', 'region': 'CA', }) def test_quantity(self): """Value that is sent to AvaTax for the Quantity.""" quantity = self.captured_arguments['lines'][0]['quantity'] self.assertEqual(self.sale_order.order_line.product_uom_qty, quantity) def test_amount(self): """Value that is sent to AvaTax for the Amount.""" amount = self.captured_arguments['lines'][0]['amount'] self.assertEqual(self.sale_order.order_line.price_subtotal, amount) def test_tax_code(self): """Value that is sent to AvaTax for the Tax Code.""" tax_code = self.captured_arguments['lines'][0]['taxCode'] self.assertEqual(self.sale_order.order_line.product_id.avatax_category_id.code, tax_code) def test_sales_order(self): """Ensure that invoices are processed through a logical document lifecycle.""" self.assertEqual(self.captured_arguments['type'], 'SalesOrder') with self._capture_request({'lines': [], 'summary': []}) as capture: self.sale_order.action_quotation_send() self.sale_order.action_confirm() invoice = self.sale_order._create_invoices() invoice.button_external_tax_calculation() self.assertEqual(capture.val['json']['createTransactionModel']['type'], 'SalesInvoice') with self._capture_request({'lines': [], 'summary': []}) as capture: invoice.action_post() self.assertTrue(capture.val['json']['createTransactionModel']['commit']) def test_commit_tax(self): """Ensure that invoices are committed/posted for reporting appropriately.""" with self._capture_request({'lines': [], 'summary': []}) as capture: self.sale_order.action_quotation_send() self.sale_order.action_confirm() invoice = self.sale_order._create_invoices() invoice.action_post() self.assertTrue(capture.val['json']['createTransactionModel']['commit']) def test_merge_sale_orders(self): """Ensure sale orders with different shipping partner are not merged in the same invoice """ shipping_partner_b = self.env["res.partner"].create({ 'name': "Shipping Partner B", 'street': "4557 De Silva St", 'city': "Freemont", 'state_id': self.env.ref("base.state_us_13").id, 'country_id': self.env.ref("base.us").id, 'zip': "94538", }) with self._capture_request(return_value={'lines': [], 'summary': []}): sale_order_b = self.env['sale.order'].create({ 'partner_id': self.partner.id, 'partner_shipping_id': shipping_partner_b.id, 'fiscal_position_id': self.fp_avatax.id, 'date_order': '2021-01-01', 'order_line': [ (0, 0, { 'product_id': self.product.id, 'tax_id': None, 'price_unit': self.product.list_price, }), ] }) orders = self.sale_order | sale_order_b orders.action_confirm() orders._create_invoices() self.assertEqual(len(orders.invoice_ids), 2, "Different invoices should be created")