mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-28 22:28:24 +00:00
fix!: tax net_amount and not_applicable (#54687)
This commit is contained in:
@@ -94,11 +94,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8",
|
||||
@@ -187,12 +187,14 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-25 11:10:10.945027",
|
||||
"modified": "2026-05-01 00:38:53.368737",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Advance Taxes and Charges",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
}
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class AdvanceTaxesandCharges(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
project: DF.Link | None
|
||||
rate: DF.Float
|
||||
row_id: DF.Data | None
|
||||
tax_amount: DF.Currency
|
||||
|
||||
@@ -47,3 +47,12 @@ frappe.ui.form.on("Item Tax Template", {
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Item Tax Template Detail", {
|
||||
not_applicable: function (frm, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
if (row.not_applicable) {
|
||||
frappe.model.set_value(cdt, cdn, "tax_rate", 0);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,8 +27,15 @@ class ItemTaxTemplate(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.set_zero_rate_for_not_applicable_tax()
|
||||
self.validate_tax_accounts()
|
||||
|
||||
def set_zero_rate_for_not_applicable_tax(self):
|
||||
"""Ensure tax_rate is 0 for any row marked as not applicable."""
|
||||
for row in self.get("taxes"):
|
||||
if row.not_applicable:
|
||||
row.tax_rate = 0
|
||||
|
||||
def autoname(self):
|
||||
if self.company and self.title:
|
||||
abbr = frappe.get_cached_value("Company", self.company, "abbr")
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"tax_type",
|
||||
"tax_rate"
|
||||
"tax_rate",
|
||||
"not_applicable"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -21,12 +22,21 @@
|
||||
"fieldname": "tax_rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax Rate"
|
||||
"label": "Tax Rate",
|
||||
"read_only_depends_on": "eval:doc.not_applicable"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Check if this tax is not applicable to items (distinct from 0% rate)",
|
||||
"fieldname": "not_applicable",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Not Applicable"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-30 23:49:27.020639",
|
||||
"modified": "2026-04-30 23:59:22.020639",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Item Tax Template Detail",
|
||||
|
||||
@@ -14,6 +14,7 @@ class ItemTaxTemplateDetail(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
not_applicable: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -25,10 +25,12 @@
|
||||
"project",
|
||||
"section_break_9",
|
||||
"account_currency",
|
||||
"net_amount",
|
||||
"tax_amount",
|
||||
"tax_amount_after_discount_amount",
|
||||
"total",
|
||||
"column_break_14",
|
||||
"base_net_amount",
|
||||
"base_tax_amount",
|
||||
"base_total",
|
||||
"base_tax_amount_after_discount_amount",
|
||||
@@ -213,11 +215,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -241,20 +243,38 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Basis for tax calculation",
|
||||
"fieldname": "net_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Net Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Basis for tax calculation",
|
||||
"fieldname": "base_net_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Net Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-15 13:14:48.936047",
|
||||
"modified": "2026-05-01 00:38:29.543523",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Taxes and Charges",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ class PurchaseTaxesandCharges(Document):
|
||||
account_currency: DF.Link | None
|
||||
account_head: DF.Link
|
||||
add_deduct_tax: DF.Literal["Add", "Deduct"]
|
||||
base_net_amount: DF.Currency
|
||||
base_tax_amount: DF.Currency
|
||||
base_tax_amount_after_discount_amount: DF.Currency
|
||||
base_total: DF.Currency
|
||||
@@ -35,9 +36,11 @@ class PurchaseTaxesandCharges(Document):
|
||||
included_in_print_rate: DF.Check
|
||||
is_tax_withholding_account: DF.Check
|
||||
item_wise_tax_detail: DF.Code | None
|
||||
net_amount: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
project: DF.Link | None
|
||||
rate: DF.Float
|
||||
row_id: DF.Data | None
|
||||
tax_amount: DF.Currency
|
||||
|
||||
@@ -21,10 +21,12 @@
|
||||
"rate",
|
||||
"section_break_9",
|
||||
"account_currency",
|
||||
"net_amount",
|
||||
"tax_amount",
|
||||
"total",
|
||||
"tax_amount_after_discount_amount",
|
||||
"column_break_13",
|
||||
"base_net_amount",
|
||||
"base_tax_amount",
|
||||
"base_total",
|
||||
"base_tax_amount_after_discount_amount",
|
||||
@@ -190,11 +192,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -220,19 +222,36 @@
|
||||
"label": "Account Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Basis for tax calculation",
|
||||
"fieldname": "net_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Net Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Basis for tax calculation",
|
||||
"fieldname": "base_net_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Net Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-14 10:08:17.776528",
|
||||
"modified": "2026-05-01 00:37:57.880071",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Taxes and Charges",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ class SalesTaxesandCharges(Document):
|
||||
|
||||
account_currency: DF.Link | None
|
||||
account_head: DF.Link
|
||||
base_net_amount: DF.Currency
|
||||
base_tax_amount: DF.Currency
|
||||
base_tax_amount_after_discount_amount: DF.Currency
|
||||
base_total: DF.Currency
|
||||
@@ -33,9 +34,11 @@ class SalesTaxesandCharges(Document):
|
||||
included_in_paid_amount: DF.Check
|
||||
included_in_print_rate: DF.Check
|
||||
item_wise_tax_detail: DF.Code | None
|
||||
net_amount: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
project: DF.Link | None
|
||||
rate: DF.Float
|
||||
row_id: DF.Data | None
|
||||
tax_amount: DF.Currency
|
||||
|
||||
@@ -67,6 +67,7 @@ from erpnext.setup.utils import get_exchange_rate
|
||||
from erpnext.stock.doctype.item.item import get_uom_conv_factor
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
from erpnext.stock.get_item_details import (
|
||||
NOT_APPLICABLE_TAX,
|
||||
_get_item_tax_template,
|
||||
_get_item_tax_template_from_item_group,
|
||||
get_bin_details,
|
||||
@@ -1294,7 +1295,10 @@ class AccountsController(TransactionBase):
|
||||
if isinstance(item_tax_rate, str):
|
||||
item_tax_rate = parse_json(item_tax_rate)
|
||||
|
||||
for account_head, _rate in item_tax_rate.items():
|
||||
for account_head, rate in item_tax_rate.items():
|
||||
if rate == NOT_APPLICABLE_TAX:
|
||||
continue
|
||||
|
||||
row = self.get_tax_row(account_head)
|
||||
|
||||
if not row:
|
||||
@@ -3721,8 +3725,11 @@ def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True):
|
||||
|
||||
if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
|
||||
tax_map = json.loads(child_item.get("item_tax_rate"))
|
||||
for tax_type in tax_map:
|
||||
tax_rate = flt(tax_map[tax_type])
|
||||
for tax_type, tax_rate in tax_map.items():
|
||||
if tax_rate == NOT_APPLICABLE_TAX:
|
||||
continue
|
||||
|
||||
tax_rate = flt(tax_rate)
|
||||
taxes = parent_doc.get("taxes") or []
|
||||
# add new row for tax head only if missing
|
||||
found = any(tax.account_head == tax_type for tax in taxes)
|
||||
|
||||
@@ -19,7 +19,11 @@ from erpnext.controllers.accounts_controller import (
|
||||
validate_inclusive_tax,
|
||||
validate_taxes_and_charges,
|
||||
)
|
||||
from erpnext.stock.get_item_details import _get_item_tax_template, get_item_tax_map
|
||||
from erpnext.stock.get_item_details import (
|
||||
NOT_APPLICABLE_TAX,
|
||||
_get_item_tax_template,
|
||||
get_item_tax_map,
|
||||
)
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
|
||||
@@ -275,6 +279,7 @@ class calculate_taxes_and_totals:
|
||||
tax.item_wise_tax_detail = {}
|
||||
|
||||
tax_fields = [
|
||||
"net_amount",
|
||||
"total",
|
||||
"tax_amount_after_discount_amount",
|
||||
"tax_amount_for_current_item",
|
||||
@@ -345,6 +350,9 @@ class calculate_taxes_and_totals:
|
||||
if cint(tax.included_in_print_rate):
|
||||
tax_rate = self._get_tax_rate(tax, item_tax_map)
|
||||
|
||||
if tax_rate == NOT_APPLICABLE_TAX:
|
||||
return tax_slope, tax_intercept
|
||||
|
||||
if tax.charge_type == "On Net Total":
|
||||
tax_slope = tax_rate / 100.0
|
||||
|
||||
@@ -376,9 +384,12 @@ class calculate_taxes_and_totals:
|
||||
|
||||
def _get_tax_rate(self, tax, item_tax_map):
|
||||
if tax.account_head in item_tax_map:
|
||||
return flt(item_tax_map.get(tax.account_head), self.doc.precision("rate", tax))
|
||||
else:
|
||||
return tax.rate
|
||||
rate = item_tax_map[tax.account_head]
|
||||
if rate == NOT_APPLICABLE_TAX:
|
||||
return NOT_APPLICABLE_TAX
|
||||
return flt(rate, self.doc.precision("rate", tax))
|
||||
|
||||
return tax.rate
|
||||
|
||||
def calculate_net_total(self):
|
||||
self.doc.total_qty = (
|
||||
@@ -426,9 +437,12 @@ class calculate_taxes_and_totals:
|
||||
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
|
||||
for i, tax in enumerate(doc.taxes):
|
||||
# tax_amount represents the amount of tax for the current step
|
||||
current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map)
|
||||
current_net_amount, current_tax_amount = self.get_current_tax_and_net_amount(
|
||||
item, tax, item_tax_map
|
||||
)
|
||||
if frappe.flags.round_row_wise_tax:
|
||||
current_tax_amount = flt(current_tax_amount, tax.precision("tax_amount"))
|
||||
current_net_amount = flt(current_net_amount, tax.precision("net_amount"))
|
||||
|
||||
# Adjust divisional loss to the last item
|
||||
if tax.charge_type == "Actual":
|
||||
@@ -436,6 +450,10 @@ class calculate_taxes_and_totals:
|
||||
if n == len(self._items) - 1:
|
||||
current_tax_amount += actual_tax_dict[tax.idx]
|
||||
|
||||
# net_amount is the taxable basis, it feeds no total and is always
|
||||
# accumulated, unlike tax_amount which is kept from the first pass
|
||||
tax.net_amount += current_net_amount
|
||||
|
||||
# accumulate tax amount into tax.tax_amount
|
||||
if tax.charge_type != "Actual" and not (
|
||||
self.discount_amount_applied and self.doc.apply_discount_on == "Grand Total"
|
||||
@@ -486,7 +504,9 @@ class calculate_taxes_and_totals:
|
||||
|
||||
for i, tax in enumerate(doc.taxes):
|
||||
self.round_off_totals(tax)
|
||||
self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"])
|
||||
self._set_in_company_currency(
|
||||
tax, ["tax_amount", "tax_amount_after_discount_amount", "net_amount"]
|
||||
)
|
||||
|
||||
self.round_off_base_values(tax)
|
||||
self.set_cumulative_total(i, tax)
|
||||
@@ -517,8 +537,17 @@ class calculate_taxes_and_totals:
|
||||
tax.total = flt(self.doc.get("taxes")[row_idx - 1].total + tax_amount, tax.precision("total"))
|
||||
|
||||
def get_current_tax_amount(self, item, tax, item_tax_map):
|
||||
# kept for backwards compatibility with callers outside this module
|
||||
_, current_tax_amount = self.get_current_tax_and_net_amount(item, tax, item_tax_map)
|
||||
return current_tax_amount
|
||||
|
||||
def get_current_tax_and_net_amount(self, item, tax, item_tax_map):
|
||||
tax_rate = self._get_tax_rate(tax, item_tax_map)
|
||||
current_tax_amount = 0.0
|
||||
current_net_amount = 0.0
|
||||
|
||||
if tax_rate == NOT_APPLICABLE_TAX:
|
||||
return current_net_amount, current_tax_amount
|
||||
|
||||
if tax.charge_type == "Actual":
|
||||
# distribute the tax amount proportionally to each item row
|
||||
@@ -528,23 +557,25 @@ class calculate_taxes_and_totals:
|
||||
if not item.get("apply_tds") or not self.doc.tax_withholding_net_total:
|
||||
current_tax_amount = 0.0
|
||||
else:
|
||||
current_tax_amount = item.net_amount * actual / self.doc.tax_withholding_net_total
|
||||
current_net_amount = item.net_amount
|
||||
current_tax_amount = current_net_amount * actual / self.doc.tax_withholding_net_total
|
||||
else:
|
||||
current_net_amount = item.net_amount
|
||||
current_tax_amount = (
|
||||
item.net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
|
||||
current_net_amount * actual / self.doc.net_total if self.doc.net_total else 0.0
|
||||
)
|
||||
|
||||
elif tax.charge_type == "On Net Total":
|
||||
current_net_amount = item.net_amount
|
||||
current_tax_amount = (tax_rate / 100.0) * item.net_amount
|
||||
elif tax.charge_type == "On Previous Row Amount":
|
||||
current_tax_amount = (tax_rate / 100.0) * self.doc.get("taxes")[
|
||||
cint(tax.row_id) - 1
|
||||
].tax_amount_for_current_item
|
||||
current_net_amount = self.doc.get("taxes")[cint(tax.row_id) - 1].tax_amount_for_current_item
|
||||
current_tax_amount = (tax_rate / 100.0) * current_net_amount
|
||||
elif tax.charge_type == "On Previous Row Total":
|
||||
current_tax_amount = (tax_rate / 100.0) * self.doc.get("taxes")[
|
||||
cint(tax.row_id) - 1
|
||||
].grand_total_for_current_item
|
||||
current_net_amount = self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_for_current_item
|
||||
current_tax_amount = (tax_rate / 100.0) * current_net_amount
|
||||
elif tax.charge_type == "On Item Quantity":
|
||||
# don't sum current net amount: net_amount field is currency-denominated
|
||||
current_tax_amount = tax_rate * item.qty
|
||||
else:
|
||||
# Custom charge_type: rate applies to the resolver-provided base.
|
||||
@@ -553,7 +584,7 @@ class calculate_taxes_and_totals:
|
||||
if not (self.doc.get("is_consolidated") or tax.get("dont_recompute_tax")):
|
||||
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
|
||||
|
||||
return current_tax_amount
|
||||
return current_net_amount, current_tax_amount
|
||||
|
||||
def get_item_taxable_base(self, item, tax):
|
||||
"""Per-item base a custom charge_type's rate is applied to.
|
||||
|
||||
@@ -158,3 +158,141 @@ class TestTaxesAndTotals(FrappeTestCase):
|
||||
self.assertEqual(so.rounding_adjustment, 0)
|
||||
self.assertEqual(so.base_rounded_total, 0)
|
||||
self.assertEqual(so.base_rounding_adjustment, 0)
|
||||
|
||||
def test_tax_net_amount_with_not_applicable_item_tax(self):
|
||||
"""Each tax row records only the net of the items it actually applies to.
|
||||
|
||||
Two items of 100 each, one per template. Template A applies VAT 7 and
|
||||
marks VAT 19 not applicable, template B does the reverse. Both tax rows
|
||||
must report a net_amount of 100, not the full net total of 200.
|
||||
"""
|
||||
vat_7 = "_Test Account VAT - _TC"
|
||||
vat_19 = "_Test Account Service Tax - _TC"
|
||||
|
||||
templates = {}
|
||||
for title, rows in {
|
||||
"_Test NA Template A": [(vat_7, 7, 0), (vat_19, 0, 1)],
|
||||
"_Test NA Template B": [(vat_7, 0, 1), (vat_19, 19, 0)],
|
||||
}.items():
|
||||
doc = frappe.new_doc("Item Tax Template")
|
||||
doc.title = title
|
||||
doc.company = "_Test Company"
|
||||
for tax_type, tax_rate, not_applicable in rows:
|
||||
doc.append(
|
||||
"taxes",
|
||||
{"tax_type": tax_type, "tax_rate": tax_rate, "not_applicable": not_applicable},
|
||||
)
|
||||
templates[title] = doc.insert().name
|
||||
|
||||
so = make_sales_order(do_not_save=True)
|
||||
so.items = []
|
||||
for title in templates:
|
||||
so.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_tax_template": templates[title],
|
||||
},
|
||||
)
|
||||
|
||||
so.set("taxes", [])
|
||||
for account_head in (vat_7, vat_19):
|
||||
so.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": account_head,
|
||||
"description": account_head,
|
||||
"rate": 0,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
},
|
||||
)
|
||||
|
||||
so.save()
|
||||
|
||||
self.assertEqual(so.net_total, 200.0)
|
||||
self.assertEqual(so.taxes[0].net_amount, 100.0)
|
||||
self.assertEqual(so.taxes[0].tax_amount, 7.0)
|
||||
self.assertEqual(so.taxes[1].net_amount, 100.0)
|
||||
self.assertEqual(so.taxes[1].tax_amount, 19.0)
|
||||
|
||||
def test_inclusive_tax_with_not_applicable_item_tax(self):
|
||||
"""An inclusive tax row meeting an item that marks it not applicable must
|
||||
contribute no fraction, instead of raising in get_current_tax_fraction."""
|
||||
vat_19 = "_Test Account Service Tax - _TC"
|
||||
|
||||
template = frappe.new_doc("Item Tax Template")
|
||||
template.title = "_Test NA Template Inclusive"
|
||||
template.company = "_Test Company"
|
||||
template.append("taxes", {"tax_type": vat_19, "tax_rate": 0, "not_applicable": 1})
|
||||
template.insert()
|
||||
|
||||
so = make_sales_order(do_not_save=True)
|
||||
so.items = []
|
||||
so.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"rate": 119,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_tax_template": template.name,
|
||||
},
|
||||
)
|
||||
so.set("taxes", [])
|
||||
so.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": vat_19,
|
||||
"description": vat_19,
|
||||
"rate": 19,
|
||||
"included_in_print_rate": 1,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
},
|
||||
)
|
||||
|
||||
so.save()
|
||||
|
||||
# the tax does not apply, so nothing is backed out of the printed rate
|
||||
self.assertEqual(so.net_total, 119.0)
|
||||
self.assertEqual(so.taxes[0].tax_amount, 0.0)
|
||||
self.assertEqual(so.taxes[0].net_amount, 0.0)
|
||||
self.assertEqual(so.grand_total, 119.0)
|
||||
|
||||
def test_tax_net_amount_survives_grand_total_discount(self):
|
||||
"""A discount on Grand Total re-runs the calculation with
|
||||
discount_amount_applied set. net_amount is reset on that second pass, so
|
||||
it has to be accumulated there too instead of being left at zero."""
|
||||
so = make_sales_order(do_not_save=True)
|
||||
so.items = []
|
||||
so.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
},
|
||||
)
|
||||
so.set("taxes", [])
|
||||
so.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": "VAT",
|
||||
"rate": 19,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
},
|
||||
)
|
||||
so.apply_discount_on = "Grand Total"
|
||||
so.discount_amount = 100
|
||||
|
||||
calculate_taxes_and_totals(so)
|
||||
|
||||
self.assertEqual(so.taxes[0].net_amount, so.net_total)
|
||||
self.assertEqual(so.grand_total, 1090.0)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
// mirror of erpnext.stock.get_item_details.NOT_APPLICABLE_TAX
|
||||
erpnext.NOT_APPLICABLE_TAX = "N/A";
|
||||
|
||||
// Per-charge_type base resolvers, mirror of the `erpnext_taxable_base_resolvers`
|
||||
// server hook. A localization registers `fn(calc, item, tax)` returning the per-item
|
||||
// base, so the client preview matches the server for custom charge types.
|
||||
@@ -300,6 +303,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
if(cint(tax.included_in_print_rate)) {
|
||||
var tax_rate = this._get_tax_rate(tax, item_tax_map);
|
||||
|
||||
if (tax_rate === erpnext.NOT_APPLICABLE_TAX) {
|
||||
return [tax_slope, tax_intercept];
|
||||
}
|
||||
|
||||
if(tax.charge_type == "On Net Total") {
|
||||
tax_slope = (tax_rate / 100.0);
|
||||
|
||||
@@ -339,8 +346,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
_get_tax_rate(tax, item_tax_map) {
|
||||
return (Object.keys(item_tax_map).indexOf(tax.account_head) != -1) ?
|
||||
flt(item_tax_map[tax.account_head], precision("rate", tax)) : tax.rate;
|
||||
if (tax.account_head in item_tax_map) {
|
||||
let rate = item_tax_map[tax.account_head];
|
||||
if (rate === erpnext.NOT_APPLICABLE_TAX) {
|
||||
return erpnext.NOT_APPLICABLE_TAX;
|
||||
}
|
||||
return flt(rate, precision("rate", tax));
|
||||
}
|
||||
return tax.rate;
|
||||
}
|
||||
|
||||
calculate_net_total() {
|
||||
@@ -379,6 +392,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
$.each(item_tax_map, function(tax, rate) {
|
||||
if (rate === erpnext.NOT_APPLICABLE_TAX) {
|
||||
return;
|
||||
}
|
||||
let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
|
||||
if (!found) {
|
||||
let child = frappe.model.add_child(me.frm.doc, "taxes");
|
||||
@@ -429,11 +445,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
}
|
||||
|
||||
// net_amount is the taxable basis, it feeds no total and is always
|
||||
// accumulated, unlike tax_amount which is kept from the first pass
|
||||
tax.net_amount += current_net_amount;
|
||||
|
||||
// accumulate tax amount into tax.tax_amount
|
||||
if (tax.charge_type != "Actual" &&
|
||||
!(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) {
|
||||
tax.tax_amount += current_tax_amount;
|
||||
tax.net_amount += current_net_amount;
|
||||
}
|
||||
|
||||
// store tax_amount for current item as it will be used for
|
||||
@@ -519,6 +538,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
var current_tax_amount = 0.0;
|
||||
var current_net_amount = 0.0;
|
||||
|
||||
if (tax_rate === erpnext.NOT_APPLICABLE_TAX) {
|
||||
return [current_net_amount, current_tax_amount];
|
||||
}
|
||||
|
||||
// To set row_id by default as previous row.
|
||||
if(["On Previous Row Amount", "On Previous Row Total"].includes(tax.charge_type)) {
|
||||
if (tax.idx === 1) {
|
||||
|
||||
@@ -818,6 +818,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
$.each(item_tax_map, function(tax, rate) {
|
||||
if (rate === erpnext.NOT_APPLICABLE_TAX) {
|
||||
return;
|
||||
}
|
||||
let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
|
||||
if(!found) {
|
||||
let child = frappe.model.add_child(me.frm.doc, "taxes");
|
||||
@@ -1611,9 +1614,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
if (this.frm.doc.taxes && this.frm.doc.taxes.length > 0) {
|
||||
this.frm.set_currency_labels(["tax_amount", "total", "tax_amount_after_discount"], this.frm.doc.currency, "taxes");
|
||||
this.frm.set_currency_labels(["net_amount", "tax_amount", "total", "tax_amount_after_discount"], this.frm.doc.currency, "taxes");
|
||||
|
||||
this.frm.set_currency_labels(["base_tax_amount", "base_total", "base_tax_amount_after_discount"], company_currency, "taxes");
|
||||
this.frm.set_currency_labels(["base_net_amount", "base_tax_amount", "base_total", "base_tax_amount_after_discount"], company_currency, "taxes");
|
||||
}
|
||||
|
||||
if (this.frm.doc.advances && this.frm.doc.advances.length > 0) {
|
||||
|
||||
80
erpnext/regional/italy/test_italy.py
Normal file
80
erpnext/regional/italy/test_italy.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.regional.italy.utils import get_invoice_summary
|
||||
|
||||
VAT_7 = "_Test Italy VAT 7 - _TC"
|
||||
VAT_19 = "_Test Italy VAT 19 - _TC"
|
||||
|
||||
|
||||
def make_item(item_code, net_amount, tax_amount, item_tax_rate):
|
||||
return frappe._dict(
|
||||
item_code=item_code,
|
||||
net_amount=net_amount,
|
||||
tax_amount=tax_amount,
|
||||
item_tax_rate=item_tax_rate,
|
||||
)
|
||||
|
||||
|
||||
def make_tax(account_head, total, charge_type="On Net Total", **kwargs):
|
||||
return frappe._dict(
|
||||
charge_type=charge_type,
|
||||
account_head=account_head,
|
||||
rate=0,
|
||||
total=total,
|
||||
tax_exemption_reason="N4-esenti",
|
||||
tax_exemption_law="Art.10",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class TestItalyInvoiceSummary(FrappeTestCase):
|
||||
def test_not_applicable_tax_excluded_from_summary(self):
|
||||
"""An item that marks a tax not applicable belongs to another summary
|
||||
block. Counting it here inflates DatiRiepilogo and emits a block with
|
||||
AliquotaIVA 0.00 and no Natura, which SDI rejects."""
|
||||
items = [
|
||||
make_item("A", 100.0, 7.0, {VAT_7: 7.0, VAT_19: "N/A"}),
|
||||
make_item("B", 100.0, 19.0, {VAT_7: "N/A", VAT_19: 19.0}),
|
||||
]
|
||||
taxes = [make_tax(VAT_7, 107.0), make_tax(VAT_19, 126.0)]
|
||||
|
||||
summary = get_invoice_summary(items, taxes)
|
||||
|
||||
self.assertEqual(sorted(summary.keys()), ["19.0", "7.0"])
|
||||
self.assertEqual(summary["7.0"]["taxable_amount"], 100.0)
|
||||
self.assertEqual(summary["19.0"]["taxable_amount"], 100.0)
|
||||
|
||||
def test_zero_rated_tax_keeps_exemption_reason(self):
|
||||
"""A genuine 0% rate is still exempt and must carry its Natura."""
|
||||
items = [make_item("C", 100.0, 0.0, {VAT_7: 0.0})]
|
||||
|
||||
summary = get_invoice_summary(items, [make_tax(VAT_7, 100.0)])
|
||||
|
||||
self.assertEqual(list(summary.keys()), ["0.0"])
|
||||
self.assertEqual(summary["0.0"]["taxable_amount"], 100.0)
|
||||
self.assertEqual(summary["0.0"]["tax_exemption_reason"], "N4-esenti")
|
||||
|
||||
def test_all_items_not_applicable_falls_back_to_zero_vat(self):
|
||||
"""With every item excluded the summary would be empty, so the existing
|
||||
zero VAT fallback has to supply the block and its Natura."""
|
||||
items = [make_item("D", 100.0, 0.0, {VAT_7: "N/A"})]
|
||||
|
||||
summary = get_invoice_summary(items, [make_tax(VAT_7, 100.0)])
|
||||
|
||||
self.assertEqual(list(summary.keys()), ["0.0"])
|
||||
self.assertEqual(summary["0.0"]["tax_exemption_reason"], "N4-esenti")
|
||||
|
||||
def test_previous_row_tax_with_only_not_applicable_items(self):
|
||||
"""The summary key leaks out of the item loop and is read again for
|
||||
previous-row charges. Every item being excluded leaves it unset."""
|
||||
items = [make_item("A", 100.0, 0.0, {VAT_7: 0.0, VAT_19: "N/A"})]
|
||||
taxes = [
|
||||
make_tax(VAT_7, 100.0, idx=1),
|
||||
make_tax(VAT_19, 100.0, charge_type="On Previous Row Total", idx=2, row_id=None),
|
||||
]
|
||||
|
||||
summary = get_invoice_summary(items, taxes)
|
||||
|
||||
self.assertEqual(list(summary.keys()), ["0.0"])
|
||||
self.assertEqual(summary["0.0"]["taxable_amount"], 100.0)
|
||||
@@ -8,6 +8,7 @@ from frappe.utils.file_manager import remove_file
|
||||
|
||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax
|
||||
from erpnext.regional.italy import state_codes
|
||||
from erpnext.stock.get_item_details import NOT_APPLICABLE_TAX
|
||||
|
||||
|
||||
def update_itemised_tax_data(doc):
|
||||
@@ -171,13 +172,20 @@ def get_invoice_summary(items, taxes):
|
||||
|
||||
# Check item tax rates if tax rate is zero.
|
||||
if tax.rate == 0:
|
||||
key = None
|
||||
for item in items:
|
||||
item_tax_rate = item.item_tax_rate
|
||||
if isinstance(item.item_tax_rate, str):
|
||||
item_tax_rate = json.loads(item.item_tax_rate)
|
||||
|
||||
if item_tax_rate and tax.account_head in item_tax_rate:
|
||||
key = cstr(item_tax_rate[tax.account_head])
|
||||
rate = item_tax_rate[tax.account_head]
|
||||
if rate == NOT_APPLICABLE_TAX:
|
||||
# the tax does not apply to this item, so the item belongs
|
||||
# to another summary block and must not be counted here
|
||||
continue
|
||||
|
||||
key = cstr(rate)
|
||||
if key not in summary_data:
|
||||
summary_data.setdefault(
|
||||
key,
|
||||
@@ -195,10 +203,15 @@ def get_invoice_summary(items, taxes):
|
||||
summary_data[key]["tax_exemption_reason"] = tax.tax_exemption_reason
|
||||
summary_data[key]["tax_exemption_law"] = tax.tax_exemption_law
|
||||
|
||||
if summary_data.get("0.0") and tax.charge_type in [
|
||||
"On Previous Row Total",
|
||||
"On Previous Row Amount",
|
||||
]:
|
||||
if (
|
||||
key
|
||||
and summary_data.get("0.0")
|
||||
and tax.charge_type
|
||||
in [
|
||||
"On Previous Row Total",
|
||||
"On Previous Row Amount",
|
||||
]
|
||||
):
|
||||
summary_data[key]["taxable_amount"] = tax.total
|
||||
|
||||
if summary_data == {}: # Implies that Zero VAT has not been set on any item.
|
||||
|
||||
@@ -34,6 +34,8 @@ purchase_doctypes = [
|
||||
"Purchase Invoice",
|
||||
]
|
||||
|
||||
NOT_APPLICABLE_TAX = "N/A"
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True):
|
||||
@@ -806,7 +808,10 @@ def get_item_tax_map(company, item_tax_template, as_json=True):
|
||||
template = frappe.get_cached_doc("Item Tax Template", item_tax_template)
|
||||
for d in template.taxes:
|
||||
if frappe.get_cached_value("Account", d.tax_type, "company") == company:
|
||||
item_tax_map[d.tax_type] = d.tax_rate
|
||||
if d.get("not_applicable"):
|
||||
item_tax_map[d.tax_type] = NOT_APPLICABLE_TAX
|
||||
else:
|
||||
item_tax_map[d.tax_type] = d.tax_rate
|
||||
|
||||
return json.dumps(item_tax_map) if as_json else item_tax_map
|
||||
|
||||
|
||||
Reference in New Issue
Block a user