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

from datetime import date

from freezegun import freeze_time

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

from odoo.addons.hr_expense.tests.common import TestExpenseCommon


@tagged('-at_install', 'post_install')
class TestExpenses(TestExpenseCommon):
    #############################################
    #  Test Expense flows
    #############################################
    def test_expense_main_flow(self):
        """
        Test the main flows of expense
        This includes:
            - Approval flows for expense paid by company and employee up to reconciliation
            - price_unit, total_amount_currency and quantity computation
            - Split payments into one payment per expense when paid by company
            - Override account on expense
            - Payment states and payment terms
            - Unlinking payments reverts to approved state
            - Cannot delete an analytic account if linked to an expense
        """

        self.expense_employee.user_partner_id.property_supplier_payment_term_id = self.env.ref('account.account_payment_term_30days')
        expenses_by_employee = self.create_expenses([
            {
                'name': 'Employee PA 2*800 + 15%',  # Taxes are included
                'employee_id': self.expense_employee.id,
                'account_id': self.expense_account.id,  # Test with a specific account override
                'product_id': self.product_a.id,
                'quantity': 2,
                'payment_mode': 'own_account',
                'company_id': self.company_data['company'].id,
                'date': '2021-10-14',
                'analytic_distribution': {self.analytic_account_1.id: 100},
            }, {
                'name': 'Employee PB 160 + 2*15%',  # Taxes are included
                'employee_id': self.expense_employee.id,
                'product_id': self.product_b.id,
                'payment_mode': 'own_account',
                'company_id': self.company_data['company'].id,
                'date': '2021-10-13',
                'analytic_distribution': {self.analytic_account_2.id: 100},
            },
        ])
        expenses_by_company = self.create_expenses([
            {
                'name': 'Company PC 1000 + 15%',  # Taxes are included
                'employee_id': self.expense_employee.id,
                'product_id': self.product_c.id,
                'total_amount_currency': 1000.00,
                'date': '2021-10-12',
                'payment_mode': 'company_account',
                'company_id': self.company_data['company'].id,
                'tax_ids': [Command.set(self.tax_purchase_a.ids)],
            }, {
                'name': 'Company PB 160 + 2*15%',  # Taxes are included
                'employee_id': self.expense_employee.id,
                'product_id': self.product_b.id,
                'payment_mode': 'company_account',
                'company_id': self.company_data['company'].id,
                'date': '2021-10-11',
            },
        ])
        all_expenses = expenses_by_employee | expenses_by_company

        # Checking expense values at creation
        self.assertRecordValues(all_expenses, [
            {'name': 'Employee PA 2*800 + 15%', 'total_amount_currency': 1600.00, 'untaxed_amount_currency': 1391.30, 'price_unit':  800.00, 'tax_amount_currency': 208.70, 'state': 'draft'},
            {'name': 'Employee PB 160 + 2*15%', 'total_amount_currency':  160.00, 'untaxed_amount_currency':  123.08, 'price_unit':  160.00, 'tax_amount_currency':  36.92, 'state': 'draft'},
            {'name': 'Company PC 1000 + 15%',   'total_amount_currency': 1000.00, 'untaxed_amount_currency':  869.57, 'price_unit': 1000.00, 'tax_amount_currency': 130.43, 'state': 'draft'},
            {'name': 'Company PB 160 + 2*15%',  'total_amount_currency':  160.00, 'untaxed_amount_currency':  123.08, 'price_unit':  160.00, 'tax_amount_currency':  36.92, 'state': 'draft'},
        ])

        # Submitting properly change states
        all_expenses.action_submit()
        self.assertRecordValues(all_expenses, [
            {'state': 'submitted'},
            {'state': 'submitted'},
            {'state': 'submitted'},
            {'state': 'submitted'},
        ])

        # Approving properly change states & create moves & payments
        all_expenses.action_approve()
        self.assertRecordValues(all_expenses, [
            {'state': 'approved', 'account_move_id': False},
            {'state': 'approved', 'account_move_id': False},
            {'state': 'approved', 'account_move_id': False},
            {'state': 'approved', 'account_move_id': False},
        ])
        # Post a payment for 'company_account' (and its move(s)) and a receipt  for 'own_account'
        expenses_by_company.action_post()
        self.post_expenses_with_wizard(expenses_by_employee[0], date=date(2021, 10, 10))
        self.post_expenses_with_wizard(expenses_by_employee[1], date=date(2021, 10, 31))
        self.assertRecordValues(all_expenses, [
            # As the payment is not done yet those are still in "posted"
            {'payment_mode': 'own_account',     'state': 'posted'},
            {'payment_mode': 'own_account',     'state': 'posted'},
            # Expenses paid by company don't use accounting date since they are already paid and posted directly
            {'payment_mode': 'company_account', 'state': 'paid'},
            {'payment_mode': 'company_account', 'state': 'paid'},
        ])

        employee_partner_id = self.expense_user_employee.partner_id.id
        self.assertRecordValues(expenses_by_employee.account_move_id, [
            {'amount_total': 1600.00, 'ref': 'Employee PA 2*800 + 15%', 'state': 'posted', 'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 10), 'partner_id': employee_partner_id},
            {'amount_total':  160.00, 'ref': 'Employee PB 160 + 2*15%', 'state': 'posted', 'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 31), 'partner_id': employee_partner_id},
        ])

        self.assertRecordValues(expenses_by_company.account_move_id, [
            {'amount_total':  1000.00, 'ref': 'Company PC 1000 + 15%',  'state': 'posted', 'date': date(2021, 10, 12), 'invoice_date': False, 'partner_id': False},
            {'amount_total':   160.00, 'ref': 'Company PB 160 + 2*15%', 'state': 'posted', 'date': date(2021, 10, 11), 'invoice_date': False, 'partner_id': False},
        ])

        tax_account_id = self.company_data['default_account_tax_purchase'].id
        default_account_payable_id = self.company_data['default_account_payable'].id
        product_b_account_id = self.product_b.property_account_expense_id.id
        product_c_account_id = self.product_c.property_account_expense_id.id
        company_payment_account_id = self.outbound_payment_method_line.payment_account_id.id
        # One payment per expense
        self.assertRecordValues(all_expenses.account_move_id.line_ids.sorted(lambda line: (line.move_id, line)), [
            # own_account expense 1 move
            {'balance':  1391.30, 'account_id': self.expense_account.id,    'name': 'expense_employee: Employee PA 2*800 + 15%', 'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 10)},
            {'balance':   208.70, 'account_id': tax_account_id,             'name': '15%',                                       'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 10)},
            {'balance': -1600.00, 'account_id': default_account_payable_id, 'name': False,                                       'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 10)},

            # own_account expense 2 move
            {'balance':   123.08, 'account_id': product_b_account_id,       'name': 'expense_employee: Employee PB 160 + 2*15%', 'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 31)},
            {'balance':    18.46, 'account_id': tax_account_id,             'name': '15%',                                       'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 31)},
            {'balance':    18.46, 'account_id': tax_account_id,             'name': '15% (copy)',                                'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 31)},
            {'balance':  -160.00, 'account_id': default_account_payable_id, 'name': False,                                       'date': date(2021, 10, 31), 'invoice_date': date(2021, 10, 31)},

            # company_account expense 1 move
            {'balance':   869.57, 'account_id': product_c_account_id,       'name': 'expense_employee: Company PC 1000 + 15%',   'date': date(2021, 10, 12), 'invoice_date': False},
            {'balance':   130.43, 'account_id': tax_account_id,             'name': '15%',                                       'date': date(2021, 10, 12), 'invoice_date': False},
            {'balance': -1000.00, 'account_id': company_payment_account_id, 'name': 'expense_employee: Company PC 1000 + 15%',   'date': date(2021, 10, 12), 'invoice_date': False},

            # company_account expense 2 move
            {'balance':  123.08, 'account_id': product_b_account_id,        'name': 'expense_employee: Company PB 160 + 2*15%',  'date': date(2021, 10, 11), 'invoice_date': False},
            {'balance':   18.46, 'account_id': tax_account_id,              'name': '15%',                                       'date': date(2021, 10, 11), 'invoice_date': False},
            {'balance':   18.46, 'account_id': tax_account_id,              'name': '15% (copy)',                                'date': date(2021, 10, 11), 'invoice_date': False},
            {'balance': -160.00, 'account_id': company_payment_account_id,  'name': 'expense_employee: Company PB 160 + 2*15%',  'date': date(2021, 10, 11), 'invoice_date': False},

        ])

        # Check lines partners:
        self.assertRecordValues(expenses_by_employee.account_move_id.line_ids, [
            # If the test fails, it is probably because the partner is the company's partner instead of the employee's one
            {'partner_id': employee_partner_id},
            {'partner_id': employee_partner_id},
            {'partner_id': employee_partner_id},
            {'partner_id': employee_partner_id},
            {'partner_id': employee_partner_id},
            {'partner_id': employee_partner_id},
            {'partner_id': employee_partner_id},
        ])

        in_payment_state = expenses_by_employee.account_move_id._get_invoice_in_payment_state()
        first_expense_by_employee = expenses_by_employee[0]
        first_expense_by_company = expenses_by_company[0]

        # Own_account partial payment
        payment_1 = self.get_new_payment(first_expense_by_employee, 1000.0)
        liquidity_lines1 = payment_1._seek_for_lines()[0]
        self.assertEqual(first_expense_by_employee.state, in_payment_state)

        # own_account remaining payment
        payment_2 = self.get_new_payment(first_expense_by_employee, 600.0)
        liquidity_lines2 = payment_2._seek_for_lines()[0]
        self.assertEqual(first_expense_by_employee.state, in_payment_state)

        # Reconciling own_account
        statement_line = self.env['account.bank.statement.line'].create({
            'journal_id': self.company_data['default_journal_bank'].id,
            'payment_ref': 'pay_ref',
            'amount': -1600.0,
            'partner_id': self.expense_employee.work_contact_id.id,
        })

        # Reconcile without the bank reconciliation widget since the widget is in enterprise.
        _trash, st_suspense_lines, _trash = statement_line.with_context(skip_account_move_synchronization=True)._seek_for_lines()
        st_suspense_lines.account_id = liquidity_lines1.account_id
        (st_suspense_lines + liquidity_lines1 + liquidity_lines2).reconcile()
        self.assertEqual(first_expense_by_employee.state, 'paid')

        # Trying to delete analytic accounts should be forbidden if linked to an expense
        with self.assertRaises(UserError):
            (self.analytic_account_1 | self.analytic_account_2).unlink()

        # Unlinking moves
        (payment_1 | payment_2).action_draft()
        (payment_1 | payment_2).move_id.line_ids.remove_move_reconcile()
        self.assertEqual(first_expense_by_employee.state, 'posted')
        expenses_by_employee.account_move_id.button_draft()
        expenses_by_employee.account_move_id.unlink()
        self.assertFalse(expenses_by_employee.account_move_id)

        first_expense_by_company.account_move_id.origin_payment_id.unlink()
        self.assertFalse(first_expense_by_company.account_move_id)

        self.assertRecordValues(first_expense_by_employee | first_expense_by_company, [
            {'payment_mode': 'own_account',     'state': 'approved'},
            {'payment_mode': 'company_account', 'state': 'approved'},
        ])

        first_expense_by_employee.action_reset()
        self.assertEqual(first_expense_by_employee.state, 'draft')
        first_expense_by_employee.unlink()
        # Only possible if no expense linked to the account
        self.analytic_account_1.unlink()

    def test_expense_split_flow(self):
        """ Check Split Expense flow. """
        # Grant Analytic Accounting rights, to be able to modify analytic_distribution from the wizard
        self.env.user.group_ids += self.env.ref('analytic.group_analytic_accounting')

        expense = self.create_expenses({
            'tax_ids': [Command.set(self.tax_purchase_a.ids)],
            'analytic_distribution': {self.analytic_account_1.id: 100}
        })

        wizard = self.env['hr.expense.split.wizard'].browse(expense.action_split_wizard()['res_id'])

        # Check default hr.expense.split values
        self.assertRecordValues(wizard.expense_split_line_ids, [
            {
                'name': expense.name,
                'wizard_id': wizard.id,
                'expense_id': expense.id,
                'product_id': expense.product_id.id,
                'tax_ids': expense.tax_ids.ids,
                'total_amount_currency': expense.total_amount_currency / 2,
                'tax_amount_currency': 65.22,
                'employee_id': expense.employee_id.id,
                'company_id': expense.company_id.id,
                'currency_id': expense.currency_id.id,
                'analytic_distribution': expense.analytic_distribution,
            }] * 2)
        self.assertRecordValues(wizard, [{'split_possible': True, 'total_amount_currency': expense.total_amount_currency}])

        with Form(wizard) as form:
            form.expense_split_line_ids.remove(index=0)
            self.assertEqual(form.split_possible, False)

            # Check removing tax_ids and analytic_distribution
            with form.expense_split_line_ids.edit(0) as line:
                line.total_amount_currency = 200.00
                line.tax_ids.clear()
                line.analytic_distribution = {}
                self.assertEqual(line.total_amount_currency, 200.00)
                self.assertEqual(line.tax_amount_currency, 0.00)
            self.assertEqual(form.split_possible, False)

            # This line should have the same tax_ids and analytic_distribution as original expense
            with form.expense_split_line_ids.new() as line:
                line.total_amount_currency = 300.00
                self.assertEqual(line.total_amount_currency, 300.00)
                self.assertEqual(line.tax_amount_currency, 39.13)
                self.assertDictEqual(line.analytic_distribution, expense.analytic_distribution)
            self.assertEqual(form.split_possible, False)
            self.assertEqual(form.total_amount_currency, 500.00)

            # Check adding tax_ids and setting analytic_distribution
            with form.expense_split_line_ids.new() as line:
                line.total_amount_currency = 500.00
                line.tax_ids.add(self.tax_purchase_b)
                line.analytic_distribution = {self.analytic_account_2.id: 100}
                self.assertEqual(line.total_amount_currency, 500.00)
                self.assertEqual(line.tax_amount_currency, 115.38)

        # Check wizard values
        self.assertRecordValues(wizard, [
            {'total_amount_currency': 1000.00, 'total_amount_currency_original': 1000.00, 'tax_amount_currency': 154.51, 'split_possible': True}
        ])

        wizard.action_split_expense()
        # Check that split resulted into expenses with correct values
        expenses_after_split = self.env['hr.expense'].search([('name', '=', expense.name)])
        self.assertRecordValues(expenses_after_split.sorted('total_amount_currency'), [
            {
                'name': expense.name,
                'employee_id': expense.employee_id.id,
                'product_id': expense.product_id.id,
                'total_amount_currency': 200.00,
                'tax_ids': [],
                'tax_amount_currency': 0.00,
                'untaxed_amount_currency': 200.00,
                'analytic_distribution': False,
                'split_expense_origin_id': expense.id,
            }, {
                'name': expense.name,
                'employee_id': expense.employee_id.id,
                'product_id': expense.product_id.id,
                'total_amount_currency': 300.00,
                'tax_ids': [self.tax_purchase_a.id],
                'tax_amount_currency': 39.13,
                'untaxed_amount_currency': 260.87,
                'analytic_distribution': {str(self.analytic_account_1.id): 100},
                'split_expense_origin_id': expense.id,
            }, {
                'name': expense.name,
                'employee_id': expense.employee_id.id,
                'product_id': expense.product_id.id,
                'total_amount_currency': 500.00,
                'tax_ids': [self.tax_purchase_a.id, self.tax_purchase_b.id],
                'tax_amount_currency': 115.38,
                'untaxed_amount_currency': 384.62,
                'analytic_distribution': {str(self.analytic_account_2.id): 100},
                'split_expense_origin_id': expense.id,
            }
        ])

    #############################################
    #  Test Multi-currency
    #############################################

    def test_expense_multi_currencies(self):
        """
        Checks that the currency rate is recomputed properly when the total in company currency is set to a new value
        """
        foreign_currency_1 = self.other_currency
        foreign_currency_2 = self.setup_other_currency('GBP', rounding=0.01, rates=([('2016-01-01', 1 / 1.52)]))
        foreign_sale_journal = self.company_data['default_journal_sale'].copy()
        foreign_sale_journal.currency_id = foreign_currency_2.id

        foreign_expense_1, foreign_expense_2, foreign_expense_3 = self.create_expenses([{
                'name': 'foreign_expense_1',
                'payment_mode': 'company_account',
                'employee_id': self.expense_employee.id,
                'product_id': self.product_c.id,
                'total_amount_currency': 1000.00,
                'date': self.frozen_today,
                'company_id': self.company_data['company'].id,
                'currency_id': foreign_currency_1.id,  # rate is 1:2
                'tax_ids': [Command.set(self.tax_purchase_a.ids)],
            },
            {
                'name': 'foreign_expense_2',
                'payment_mode': 'company_account',
                'employee_id': self.expense_employee.id,
                'product_id': self.product_c.id,
                'total_amount_currency': 1000.00,
                'date': self.frozen_today,
                'company_id': self.company_data['company'].id,
                'currency_id': foreign_currency_2.id,  # rate is 1:1.52
                'tax_ids': [Command.set(self.tax_purchase_a.ids)],
            },
            {
                'name': 'foreign_expense_3',
                'payment_mode': 'company_account',
                'employee_id': self.expense_employee.id,
                'product_id': self.product_c.id,
                'total_amount_currency': 1000.00,
                'total_amount': 3000.00,
                'date': self.frozen_today,
                'company_id': self.company_data['company'].id,
                'currency_id': foreign_currency_2.id,  # default rate is 1:1.52, should be overridden to 1:3
                'tax_ids': [Command.set(self.tax_purchase_a.ids)],
            },
        ]).sorted('name')
        all_expenses = foreign_expense_1 | foreign_expense_2 | foreign_expense_3
        self.assertRecordValues(all_expenses, [
            {'total_amount':  500.00, 'total_amount_currency': 1000.00, 'currency_rate': 0.50},
            {'total_amount': 1520.00, 'total_amount_currency': 1000.00, 'currency_rate': 1.52},
            {'total_amount': 3000.00, 'total_amount_currency': 1000.00, 'currency_rate': 3.00},
        ])

        # Manually changing rate on the two first expenses after creation to check they recompute properly
        # Back-end override
        foreign_expense_1.total_amount = 1000.00
        self.assertRecordValues(foreign_expense_1, [
            {'total_amount': 1000.00, 'total_amount_currency': 1000.00, 'currency_rate': 1.0},
        ])

        # Front-end override
        with Form(foreign_expense_2) as expense_form:
            expense_form.total_amount = 2000.00
        self.assertRecordValues(foreign_expense_2, [
            {'total_amount': 2000.00, 'total_amount_currency': 1000.00, 'currency_rate': 2.0},
        ])

        # Move creation should not touch the rates anymore
        all_expenses.action_submit()
        all_expenses._do_approve()  # Skip duplicate wizard
        self.post_expenses_with_wizard(all_expenses, journal=foreign_sale_journal)
        self.assertRecordValues(all_expenses.account_move_id.sorted('id'), [
            {'amount_total_in_currency_signed': 1000.00, 'amount_total_signed': 1000.00, 'currency_id': foreign_currency_1.id},
            {'amount_total_in_currency_signed': 1000.00, 'amount_total_signed': 2000.00, 'currency_id': foreign_currency_2.id},
            {'amount_total_in_currency_signed': 1000.00, 'amount_total_signed': 3000.00, 'currency_id': foreign_currency_2.id},
        ])
        self.assertRecordValues(all_expenses.account_move_id.origin_payment_id.sorted('id'), [
            {'amount': 1000.00, 'payment_type': 'outbound', 'currency_id': foreign_currency_1.id},
            {'amount': 1000.00, 'payment_type': 'outbound', 'currency_id': foreign_currency_2.id},
            {'amount': 1000.00, 'payment_type': 'outbound', 'currency_id': foreign_currency_2.id},
        ])

    #############################################
    #  Test Corner Cases
    #############################################

    def test_expense_company_dates(self):
        expenses = self.create_expenses([
            {
                'name': 'Car Travel Expenses',
                'employee_id': self.expense_employee.id,
                'product_id': self.product_c.id,
                'total_amount': 350.00,
                'payment_mode': 'company_account',
                'date': '2024-01-01',
            },
            {
                'name': 'Lunch expense',
                'employee_id': self.expense_employee.id,
                'product_id': self.product_c.id,
                'total_amount': 90.00,
                'payment_mode': 'company_account',
                'date': '2024-01-12',
            },
        ]).sorted()  # By date desc

        expenses.action_submit()
        expenses.action_approve()
        expenses.action_post()

        move_twelve_january, move_first_january = expenses.account_move_id.sorted() # By date desc

        self.assertEqual(
            move_twelve_january.date,
            fields.Date.to_date('2024-01-12'),
            'move date should be the same as the expense date'
        )
        self.assertEqual(
            move_first_january.date,
            fields.Date.to_date('2024-01-01'),
            'move date should be the same as the expense date'
        )
        self.assertTrue(90 == move_twelve_january.amount_total == move_twelve_january.origin_payment_id.amount)
        self.assertTrue(350 == move_first_january.amount_total == move_first_january.origin_payment_id.amount)
        self.assertRecordValues(expenses, [
            {'date': fields.Date.from_string('2024-01-12'), 'total_amount':  90.00, 'state': 'paid'},
            {'date': fields.Date.from_string('2024-01-01'),'total_amount': 350.00, 'state': 'paid'},
        ])

    def test_corner_case_defaults_values_from_product(self):
        """ As soon as you set a product, the expense name, uom, taxes and account are set according to the product. """
        # Disable multi-uom
        self.env.ref('base.group_user').implied_ids -= self.env.ref('uom.group_uom')
        self.expense_user_employee.group_ids -= self.env.ref('uom.group_uom')

        # Use the expense employee
        Expense = self.env['hr.expense'].with_user(self.expense_user_employee)

        # Make sure the multi-uom is correctly disabled for the user creating the expense
        self.assertFalse(Expense.env.user.has_group('uom.group_uom'))

        # Use a product not using the default uom "Unit(s)"
        product = Expense.env.ref('hr_expense.expense_product_mileage')

        expense_form = Form(Expense)
        expense_form.product_id = product
        expense = expense_form.save()
        self.assertEqual(expense.name, product.display_name)
        self.assertEqual(expense.product_uom_id, product.uom_id)
        self.assertEqual(expense.tax_ids, product.supplier_taxes_id.filtered(lambda t: t.company_id == expense.company_id))
        self.assertEqual(expense.account_id, product._get_product_accounts()['expense'])

    def test_attachments_in_move_from_own_expense(self):
        """ Checks that journal entries created form expense reports paid by employee have a copy of the attachments in the expense. """
        expense = self.create_expenses({'name': 'Employee expense'})
        expense_2 = self.create_expenses({'name': 'Employee expense 2'})
        attachment = self.env['ir.attachment'].create({
            'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
            'name': 'file1.png',
            'res_model': 'hr.expense',
            'res_id': expense.id,
        })
        attachment_2 = self.env['ir.attachment'].create({
            'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
            'name': 'file2.png',
            'res_model': 'hr.expense',
            'res_id': expense_2.id,
        })

        expense.message_main_attachment_id = attachment
        expense_2.message_main_attachment_id = attachment_2
        expenses = expense | expense_2

        expenses.action_submit()
        expenses._do_approve()  # Skip duplicate wizard
        self.post_expenses_with_wizard(expenses)

        self.assertRecordValues(expenses.account_move_id.attachment_ids.sorted('name'), [
            {
                'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
                'name': 'file1.png',
                'res_model': 'account.move',
                'res_id': expense.account_move_id.id,
            },
            {
                'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
                'name': 'file2.png',
                'res_model': 'account.move',
                'res_id': expense_2.account_move_id.id,
            }
        ])

    def test_attachments_in_move_from_company_expense(self):
        """ Checks that journal entries created form expense reports paid by company have a copy of the attachments in the expense. """
        expense = self.create_expenses({
            'name': 'Company expense',
            'payment_mode': 'company_account',
        })
        expense_2 = self.create_expenses({
            'name': 'Company expense 2',
            'payment_mode': 'company_account',
        })
        attachment = self.env['ir.attachment'].create({
            'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
            'name': 'file1.png',
            'res_model': 'hr.expense',
            'res_id': expense.id,
        })
        attachment_2 = self.env['ir.attachment'].create({
            'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
            'name': 'file2.png',
            'res_model': 'hr.expense',
            'res_id': expense_2.id,
        })

        expense.message_main_attachment_id = attachment
        expense_2.message_main_attachment_id = attachment_2
        expenses = expense | expense_2

        expenses.action_submit()
        expenses._do_approve()  # Skip duplicate wizard
        expenses.action_post()

        expense_move = expense.account_move_id
        expense_2_move = expense_2.account_move_id
        self.assertRecordValues(expense_move.attachment_ids, [{
            'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
            'name': 'file1.png',
            'res_model': 'account.move',
            'res_id': expense_move.id
        }])

        self.assertRecordValues(expense_2_move.attachment_ids, [{
            'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
            'name': 'file2.png',
            'res_model': 'account.move',
            'res_id': expense_2_move.id
        }])

    def test_multiple_attachments_in_move_from_company_expense(self):
        """ Checks that all attachments from expense are copied to their journal entries. """
        expense = self.create_expenses({
            'name': 'Company expense',
            'payment_mode': 'company_account',
        })
        self.env['ir.attachment'].create([{
            'raw': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=",
            'name': f'file{i}.png',
            'res_model': 'hr.expense',
            'res_id': expense.id,
        } for i in range(1, 3)])

        expense.action_submit()
        expense._do_approve()  # Skip duplicate wizard
        expense.action_post()

        self.assertEqual(len(expense.account_move_id.attachment_ids), 2)

    def test_expense_payment_method(self):
        default_payment_method_line = self.company_data['default_journal_bank'].outbound_payment_method_line_ids[0]
        check_method = self.env['account.payment.method'].sudo().create({
            'name': 'Print checks',
            'code': 'check_printing_expense_test',
            'payment_type': 'outbound',
        })
        new_payment_method_line = self.env['account.payment.method.line'].create({
            'name': 'Check',
            'payment_method_id': check_method.id,
            'journal_id': self.company_data['default_journal_bank'].id,
            'payment_account_id': self.inbound_payment_method_line.payment_account_id.id,
        })

        expense = self.create_expenses({
            'payment_method_line_id': default_payment_method_line.id,
            'payment_mode': 'company_account',
        })

        self.assertRecordValues(expense, [{'payment_method_line_id': default_payment_method_line.id}])
        expense.payment_method_line_id = new_payment_method_line

        expense.action_submit()
        expense.action_approve()
        expense.action_post()
        self.assertRecordValues(expense.account_move_id.origin_payment_id, [{'payment_method_line_id': new_payment_method_line.id}])

    @freeze_time('2024-01-01')
    def test_expense_vendor(self):
        """ This test will do a basic flow when a vendor is set on the expense """
        vendor_a = self.env['res.partner'].create({'name': 'Ruben'})
        expense = self.create_expenses({
            'payment_mode': 'company_account',
            'vendor_id': vendor_a.id,
        })
        expense.action_submit()
        expense.action_approve()
        expense.action_post()

        self.assertEqual(vendor_a.id, expense.account_move_id.line_ids.partner_id.id)

    def test_payment_edit_fields(self):
        """ Test that some payment fields cannot be modified once linked with an expense """
        expense = self.create_expenses({
            'payment_mode': 'company_account',
            'total_amount_currency': 1000.00,
        })
        expense.action_submit()
        expense.action_approve()
        expense.action_post()
        payment = expense.account_move_id.origin_payment_id

        with self.assertRaises(UserError, msg="Cannot edit payment amount after linking to an expense"):
            payment.write({'amount': 500})

        payment.write({'is_sent': True})

    def test_corner_case_expense_submitted_cannot_be_zero(self):
        """
        Test that the expenses are not submitted if the total amount is 0.0 nor able to be edited that way
        unless unlinking it from the expense.
        """
        expense = self.create_expenses({'total_amount': 0.0, 'total_amount_currency': 0.0})

        # CASE 1: FORBIDS Trying to submit an expense with a total_amount(_currency) of 0.0
        with self.assertRaises(UserError):
            expense.action_submit()

        # CASE 2: FORBIDS Trying to change the total_amount(_currency) to 0.0 when the expense is submitted to the manager
        expense.total_amount_currency = 1000
        expense.action_submit()
        with self.assertRaises(UserError):
            expense.total_amount_currency = 0.0
        with self.assertRaises(UserError):
            expense.total_amount = 0.0

        # CASE 3: FORBIDS Trying to change the total_amount(_currency) to 0.0 when the expense is approved
        expense.action_approve()
        with self.assertRaises(UserError):
            expense.total_amount_currency = 0.0
        with self.assertRaises(UserError):
            expense.total_amount = 0.0

        # CASE 4: FORBIDS Trying to change the total_amount(_currency) to 0.0 when the expense is posted and the account move created
        self.post_expenses_with_wizard(expense)
        with self.assertRaises(UserError):
            expense.total_amount_currency = 0.0
        with self.assertRaises(UserError):
            expense.total_amount = 0.0

        # CASE 5: ALLOWS Changing the total_amount(_currency) to 0.0 when the expense is reset to draft
        expense.account_move_id.button_draft()
        expense.account_move_id.unlink()
        expense.action_reset()
        expense.write({'total_amount_currency': 0.0, 'total_amount': 0.0})

        # CASE 6: FORBIDS Setting the amounts to 0 while submitting the expense
        expense.write({'total_amount_currency': 1000.0, 'total_amount': 1000.0})
        with self.assertRaises(UserError):
            expense.write({'total_amount_currency': 0.0, 'state': 'submitted'})
        with self.assertRaises(UserError):
            expense.write({'total_amount': 0.0, 'state': 'submitted'})

        # CASE 7: ALLOWS Setting the amounts to 0 while resetting the expense to draft
        expense.write({'total_amount_currency': 0.0, 'total_amount': 0.0, 'state': 'draft'})

    def test_foreign_currencies_total(self):
        """ Check that the dashboard computes amount properly in company currency """
        self.create_expenses([{
                'name': 'Company expense',
                'payment_mode': 'company_account',
                'total_amount_currency': 1000.00,
                'employee_id': self.expense_employee.id,
            },
            {
                'name': 'Employee expense',
                'payment_mode': 'own_account',
                'currency_id': self.other_currency.id,
                'total_amount_currency': 1000.00,
                'total_amount': 2000.00,
                'employee_id': self.expense_employee.id,
            },
        ])
        expense_data = self.env['hr.expense'].with_user(self.expense_user_employee).get_expense_dashboard()
        self.assertEqual(expense_data['draft']['amount'], 3000.00)

    def test_update_expense_price_on_product_standard_price(self):
        """
        Tests that updating the standard price of a product will update all the un-submitted
        expenses using that product as a category.
        """
        product = self.env['product.product'].create({
            'name': 'Product',
            'standard_price': 100.0,
        })
        expenses = expense_no_update, expense_update = self.create_expenses([
            {'name': name, 'product_id': product.id, 'total_amount': 100.0}
            for name in ('test no update', 'test update')
        ]).sorted('name')

        self.assertRecordValues(expenses.sorted('name'), [
            {'name': 'test no update', 'price_unit': 100.0, 'quantity': 1, 'total_amount': 100.0},
            {'name':    'test update', 'price_unit': 100.0, 'quantity': 1, 'total_amount': 100.0},
        ])
        expense_no_update.action_submit()  # No update when the expense is submitted

        product.standard_price = 200.0
        self.assertRecordValues(expenses.sorted('name'), [
            {'name': 'test no update', 'price_unit': 100.0, 'quantity': 1.0, 'total_amount': 100.0},
            {'name':    'test update', 'price_unit': 200.0, 'quantity': 1.0, 'total_amount': 200.0},  # total is updated
        ])

        expense_update.quantity = 5
        self.assertRecordValues(expenses.sorted('name'), [
            {'name': 'test no update', 'price_unit': 100.0, 'quantity': 1, 'total_amount':  100.0},
            {'name':    'test update', 'price_unit': 200.0, 'quantity': 5, 'total_amount': 1000.0},  # total is updated
        ])

        product.standard_price = 0.0
        self.assertRecordValues(expenses.sorted('name'), [
            {'name': 'test no update', 'price_unit':  100.0, 'quantity': 1, 'total_amount':  100.0},
            {'name':    'test update', 'price_unit': 1000.0, 'quantity': 1, 'total_amount': 1000.0},  # quantity & price_unit only are updated
        ])

        expenses.action_submit()  # This expense should not be updated any more
        product.standard_price = 300.0
        self.assertRecordValues(expenses.sorted('name'), [
            {'name': 'test no update', 'price_unit':  100.0, 'quantity': 1, 'total_amount':  100.0},
            {'name':    'test update', 'price_unit': 1000.0, 'quantity': 1, 'total_amount': 1000.0},  # no update
        ])

    def test_expense_standard_price_update_warning(self):
        expense_cat_A = self.env['product.product'].create({
            'name': 'Category A',
            'default_code': 'CA',
            'standard_price': 0.0,
        })
        expense_cat_B = self.env['product.product'].create({
            'name': 'Category B',
            'default_code': 'CB',
            'standard_price': 0.0,
        })
        expense_cat_C = self.env['product.product'].create({
            'name': 'Category C',
            'default_code': 'CC',
            'standard_price': 0.0,
        })
        self.create_expenses([
            {
                'name': 'Expense 1',
                'product_id': expense_cat_A.id,
                'total_amount': 1,
            },
            {
                'name': 'Expense 2',
                'product_id': expense_cat_B.id,
                'total_amount': 5,
            },
        ])

        # At first, there is no warning message on the categories because their prices are 0
        self.assertFalse(expense_cat_A.standard_price_update_warning)
        self.assertFalse(expense_cat_B.standard_price_update_warning)
        self.assertFalse(expense_cat_C.standard_price_update_warning)

        # When modifying the price of the first category, a message should appear as a an expense will be modified.
        with Form(expense_cat_A, view="hr_expense.product_product_expense_form_view") as form:
            form.standard_price = 5
            self.assertTrue(form.standard_price_update_warning)

        # When modifying the price of the second category, no message should appear as the price of the linked
        # expense is the price of the category that is going to be saved.
        with Form(expense_cat_B, view="hr_expense.product_product_expense_form_view") as form:
            form.standard_price = 5
            self.assertFalse(form.standard_price_update_warning)

        # When modifying the price of the their category, no message should appear as no expense is linked to it.
        with Form(expense_cat_C, view="hr_expense.product_product_expense_form_view") as form:
            form.standard_price = 5
            self.assertFalse(form.standard_price_update_warning)

    def test_compute_standard_price_update_warning_product_with_and_without_expense(self):
        """
        Test that the compute doesn't raise an error with mixed recordsets (products used in expenses and not used in expenses)
        """
        product_expensed = self.env['product.product'].create({
            'name': 'Category A',
            'default_code': 'CA',
            'standard_price': 0.0,
        })
        product_not_expensed = self.env['product.product'].create({
            'name': 'Category B',
            'default_code': 'CB',
            'standard_price': 0.0,
        })
        self.env['hr.expense'].create({
            'employee_id': self.expense_employee.id,
            'name': 'Expense 1',
            'product_id': product_expensed.id,
            'total_amount': 1,
        })

        (product_expensed | product_not_expensed)._compute_standard_price_update_warning()

    def test_expense_multi_company(self):
        main_company = self.company_data['company']
        other_company = self.company_data_2['company']
        self.expense_employee.sudo().company_id = other_company

        # The expense employee is able to create an expense for company_2.
        # product_a needs a standard_price in company_2
        self.product_a.with_context(allowed_company_ids=self.company_data_2['company'].ids).standard_price = 100

        Expense = self.env['hr.expense'].with_user(self.expense_user_employee).with_context(allowed_company_ids=other_company.ids)
        expense_approve = Expense.create([{
            'name': 'First Expense for employee',
            'employee_id': self.expense_employee.id,
            'date': '2016-01-01',
            'product_id': self.product_a.id,
            'quantity': 1200.0,
        }])
        expense_refuse = Expense.create([{
            'name': 'Second Expense for employee',
            'employee_id': self.expense_employee.id,
            'date': '2016-01-01',
            'product_id': self.product_a.id,
            'quantity': 1000.0,
        }])
        expenses = expense_approve | expense_refuse
        self.assertRecordValues(expenses, [
            {'company_id': self.company_data_2['company'].id},
            {'company_id': self.company_data_2['company'].id},
        ])

        # The expense employee is able to submit the expense.
        expenses.with_user(self.expense_user_employee).action_submit()

        # An expense manager is not able to approve nor refuse without access to company_2.
        with self.assertRaises(UserError):
            expense_approve \
                .with_user(self.expense_user_manager) \
                .with_context(allowed_company_ids=main_company.ids, company_id=main_company.id) \
                .action_approve()

        with self.assertRaises(UserError):
            expense_refuse \
                .with_user(self.expense_user_manager) \
                .with_context(allowed_company_ids=main_company.ids) \
                ._do_refuse('failed')

        # An expense manager is able to approve/refuse with access to company_2.
        expense_approve \
            .with_user(self.expense_user_manager) \
            .with_context(allowed_company_ids=other_company.ids) \
            .action_approve()
        expense_refuse \
            .with_user(self.expense_user_manager) \
            .with_context(allowed_company_ids=other_company.ids) \
            ._do_refuse('failed')

        # An expense manager having accounting access rights is not able to post the journal entry without access
        # to company_2.
        with self.assertRaises(UserError):
            self.post_expenses_with_wizard(expense_approve.with_user(self.env.user).with_context(allowed_company_ids=main_company.ids))

        # An expense manager having accounting access rights is able to post the journal entry with access to
        # company_2.
        self.post_expenses_with_wizard(expense_approve.with_user(self.env.user).with_context(allowed_company_ids=other_company.ids))

    def test_tax_is_used_when_in_transactions(self):
        """ Ensures that a tax is set to used when it is part of some transactions """
        # Account.move is one type of transaction
        tax_expense = self.env['account.tax'].create({
            'name': 'test_is_used_expenses',
            'amount': '100',
            'include_base_amount': True,
        })

        self.create_expenses({'tax_ids': [Command.set(tax_expense.ids)]})
        tax_expense.invalidate_model(fnames=['is_used'])
        self.assertTrue(tax_expense.is_used)

    def test_expense_by_company_with_caba_tax(self):
        """ When using cash basis tax in an expense paid by the company, the transition account should not be used. """

        caba_tag = self.env['account.account.tag'].create({
            'name': 'Cash Basis Tag Final Account',
            'applicability': 'taxes',
        })
        caba_transition_account = self.env['account.account'].create({
            'name': 'Cash Basis Tax Transition Account',
            'account_type': 'asset_current',
            'code': '131001',
            'reconcile': True,
        })
        caba_tax = self.env['account.tax'].create({
            'name': 'Cash Basis Tax',
            'tax_exigibility': 'on_payment',
            'amount': 15,
            'cash_basis_transition_account_id': caba_transition_account.id,
            'invoice_repartition_line_ids': [
                Command.create({
                    'factor_percent': 100,
                    'repartition_type': 'base',
                }),
                Command.create({
                    'factor_percent': 100,
                    'repartition_type': 'tax',
                    'tag_ids': caba_tag.ids,
                }),
            ]
        })

        expense = self.create_expenses({
            'payment_mode': 'company_account',
            'tax_ids': [Command.set(caba_tax.ids)],
        })
        expense.action_submit()
        expense.action_approve()
        expense.action_post()
        moves = expense.account_move_id
        tax_lines = moves.line_ids.filtered(lambda line: line.tax_line_id == caba_tax)
        self.assertNotEqual(tax_lines.account_id, caba_transition_account, "The tax should not be on the transition account")
        self.assertEqual(tax_lines.tax_tag_ids, caba_tag, "The tax should still retrieve its tags")

    def test_expense_mandatory_analytic_plan_product_category(self):
        """
        Check that when an analytic plan has a mandatory applicability matching
        product category this is correctly triggered
        """
        self.env['account.analytic.applicability'].create({
            'business_domain': 'expense',
            'analytic_plan_id': self.analytic_plan.id,
            'applicability': 'mandatory',
            'product_categ_id': self.product_a.categ_id.id,
        })

        expense = self.create_expenses({
            'product_id': self.product_a.id,
            'quantity': 350.00,
            'payment_mode': 'company_account',
        })

        expense.action_submit()
        with self.assertRaises(ValidationError, msg="One or more lines require a 100% analytic distribution."):
            expense.with_context(validate_analytic=True).action_approve()
        expense.analytic_distribution = {self.analytic_account_1.id: 100.00}
        expense.with_context(validate_analytic=True).action_approve()

    def test_expense_no_stealing_from_employees(self):
        """
        Test to check that the company doesn't steal their employee when the commercial_partner_id of the employee partner
        is the company
        """
        self.expense_employee.user_partner_id.parent_id = self.env.company.partner_id
        self.assertEqual(self.env.company.partner_id, self.expense_employee.user_partner_id.commercial_partner_id)

        expense = self.create_expenses({'employee_id': self.expense_employee.id})
        expense.action_submit()
        expense.action_approve()
        self.post_expenses_with_wizard(expense)
        move = expense.account_move_id

        self.assertNotEqual(move.commercial_partner_id, self.env.company.partner_id)
        self.assertEqual(move.partner_id, self.expense_employee.user_partner_id)
        self.assertEqual(move.commercial_partner_id, self.expense_employee.user_partner_id)

    def test_expense_set_total_amount_to_0(self):
        """ Checks that amount fields are correctly updating when setting total_amount to 0 """
        expense = self.create_expenses({
            'product_id': self.product_c.id,
            'total_amount_currency': 100.0,
        })
        expense.total_amount_currency = 0.0
        self.assertTrue(expense.currency_id.is_zero(expense.tax_amount))
        self.assertTrue(expense.company_currency_id.is_zero(expense.total_amount))

    def test_expense_set_quantity_to_0(self):
        """ Checks that amount fields except for unit_amount are correctly updating when setting quantity to 0 """
        expense = self.create_expenses({
            'product_id': self.product_b.id,
            'quantity': 10
        })
        expense.quantity = 0
        self.assertTrue(expense.currency_id.is_zero(expense.total_amount_currency))
        self.assertEqual(expense.company_currency_id.compare_amounts(expense.price_unit, self.product_b.standard_price), 0)

    def test_employee_expense_in_foreign_currency(self):
        """ Checks that the currency of the posted entries is always the company currency """
        expense = self.create_expenses({
            'payment_mode': 'own_account',
            'currency_id': self.other_currency.id,
        })
        expense.action_submit()
        expense.action_approve()
        expense._post_without_wizard()
        self.assertRecordValues(
            expense.account_move_id,
            [{'amount_total': 500.0, 'currency_id': expense.account_move_id.company_currency_id.id}],
        )

    def test_company_expense_sepa_ct_trust_bypass(self):
        """
        Ensure company-paid expenses using SEPA CT post without requiring a trusted recipient bank account.
        This validates the bypass in account.payment.action_post for expense-originated payments.
        """
        self.env.ref('base.EUR').active = True
        bank_journal = self.company_data['default_journal_bank']
        bank = self.env['res.bank'].create({
            'name': 'BNP Paribas',
            'bic': 'GEBABEBB',
        })
        bank_journal.write({
            'bank_id': bank.id,
            'bank_acc_number': 'BE48363523682327',
            'currency_id': self.env.ref('base.EUR').id,
        })

        sepa_ct_line = bank_journal.outbound_payment_method_line_ids.filtered(lambda l: l.code == 'sepa_ct')
        if not sepa_ct_line:
            self.skipTest("SEPA Credit Transfer payment method not available (account_sepa module not installed)")

        expense = self.create_expenses({
            'name': 'Hotel',
            'payment_mode': 'company_account',
            'payment_method_line_id': sepa_ct_line.id,
            'total_amount_currency': 100.00,
            'currency_id': self.env.ref('base.EUR').id,
        })

        expense.action_submit()
        expense.action_approve()
        # Should not raise trust validation error despite missing recipient partner bank
        expense.action_post()

    def test_expense_analytic_vendor_bill_count(self):
        """
        Verify that purchase receipts appear when opening a vendor bill via the smart button,
        and that the vendor bill count matches the records shown.
        """
        expense = self.create_expenses([
            {
                'name': 'Employee PA 2*800 + 15%',
                'analytic_distribution': {self.analytic_account_1.id: 100},
            }])
        expense.action_submit()
        expense.action_approve()
        self.post_expenses_with_wizard(expense)

        self.assertEqual('in_receipt', expense.account_move_id.move_type)

        vendor_bill_view = self.analytic_account_1.action_view_vendor_bill()
        self.assertTrue(expense.account_move_id.id in vendor_bill_view['domain'][0][2])

    def test_expense_paid_company_no_autobalancing_line(self):
        """
        Test that when creating the move associated with an expense paid by company, no autobalancing line
        appears when an analytic is added to a move line.
        """
        expense = self.create_expenses({
            'name': 'Expense for John Smith',
            'employee_id': self.expense_employee.id,
            'total_amount_currency': 100.0,
            'product_id': self.product_c.id,
            'payment_mode': 'company_account',
            'company_id': self.company_data['company'].id,
            'tax_ids': [self.tax_sale_a.id],
            })

        expense.action_submit()
        expense.action_approve()
        expense.analytic_distribution = {self.analytic_account_1.id: 100.00}
        expense.action_post()

        # Check that there is no fourth autobalancing line on the account move
        self.assertEqual(expense.account_move_id.line_ids.mapped('balance'), [86.96, 13.04, -100.0])
