# Part of Odoo. See LICENSE file for full copyright and licensing details.

from datetime import timedelta
from unittest.mock import patch

from freezegun import freeze_time

from odoo import fields
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.fields import Command
from odoo.tests import Form, HttpCase, tagged

from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.mail.tests.common import MailCommon
from odoo.addons.sale.tests.common import SaleCommon


@tagged('post_install', '-at_install')
class TestSaleOrder(SaleCommon):

    # Those tests do not rely on accounting common on purpose
    #   If you need the accounting setup, use other classes (TestSaleToInvoice probably)

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.partner1, cls.partner2 = cls.env['res.partner'].create([
            {'name': 'Partner 1'},
            {'name': 'Partner 2'},
        ])

    def test_computes_auto_fill(self):
        free_product, dummy_product = self.env['product.product'].create([{
            'name': 'Free product',
            'list_price': 0.0,
        }, {
            'name': 'Dummy product',
            'list_price': 0.0,
        }])
        # Test pre-computes of lines with order
        order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [
                Command.create({
                    'display_type': 'line_section',
                    'name': 'Dummy section',
                }),
                Command.create({
                    'display_type': 'line_section',
                    'name': 'Dummy section',
                }),
                Command.create({
                    'product_id': free_product.id,
                }),
                Command.create({
                    'product_id': dummy_product.id,
                })
            ]
        })

        # Test pre-computes of lines creation alone
        # Ensures the creation works fine even if the computes
        # are triggered after the defaults
        order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
        })
        self.env['sale.order.line'].create([
            {
                'display_type': 'line_section',
                'name': 'Dummy section',
                'order_id': order.id,
            }, {
                'display_type': 'line_section',
                'name': 'Dummy section',
                'order_id': order.id,
            }, {
                'product_id': free_product.id,
                'order_id': order.id,
            }, {
                'product_id': dummy_product.id,
                'order_id': order.id,
            }
        ])

    def test_sale_order_standard_flow(self):
        self.assertEqual(self.sale_order.amount_total, 725.0, 'Sale: total amount is wrong')
        self.sale_order.order_line._compute_product_updatable()
        self.assertTrue(self.sale_order.order_line[0].product_updatable)

        # send quotation
        email_act = self.sale_order.action_quotation_send()
        email_ctx = email_act.get('context', {})
        self.sale_order.with_context(**email_ctx).message_post_with_source(
            self.env['mail.template'].browse(email_ctx.get('default_template_id')),
            subtype_xmlid='mail.mt_comment',
        )
        self.assertTrue(self.sale_order.state == 'sent', 'Sale: state after sending is wrong')
        self.sale_order.order_line._compute_product_updatable()
        self.assertTrue(self.sale_order.order_line[0].product_updatable)

        # confirm quotation
        self.sale_order.action_confirm()
        self.assertTrue(self.sale_order.state == 'sale')
        self.assertTrue(self.sale_order.invoice_status == 'to invoice')

    def test_sale_order_send_to_self(self):
        # when sender(logged in user) is also present in recipients of the mail composer,
        # user should receive mail.
        sale_order = self.env['sale.order'].with_user(self.sale_user).create({
            'partner_id': self.sale_user.partner_id.id,
        })
        email_ctx = sale_order.action_quotation_send().get('context', {})
        # We need to prevent auto mail deletion, and so we copy the template and send the mail with
        # added configuration in copied template. It will allow us to check whether mail is being
        # sent to to author or not (in case author is present in 'Recipients' of composer).
        mail_template = self.env['mail.template'].browse(email_ctx.get('default_template_id')).copy({'auto_delete': False})
        # send the mail with same user as customer
        sale_order.with_context(**email_ctx).with_user(self.sale_user).message_post_with_source(
            mail_template,
            subtype_xmlid='mail.mt_comment',
        )
        self.assertTrue(sale_order.state == 'sent', 'Sale : state should be changed to sent')
        mail_message = sale_order.message_ids[0]
        self.assertEqual(mail_message.author_id, sale_order.partner_id, 'Sale: author should be same as customer')
        self.assertEqual(mail_message.author_id, mail_message.partner_ids, 'Sale: author should be in composer recipients thanks to "partner_to" field set on template')
        self.assertEqual(mail_message.partner_ids, mail_message.sudo().mail_ids.recipient_ids, 'Sale: author should receive mail due to presence in composer recipients')

    def test_sale_sequence(self):
        self.env['ir.sequence'].search([
            ('code', '=', 'sale.order'),
        ]).write({
            'use_date_range': True, 'prefix': 'SO/%(range_year)s/',
        })
        sale_order = self.sale_order.copy({'date_order': '2019-01-01'})
        self.assertTrue(sale_order.name.startswith('SO/2019/'))
        sale_order = self.sale_order.copy({'date_order': '2020-01-01'})
        self.assertTrue(sale_order.name.startswith('SO/2020/'))
        # In EU/BXL tz, this is actually already 01/01/2020
        sale_order = self.sale_order.with_context(tz='Europe/Brussels').copy({'date_order': '2019-12-31 23:30:00'})
        self.assertTrue(sale_order.name.startswith('SO/2020/'))

    def test_unlink_cancel(self):
        """ Test deleting and cancelling sales orders depending on their state and on the user's rights """
        # SO in state 'draft' can be deleted
        so_copy = self.sale_order.copy()
        with self.assertRaises(AccessError):
            so_copy.with_user(self.sale_user).unlink()
        self.assertTrue(so_copy.unlink(), 'Sale: deleting a quotation should be possible')

        # SO in state 'cancel' can be deleted
        so_copy = self.sale_order.copy()
        so_copy.action_confirm()
        self.assertTrue(so_copy.state == 'sale', 'Sale: SO should be in state "sale"')
        so_copy._action_cancel()
        self.assertTrue(so_copy.state == 'cancel', 'Sale: SO should be in state "cancel"')
        with self.assertRaises(AccessError):
            so_copy.with_user(self.sale_user).unlink()
        self.assertTrue(so_copy.unlink(), 'Sale: deleting a cancelled SO should be possible')

        # SO in state 'sale' cannot be deleted
        self.sale_order.action_confirm()
        self.assertTrue(self.sale_order.state == 'sale', 'Sale: SO should be in state "sale"')
        with self.assertRaises(UserError):
            self.sale_order.unlink()

        self.sale_order.action_lock()
        self.assertTrue(self.sale_order.state == 'sale')
        self.assertTrue(self.sale_order.locked)
        with self.assertRaises(UserError):
            self.sale_order.unlink()

    def test_compute_packaging_00(self):
        """Create a SO and use packaging. Check we suggested suitable packaging
        according to the product_qty. Also check product_qty or product_packaging
        are correctly calculated when one of them changed.
        """
        # Required for `product_packaging_qty` to be visible in the view
        self.env.user.groups_id += self.env.ref('product.group_stock_packaging')
        packaging_single, packaging_dozen = self.env['product.packaging'].create([{
            'name': "I'm a packaging",
            'product_id': self.product.id,
            'qty': 1.0,
        }, {
            'name': "I'm also a packaging",
            'product_id': self.product.id,
            'qty': 12.0,
        }])

        so = self.empty_order
        so_form = Form(so)
        with so_form.order_line.new() as line:
            line.product_id = self.product
            line.product_uom_qty = 1.0
        so_form.save()
        self.assertEqual(so.order_line.product_packaging_id, packaging_single)
        self.assertEqual(so.order_line.product_packaging_qty, 1.0)
        with so_form.order_line.edit(0) as line:
            line.product_packaging_qty = 2.0
        so_form.save()
        self.assertEqual(so.order_line.product_uom_qty, 2.0)

        with so_form.order_line.edit(0) as line:
            line.product_uom_qty = 24.0
        so_form.save()
        self.assertEqual(so.order_line.product_packaging_id, packaging_dozen)
        self.assertEqual(so.order_line.product_packaging_qty, 2.0)
        with so_form.order_line.edit(0) as line:
            line.product_packaging_qty = 1.0
        so_form.save()
        self.assertEqual(so.order_line.product_uom_qty, 12)

        packaging_pack_of_10 = self.env['product.packaging'].create({
            'name': "PackOf10",
            'product_id': self.product.id,
            'qty': 10.0,
        })
        packaging_pack_of_20 = self.env['product.packaging'].create({
            'name': "PackOf20",
            'product_id': self.product.id,
            'qty': 20.0,
        })

        so2 = self.env['sale.order'].create({
            'partner_id': self.partner.id,
        })
        so2_form = Form(so2)
        with so2_form.order_line.new() as line:
            line.product_id = self.product
            line.product_uom_qty = 10
        so2_form.save()
        self.assertEqual(so2.order_line.product_packaging_id.id, packaging_pack_of_10.id)
        self.assertEqual(so2.order_line.product_packaging_qty, 1.0)

        with so2_form.order_line.edit(0) as line:
            line.product_packaging_qty = 2
        so2_form.save()
        self.assertEqual(so2.order_line.product_uom_qty, 20)
        # we should have 2 pack of 10, as we've set the package_qty manually,
        # we shouldn't recompute the packaging_id, since the package_qty is protected,
        # therefor cannot be recomputed during the same transaction, which could lead
        # to an incorrect line like (qty=20,pack_qty=2,pack_id=PackOf20)
        self.assertEqual(so2.order_line.product_packaging_qty, 2)
        self.assertEqual(so2.order_line.product_packaging_id.id, packaging_pack_of_10.id)

        with so2_form.order_line.edit(0) as line:
            line.product_packaging_id = packaging_pack_of_20
        so2_form.save()
        self.assertEqual(so2.order_line.product_uom_qty, 20)
        # we should have 1 pack of 20, as we've set the package type manually
        self.assertEqual(so2.order_line.product_packaging_qty, 1)
        self.assertEqual(so2.order_line.product_packaging_id.id, packaging_pack_of_20.id)

    def test_compute_packaging_01(self):
        """Create a SO and use packaging in a multicompany environment.
        Ensure any suggested packaging matches the SO's.
        """
        company2 = self.env['res.company'].create([{'name': 'Company 2'}])
        generic_single_pack = self.env['product.packaging'].create({
            'name': "single pack",
            'product_id': self.product.id,
            'qty': 1.0,
            'company_id': False,
        })
        company2_pack_of_10 = self.env['product.packaging'].create({
            'name': "pack of 10 by Company 2",
            'product_id': self.product.id,
            'qty': 10.0,
            'company_id': company2.id,
        })

        so1 = self.empty_order
        so1_form = Form(so1)
        with so1_form.order_line.new() as line:
            line.product_id = self.product
            line.product_uom_qty = 10.0
        so1_form.save()
        self.assertEqual(so1.order_line.product_packaging_id, generic_single_pack)
        self.assertEqual(so1.order_line.product_packaging_qty, 10.0)

        so2 = self.env['sale.order'].with_company(company2).create({
            'partner_id': self.partner.id,
        })
        so2_form = Form(so2)
        with so2_form.order_line.new() as line:
            line.product_id = self.product
            line.product_uom_qty = 10.0
        so2_form.save()
        self.assertEqual(so2.order_line.product_packaging_id, company2_pack_of_10)
        self.assertEqual(so2.order_line.product_packaging_qty, 1.0)

    def _create_sale_order(self):
        """Create dummy sale order (without lines)"""
        return self.env['sale.order'].with_context(
            default_sale_order_template_id=False
            # Do not modify test behavior even if sale_management is installed
        ).create({
            'partner_id': self.partner.id,
        })

    def test_invoicing_terms(self):
        # Enable invoicing terms
        self.env['ir.config_parameter'].sudo().set_param('account.use_invoice_terms', True)

        # Plain invoice terms
        self.env.company.terms_type = 'plain'
        self.env.company.invoice_terms = "Coin coin"
        sale_order = self._create_sale_order()
        self.assertEqual(sale_order.note, "<p>Coin coin</p>")

        # Html invoice terms (/terms page)
        self.env.company.terms_type = 'html'
        sale_order = self._create_sale_order()
        self.assertTrue(sale_order.note.startswith("<p>Terms &amp; Conditions: "))

    def test_validity_days(self):
        self.env.company.quotation_validity_days = 5
        with freeze_time("2020-05-02"):
            sale_order = self._create_sale_order()

            self.assertEqual(sale_order.validity_date, fields.Date.today() + timedelta(days=5))
        self.env.company.quotation_validity_days = 0
        sale_order = self._create_sale_order()
        self.assertFalse(
            sale_order.validity_date,
            "No validity date must be specified if the company validity duration is 0")

    def test_so_names(self):
        """Test custom context key for display_name & name_search.

        Note: this key is used in sale_expense & sale_timesheet modules.
        """
        SaleOrder = self.env['sale.order'].with_context(sale_show_partner_name=True)

        res = SaleOrder.name_search(name=self.sale_order.partner_id.name)
        self.assertEqual(res[0][0], self.sale_order.id)

        self.assertNotIn(self.sale_order.partner_id.name, self.sale_order.display_name)
        self.assertIn(
            self.sale_order.partner_id.name,
            self.sale_order.with_context(sale_show_partner_name=True).display_name)

    def test_sol_names(self):
        """Check that the SOL description gets used for the display name."""
        self.sale_order.order_line = [
            Command.create({'is_downpayment': True}),
            Command.create({'display_type': 'line_note', 'name': "Foo\nBar\nBaz"}),
        ]
        sol1, sol2, sol3, sol4 = self.sale_order.order_line
        sol1.name += "\nOK THANK YOU\nGOOD BYE"

        self.assertEqual(
            sol1.display_name,
            f"{self.sale_order.name} - OK THANK YOU ({self.partner.name})",
            "Product line with description should display the first line of description",
        )
        self.assertEqual(
            sol2.display_name,
            f"{self.sale_order.name} - {sol2.product_id.name} ({self.partner.name})",
            "Product line without description should display the product name",
        )
        self.assertEqual(
            sol3.display_name,
            f"{self.sale_order.name} - {sol3.name} ({self.partner.name})",
            "Down payment line should display the down payment name",
        )
        self.assertEqual(
            sol4.display_name,
            f"{self.sale_order.name} - Foo ({self.partner.name})",
            "Multi-line note should display the first line only",
        )

    def test_state_changes(self):
        """Test some untested state changes methods & logic."""
        self.sale_order.action_quotation_sent()

        self.assertEqual(self.sale_order.state, 'sent')
        self.assertIn(self.sale_order.partner_id, self.sale_order.message_follower_ids.partner_id)

        self.env.user.groups_id += self.env.ref('sale.group_auto_done_setting')
        self.sale_order.action_confirm()
        self.assertEqual(self.sale_order.state, 'sale')
        self.assertTrue(self.sale_order.locked)
        with self.assertRaises(UserError):
            self.sale_order.action_confirm()

        self.sale_order.action_unlock()
        self.assertEqual(self.sale_order.state, 'sale')

    def test_sol_name_search(self):
        # Shouldn't raise
        self.env['sale.order']._search([('order_line', 'ilike', 'product')])

        name_search_data = self.env['sale.order.line'].name_search(name=self.sale_order.name)
        sol_ids_found = dict(name_search_data).keys()
        self.assertEqual(list(sol_ids_found), self.sale_order.order_line.ids)

    def test_zero_quantity(self):
        """
            If the quantity set is 0 it should remain to 0
            Test that changing the uom do not change the quantity
        """
        order_line = self.sale_order.order_line[0]
        order_line.product_uom_qty = 0.0
        order_line.product_uom = self.uom_dozen
        self.assertEqual(order_line.product_uom_qty, 0.0)

    def test_discount_rounding(self):
        """
            Check the discount is properly rounded and the price subtotal
            computed with this rounded discount
        """
        sale_order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [(0, 0, {
                'product_id': self.product.id,
                'product_uom_qty': 1,
                'price_unit': 192,
                'discount': 74.246,
            })]
        })
        self.assertEqual(sale_order.order_line.price_subtotal, 49.44, "Subtotal should be equal to 192 * (1 - 0.7425)")
        self.assertEqual(sale_order.order_line.discount, 74.25)

    def test_tax_amount_rounding(self):
        """ Check order amounts are rounded according to settings """

        tax_a = self.env['account.tax'].create({
            'name': 'Test tax',
            'type_tax_use': 'sale',
            'price_include_override': 'tax_excluded',
            'amount_type': 'percent',
            'amount': 15.0,
        })

        # Test Round per Line (default)
        self.env.company.tax_calculation_rounding_method = 'round_per_line'
        sale_order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [
                Command.create({
                    'product_id': self.product.id,
                    'product_uom_qty': 1,
                    'price_unit': 6.7,
                    'discount': 0,
                    'tax_id': tax_a.ids,
                }),
                Command.create({
                    'product_id': self.product.id,
                    'product_uom_qty': 1,
                    'price_unit': 6.7,
                    'discount': 0,
                    'tax_id': tax_a.ids,
                }),
            ],
        })
        self.assertEqual(sale_order.amount_total, 15.42, "")

        # Test Round Globally
        self.env.company.tax_calculation_rounding_method = 'round_globally'
        sale_order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [
                Command.create({
                    'product_id': self.product.id,
                    'product_uom_qty': 1,
                    'price_unit': 6.7,
                    'discount': 0,
                    'tax_id': tax_a.ids,
                }),
                Command.create({
                    'product_id': self.product.id,
                    'product_uom_qty': 1,
                    'price_unit': 6.7,
                    'discount': 0,
                    'tax_id': tax_a.ids,
                }),
            ],
        })
        self.assertEqual(sale_order.amount_total, 15.41, "")

    def test_order_auto_lock_with_public_user(self):
        public_user = self.env.ref('base.public_user')
        self.sale_order.create_uid.groups_id += self.env.ref('sale.group_auto_done_setting')
        self.sale_order.with_user(public_user.id).sudo().action_confirm()

        self.assertFalse(public_user.has_group('sale.group_auto_done_setting'))
        self.assertTrue(self.sale_order.locked)

    def test_draft_quotation_followers(self):
        sale_order = self.env['sale.order'].create({
            'partner_id': self.partner1.id,
        })

        sale_order.partner_id = self.partner2

        self.assertNotIn(self.partner2, sale_order.message_partner_ids)

    def test_sent_quotation_followers(self):
        sale_order = self.env['sale.order'].create({
            'partner_id': self.partner1.id,
        })
        sale_order.action_quotation_sent()

        sale_order.partner_id = self.partner2

        self.assertIn(self.partner2, sale_order.message_partner_ids)

    def test_so_discount_is_not_reset(self):
        """ Discounts should not be recomputed on order confirmation """
        with patch(
            'odoo.addons.sale.models.sale_order_line.SaleOrderLine'
            '._compute_discount'
        ) as patched:
            self.sale_order.action_confirm()
            self.sale_order.order_line.flush_recordset(['discount'])
            patched.assert_not_called()

    def test_so_company_empty(self):
        """Check emptying company on SO form"""
        company_2 = self.env['res.company'].create({
            'name': 'Company 2'
        })
        self.env.companies = [self.env.company, company_2]
        so_form = Form(self.env['sale.order'])
        with self.assertRaises(ValidationError):
            so_form.company_id = self.env['res.company']

    def test_so_is_not_invoiceable_if_only_discount_line_is_to_invoice(self):
        self.sale_order.order_line.product_id.invoice_policy = 'delivery'
        self.sale_order.action_confirm()

        self.assertEqual(self.sale_order.invoice_status, 'no')
        standard_lines = self.sale_order.order_line

        self.env['sale.order.discount'].create({
            'sale_order_id': self.sale_order.id,
            'discount_amount': 33,
            'discount_type': 'amount',
        }).action_apply_discount()

        # Only the discount line is invoiceable (there are lines not invoiced and not invoiceable)
        discount_line = self.sale_order.order_line - standard_lines
        self.assertEqual(discount_line.invoice_status, 'to invoice')
        self.assertEqual(self.sale_order.invoice_status, 'no')

    def test_so_is_invoiceable_if_only_discount_line_remains_to_invoice(self):
        self.sale_order.order_line.product_id.invoice_policy = 'delivery'
        self.sale_order.action_confirm()

        self.assertEqual(self.sale_order.invoice_status, 'no')
        standard_lines = self.sale_order.order_line

        for sol in standard_lines:
            sol.qty_delivered = sol.product_uom_qty
        self.sale_order._create_invoices()

        self.assertEqual(self.sale_order.invoice_status, 'invoiced')

        self.env['sale.order.discount'].create({
            'sale_order_id': self.sale_order.id,
            'discount_amount': 33,
            'discount_type': 'amount',
        }).action_apply_discount()

        # Only the discount line is invoiceable (there are no other lines remaining to invoice)
        discount_line = (self.sale_order.order_line - standard_lines)
        self.assertEqual(discount_line.invoice_status, 'to invoice')
        self.assertEqual(self.sale_order.invoice_status, 'to invoice')

    def test_sale_order_line_product_taxes_on_branch(self):
        """ Check taxes populated on SO lines from product on branch company.
            Taxes from the branch company should be taken with a fallback on parent company.
        """
        # create the following branch hierarchy:
        #     Parent company
        #         |----> Branch X
        #                   |----> Branch XX
        company = self.env.company
        branch_x = self.env['res.company'].create({
            'name': 'Branch X',
            'country_id': company.country_id.id,
            'parent_id': company.id,
        })
        branch_xx = self.env['res.company'].create({
            'name': 'Branch XX',
            'country_id': company.country_id.id,
            'parent_id': branch_x.id,
        })
        # create taxes for the parent company and its branches
        tax_groups = self.env['account.tax.group'].create([{
            'name': 'Tax Group',
            'company_id': company.id,
        }, {
            'name': 'Tax Group X',
            'company_id': branch_x.id,
        }, {
            'name': 'Tax Group XX',
            'company_id': branch_xx.id,
        }])
        tax_a = self.env['account.tax'].create({
            'name': 'Tax A',
            'type_tax_use': 'sale',
            'amount_type': 'percent',
            'amount': 10,
            'tax_group_id': tax_groups[0].id,
            'company_id': company.id,
        })
        tax_b = self.env['account.tax'].create({
            'name': 'Tax B',
            'type_tax_use': 'sale',
            'amount_type': 'percent',
            'amount': 15,
            'tax_group_id': tax_groups[0].id,
            'company_id': company.id,
        })
        tax_x = self.env['account.tax'].create({
            'name': 'Tax X',
            'type_tax_use': 'sale',
            'amount_type': 'percent',
            'amount': 20,
            'tax_group_id': tax_groups[1].id,
            'company_id': branch_x.id,
        })
        tax_xx = self.env['account.tax'].create({
            'name': 'Tax XX',
            'type_tax_use': 'sale',
            'amount_type': 'percent',
            'amount': 25,
            'tax_group_id': tax_groups[2].id,
            'company_id': branch_xx.id,
        })
        # create several products with different taxes combination
        product_all_taxes = self.env['product.product'].create({
            'name': 'Product all taxes',
            'taxes_id': [Command.set((tax_a + tax_b + tax_x + tax_xx).ids)],
        })
        product_no_xx_tax = self.env['product.product'].create({
            'name': 'Product no tax from XX',
            'taxes_id': [Command.set((tax_a + tax_b + tax_x).ids)],
        })
        product_no_branch_tax = self.env['product.product'].create({
            'name': 'Product no tax from branch',
            'taxes_id': [Command.set((tax_a + tax_b).ids)],
        })
        product_no_tax = self.env['product.product'].create({
            'name': 'Product no tax',
            'taxes_id': [],
        })
        # create a SO from Branch XX
        so_form = Form(self.env['sale.order'].with_company(branch_xx))
        so_form.partner_id = self.partner
        # add 4 SO lines with the different products:
        # - Product all taxes           => tax from Branch XX should be set
        # - Product no tax from XX      => tax from Branch X should be set
        # - Product no tax from branch  => 2 taxes from parent company should be set
        # - Product no tax              => no tax should be set
        with so_form.order_line.new() as line:
            line.product_id = product_all_taxes
        with so_form.order_line.new() as line:
            line.product_id = product_no_xx_tax
        with so_form.order_line.new() as line:
            line.product_id = product_no_branch_tax
        with so_form.order_line.new() as line:
            line.product_id = product_no_tax
        so = so_form.save()
        self.assertRecordValues(so.order_line, [
            {'product_id': product_all_taxes.id, 'tax_id': tax_xx.ids},
            {'product_id': product_no_xx_tax.id, 'tax_id': tax_x.ids},
            {'product_id': product_no_branch_tax.id, 'tax_id': (tax_a + tax_b).ids},
            {'product_id': product_no_tax.id, 'tax_id': []},
        ])

    def test_price_recomputation_on_readonly_unit_price(self):
        """Make sure that price computation works fine when unit price is readonly.

        Since the client doesn't send readonly fields, flagging the field as readonly
        will result in the `price_unit` being absent from the values, but not the
        `technical_price_unit` field, which would disable the price computation.

        This test makes sure that the `technical_price_unit` is correctly discarded
        if not provided in the same request as the `price_unit`
        """
        self.pricelist.item_ids = [
            Command.create({
                'product_id': self.product.id,
                'fixed_price': 22.0,
                'min_quantity': 3.0,
            })
        ]

        # Order update
        product_sol = self.sale_order.order_line[0]
        self.assertNotEqual(product_sol.price_unit, 22)
        self.sale_order.write({
            'order_line': [Command.update(
                product_sol.id,
                {'product_uom_qty': 4.0, 'technical_price_unit': 22.0}
            )],
        })
        self.assertEqual(product_sol.price_unit, 22.0)

        # Order creation
        new_order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [
                Command.create({
                    'product_id': self.product.id,
                    'product_uom_qty': 5.0,
                    'technical_price_unit': 22.0,
                }),
            ],
        })
        self.assertEqual(new_order.order_line.price_unit, 22.0)


@tagged('post_install', '-at_install')
class TestSaleOrderInvoicing(AccountTestInvoicingCommon, SaleCommon):
    def test_invoice_state_when_ordered_quantity_is_negative(self):
        """When you invoice a SO line with a product that is invoiced on ordered quantities and has negative ordered quantity,
        this test ensures that the  invoicing status of the SO line is 'invoiced' (and not 'upselling')."""
        sale_order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [(0, 0, {
                'product_id': self.product.id,
                'product_uom_qty': -1,
            })]
        })
        sale_order.action_confirm()
        sale_order._create_invoices(final=True)
        self.assertTrue(sale_order.invoice_status == 'invoiced', 'Sale: The invoicing status of the SO should be "invoiced"')


@tagged('post_install', '-at_install')
class TestSalesTeam(SaleCommon):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()

        # set up users
        cls.sale_team_2 = cls.env['crm.team'].create({
            'name': 'Test Sales Team (2)',
        })
        cls.user_in_team = cls.env['res.users'].create({
            'email': 'team0user@example.com',
            'login': 'team0user',
            'name': 'User in Team 0',
        })
        cls.sale_team.write({'member_ids': [4, cls.user_in_team.id]})
        cls.user_not_in_team = cls.env['res.users'].create({
            'email': 'noteamuser@example.com',
            'login': 'noteamuser',
            'name': 'User Not In Team',
        })

    def test_assign_sales_team_from_partner_user(self):
        """Use the team from the customer's sales person, if it is set"""
        partner = self.env['res.partner'].create({
            'name': 'Customer of User In Team',
            'user_id': self.user_in_team.id,
        })
        sale_order = self.env['sale.order'].create({
            'partner_id': partner.id,
        })
        self.assertEqual(sale_order.team_id.id, self.sale_team.id, 'Should assign to team of sales person')

    def test_assign_sales_team_when_changing_user(self):
        """When we assign a sales person, change the team on the sales order to their team"""
        sale_order = self.env['sale.order'].create({
            'user_id': self.user_not_in_team.id,
            'partner_id': self.partner.id,
            'team_id': self.sale_team_2.id
        })
        sale_order.user_id = self.user_in_team
        self.assertEqual(sale_order.team_id.id, self.sale_team.id, 'Should assign to team of sales person')

    def test_keep_sales_team_when_changing_user_with_no_team(self):
        """When we assign a sales person that has no team, do not reset the team to default"""
        sale_order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'team_id': self.sale_team_2.id
        })
        sale_order.user_id = self.user_not_in_team
        self.assertEqual(sale_order.team_id.id, self.sale_team_2.id, 'Should not reset the team to default')

    def test_sale_order_analytic_distribution_change(self):
        self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')

        analytic_plan = self.env['account.analytic.plan'].create({'name': 'Plan Test'})
        analytic_account_super = self.env['account.analytic.account'].create({'name': 'Super Account', 'plan_id': analytic_plan.id})
        analytic_account_great = self.env['account.analytic.account'].create({'name': 'Great Account', 'plan_id': analytic_plan.id})
        super_product = self.env['product.product'].create({'name': 'Super Product'})
        great_product = self.env['product.product'].create({'name': 'Great Product'})
        product_no_account = self.env['product.product'].create({'name': 'Product No Account'})
        self.env['account.analytic.distribution.model'].create([
            {
                'analytic_distribution': {analytic_account_super.id: 100},
                'product_id': super_product.id,
            },
            {
                'analytic_distribution': {analytic_account_great.id: 100},
                'product_id': great_product.id,
            },
        ])
        partner = self.env['res.partner'].create({'name': 'Test Partner'})
        sale_order = self.env['sale.order'].create({
            'partner_id': partner.id,
        })
        sol = self.env['sale.order.line'].create({
            'name': super_product.name,
            'product_id': super_product.id,
            'order_id': sale_order.id,
        })

        self.assertEqual(sol.analytic_distribution, {str(analytic_account_super.id): 100}, "The analytic distribution should be set to Super Account")
        sol.write({'product_id': great_product.id})
        self.assertEqual(sol.analytic_distribution, {str(analytic_account_great.id): 100}, "The analytic distribution should be set to Great Account")

        so_no_analytic_account = self.env['sale.order'].create({
            'partner_id': partner.id,
        })
        sol_no_analytic_account = self.env['sale.order.line'].create({
            'name': super_product.name,
            'product_id': super_product.id,
            'order_id': so_no_analytic_account.id,
            'analytic_distribution': False,
        })
        so_no_analytic_account.action_confirm()
        self.assertFalse(sol_no_analytic_account.analytic_distribution, "The compute should not overwrite what the user has set.")

        sale_order.action_confirm()
        sol_on_confirmed_order = self.env['sale.order.line'].create({
            'name': super_product.name,
            'product_id': super_product.id,
            'order_id': sale_order.id,
        })

        self.assertEqual(
            sol_on_confirmed_order.analytic_distribution,
            {str(analytic_account_super.id): 100},
            "The analytic distribution should be set to Super Account, even for confirmed orders"
        )


    def test_cannot_assign_tax_of_mismatch_company(self):
        """ Test that sol cannot have assigned tax belonging to a different company from that of the sale order. """
        company_a = self.env['res.company'].create({'name': 'A'})
        company_b = self.env['res.company'].create({'name': 'B'})
        tax_group_a = self.env['account.tax.group'].create({'name': 'A', 'company_id': company_a.id})
        tax_group_b = self.env['account.tax.group'].create({'name': 'B', 'company_id': company_b.id})
        country = self.env['res.country'].search([], limit=1)

        tax_a = self.env['account.tax'].create({
            'name': 'A',
            'amount': 10,
            'company_id': company_a.id,
            'tax_group_id': tax_group_a.id,
            'country_id': country.id,
        })
        tax_b = self.env['account.tax'].create({
            'name': 'B',
            'amount': 10,
            'company_id': company_b.id,
            'tax_group_id': tax_group_b.id,
            'country_id': country.id,
        })

        sale_order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'company_id': company_a.id
        })
        product = self.env['product.product'].create({'name': 'Product'})

        # In sudo to simulate an user that have access to both companies.
        sol = self.env['sale.order.line'].sudo().create({
            'name': product.name,
            'product_id': product.id,
            'order_id': sale_order.id,
            'tax_id': tax_a,
        })

        with self.assertRaises(UserError):
            sol.tax_id = tax_b

    def test_assign_tax_multi_company(self):
        root_company = self.env['res.company'].create({'name': 'B0 company'})
        root_company.write({'child_ids': [
            Command.create({'name': 'B1 company'}),
            Command.create({'name': 'B2 company'}),
        ]})

        country = self.env['res.country'].search([], limit=1)
        basic_tax_group = self.env['account.tax.group'].create({'name': 'basic group', 'country_id': country.id})
        tax_b0 = self.env['account.tax'].create({
            'name': 'B0 tax',
            'company_id': root_company.id,
            'amount': 10,
            'tax_group_id': basic_tax_group.id,
            'country_id': country.id,
        })
        tax_b1 = self.env['account.tax'].create({
            'name': 'B1 tax',
            'company_id': root_company.child_ids[0].id,
            'amount': 11,
            'tax_group_id': basic_tax_group.id,
            'country_id': country.id,
        })
        tax_b2 = self.env['account.tax'].create({
            'name': 'B2 tax',
            'company_id': root_company.child_ids[1].id,
            'amount': 20,
            'tax_group_id': basic_tax_group.id,
            'country_id': country.id,
        })

        sale_order = self.env['sale.order'].create({'partner_id': self.partner.id, 'company_id': root_company.child_ids[0].id})
        product = self.env['product.product'].create({'name': 'Product'})

        # In sudo to simulate an user that have access to both companies.
        sol_b1 = self.env['sale.order.line'].sudo().create({
            'name': product.name,
            'product_id': product.id,
            'order_id': sale_order.id,
            'tax_id': tax_b1,
        })

        # should not raise anything
        sol_b1.tax_id = tax_b0
        sol_b1.tax_id = tax_b1
        # should raise (b2 is not on the same branch lineage as b1)
        with self.assertRaises(UserError):
            sol_b1.tax_id = tax_b2

    def test_downpayment_amount_constraints(self):
        """Down payment amounts should be in the interval ]0, 1]."""

        self.sale_order.require_payment = True
        with self.assertRaises(ValidationError):
            self.sale_order.prepayment_percent = -1
        with self.assertRaises(ValidationError):
            self.sale_order.prepayment_percent = 1.01

    def test_qty_delivered_on_creation(self):
        """Checks that the qty delivered of sol is automatically set to 0.0 when an so is created"""
        sale_order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [
                Command.create({
                    'product_id': self.product.id,
                })],
        })
        self.assertEqual(self.env['sale.order.line'].search(['&', ('order_id', '=', sale_order.id), ('qty_delivered', '=', 0.0)]), sale_order.order_line)

    def test_action_recompute_taxes(self):
        '''
        This test verifies the taxes recomputation action that can be triggered
        after updating the fiscal position on a sale order document.
        '''
        special_tax = self.env['account.tax'].create({
            'name': "special_tax_10",
            'amount_type': 'percent',
            'amount': 25.0,
            'include_base_amount': True,
            'price_include_override': 'tax_included',
        })

        mapped_tax_a = self.env['account.tax'].create({
            'name': "tax_a",
            'amount_type': 'percent',
            'amount': 12.5,
            'include_base_amount': True,
            'price_include_override': 'tax_included',
        })

        mapped_tax_b = self.env['account.tax'].create({
            'name': "tax_b",
            'amount_type': 'percent',
            'amount': 5.0,
            'include_base_amount': True,
            'price_include_override': 'tax_included',
        })

        sales_tax = self.env['account.tax'].create({
            'name': "VAT 20%",
            'amount_type': 'percent',
            'amount': 20.0,
            'price_include_override': 'tax_included',
        })

        mapping_a = self.env['account.fiscal.position'].create({
            'name': 'Special Tax Reduction',
            'tax_ids': [Command.create({'tax_src_id': special_tax.id, 'tax_dest_id': mapped_tax_a.id})],
        })
        mapping_b = self.env['account.fiscal.position'].create({
            'name': 'Special Tax Reduction',
            'tax_ids': [Command.create({'tax_src_id': special_tax.id, 'tax_dest_id': mapped_tax_b.id})],
        })

        # taxes and standard price need to be set on the product, as they will be
        # recomputed when changing the fiscal position.
        self.product.write({
            'lst_price': 300,
            'taxes_id': [Command.set((special_tax + sales_tax).ids)],
        })

        order = self.env['sale.order'].create({
            'partner_id': self.partner.id,
            'order_line': [
                Command.create({
                    'product_id': self.product.id,
                    'product_uom_qty': 1.0,
                }),
            ],
        })

        self.assertEqual(order.amount_total, 300)
        self.assertEqual(order.amount_tax, 100)
        order.fiscal_position_id = mapping_a
        order._recompute_prices()
        order.action_update_taxes()
        self.assertEqual(order.amount_total, 270)
        self.assertEqual(order.amount_tax, 70)
        order.fiscal_position_id = mapping_b
        order._recompute_prices()
        order.action_update_taxes()
        self.assertEqual(order.amount_total, 252)
        self.assertEqual(order.amount_tax, 52)

@tagged('post_install', '-at_install')
class TestSaleMailComposerUI(MailCommon, HttpCase):
    @classmethod
    def setUpClass(cls):
        super(TestSaleMailComposerUI, cls).setUpClass()
        cls.env['mail.alias.domain'].create({'name': 'example.com'})
        cls.partner = cls.env['res.partner'].create({
            'name': 'test customer',
            'email': 'dummy@example.com'
        })
        cls.quotation = cls.env['sale.order'].create({
            'partner_id': cls.partner.id,
        })

    def test_mail_attachment_removal_tour(self):
        url = f"/odoo/sales/{self.quotation.id}"
        with self.mock_mail_app():
            self.start_tour(
                url,
                "mail_attachment_removal_tour",
                login="admin",
            )
