Skip to content

Commit 4e51a4a

Browse files
committed
[IMP] l10n_id_efaktur_coretax: Update VAT calculation and add restriction
Update the VAT calculation inside the E-faktur XML based on tax group added validation to prevent inproper tax combination task-4948267 closes odoo#236625 Signed-off-by: Nicolas Viseur (vin) <[email protected]> Signed-off-by: Kaleb Juliu (kaju) <[email protected]>
1 parent f0dfda4 commit 4e51a4a

File tree

5 files changed

+287
-64
lines changed

5 files changed

+287
-64
lines changed

addons/l10n_id_efaktur/models/account_move.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,6 @@ def _compute_show_kode_transaksi(self):
8383
and move.country_code == 'ID'
8484
)
8585

86-
@api.constrains('l10n_id_kode_transaksi', 'line_ids', 'partner_id')
87-
def _constraint_kode_ppn(self):
88-
ppn_tag = self.env.ref('l10n_id.ppn_tag')
89-
for move in self.filtered(lambda m: m.l10n_id_need_kode_transaksi and m.l10n_id_kode_transaksi != '08'):
90-
if any(ppn_tag.id in line.tax_tag_ids.ids for line in move.line_ids if line.display_type == 'product') \
91-
and any(ppn_tag.id not in line.tax_tag_ids.ids for line in move.line_ids if line.display_type == 'product'):
92-
raise UserError(_('Cannot mix VAT subject and Non-VAT subject items in the same invoice with this kode transaksi.'))
93-
for move in self.filtered(lambda m: m.l10n_id_need_kode_transaksi and m.l10n_id_kode_transaksi == '08'):
94-
if any(ppn_tag.id in line.tax_tag_ids.ids for line in move.line_ids if line.display_type == 'product'):
95-
raise UserError('Kode transaksi 08 is only for non VAT subject items.')
96-
9786
@api.constrains('l10n_id_tax_number')
9887
def _constrains_l10n_id_tax_number(self):
9988
for record in self.filtered('l10n_id_tax_number'):

addons/l10n_id_efaktur_coretax/i18n/l10n_id_efaktur_coretax.pot

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
#
55
msgid ""
66
msgstr ""
7-
"Project-Id-Version: Odoo Server 18.0\n"
7+
"Project-Id-Version: Odoo Server 17.0+e\n"
88
"Report-Msgid-Bugs-To: \n"
9-
"POT-Creation-Date: 2025-11-03 15:43+0000\n"
10-
"PO-Revision-Date: 2025-11-03 15:43+0000\n"
9+
"POT-Creation-Date: 2025-11-17 07:58+0000\n"
10+
"PO-Revision-Date: 2025-11-17 07:58+0000\n"
1111
"Last-Translator: \n"
1212
"Language-Team: \n"
1313
"MIME-Version: 1.0\n"
@@ -700,6 +700,57 @@ msgstr ""
700700
#: model:ir.model.fields,field_description:l10n_id_efaktur_coretax.field_res_partner__l10n_id_kode_transaksi
701701
#: model:ir.model.fields,field_description:l10n_id_efaktur_coretax.field_res_users__l10n_id_kode_transaksi
702702
msgid "Invoice Transaction Code"
703+
704+
#. odoo-python
705+
#: code:addons/l10n_id_efaktur_coretax/models/account_move.py:0
706+
#, python-format
707+
msgid "Invoice %s: can only have one STLG group."
708+
msgstr ""
709+
710+
#. module: l10n_id_efaktur_coretax
711+
#. odoo-python
712+
#: code:addons/l10n_id_efaktur_coretax/models/account_move.py:0
713+
#, python-format
714+
msgid "Invoice %s: can only have one tax group (excluding STLG)."
715+
msgstr ""
716+
717+
#. module: l10n_id_efaktur_coretax
718+
#. odoo-python
719+
#: code:addons/l10n_id_efaktur_coretax/models/account_move.py:0
720+
msgid ""
721+
"Invoice %(inv)s: line '%(line)s' contains both Luxury-Goods and Non-Luxury-"
722+
"Goods taxes."
723+
msgstr ""
724+
725+
#. module: l10n_id_efaktur_coretax
726+
#. odoo-python
727+
#: code:addons/l10n_id_efaktur_coretax/models/account_move.py:0
728+
msgid ""
729+
"Invoice %(inv)s: line '%(line)s' contains both Non-Luxury-Goods and STLG "
730+
"taxes."
731+
msgstr ""
732+
733+
#. module: l10n_id_efaktur_coretax
734+
#. odoo-python
735+
#: code:addons/l10n_id_efaktur_coretax/models/account_move.py:0
736+
msgid ""
737+
"Invoice %(inv)s: line '%(line)s' has STLG tax but missing the required "
738+
"Luxury-Goods tax."
739+
msgstr ""
740+
741+
#. module: l10n_id_efaktur_coretax
742+
#. odoo-python
743+
#: code:addons/l10n_id_efaktur_coretax/models/account_move.py:0
744+
msgid ""
745+
"Invoice %(inv)s: transaction code %(kode)s does not allow 0%% (Zero-rated or"
746+
" Exempt) taxes."
747+
msgstr ""
748+
749+
#. module: l10n_id_efaktur_coretax
750+
#. odoo-python
751+
#: code:addons/l10n_id_efaktur_coretax/models/account_move.py:0
752+
msgid ""
753+
"Invoice %(inv)s: transaction code %(kode)s must always have tax amount 0%%."
703754
msgstr ""
704755

705756
#. module: l10n_id_efaktur_coretax
@@ -871,6 +922,13 @@ msgstr ""
871922
msgid "Passport"
872923
msgstr ""
873924

925+
#. module: l10n_id_efaktur_coretax
926+
#. odoo-python
927+
#: code:addons/l10n_id_efaktur_coretax/models/account_move_line.py:0
928+
#, python-format
929+
msgid "Price for line '%s' cannot be a negative amount. Please check again."
930+
msgstr ""
931+
874932
#. module: l10n_id_efaktur_coretax
875933
#: model:ir.model,name:l10n_id_efaktur_coretax.model_product_template
876934
msgid "Product"
@@ -1001,7 +1059,8 @@ msgstr ""
10011059
#. module: l10n_id_efaktur_coretax
10021060
#. odoo-python
10031061
#: code:addons/l10n_id_efaktur_coretax/models/account_move.py:0
1004-
msgid "Unable to download E-faktur fot he following reasons(s):"
1062+
#, python-format
1063+
msgid "Unable to download E-faktur for the following reason(s):"
10051064
msgstr ""
10061065

10071066
#. module: l10n_id_efaktur_coretax

addons/l10n_id_efaktur_coretax/models/account_move.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,66 @@ def _compute_l10n_id_coretax_add_info(self):
185185
if digits:
186186
move.l10n_id_coretax_add_info_08 = f"TD.005{digits[-2:]}"
187187

188+
def _validate_tax_groups(self):
189+
err_messages = []
190+
allowed_codes = {'01', '02', '03', '04', '05', '06', '09', '10'}
191+
must_be_zero_codes = {'07', '08'}
192+
193+
for move in self:
194+
kode = move.l10n_id_kode_transaksi
195+
non_luxury_group = self.env['account.chart.template'].with_company(move.company_id.id).ref("l10n_id_tax_group_non_luxury_goods", raise_if_not_found=False)
196+
luxury_group = self.env['account.chart.template'].with_company(move.company_id.id).ref("l10n_id_tax_group_luxury_goods", raise_if_not_found=False)
197+
zero_group = self.env['account.chart.template'].with_company(move.company_id.id).ref("l10n_id_tax_group_0", raise_if_not_found=False)
198+
exempt_group = self.env['account.chart.template'].with_company(move.company_id.id).ref("l10n_id_tax_group_exempt", raise_if_not_found=False)
199+
stlg_group = self.env['account.chart.template'].with_company(move.company_id.id).ref("l10n_id_tax_group_stlg", raise_if_not_found=False)
200+
product_lines = move.line_ids.filtered(lambda line: line.display_type == 'product')
201+
all_taxes = product_lines.mapped('tax_ids')
202+
tax_groups = set(all_taxes.mapped('tax_group_id'))
203+
204+
# Multiple tax groups check
205+
if len([g for g in tax_groups if g not in {stlg_group}]) > 1:
206+
err_messages.append(_("Invoice %s: can only have one tax group (excluding STLG).", move.name or ''))
207+
if len([g for g in tax_groups if g == stlg_group]) > 1:
208+
err_messages.append(_("Invoice %s: can only have one STLG group.", move.name or ''))
209+
210+
# Allowed codes (01-06, 09, 10)
211+
if kode in allowed_codes:
212+
for line in product_lines:
213+
line_tax_groups = set(line.tax_ids.mapped('tax_group_id'))
214+
if luxury_group and non_luxury_group and {luxury_group, non_luxury_group}.issubset(line_tax_groups):
215+
err_messages.append(_(
216+
"Invoice %(inv)s: line '%(line)s' contains both Luxury-Goods and Non-Luxury-Goods taxes.",
217+
inv=move.name or '', line=line.product_id.display_name or '')
218+
)
219+
if non_luxury_group and stlg_group and {non_luxury_group, stlg_group}.issubset(line_tax_groups):
220+
err_messages.append(_(
221+
"Invoice %(inv)s: line '%(line)s' contains both Non-Luxury-Goods and STLG taxes.",
222+
inv=move.name or '', line=line.product_id.display_name or '')
223+
)
224+
if stlg_group and stlg_group in line_tax_groups:
225+
if not (luxury_group and luxury_group in line_tax_groups):
226+
err_messages.append(_(
227+
"Invoice %(inv)s: line '%(line)s' has STLG tax but missing the required Luxury-Goods tax.",
228+
inv=move.name or '', line=line.product_id.display_name or '')
229+
)
230+
for tax in line.tax_ids:
231+
if ((hasattr(tax, 'amount') and float(tax.amount) == 0.0) or (tax.tax_group_id in {zero_group, exempt_group})):
232+
err_messages.append(_(
233+
"Invoice %(inv)s: transaction code %(kode)s does not allow 0%% (Zero-rated or Exempt) taxes.",
234+
inv=move.name or '', kode=kode)
235+
)
236+
237+
# Must-be-zero codes (07-08)
238+
elif kode in must_be_zero_codes:
239+
for line in product_lines:
240+
for tax in line.tax_ids:
241+
if hasattr(tax, 'amount') and float(tax.amount) != 0.0:
242+
err_messages.append(_(
243+
"Invoice %(inv)s: transaction code %(kode)s must always have tax amount 0%%.",
244+
inv=move.name or '', kode=kode)
245+
)
246+
return err_messages
247+
188248
def download_efaktur(self):
189249
"""OVERRIDE l10n_id_efaktur
190250
@@ -232,8 +292,11 @@ def download_efaktur(self):
232292
if not (record.l10n_id_coretax_add_info_08 and record.l10n_id_coretax_facility_info_08):
233293
err_messages.append(_("Invoice %s doesn't contain the Additional info and Facility Stamp yet (Kode 08)", record.name))
234294

295+
# Check tax groups
296+
err_messages.extend(self._validate_tax_groups())
297+
235298
if err_messages:
236-
err_messages = [_('Unable to download E-faktur fot he following reasons(s):')] + err_messages
299+
err_messages = [_('Unable to download E-faktur for the following reason(s):')] + err_messages
237300
raise ValidationError('\n - '.join(err_messages))
238301

239302
# All invoices in self have no documents; we can create a new one for them.

addons/l10n_id_efaktur_coretax/models/account_move_line.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Part of Odoo. See LICENSE file for full copyright and licensing details.
22

3-
from odoo import models
4-
from odoo.tools.float_utils import float_repr
3+
from odoo import _, models
4+
from odoo.tools.float_utils import float_repr, float_compare
5+
from odoo.exceptions import ValidationError
56

67

78
class AccountMoveLine(models.Model):
@@ -12,6 +13,9 @@ def _l10n_id_coretax_build_invoice_line_vals(self, vals):
1213
self.ensure_one()
1314
idr = self.env.ref('base.IDR')
1415

16+
if float_compare(self.price_subtotal, 0.0, precision_rounding=self.currency_id.rounding) < 0:
17+
raise ValidationError(_("Price for line '%s' cannot be a negative amount. Please check again.", self.name))
18+
1519
# initialize
1620
if not vals.get('lines'):
1721
vals['lines'] = []
@@ -21,8 +25,15 @@ def _l10n_id_coretax_build_invoice_line_vals(self, vals):
2125
# Separate tax into the regular and luxury component
2226
ChartTemplate = self.env['account.chart.template'].with_company(self.company_id)
2327
luxury_tax_group = ChartTemplate.ref('l10n_id_tax_group_luxury_goods', raise_if_not_found=False)
28+
stlg_tax_group = ChartTemplate.ref('l10n_id_tax_group_stlg', raise_if_not_found=False)
29+
zero_tax_group_0 = ChartTemplate.ref('l10n_id_tax_group_0', raise_if_not_found=False)
30+
zero_tax_group_exempt = ChartTemplate.ref('l10n_id_tax_group_exempt', raise_if_not_found=False)
31+
zero_tax_groups = {zero_tax_group_0, zero_tax_group_exempt} - {False}
32+
33+
zero_tax = self.tax_ids.filtered(lambda tax: tax.tax_group_id in zero_tax_groups)
2434
luxury_tax = self.tax_ids.filtered(lambda tax: tax.tax_group_id == luxury_tax_group)
25-
regular_tax = self.tax_ids - luxury_tax
35+
stlg_tax = self.tax_ids.filtered(lambda tax: tax.tax_group_id == stlg_tax_group)
36+
regular_tax = self.tax_ids - luxury_tax - stlg_tax - zero_tax
2637

2738
# "Price" is unit price calculation excluding tax and discount
2839
# "TotalDiscount" is total of "Price" * quantity * discount
@@ -38,23 +49,15 @@ def _l10n_id_coretax_build_invoice_line_vals(self, vals):
3849
"TotalDiscount": idr.round(self.discount * tax_res['total_excluded'] * self.quantity / 100),
3950
"TaxBase": idr.round(self.price_subtotal), # DPP
4051
"VATRate": 12,
41-
"STLGRate": sum(luxury_tax.mapped('amount')) if luxury_tax else 0.0,
52+
"STLGRate": stlg_tax.amount if stlg_tax else 0.0,
4253
}
43-
44-
# Code 04 represents "Using other value as tax base". This code is now the norm
45-
# being used if user is selling non-luxury item where we have to multiply original price
46-
# by ratio of 11/12 and having VATRate of 12 resulting to effectively 11% tax.
47-
if self.move_id.l10n_id_kode_transaksi == "04":
48-
line_val['VATRate'] = 12
49-
line_val['OtherTaxBase'] = idr.round(self.price_subtotal * 11 / 12)
50-
# For all other code, OtherTaxBase will follow TaxBase and calculation of VAT should follow the amount of tax itself
51-
else:
52-
line_val['VATRate'] = sum(regular_tax.mapped('amount'))
54+
if self.move_id.l10n_id_kode_transaksi == "01" or (not regular_tax and not zero_tax):
5355
line_val['OtherTaxBase'] = line_val['TaxBase']
56+
else:
57+
line_val['OtherTaxBase'] = idr.round(self.price_subtotal * 11 / 12)
5458

5559
line_val['VAT'] = idr.round(line_val['OtherTaxBase'] * line_val['VATRate'] / 100)
5660
line_val['STLG'] = idr.round(line_val['STLGRate'] * line_val['OtherTaxBase'] / 100)
57-
5861
# for numerical attributes in line_val, use float_repr to ensure proper formatting
5962
numerical_fields = ['Price', 'TotalDiscount', 'TaxBase', 'OtherTaxBase', 'VAT', 'STLG']
6063
for field in numerical_fields:

0 commit comments

Comments
 (0)