mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-13 17:20:36 +00:00
fix!: tax net_amount and not_applicable (#54687)
This commit is contained in:
@@ -94,11 +94,11 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Project",
|
"label": "Project",
|
||||||
"options": "Project"
|
"options": "Project"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_8",
|
"fieldname": "section_break_8",
|
||||||
@@ -187,12 +187,14 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-11-25 11:10:10.945027",
|
"modified": "2026-05-01 00:38:53.368737",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Advance Taxes and Charges",
|
"name": "Advance Taxes and Charges",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "ASC"
|
"sort_order": "ASC",
|
||||||
}
|
"states": []
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class AdvanceTaxesandCharges(Document):
|
|||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
project: DF.Link | None
|
||||||
rate: DF.Float
|
rate: DF.Float
|
||||||
row_id: DF.Data | None
|
row_id: DF.Data | None
|
||||||
tax_amount: DF.Currency
|
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
|
# end: auto-generated types
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.set_zero_rate_for_not_applicable_tax()
|
||||||
self.validate_tax_accounts()
|
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):
|
def autoname(self):
|
||||||
if self.company and self.title:
|
if self.company and self.title:
|
||||||
abbr = frappe.get_cached_value("Company", self.company, "abbr")
|
abbr = frappe.get_cached_value("Company", self.company, "abbr")
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"tax_type",
|
"tax_type",
|
||||||
"tax_rate"
|
"tax_rate",
|
||||||
|
"not_applicable"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -21,12 +22,21 @@
|
|||||||
"fieldname": "tax_rate",
|
"fieldname": "tax_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_list_view": 1,
|
"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,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-04-30 23:49:27.020639",
|
"modified": "2026-04-30 23:59:22.020639",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Item Tax Template Detail",
|
"name": "Item Tax Template Detail",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class ItemTaxTemplateDetail(Document):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
|
not_applicable: DF.Check
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
|||||||
@@ -25,10 +25,12 @@
|
|||||||
"project",
|
"project",
|
||||||
"section_break_9",
|
"section_break_9",
|
||||||
"account_currency",
|
"account_currency",
|
||||||
|
"net_amount",
|
||||||
"tax_amount",
|
"tax_amount",
|
||||||
"tax_amount_after_discount_amount",
|
"tax_amount_after_discount_amount",
|
||||||
"total",
|
"total",
|
||||||
"column_break_14",
|
"column_break_14",
|
||||||
|
"base_net_amount",
|
||||||
"base_tax_amount",
|
"base_tax_amount",
|
||||||
"base_total",
|
"base_total",
|
||||||
"base_tax_amount_after_discount_amount",
|
"base_tax_amount_after_discount_amount",
|
||||||
@@ -213,11 +215,11 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Project",
|
"label": "Project",
|
||||||
"options": "Project"
|
"options": "Project"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -241,20 +243,38 @@
|
|||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Is Tax Withholding Account",
|
"label": "Is Tax Withholding Account",
|
||||||
"read_only": 1
|
"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,
|
"grid_page_length": 50,
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-04-15 13:14:48.936047",
|
"modified": "2026-05-01 00:38:29.543523",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Taxes and Charges",
|
"name": "Purchase Taxes and Charges",
|
||||||
"naming_rule": "Random",
|
"naming_rule": "Random",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@ class PurchaseTaxesandCharges(Document):
|
|||||||
account_currency: DF.Link | None
|
account_currency: DF.Link | None
|
||||||
account_head: DF.Link
|
account_head: DF.Link
|
||||||
add_deduct_tax: DF.Literal["Add", "Deduct"]
|
add_deduct_tax: DF.Literal["Add", "Deduct"]
|
||||||
|
base_net_amount: DF.Currency
|
||||||
base_tax_amount: DF.Currency
|
base_tax_amount: DF.Currency
|
||||||
base_tax_amount_after_discount_amount: DF.Currency
|
base_tax_amount_after_discount_amount: DF.Currency
|
||||||
base_total: DF.Currency
|
base_total: DF.Currency
|
||||||
@@ -35,9 +36,11 @@ class PurchaseTaxesandCharges(Document):
|
|||||||
included_in_print_rate: DF.Check
|
included_in_print_rate: DF.Check
|
||||||
is_tax_withholding_account: DF.Check
|
is_tax_withholding_account: DF.Check
|
||||||
item_wise_tax_detail: DF.Code | None
|
item_wise_tax_detail: DF.Code | None
|
||||||
|
net_amount: DF.Currency
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
project: DF.Link | None
|
||||||
rate: DF.Float
|
rate: DF.Float
|
||||||
row_id: DF.Data | None
|
row_id: DF.Data | None
|
||||||
tax_amount: DF.Currency
|
tax_amount: DF.Currency
|
||||||
|
|||||||
@@ -21,10 +21,12 @@
|
|||||||
"rate",
|
"rate",
|
||||||
"section_break_9",
|
"section_break_9",
|
||||||
"account_currency",
|
"account_currency",
|
||||||
|
"net_amount",
|
||||||
"tax_amount",
|
"tax_amount",
|
||||||
"total",
|
"total",
|
||||||
"tax_amount_after_discount_amount",
|
"tax_amount_after_discount_amount",
|
||||||
"column_break_13",
|
"column_break_13",
|
||||||
|
"base_net_amount",
|
||||||
"base_tax_amount",
|
"base_tax_amount",
|
||||||
"base_total",
|
"base_total",
|
||||||
"base_tax_amount_after_discount_amount",
|
"base_tax_amount_after_discount_amount",
|
||||||
@@ -190,11 +192,11 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Project",
|
"label": "Project",
|
||||||
"options": "Project"
|
"options": "Project"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -220,19 +222,36 @@
|
|||||||
"label": "Account Currency",
|
"label": "Account Currency",
|
||||||
"options": "Currency",
|
"options": "Currency",
|
||||||
"read_only": 1
|
"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,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-01-14 10:08:17.776528",
|
"modified": "2026-05-01 00:37:57.880071",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Taxes and Charges",
|
"name": "Sales Taxes and Charges",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
"states": []
|
"states": []
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ class SalesTaxesandCharges(Document):
|
|||||||
|
|
||||||
account_currency: DF.Link | None
|
account_currency: DF.Link | None
|
||||||
account_head: DF.Link
|
account_head: DF.Link
|
||||||
|
base_net_amount: DF.Currency
|
||||||
base_tax_amount: DF.Currency
|
base_tax_amount: DF.Currency
|
||||||
base_tax_amount_after_discount_amount: DF.Currency
|
base_tax_amount_after_discount_amount: DF.Currency
|
||||||
base_total: DF.Currency
|
base_total: DF.Currency
|
||||||
@@ -33,9 +34,11 @@ class SalesTaxesandCharges(Document):
|
|||||||
included_in_paid_amount: DF.Check
|
included_in_paid_amount: DF.Check
|
||||||
included_in_print_rate: DF.Check
|
included_in_print_rate: DF.Check
|
||||||
item_wise_tax_detail: DF.Code | None
|
item_wise_tax_detail: DF.Code | None
|
||||||
|
net_amount: DF.Currency
|
||||||
parent: DF.Data
|
parent: DF.Data
|
||||||
parentfield: DF.Data
|
parentfield: DF.Data
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
|
project: DF.Link | None
|
||||||
rate: DF.Float
|
rate: DF.Float
|
||||||
row_id: DF.Data | None
|
row_id: DF.Data | None
|
||||||
tax_amount: DF.Currency
|
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.item.item import get_uom_conv_factor
|
||||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||||
from erpnext.stock.get_item_details import (
|
from erpnext.stock.get_item_details import (
|
||||||
|
NOT_APPLICABLE_TAX,
|
||||||
_get_item_tax_template,
|
_get_item_tax_template,
|
||||||
_get_item_tax_template_from_item_group,
|
_get_item_tax_template_from_item_group,
|
||||||
get_bin_details,
|
get_bin_details,
|
||||||
@@ -1294,7 +1295,10 @@ class AccountsController(TransactionBase):
|
|||||||
if isinstance(item_tax_rate, str):
|
if isinstance(item_tax_rate, str):
|
||||||
item_tax_rate = parse_json(item_tax_rate)
|
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)
|
row = self.get_tax_row(account_head)
|
||||||
|
|
||||||
if not row:
|
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:
|
if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
|
||||||
tax_map = json.loads(child_item.get("item_tax_rate"))
|
tax_map = json.loads(child_item.get("item_tax_rate"))
|
||||||
for tax_type in tax_map:
|
for tax_type, tax_rate in tax_map.items():
|
||||||
tax_rate = flt(tax_map[tax_type])
|
if tax_rate == NOT_APPLICABLE_TAX:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tax_rate = flt(tax_rate)
|
||||||
taxes = parent_doc.get("taxes") or []
|
taxes = parent_doc.get("taxes") or []
|
||||||
# add new row for tax head only if missing
|
# add new row for tax head only if missing
|
||||||
found = any(tax.account_head == tax_type for tax in taxes)
|
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_inclusive_tax,
|
||||||
validate_taxes_and_charges,
|
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
|
from erpnext.utilities.regional import temporary_flag
|
||||||
|
|
||||||
|
|
||||||
@@ -275,6 +279,7 @@ class calculate_taxes_and_totals:
|
|||||||
tax.item_wise_tax_detail = {}
|
tax.item_wise_tax_detail = {}
|
||||||
|
|
||||||
tax_fields = [
|
tax_fields = [
|
||||||
|
"net_amount",
|
||||||
"total",
|
"total",
|
||||||
"tax_amount_after_discount_amount",
|
"tax_amount_after_discount_amount",
|
||||||
"tax_amount_for_current_item",
|
"tax_amount_for_current_item",
|
||||||
@@ -345,6 +350,9 @@ class calculate_taxes_and_totals:
|
|||||||
if cint(tax.included_in_print_rate):
|
if cint(tax.included_in_print_rate):
|
||||||
tax_rate = self._get_tax_rate(tax, item_tax_map)
|
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":
|
if tax.charge_type == "On Net Total":
|
||||||
tax_slope = tax_rate / 100.0
|
tax_slope = tax_rate / 100.0
|
||||||
|
|
||||||
@@ -376,9 +384,12 @@ class calculate_taxes_and_totals:
|
|||||||
|
|
||||||
def _get_tax_rate(self, tax, item_tax_map):
|
def _get_tax_rate(self, tax, item_tax_map):
|
||||||
if tax.account_head in 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))
|
rate = item_tax_map[tax.account_head]
|
||||||
else:
|
if rate == NOT_APPLICABLE_TAX:
|
||||||
return tax.rate
|
return NOT_APPLICABLE_TAX
|
||||||
|
return flt(rate, self.doc.precision("rate", tax))
|
||||||
|
|
||||||
|
return tax.rate
|
||||||
|
|
||||||
def calculate_net_total(self):
|
def calculate_net_total(self):
|
||||||
self.doc.total_qty = (
|
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)
|
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
|
||||||
for i, tax in enumerate(doc.taxes):
|
for i, tax in enumerate(doc.taxes):
|
||||||
# tax_amount represents the amount of tax for the current step
|
# 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:
|
if frappe.flags.round_row_wise_tax:
|
||||||
current_tax_amount = flt(current_tax_amount, tax.precision("tax_amount"))
|
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
|
# Adjust divisional loss to the last item
|
||||||
if tax.charge_type == "Actual":
|
if tax.charge_type == "Actual":
|
||||||
@@ -436,6 +450,10 @@ class calculate_taxes_and_totals:
|
|||||||
if n == len(self._items) - 1:
|
if n == len(self._items) - 1:
|
||||||
current_tax_amount += actual_tax_dict[tax.idx]
|
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
|
# accumulate tax amount into tax.tax_amount
|
||||||
if tax.charge_type != "Actual" and not (
|
if tax.charge_type != "Actual" and not (
|
||||||
self.discount_amount_applied and self.doc.apply_discount_on == "Grand Total"
|
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):
|
for i, tax in enumerate(doc.taxes):
|
||||||
self.round_off_totals(tax)
|
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.round_off_base_values(tax)
|
||||||
self.set_cumulative_total(i, 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"))
|
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):
|
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)
|
tax_rate = self._get_tax_rate(tax, item_tax_map)
|
||||||
current_tax_amount = 0.0
|
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":
|
if tax.charge_type == "Actual":
|
||||||
# distribute the tax amount proportionally to each item row
|
# 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:
|
if not item.get("apply_tds") or not self.doc.tax_withholding_net_total:
|
||||||
current_tax_amount = 0.0
|
current_tax_amount = 0.0
|
||||||
else:
|
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:
|
else:
|
||||||
|
current_net_amount = item.net_amount
|
||||||
current_tax_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":
|
elif tax.charge_type == "On Net Total":
|
||||||
|
current_net_amount = item.net_amount
|
||||||
current_tax_amount = (tax_rate / 100.0) * item.net_amount
|
current_tax_amount = (tax_rate / 100.0) * item.net_amount
|
||||||
elif tax.charge_type == "On Previous Row Amount":
|
elif tax.charge_type == "On Previous Row Amount":
|
||||||
current_tax_amount = (tax_rate / 100.0) * self.doc.get("taxes")[
|
current_net_amount = self.doc.get("taxes")[cint(tax.row_id) - 1].tax_amount_for_current_item
|
||||||
cint(tax.row_id) - 1
|
current_tax_amount = (tax_rate / 100.0) * current_net_amount
|
||||||
].tax_amount_for_current_item
|
|
||||||
elif tax.charge_type == "On Previous Row Total":
|
elif tax.charge_type == "On Previous Row Total":
|
||||||
current_tax_amount = (tax_rate / 100.0) * self.doc.get("taxes")[
|
current_net_amount = self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_for_current_item
|
||||||
cint(tax.row_id) - 1
|
current_tax_amount = (tax_rate / 100.0) * current_net_amount
|
||||||
].grand_total_for_current_item
|
|
||||||
elif tax.charge_type == "On Item Quantity":
|
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
|
current_tax_amount = tax_rate * item.qty
|
||||||
else:
|
else:
|
||||||
# Custom charge_type: rate applies to the resolver-provided base.
|
# 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")):
|
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)
|
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):
|
def get_item_taxable_base(self, item, tax):
|
||||||
"""Per-item base a custom charge_type's rate is applied to.
|
"""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.rounding_adjustment, 0)
|
||||||
self.assertEqual(so.base_rounded_total, 0)
|
self.assertEqual(so.base_rounded_total, 0)
|
||||||
self.assertEqual(so.base_rounding_adjustment, 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
|
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
// License: GNU General Public License v3. See license.txt
|
// 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`
|
// 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
|
// 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.
|
// 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)) {
|
if(cint(tax.included_in_print_rate)) {
|
||||||
var tax_rate = this._get_tax_rate(tax, item_tax_map);
|
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") {
|
if(tax.charge_type == "On Net Total") {
|
||||||
tax_slope = (tax_rate / 100.0);
|
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) {
|
_get_tax_rate(tax, item_tax_map) {
|
||||||
return (Object.keys(item_tax_map).indexOf(tax.account_head) != -1) ?
|
if (tax.account_head in item_tax_map) {
|
||||||
flt(item_tax_map[tax.account_head], precision("rate", tax)) : tax.rate;
|
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() {
|
calculate_net_total() {
|
||||||
@@ -379,6 +392,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$.each(item_tax_map, function(tax, rate) {
|
$.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);
|
let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
|
||||||
if (!found) {
|
if (!found) {
|
||||||
let child = frappe.model.add_child(me.frm.doc, "taxes");
|
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
|
// accumulate tax amount into tax.tax_amount
|
||||||
if (tax.charge_type != "Actual" &&
|
if (tax.charge_type != "Actual" &&
|
||||||
!(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) {
|
!(me.discount_amount_applied && me.frm.doc.apply_discount_on=="Grand Total")) {
|
||||||
tax.tax_amount += current_tax_amount;
|
tax.tax_amount += current_tax_amount;
|
||||||
tax.net_amount += current_net_amount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// store tax_amount for current item as it will be used for
|
// 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_tax_amount = 0.0;
|
||||||
var current_net_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.
|
// To set row_id by default as previous row.
|
||||||
if(["On Previous Row Amount", "On Previous Row Total"].includes(tax.charge_type)) {
|
if(["On Previous Row Amount", "On Previous Row Total"].includes(tax.charge_type)) {
|
||||||
if (tax.idx === 1) {
|
if (tax.idx === 1) {
|
||||||
|
|||||||
@@ -818,6 +818,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
|
|
||||||
$.each(item_tax_map, function(tax, rate) {
|
$.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);
|
let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
|
||||||
if(!found) {
|
if(!found) {
|
||||||
let child = frappe.model.add_child(me.frm.doc, "taxes");
|
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) {
|
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) {
|
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.controllers.taxes_and_totals import get_itemised_tax
|
||||||
from erpnext.regional.italy import state_codes
|
from erpnext.regional.italy import state_codes
|
||||||
|
from erpnext.stock.get_item_details import NOT_APPLICABLE_TAX
|
||||||
|
|
||||||
|
|
||||||
def update_itemised_tax_data(doc):
|
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.
|
# Check item tax rates if tax rate is zero.
|
||||||
if tax.rate == 0:
|
if tax.rate == 0:
|
||||||
|
key = None
|
||||||
for item in items:
|
for item in items:
|
||||||
item_tax_rate = item.item_tax_rate
|
item_tax_rate = item.item_tax_rate
|
||||||
if isinstance(item.item_tax_rate, str):
|
if isinstance(item.item_tax_rate, str):
|
||||||
item_tax_rate = json.loads(item.item_tax_rate)
|
item_tax_rate = json.loads(item.item_tax_rate)
|
||||||
|
|
||||||
if item_tax_rate and tax.account_head in 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:
|
if key not in summary_data:
|
||||||
summary_data.setdefault(
|
summary_data.setdefault(
|
||||||
key,
|
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_reason"] = tax.tax_exemption_reason
|
||||||
summary_data[key]["tax_exemption_law"] = tax.tax_exemption_law
|
summary_data[key]["tax_exemption_law"] = tax.tax_exemption_law
|
||||||
|
|
||||||
if summary_data.get("0.0") and tax.charge_type in [
|
if (
|
||||||
"On Previous Row Total",
|
key
|
||||||
"On Previous Row Amount",
|
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
|
summary_data[key]["taxable_amount"] = tax.total
|
||||||
|
|
||||||
if summary_data == {}: # Implies that Zero VAT has not been set on any item.
|
if summary_data == {}: # Implies that Zero VAT has not been set on any item.
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ purchase_doctypes = [
|
|||||||
"Purchase Invoice",
|
"Purchase Invoice",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
NOT_APPLICABLE_TAX = "N/A"
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True):
|
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)
|
template = frappe.get_cached_doc("Item Tax Template", item_tax_template)
|
||||||
for d in template.taxes:
|
for d in template.taxes:
|
||||||
if frappe.get_cached_value("Account", d.tax_type, "company") == company:
|
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
|
return json.dumps(item_tax_map) if as_json else item_tax_map
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user