[FIX] l10n_id_efaktur: rounding improvement
The efaktur export has rounding issues due to the fact that we need to display both the amount_untaxed and amount_tax rounded to 0 decimals as well as the detail of each line. As the rounding of the sum will most of the time be different to the sum of rounded lines, the government website refuse the resulting file. This fix will try to improve things a bit by: - Avoiding to round the values while we are still manipulating them. - Round the values once when the initial computation is done for the lines. Once all lines have been computed, we will: - Find the difference, if any, between the rounded amount_untaxed and amount_tax and the line totals and round that too. - Update the first line with enough value in the result with that value in order to correct the total - Finally, round the values for representation to two decimals. If we cannot correct it in a single line, the correction is split in multiple lines. Hopefully, these steps will help ensure that the sum of the lines will be equal to the rounded values of the amount_untaxed and amount_tax and thus be correct. Task id # 3420220 closes odoo/odoo#136996 X-original-commit: 65e7446ffad5c87ec44f1012e709606b44f3af39 Signed-off-by: Brice Bartoletti (bib) <bib@odoo.com>
This commit is contained in:
@@ -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']
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user