Skip to content
76 changes: 59 additions & 17 deletions eu_einvoice/european_e_invoice/custom/sales_invoice.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from frappe.core.utils import html2text
from frappe.model.naming import parse_naming_series
from frappe.utils import cstr
from frappe.utils.data import date_diff, flt, getdate, to_markdown
from frappe.utils.data import cint, date_diff, flt, getdate, to_markdown

from eu_einvoice.common_codes import CommonCodeRetriever
from eu_einvoice.schematron import get_validation_errors
Expand Down Expand Up @@ -502,6 +502,23 @@ def _add_line_item(self, item: SalesInvoiceItem):
li.settlement.monetary_summation.total_amount = flt(item.net_amount, item.precision("net_amount"))
self.doc.trade.items.add(li)

def _sum_item_net_matching_on_net_total_rate(self, tax) -> float | None:
"""Sum ``net_amount`` for items whose resolved VAT % matches this tax row (multi-rate invoices)."""
tax_row_vat_percent = flt(
tax.rate or frappe.db.get_value("Account", tax.account_head, "tax_rate") or 0.0
)
if not tax_row_vat_percent:
return 0.0
sum_net_amount = 0
for line_item in self.invoice.items:
item_rate = get_item_rate(line_item.item_tax_template, self.invoice.taxes)
if item_rate is None:
return None
if flt(item_rate) == tax_row_vat_percent:
sum_net_amount += line_item.net_amount

return flt(sum_net_amount, self.invoice.precision("net_total"))

def _add_taxes_and_charges(self):
tax_added = False
for i, tax in enumerate(self.invoice.taxes):
Expand All @@ -528,6 +545,14 @@ def _add_taxes_and_charges(self):

self.doc.trade.settlement.service_charge.add(service_charge)
elif tax.charge_type == "On Net Total":
tax_rate = tax.rate or frappe.db.get_value("Account", tax.account_head, "tax_rate") or 0
# Skip rows with 0% tax rate and non-zero tax amount: Assumption here is that if a tax_rate
# is not 0% and the tax amount is 0, then the tax is not applicable, as no item with
# this tax rate exists and hence it would throw validation errors.
# Possible 0% tax rates and their codes can be applied as 0% VAT, does not
# create a tax_amount.
if tax.tax_amount == 0 and tax_rate != 0:
continue
trade_tax = ApplicableTradeTax()
trade_tax.calculated_amount = tax.tax_amount
trade_tax.type_code = "VAT"
Expand All @@ -538,7 +563,7 @@ def _add_taxes_and_charges(self):
("Sales Taxes and Charges Template", self.invoice.taxes_and_charges),
]
)
tax_rate = tax.rate or frappe.db.get_value("Account", tax.account_head, "tax_rate") or 0

trade_tax.rate_applicable_percent = tax_rate

if len(self.invoice.taxes) == 1:
Expand All @@ -548,15 +573,15 @@ def _add_taxes_and_charges(self):
# We only have one tax rate on the line items, but it was not specified on the tax row
# so we use the tax rate from the line items.
trade_tax.rate_applicable_percent = self.item_tax_rates.pop()
elif hasattr(tax, "net_amount"):
trade_tax.basis_amount = tax.net_amount
elif hasattr(tax, "custom_net_amount"):
trade_tax.basis_amount = tax.custom_net_amount
elif tax.tax_amount and tax_rate:
# We don't know the basis amount for this tax, so we try to calculate it
trade_tax.basis_amount = round(tax.tax_amount / tax_rate * 100, 2)
else:
trade_tax.basis_amount = 0
basis = (
self._sum_item_net_matching_on_net_total_rate(tax)
or (hasattr(tax, "net_amount") and flt(tax.net_amount))
or (hasattr(tax, "custom_net_amount") and flt(tax.custom_net_amount))
or (tax.tax_amount and tax_rate and round(tax.tax_amount / tax_rate * 100, 2))
or 0
)
trade_tax.basis_amount = basis
Comment thread
0xD0M1M0 marked this conversation as resolved.

self.doc.trade.settlement.trade_tax.add(trade_tax)
tax_added = True
Expand Down Expand Up @@ -947,19 +972,36 @@ def _attach_xml_file(doc: SalesInvoice, xml_content: bytes, field_name: str | No
doc.db_set(field_name, file_doc.file_url)


def get_item_rate(item_tax_template: str | None, taxes: list[dict]) -> float | None:
"""Get the tax rate for an item from the item tax template and the taxes table."""
def get_item_rate(item_tax_template: str | None, taxes: list) -> float | None:
"""Resolve the VAT % for this line from the Item Tax Template and the invoice tax rows.

1) Return the ``tax_rate`` of the first template row (in template order) whose ``tax_type``
matches one of the invoice's ``account_head`` values. On ERPNext v15 and earlier, only
non-zero rates are considered. From v16 (``not_applicable`` on **Item Tax Template Detail**),
zero rates are included and rows with ``not_applicable`` set are excluded.
2) If the template did not match: if there is exactly one *On Net Total* row, use its ``rate``.
"""
if item_tax_template:
# match the accounts from the taxes table with the rate from the item tax template
tax_template = frappe.get_doc("Item Tax Template", item_tax_template)
tax_template = frappe.get_cached_doc("Item Tax Template", item_tax_template)
applicable_accounts = [tax.account_head for tax in taxes if tax.account_head]
has_not_applicable = bool(frappe.get_meta("Item Tax Template Detail").get_field("not_applicable"))

def _template_row_matches(item_tax) -> bool:
if item_tax.tax_type not in applicable_accounts:
return False
if has_not_applicable:
return not cint(getattr(item_tax, "not_applicable", 0))
return bool(item_tax.tax_rate)

for item_tax in tax_template.taxes:
if item_tax.tax_type in applicable_accounts:
if _template_row_matches(item_tax):
return item_tax.tax_rate

# if only one tax is on net total, return its rate
tax_rates = [invoice_tax.rate for invoice_tax in taxes if invoice_tax.charge_type == "On Net Total"]
tax_rates = [
invoice_tax.rate
for invoice_tax in taxes
if invoice_tax.charge_type == "On Net Total" and invoice_tax.rate is not None
]
return tax_rates[0] if len(tax_rates) == 1 else None


Expand Down
Loading