import csv

from odoo import Command
from odoo.exceptions import UserError, ValidationError, RedirectWarning
from odoo.tests import tagged
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.addons.l10n_id_efaktur.models.efaktur_document import FK_HEAD_LIST, LT_HEAD_LIST, OF_HEAD_LIST, _csv_row


@tagged('post_install', '-at_install', 'post_install_l10n')
class TestIndonesianEfaktur(AccountTestInvoicingCommon):

    @classmethod
    @AccountTestInvoicingCommon.setup_country('id')
    def setUpClass(cls):
        super().setUpClass()

        cls.company.write({
            'street': 'test',
            'phone': '12345',
        })
        cls.partner_id = cls.env['res.partner'].create({"name": "l10ntest", "l10n_id_pkp": True, "l10n_id_kode_transaksi": "01", "l10n_id_nik": "12345", "vat": "000000000000000"})
        cls.partner_id_vat = cls.env['res.partner'].create({"name": "l10ntest3", "l10n_id_pkp": True, "l10n_id_kode_transaksi": "01", "l10n_id_nik": "67890", "vat": "010000000000000"})
        cls.tax_id = cls.env['account.tax'].create({"name": "test tax", "type_tax_use": "sale", "amount": 10.0, "price_include_override": "tax_included"})

        cls.efaktur = cls.env['l10n_id_efaktur.efaktur.range'].create({'min': '0000000000001', 'max': '0000000000010'})
        cls.maxDiff = None

        # multi-company setup
        cls.company_data_2 = cls.setup_other_company()

    def test_efaktur_csv_output_1(self):
        """
        Test to ensure that the output csv data contains tax-excluded prices regardless of whether the tax configuration is tax-included or tax-excluded.
        Current test is using price of 110 which is tax-included with tax of amount 10%. So the unit price listed has to be 100 whereas the original result would have 110 instead.
        """
        # to check the diff when test fails
        out_invoice = self.env['account.move'].create({
            'move_type': 'out_invoice',
            'partner_id': self.partner_id.id,
            'invoice_date': '2019-05-01',
            'date': '2019-05-01',
            'invoice_line_ids': [
                Command.create({'name': 'line1', 'price_unit': 110.0, 'tax_ids': self.tax_id.ids}),
            ],
            'l10n_id_kode_transaksi': "01",
        })
        out_invoice.action_post()
        out_invoice.download_efaktur()

        efaktur_csv_output = out_invoice.l10n_id_efaktur_document._generate_efaktur_invoice(',')
        output_head = '%s%s%s' % (
            _csv_row(FK_HEAD_LIST, ','),
            _csv_row(LT_HEAD_LIST, ','),
            _csv_row(OF_HEAD_LIST, ','),
        )
        # remaining lines
        line_4 = '"FK","01","0","0000000000001","5","2019","1/5/2019","000000000000000","12345#NIK#NAMA#l10ntest","","100","10","0","","0","0","0","0","INV/2019/00001","0"\n'
        line_5 = '"OF","","","100.00","1.0","100.00","0","100.00","10.00","0","0"\n'

        efaktur_csv_expected = output_head + line_4 + line_5

        self.assertEqual(efaktur_csv_expected, efaktur_csv_output)

    def test_efaktur_csv_output_decimal_place(self):
        """
        Test to ensure that decimal place conversion is only done when inputting to csv
        This is to test original calculation of invoice_line_total_price: invoice_line_total_price = invoice_line_unit_price * line.quantity
        as invoice_line_unit_price is already converted to be tax-excluded and set to the decimal place as configured on the currency, the calculation of total could be flawed.

        In this test case, the tax-included price unit is 110.11, hence tax-excluded is 100.1,
        invoice_line_unit_price will be 100, if we continue with the calculation of total price, it will be 100*400 = 40000
        eventhough the total is supposed to be 100.1*400 = 40040, there is a 40 discrepancy
        """
        out_invoice = self.env['account.move'].create({
            'move_type': 'out_invoice',
            'partner_id': self.partner_id.id,
            'invoice_date': '2019-05-01',
            'date': '2019-05-01',
            'invoice_line_ids': [
                Command.create({'name': 'line1', 'price_unit': 110.11, 'quantity': 400, 'tax_ids': self.tax_id.ids})
            ],
            'l10n_id_kode_transaksi': '01'
        })
        out_invoice.action_post()
        out_invoice.download_efaktur()

        efaktur_csv_output = out_invoice.l10n_id_efaktur_document._generate_efaktur_invoice(',')
        output_head = '%s%s%s' % (
            _csv_row(FK_HEAD_LIST, ','),
            _csv_row(LT_HEAD_LIST, ','),
            _csv_row(OF_HEAD_LIST, ','),
        )
        line_4 = '"FK","01","0","0000000000001","5","2019","1/5/2019","000000000000000","12345#NIK#NAMA#l10ntest","","40040","4004","0","","0","0","0","0","INV/2019/00001","0"\n'
        line_5 = '"OF","","","100.10","400.0","40040.00","0","40040.00","4004.00","0","0"\n'

        efaktur_csv_expected = output_head + line_4 + line_5
        self.assertEqual(efaktur_csv_expected, efaktur_csv_output)

    def test_efaktur_use_vat(self):
        """ Test to ensure that the e-faktur uses the VAT on NPWP column of efaktur when
        VAT is non-zeros """
        out_invoice = self.env['account.move'].create({
            'move_type': 'out_invoice',
            'partner_id': self.partner_id_vat.id,
            'invoice_date': '2019-05-01',
            'date': '2019-05-01',
            'invoice_line_ids': [
                (0, 0, {'name': 'line1', 'price_unit': 110.11, 'quantity': 400, 'tax_ids': self.tax_id.ids})
            ],
            'l10n_id_kode_transaksi': '01'
        })
        out_invoice.action_post()
        out_invoice.download_efaktur()

        efaktur_csv_output = out_invoice.l10n_id_efaktur_document._generate_efaktur_invoice(',')
        output_head = '%s%s%s' % (
            _csv_row(FK_HEAD_LIST, ','),
            _csv_row(LT_HEAD_LIST, ','),
            _csv_row(OF_HEAD_LIST, ','),
        )
        line_4 = '"FK","01","0","0000000000001","5","2019","1/5/2019","010000000000000","l10ntest3","","40040","4004","0","","0","0","0","0","INV/2019/00001","0"\n'
        line_5 = '"OF","","","100.10","400.0","40040.00","0","40040.00","4004.00","0","0"\n'

        efaktur_csv_expected = output_head + line_4 + line_5
        self.assertEqual(efaktur_csv_expected, efaktur_csv_output)

    def test_efaktur_no_vat_nik(self):
        """ Test to ensure that when no VAT and NIK is supplied, a RedirectWarning should be raised """
        partner_no_vat_nik = self.env['res.partner'].create({"name": "l10ntest4", "l10n_id_pkp": True, "l10n_id_kode_transaksi": "01"})
        out_invoice = self.env['account.move'].create({
            'move_type': 'out_invoice',
            'partner_id': partner_no_vat_nik.id,
            'invoice_date': '2019-05-01',
            'date': '2019-05-01',
            'invoice_line_ids': [
                (0, 0, {'name': 'line1', 'price_unit': 110.11, 'quantity': 400, 'tax_ids': self.tax_id.ids})
            ],
            'l10n_id_kode_transaksi': '01'
        })
        out_invoice.action_post()
        with self.assertRaises(RedirectWarning):
            out_invoice.download_efaktur()

    def test_efaktur_nik_with_no_vat(self):
        """ Test to ensure if there is contact has no VAT but has NIK

        NPWP would contain NIK, NAMA contains customer's name, REFERENSI would contain invoice name with customer's NIK"""

        partner_nik_no_vat = self.env['res.partner'].create({"name": "l10ntest4", "l10n_id_pkp": True, "l10n_id_kode_transaksi": "01", "l10n_id_nik": "1532167"})
        out_invoice = self.env['account.move'].create({
            'move_type': 'out_invoice',
            'partner_id': partner_nik_no_vat.id,
            'invoice_date': '2019-05-01',
            'date': '2019-05-01',
            'invoice_line_ids': [
                (0, 0, {'name': 'line1', 'price_unit': 110.11, 'quantity': 400, 'tax_ids': self.tax_id.ids})
            ],
            'l10n_id_kode_transaksi': '01'
        })
        out_invoice.action_post()
        out_invoice.download_efaktur()

        efaktur_csv_output = out_invoice.l10n_id_efaktur_document._generate_efaktur_invoice(',')
        output_head = '%s%s%s' % (
            _csv_row(FK_HEAD_LIST, ','),
            _csv_row(LT_HEAD_LIST, ','),
            _csv_row(OF_HEAD_LIST, ','),
        )
        line_4 = '"FK","01","0","0000000000001","5","2019","1/5/2019","1532167","l10ntest4","","40040","4004","0","","0","0","0","0","INV/2019/00001 1532167","0"\n'
        line_5 = '"OF","","","100.10","400.0","40040.00","0","40040.00","4004.00","0","0"\n'

        efaktur_csv_expected = output_head + line_4 + line_5
        self.assertEqual(efaktur_csv_expected, efaktur_csv_output)

    def test_efaktur_total_rounding_accuracy(self):
        """ Use case:
        Using a tax of 11% price included on every line:

        line | qty | price | subtotal
        1    | 24  | 57851 | 1250832.43
        2    | 24  | 65184 | 1409383.78
        3    | 24  | 77134 | 1667762.16
        4    | 24  | 87835 | 1899135.14
        5    | 20  | 180342| 3249405.41

        Untaxed Amount: 9474250.92
        Taxes: 1042176.08
        Total: 10518936.00

        Efaktur will display both:
        -The detail of the lines.
        and
        - the amount_untaxed and amount_tax rounded to 0 decimals.

        The sum of the lines MUST exactly match with the amount_untaxed and amount_tax.
        Which most of the case won't be happening because sum(rounded(vals)) is usually not equal to rounded(sum(vals))
        when using integers.

        To remediate to that issue we are putting the difference in the amount of the first line.
        """
        # Prepare the test invoice.
        tax_id = self.env["account.tax"].create(
            {"name": "test tax 11", "type_tax_use": "sale", "amount": 11.0, "price_include_override": "tax_included"}
        )
        invoice = self.env['account.move'].create({
            'move_type': 'out_invoice',
            'partner_id': self.partner_id.id,
            'invoice_date': '2019-05-01',
            'date': '2019-05-01',
            'invoice_line_ids': [
                Command.create({'name': 'line1', 'price_unit': 57851, 'quantity': 24, 'tax_ids': tax_id.ids}),
                Command.create({'name': 'line2', 'price_unit': 65184, 'quantity': 24, 'tax_ids': tax_id.ids}),
                Command.create({'name': 'line3', 'price_unit': 77134, 'quantity': 24, 'tax_ids': tax_id.ids}),
                Command.create({'name': 'line4', 'price_unit': 87835, 'quantity': 24, 'tax_ids': tax_id.ids}),
                Command.create({'name': 'line5', 'price_unit': 180342, 'quantity': 20, 'tax_ids': tax_id.ids}),
            ],
            'l10n_id_kode_transaksi': '01',
        })
        invoice.action_post()
        invoice.download_efaktur()
        # Generate the generate efaktur csv.
        efaktur_csv_output = invoice.l10n_id_efaktur_document._generate_efaktur_invoice(',')
        # Validate the result: the sum of the lines must exactly match with the amount_untaxed and amount_tax.
        dict_reader = csv.DictReader(efaktur_csv_output.splitlines(), delimiter=",")

        amount_untaxed_total = 0
        amount_tax_total = 0
        amount_untaxed_sum = 0
        amount_tax_sum = 0
        for row in dict_reader:
            row_code = row["FK"]
            # Besides the first row, FK is used for the total row.
            if row_code == "FK":
                amount_untaxed_total = int(row["JUMLAH_DPP"])  # These are rounded to 0 decimal in the file.
                amount_tax_total = int(row["JUMLAH_PPN"])  # These are rounded to 0 decimal in the file.
            # OF lines are the data lines, but also used in another case (for codes). Data lines do not have a KD_JENIS_TRANSAKSI though.
            elif row_code == "OF" and not row["KD_JENIS_TRANSAKSI"]:
                amount_untaxed_sum += float(row["NPWP"])  # These are not.
                amount_tax_sum += float(row["NAMA"])  # These are not.

        self.assertEqual(amount_untaxed_total, amount_untaxed_sum)
        self.assertEqual(amount_tax_total, amount_tax_sum)

    def test_efaktur_do_not_consume_code(self):
        """ Ensure that an invoice with no taxes at all will not consume a code. """
        available_code = self.efaktur.available
        # 1. No taxes
        out_invoice_no_taxes = self.env["account.move"].create({
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": False}),
            ],
            "l10n_id_kode_transaksi": "01",
        })
        out_invoice_no_taxes.action_post()
        # The tax number is not set.
        self.assertFalse(out_invoice_no_taxes.l10n_id_tax_number)
        # No codes have been consumed.
        self.assertEqual(self.efaktur.available, available_code)

        with self.assertRaises(ValidationError, msg='E-faktur is not available for invoices without any taxes.'), self.cr.savepoint():
            out_invoice_no_taxes.download_efaktur()

    def test_efaktur_consume_code(self):
        """ Ensure that an invoice with taxes will consume a code. """
        available_code = self.efaktur.available
        out_invoice_no_taxes = self.env["account.move"].create({
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        })
        out_invoice_no_taxes.action_post()
        # The tax number is set.
        self.assertEqual(out_invoice_no_taxes.l10n_id_tax_number, '0100000000000001')
        # A code has been consumed.
        self.assertEqual(self.efaktur.available, available_code - 1)
        # No error is raised when downloading.
        out_invoice_no_taxes.download_efaktur()

    def test_efaktur_post_multi_invoices(self):
        """ Test to amke sure if we post 2 invoices at once, each of them have their own
        separate eTax number"""

        # Create 2 separate invoices
        out_invoices = self.env['account.move'].create([{
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        }, {
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 120.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        }])

        # Post them together
        out_invoices.action_post()

        # Should consume the number 1 and 2
        self.assertEqual(out_invoices[0].l10n_id_tax_number, "0100000000000001")
        self.assertEqual(out_invoices[1].l10n_id_tax_number, "0100000000000002")

    def test_available_range_count(self):
        """ Test on the correctness of l10n_id_available_range_count computation given ranges
        across multi-company setup """
        company_2 = self.company_data_2['company']
        tax_id = self.env['account.tax'].create({"name": "test tax", "type_tax_use": "sale", "amount": 10.0, "price_include": True, "company_id": company_2.id})

        # create 3 ranges on the other company where one has no availability
        self.env['l10n_id_efaktur.efaktur.range'].create({'min': "%013d" % 21, 'max': '%013d' % 30, 'company_id': company_2.id})
        self.env['l10n_id_efaktur.efaktur.range'].create({'min': "%013d" % 31, 'max': '%013d' % 40, 'company_id': company_2.id})
        self.env['l10n_id_efaktur.efaktur.range'].create({'min': "%013d" % 41, 'max': '%013d' % 50, 'available': 0, 'company_id': company_2.id})

        # Create separate invoice on each company
        invoices = self.env["account.move"].create([{
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
            "company_id": self.company.id,
        }, {
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
            "company_id": company_2.id
        }])

        # only 1 available range for first invoice, and 2 for second inovice
        self.assertEqual(invoices[0].l10n_id_available_range_count, 1)
        self.assertEqual(invoices[1].l10n_id_available_range_count, 2)

    # =======================================
    # E-faktur range management related tests
    # =======================================

    def test_change_min_max(self):
        """ Test the value of available when changing min and max of range and
        some restrictions to changing min and max of a range"""
        self.assertEqual(self.efaktur.available, 10)

        # Suppose range is 1 - 10, and because it's unused, availability is 10
        # if range becomes 4 - 10, then we remove 3 available numbers
        self.efaktur.min = "0000000000004"
        self.assertEqual(self.efaktur.available, 7)

        # Vice-versa if we turn range into 3 - 10, then we add 1 available number
        self.efaktur.min = "0000000000003"
        self.assertEqual(self.efaktur.available, 8)

        # If range turns into 3 - 8 , 2 available numbers are removed
        self.efaktur.max = "0000000000008"
        self.assertEqual(self.efaktur.available, 6)

        # If range turns into 3 - 9, there is extra 1 available number
        self.efaktur.max = "0000000000009"
        self.assertEqual(self.efaktur.available, 7)

        # If we update both together, then resulting availalbe number is
        # combined behaviour of changing min and max
        self.efaktur.write({'min': "0000000000005", "max": "0000000000005"})
        self.assertEqual(self.efaktur.available, 1)

        self.efaktur.write({'min': "0000000000008", "max": "0000000000010"})
        self.assertEqual(self.efaktur.available, 3)

        # Reset the e-Faktur range back to 1-10
        self.efaktur.write({'min': "0000000000001"})

        # Suppose we use the range 4 times, then the next_num = 5 and availability = 6
        for i in range(4):
            self.efaktur.pop_number()

        # Once a range is used, then we can't change the min to any number
        with self.assertRaises(ValidationError):
            self.efaktur.write({'min': '0000000000003'})

        # We cannot change the range's max to any number less than or equal to last used numbers
        with self.assertRaises(ValidationError):
            self.efaktur.write({'max': '0000000000003'})

        # If we change the max value to number above the last used number, default behaviour applies
        self.efaktur.write({'max': '0000000000007'})
        self.assertEqual(self.efaktur.available, 3)

    def test_next_num(self):
        """ Test if next_num computation is correct after altering availability.

        Range: 0000000000001 - 0000000000010
        Base availability = 10
        """
        self.assertEqual(self.efaktur.next_num, 1)

        self.efaktur.available = 5
        self.assertEqual(self.efaktur.next_num, 6)

    def test_efaktur_delete_range(self):
        """ Ensure that user can only delete e-Faktur ranges that are unused """
        # Use the main range and should raise error when being deleted
        self.efaktur.pop_number()

        with self.assertRaises(UserError):
            self.efaktur.unlink()

        # If a range is unused, there should be no problem when deleting it
        new_range = self.env['l10n_id_efaktur.efaktur.range'].create({"min": "%013d" % 11, "max": "%013d" % 20})
        new_range.unlink()

    def test_efaktur_release_last_number(self):
        """ Test that range min/max is unaffected if we're returning the last released number of that range
        i.e. range: 1-10, 1 is used and then released back, range still 1-10 with 10 availability
        """
        out_invoice = self.env["account.move"].create({
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        })
        out_invoice.action_post()
        self.assertEqual(self.efaktur.available, 9)

        out_invoice.button_draft()
        out_invoice.button_cancel()
        out_invoice.reset_efaktur()

        self.assertEqual(self.efaktur.available, 10)

        # Generate 8 invoices and reset the last invoice, should only add availability without
        # affecting the range
        invoices = self.env["account.move"].create([{
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        } for i in range(8)])
        invoices.action_post()
        self.assertEqual(invoices[-1].l10n_id_tax_number, '0100000000000008')
        self.assertEqual(self.efaktur.available, 2)

        last_inv = invoices[-1]
        last_inv.button_draft()
        last_inv.button_cancel()
        last_inv.reset_efaktur()

        self.assertEqual(self.efaktur.min, '0000000000001')
        self.assertEqual(self.efaktur.max, '0000000000010')
        self.assertEqual(self.efaktur.available, 3)

    def test_efaktur_release_number_not_last(self):
        """ Ensure when the number returned is not last released number, split the efaktur range into 2 parts
        range: 1-10, used up to 2, then release 1 => split range into [1-1] available 1 & [2-10] available 8
        """
        invoices = self.env["account.move"].create([{
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        }, {
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        }])
        invoices[0].action_post()
        invoices[1].action_post()

        invoices[0].button_cancel()
        invoices[0].reset_efaktur()

        self.assertEqual(self.efaktur.min, '0000000000001')
        self.assertEqual(self.efaktur.max, '0000000000001')
        self.assertEqual(self.efaktur.available, 1)

        # To ensure getting the exact next range, in case there's other ranges being created
        other_range = self.env['l10n_id_efaktur.efaktur.range'].search(
            [('id', '!=', self.efaktur.id), ('min', '>', self.efaktur.min)],
            limit=1,
            order='min ASC'
        )

        self.assertEqual(other_range.min, '0000000000002')
        self.assertEqual(other_range.max, '0000000000010')
        self.assertEqual(other_range.available, 8)

    # ==========================================
    # Multiple Range Selection on Invoice
    # ==========================================

    def test_multiple_range_select_on_invoice(self):
        """ Test some behaviours related to having multiple available ranges on invoice """
        out_invoice_1 = self.env["account.move"].create({
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        })

        # Create another range, and there should be 2 ranges available
        efaktur_range = self.env['l10n_id_efaktur.efaktur.range'].create({'min': '0000000000011', 'max': '0000000000020'})
        self.assertEqual(out_invoice_1.l10n_id_available_range_count, 2)

        # if no range selected for it, we auto-assign the smallest range
        out_invoice_1.action_post()
        self.assertEqual(out_invoice_1.l10n_id_efaktur_range, self.efaktur)
        self.assertEqual(out_invoice_1.l10n_id_tax_number, '0100000000000001')

        # Invoice selects a specific range
        out_invoice_2 = self.env["account.move"].create({
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
            "l10n_id_efaktur_range": efaktur_range.id
        })
        out_invoice_2.action_post()
        self.assertEqual(out_invoice_2.l10n_id_tax_number, '0100000000000011')

    # ============================================
    # Reversing invoice with e-Faktur number
    # ============================================
    def test_efaktur_reverse_invoice_flow(self):
        """ Testing when invoice is created, then we reverse and make replacement invoice,
        the l10n_id_replace_invoice_id is set, then if we confirm it, will generate the correct eTax
        number that replaces the old invoice's"""

        # Create invoice and generate the e-Faktur number
        out_invoice_1 = self.env["account.move"].create({
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        })
        out_invoice_1.action_post()
        out_invoice_1.download_efaktur()
        # eTax number of original invoice should be (l10n_id_kode_transaksi)0(faktur number in 13 digits)
        self.assertEqual(out_invoice_1.l10n_id_tax_number, "0100000000000001")

        # Reverse and create the replacement invoice
        move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=out_invoice_1.ids).create({
            "reason": "Correcting Price",
            "journal_id": out_invoice_1.journal_id.id,
        })
        reversal = move_reversal.reverse_moves(is_modify=True)
        replacement_invoice = self.env['account.move'].browse(reversal['res_id'])

        # Replacement invoice should immediately have the l10n_id_replace_invoice_id
        self.assertEqual(replacement_invoice.l10n_id_replace_invoice_id, out_invoice_1)

        # Correct the unit price, then confirm to generate eTax number
        replacement_invoice.invoice_line_ids[0].price_unit = 150.0
        replacement_invoice.action_post()

        # Replacement invoice now should have etax number of (l10n_id_kode_transaksi)1(13 digit number)
        # where the 13 digit number is the number being used on the original invoice
        self.assertEqual(replacement_invoice.l10n_id_tax_number, "0110000000000001")

    def test_efaktur_show_error_no_document_invoice(self):
        """ Ensuring that when reversing a move and creating replacement invoice
        with l10n_id_replace_invoice_id filled in, it can't generate new tax number unless
        the original invoice has generated l10n_id_efaktur_document"""

        # Create the invoice, post it but not generating the efaktur document
        out_invoice_1 = self.env["account.move"].create({
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        })
        out_invoice_1.action_post()

        # Reverse move and create replacement invoice
        move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=out_invoice_1.ids).create({
            "reason": "Correcting Price",
            "journal_id": out_invoice_1.journal_id.id,
        })
        reversal = move_reversal.reverse_moves(is_modify=True)
        replacement_invoice = self.env['account.move'].browse(reversal['res_id'])

        # should be empty recordset since no efaktur document generated on the original invoice
        self.assertEqual(replacement_invoice.l10n_id_replace_invoice_id, self.env['account.move'])

    # ============================================
    # Test generating e-Faktur document flow
    # ============================================

    def test_efaktur_download_multi_company(self):
        """ Ensure that generating efaktur document for invoices across multi company is not allowed """
        # Setup company
        company_2 = self.company_data_2['company']
        tax_id = self.env['account.tax'].create({"name": "test tax", "type_tax_use": "sale", "amount": 10.0, "price_include": True, "company_id": company_2.id})
        self.env['l10n_id_efaktur.efaktur.range'].create({'min': "%013d" % 21, 'max': '%013d' % 30, 'company_id': company_2.id})

        # Setup company across 2 companies
        invoices = self.env["account.move"].create([{
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
            "company_id": self.company.id,
        }, {
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
            "company_id": company_2.id
        }])

        invoices.action_post()

        # Should raise error because the 2 invoices are from 2 different companies
        with self.assertRaises(UserError):
            (invoices).download_efaktur()

    def test_efaktur_download_separate(self):
        """ Test that when we download separately on 2 invoices, each will link to different document.
        If we try to download the 2 together, a RedirectWarning should be raised """

        empty_document = self.env['l10n_id_efaktur.document']
        # Create 2 invoices
        invoices = self.env["account.move"].create([{
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        } for i in range(2)])

        invoices.action_post()

        # Download efaktur separately
        invoices[0].download_efaktur()
        invoices[1].download_efaktur()

        # l10n_id_efaktur_document should be filled in both documents AND different from each other
        self.assertNotEqual(invoices[0].l10n_id_efaktur_document, empty_document)
        self.assertNotEqual(invoices[1].l10n_id_efaktur_document, empty_document)
        self.assertNotEqual(invoices[0].l10n_id_efaktur_document, invoices[1].l10n_id_efaktur_document)

        # If we try to download together, a RedirectWarning should be raised
        with self.assertRaises(RedirectWarning):
            invoices.download_efaktur()

    def test_efaktur_download_together(self):
        """ Test that when we download efaktur for 2 invoices together, they will refer to the same document
        If we try to download separately on both invoices, RedirectWarning should be raised. """

        empty_document = self.env['l10n_id_efaktur.document']
        # Create 2 invoices
        invoices = self.env["account.move"].create([{
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        } for i in range(2)])

        invoices.action_post()

        # Download efaktur together
        invoices.download_efaktur()

        # document should be filled in and both invoice should refer to the same document
        self.assertNotEqual(invoices[0].l10n_id_efaktur_document, empty_document)
        self.assertNotEqual(invoices[1].l10n_id_efaktur_document, empty_document)
        self.assertEqual(invoices[0].l10n_id_efaktur_document, invoices[1].l10n_id_efaktur_document)

        # If we try to download separately, RedirectWarning should be raised
        with self.assertRaises(RedirectWarning):
            invoices[0].download_efaktur()
        with self.assertRaises(RedirectWarning):
            invoices[1].download_efaktur()

    def test_efaktur_download_reset_draft(self):
        """ Test that after efaktur document is generated on invoices together. When we reset invoice 1
        to draft, it should unlink invoice 1 from doucment while keeping invoice.2. Message should be logged
        on invoice 1 to notify users. Attachment should be unlinked from the document as well"""

        # Create 2 invoices
        invoices = self.env["account.move"].create([{
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        } for i in range(2)])

        invoices.action_post()

        # Download efaktur together
        invoices.download_efaktur()

        # If we set invoice 1 to draft, invoice 1 should be unlinked from list of inovices
        # of that document but keeps invoice 2
        document = invoices[0].l10n_id_efaktur_document
        invoices[0].button_draft()

        self.assertTrue(invoices[0].id not in document.invoice_ids.ids and invoices[1].id in document.invoice_ids.ids)

        # document should have no attachment
        self.assertEqual(document.attachment_id, self.env['ir.attachment'])

    def test_efaktur_download_mismatch_flow(self):
        """ Test the flow when you generate 3 invoices (inv1, inv2, ine) generate document(1) and
        document(2,3). When we try to download inv(1,2) redirect warning would be raised, and it should
        provide the view of 2 documents concerned. Then, we edit such that document(1) adds invoice 2.
        Afterwards, regenerate both documents. We are expected to be able to download invoice(1,2) without error now,
        while also still using the same efaktur document record as before """

        # Generate 3 invoices
        invoices = self.env["account.move"].create([{
            "move_type": "out_invoice",
            "partner_id": self.partner_id.id,
            "invoice_date": "2019-05-01",
            "date": "2019-05-01",
            "invoice_line_ids": [
                Command.create({"name": "line1", "price_unit": 110.0, "tax_ids": self.tax_id.ids}),
            ],
            "l10n_id_kode_transaksi": "01",
        } for i in range(3)])
        invoices.action_post()

        # Generate document in group (1), and (2, 3)
        invoices[0].download_efaktur()
        invoices[1:].download_efaktur()

        # Suppose now we want to generate for (1, 2, 3) instead, it should trigger a redirect warning
        # that includes information on the efaktur document involved

        with self.assertRaises(RedirectWarning) as error:
            invoices[:2].download_efaktur()

        action = error.exception.args[1]
        document_ids = action['domain'][0][2]

        # Make sure the documents involved are only the between the document of 1, 2, and 3
        self.assertEqual(document_ids, invoices.l10n_id_efaktur_document.ids)

        # Store old document id and attachment for before-after comparison in the end
        old_document = invoices[0].l10n_id_efaktur_document

        # Unlink invoice 3 from 2nd document, link to first document and regenerate both the document attachments
        document_to_edit = self.env['l10n_id_efaktur.document'].browse(document_ids)
        document_to_edit[1].invoice_ids = [Command.unlink(invoices[1].id)]
        document_to_edit[1].action_regenerate()

        document_to_edit[0].invoice_ids = [Command.link(invoices[1].id)]
        document_to_edit[0].action_regenerate()

        # Should allow download of invoice(1,2,3) together where the efaktur document is the same as
        # the old invoice 1's document
        invoices[:2].download_efaktur()
        self.assertEqual(invoices[1].l10n_id_efaktur_document, old_document)
