diff --git a/addons/l10n_id_efaktur/models/account_move.py b/addons/l10n_id_efaktur/models/account_move.py index 6d66a359b9f..0500edc0abd 100644 --- a/addons/l10n_id_efaktur/models/account_move.py +++ b/addons/l10n_id_efaktur/models/account_move.py @@ -144,6 +144,8 @@ class AccountMove(models.Model): _csv_row(OF_HEAD_LIST, delimiter), ) + idr = self.env.ref('base.IDR') + for move in self.filtered(lambda m: m.state == 'posted'): eTax = move._prepare_etax() @@ -173,7 +175,7 @@ class AccountMove(models.Model): eTax['NPWP'] = invoice_npwp eTax['NAMA'] = move.partner_id.name if eTax['NPWP'] == '000000000000000' else move.partner_id.l10n_id_tax_name or move.partner_id.name eTax['ALAMAT_LENGKAP'] = move.partner_id.contact_address.replace('\n', '') if eTax['NPWP'] == '000000000000000' else move.partner_id.l10n_id_tax_address or street - eTax['JUMLAH_DPP'] = int(float_round(move.amount_untaxed, 0)) # currency rounded to the unit + eTax['JUMLAH_DPP'] = int(float_round(move.amount_untaxed, 0, rounding_method="DOWN")) # currency rounded to the unit eTax['JUMLAH_PPN'] = int(float_round(move.amount_tax, 0, rounding_method="DOWN")) # tax amount ALWAYS rounded down eTax['ID_KETERANGAN_TAMBAHAN'] = '1' if move.l10n_id_kode_transaksi == '07' else '' eTax['REFERENSI'] = number_ref @@ -196,7 +198,7 @@ class AccountMove(models.Model): # *invoice_line_discount_m2m is discount price use for diskon's column # *line.price_subtotal is subtotal price use for dpp's column # *tax_line or free_tax_line is tax price use for ppn's column - free_tax_line = tax_line = bruto_total = total_discount = 0.0 + free_tax_line = tax_line = 0.0 for tax in line.tax_ids: if tax.amount > 0: @@ -210,10 +212,10 @@ class AccountMove(models.Model): line_dict = { 'KODE_OBJEK': line.product_id.default_code or '', 'NAMA': line.product_id.name or '', - 'HARGA_SATUAN': float_repr(float_round(invoice_line_unit_price, 0), 0), + 'HARGA_SATUAN': float_repr(idr.round(invoice_line_unit_price), idr.decimal_places), 'JUMLAH_BARANG': line.quantity, - 'HARGA_TOTAL': float_repr(float_round(invoice_line_total_price, 0), 0), - 'DPP': float_round(line.price_subtotal, 0), + 'HARGA_TOTAL': idr.round(invoice_line_total_price), + 'DPP': line.price_subtotal, 'product_id': line.product_id.id, } @@ -223,7 +225,7 @@ class AccountMove(models.Model): line_dict.update({ 'DISKON': float_round(invoice_line_total_price - line.price_subtotal, 0), - 'PPN': float_round(free_tax_line, 0), + 'PPN': free_tax_line, }) free.append(line_dict) elif line.price_subtotal != 0.0: @@ -231,7 +233,7 @@ class AccountMove(models.Model): line_dict.update({ 'DISKON': float_round(invoice_line_discount_m2m, 0), - 'PPN': float_round(tax_line, 0), + 'PPN': tax_line, }) sales.append(line_dict) @@ -254,22 +256,59 @@ class AccountMove(models.Model): if tax.amount > 0: tax_line += sale['DPP'] * (tax.amount / 100.0) - sale['PPN'] = int(float_round(tax_line, 0)) + sale['PPN'] = tax_line free.remove(f) sub_total_before_adjustment += sale['DPP'] sub_total_ppn_before_adjustment += sale['PPN'] - bruto_total += sale['DISKON'] - total_discount += float_round(sale['DISKON'], 2) - # Change the values to string format after being used sale.update({ - 'DPP': float_repr(sale['DPP'], 0), - 'PPN': float_repr(sale['PPN'], 0), + # Use the db currency rounding to float_round the DPP/PPN. + # As we will correct them we need them to be close to the final result. + 'DPP': idr.round(sale['DPP']), + 'PPN': idr.round(sale['PPN']), 'DISKON': float_repr(sale['DISKON'], 0), }) + + # The total of the base (DPP) and taxes (PPN) must be a integer, equal to the JUMLAH_DPP and JUMLAH_PPN + # To do so, we adjust the first line in order to achieve the correct total + if sales: + diff_dpp = idr.round(eTax['JUMLAH_DPP'] - sum([sale['DPP'] for sale in sales])) + total_sales_ppn = idr.round(eTax['JUMLAH_PPN'] - sum([sale['PPN'] for sale in sales])) + # We will add the differences to the first line for which adding the difference will not result in a negative value. + for sale in sales: + if sale['DPP'] + diff_dpp >= 0 and sale['PPN'] + total_sales_ppn >= 0: + sale['HARGA_TOTAL'] += diff_dpp + sale['DPP'] += diff_dpp + diff_dpp = 0 + sale['PPN'] += total_sales_ppn + total_sales_ppn = 0 + break + + # We couldn't adjust everything in a single line as their values is too low. + # So we will instead slit the adjustment in multiple lines. + if diff_dpp or total_sales_ppn: + for sale in sales: + # DPP + sale_dpp = sale['DPP'] + sale["DPP"] = max(0, sale["DPP"] + diff_dpp) + diff_dpp -= (sale["DPP"] - sale_dpp) + sale['HARGA_TOTAL'] = sale["DPP"] + # PPN + sale_ppn = sale['PPN'] + sale["PPN"] = max(0, sale["PPN"] + total_sales_ppn) + total_sales_ppn -= (sale["PPN"] - sale_ppn) + + # Values now being corrected, we can format them for the CSV + for sale in sales: + sale.update({ + 'HARGA_TOTAL': float_repr(sale['HARGA_TOTAL'], idr.decimal_places), + 'DPP': float_repr(sale['DPP'], idr.decimal_places), + 'PPN': float_repr(sale['PPN'], idr.decimal_places), + }) + output_head += _csv_row(fk_values_list, delimiter) for sale in sales: of_values_list = ['OF'] + [str(sale[f]) for f in OF_HEAD_LIST[1:-2]] + ['0', '0'] diff --git a/addons/l10n_id_efaktur/tests/test_l10n_id_efaktur.py b/addons/l10n_id_efaktur/tests/test_l10n_id_efaktur.py index 85dff914859..7d36befbaa3 100644 --- a/addons/l10n_id_efaktur/tests/test_l10n_id_efaktur.py +++ b/addons/l10n_id_efaktur/tests/test_l10n_id_efaktur.py @@ -1,6 +1,10 @@ +import csv + +from odoo import Command from odoo.tests import tagged, common from odoo.addons.l10n_id_efaktur.models.account_move import FK_HEAD_LIST, LT_HEAD_LIST, OF_HEAD_LIST, _csv_row + @tagged('post_install', '-at_install', 'post_install_l10n') class TestIndonesianEfaktur(common.TransactionCase): def setUp(self): @@ -65,7 +69,7 @@ class TestIndonesianEfaktur(common.TransactionCase): ) # remaining lines line_4 = '"FK","01","0","0000000000001","5","2019","1/5/2019","12345","l10ntest","","100","10","0","","0","0","0","0","INV/2019/00001 12345","0"\n' - line_5 = '"OF","","","100","1.0","100","0","100","10","0","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 @@ -88,7 +92,75 @@ class TestIndonesianEfaktur(common.TransactionCase): _csv_row(OF_HEAD_LIST, ','), ) line_4 = '"FK","01","0","0000000000002","5","2019","1/5/2019","12345","l10ntest","","40040","4004","0","","0","0","0","0","INV/2019/00002 12345","0"\n' - line_5 = '"OF","","","100","400.0","40040","0","40040","4004","0","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": True} + ) + 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() + # Generate the generate efaktur csv. + efaktur_csv_output = invoice._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)