feat!: Item Wise Tax Details Table (#48692)

* fix: Add `Item Wise Tax Detail` Table and update related doctypes

* fix: remove setting item_wise_tax_details in client side

* fix: Remove redundant code for updating item_wise_tax_details after rename

* fix: Add 'dont_recompute_tax' field to Item Wise Tax Detail

* fix: update item_wise_tax_details after validations

* chore: remove redundant code from payment_entry.js

* fix: changes in POS for item_wise_tax_details

* fix: handle merge taxes

* fix: update test case and fix precision issue

* chore: remove debugging statement

* chore: remove redundant import

* chore: linters

* chore: remove redundant code and minor refactor

* fix: correct function args

* fix: fix test cases

* fix: item wise sales register report

* fix: remove dont recompute from item wise tax details and calculation for deduct

* fix: do not retain old rows

* fix: added validation for item wise tax details

* fix: tax merging for pos

* fix: vat audit report(regional report)

* fix: query issue in item-wise sales register

* fix: set other_charges using temp object

* fix: precision issue in validation

* fix: changes as per failing test cases

* fix: tax merging

* fix: set no_copy for item wise tax detail

* fix: correct select field in query and other charged in item_wise_purchase_register

* fix: do not include rows with missing item or tax in merge_taxes

* fix: respect row wise rounding

* chore: remove unused import

* chore: incorrect tuple creation

* fix: handle rounding adjustment

* fix: currency option in item wise tax detail doctype

* fix: patch to migrate item_wise tax_details to table

* chore: remove item_wise_tax_detail from taxes table

* fix: use base_tax_withholding_net_total instead of tax_withholding_net_total

* fix: implemet item_wise_tax_detail for e-invoice (italy)

* fix: fetch document by doctypes in migration patch

* fix: fix multiple syntax errors and inconsistent variable usage

* fix: remove deprecated settings and update item wise tax details flag

* fix: enhance validation for item wise tax details and handle discrepancies

* fix: increase chunk size for migration and improve item-wise tax detail calculations

* fix: delete existing item-wise tax details to prevent duplicates during migration

* fix: remove unnecessary docstatus filter from tax details query

* fix: streamline validation checks in item wise tax details adjustment

* fix: update additional fields to reference item and invoice attributes in tax detail queries

* fix: Restrict tax query to the selected invoices in vat audit report

* fix: use `base_tax_withholding_net_total` for calculation in patch

* fix: set tax row_id and idx to None instead of empty strings

* fix: remove unused precision parameter from rounding differences handler

* fix: update docstatus in item_wise_tax_details as per doc

* fix: remove empty on_update method from SalesOrder class

* fix: remove empty on_update method from PurchaseOrder class

* fix: incorporate zero cutoff in tax calculation logic

* fix: increase threshold for rounding diff
This commit is contained in:
Lakshit Jain
2025-11-17 19:02:31 +05:30
committed by GitHub
parent 47b7214580
commit 91f3c82bdf
43 changed files with 1001 additions and 622 deletions

View File

@@ -6,7 +6,7 @@ from frappe import _
from frappe.utils import cstr, flt
from frappe.utils.file_manager import remove_file
from erpnext.controllers.taxes_and_totals import ItemWiseTaxDetail, get_itemised_tax
from erpnext.controllers.taxes_and_totals import get_itemised_tax
from erpnext.regional.italy import state_codes
from erpnext.stock.utils import get_default_stock_uom
@@ -18,7 +18,7 @@ def update_itemised_tax_data(doc):
if doc.doctype == "Purchase Invoice":
return
itemised_tax = get_itemised_tax(doc.taxes)
itemised_tax = get_itemised_tax(doc)
for row in doc.items:
tax_rate = 0.0
@@ -79,7 +79,7 @@ def prepare_invoice(invoice, progressive_number):
invoice.transmission_format_code = "FPR12"
invoice.e_invoice_items = [item for item in invoice.items]
tax_data = get_invoice_summary(invoice.e_invoice_items, invoice.taxes)
tax_data = get_invoice_summary(invoice.e_invoice_items, invoice.taxes, invoice.item_wise_tax_details)
invoice.tax_data = tax_data
# Check if stamp duty (Bollo) of 2 EUR exists.
@@ -140,8 +140,9 @@ def download_zip(files, output_filename):
zip_stream.close()
def get_invoice_summary(items, taxes):
def get_invoice_summary(items, taxes, item_wise_tax_details):
summary_data = frappe._dict()
taxes_wise_tax_details = {d.tax_row: d for d in item_wise_tax_details}
for tax in taxes:
# Include only VAT charges.
if tax.charge_type == "Actual":
@@ -151,91 +152,63 @@ def get_invoice_summary(items, taxes):
if tax.charge_type in ["On Previous Row Total", "On Previous Row Amount"]:
reference_row = next((row for row in taxes if row.idx == int(tax.row_id or 0)), None)
if reference_row:
items.append(
frappe._dict(
idx=len(items) + 1,
item_code=reference_row.description,
item_name=reference_row.description,
description=reference_row.description,
rate=reference_row.tax_amount,
qty=1.0,
amount=reference_row.tax_amount,
stock_uom=get_default_stock_uom(),
tax_rate=tax.rate,
tax_amount=(reference_row.tax_amount * tax.rate) / 100,
net_amount=reference_row.tax_amount,
taxable_amount=reference_row.tax_amount,
item_tax_rate={tax.account_head: tax.rate},
charges=True,
)
)
append_row_as_charges(items, tax, reference_row, summary_data)
# Check item tax rates if tax rate is zero.
if tax.rate == 0:
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)
for row in taxes_wise_tax_details.get(tax.name) or []:
update_summary_details(summary_data, tax, row.rate, row.amount, row.taxable_amount)
if item_tax_rate and tax.account_head in item_tax_rate:
key = cstr(item_tax_rate[tax.account_head])
if key not in summary_data:
summary_data.setdefault(
key,
{
"tax_amount": 0.0,
"taxable_amount": 0.0,
"tax_exemption_reason": "",
"tax_exemption_law": "",
},
)
summary_data[key]["tax_amount"] += item.tax_amount
summary_data[key]["taxable_amount"] += item.net_amount
if key == "0.0":
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",
]:
summary_data[key]["taxable_amount"] = tax.total
if summary_data == {}: # Implies that Zero VAT has not been set on any item.
summary_data.setdefault(
"0.0",
{
"tax_amount": 0.0,
"taxable_amount": tax.total,
"tax_exemption_reason": tax.tax_exemption_reason,
"tax_exemption_law": tax.tax_exemption_law,
},
)
else:
item_wise_tax_detail = json.loads(tax.item_wise_tax_detail)
# TODO: with net_amount stored inside item_wise_tax_detail, this entire block seems obsolete and redundant
for _item_code, tax_data in item_wise_tax_detail.items():
tax_data = ItemWiseTaxDetail(**tax_data)
if tax_data.tax_rate != tax.rate:
continue
key = cstr(tax.rate)
if not summary_data.get(key):
summary_data.setdefault(key, {"tax_amount": 0.0, "taxable_amount": 0.0})
summary_data[key]["tax_amount"] += tax_data.tax_amount
summary_data[key]["taxable_amount"] += tax_data.net_amount
for item in items:
key = cstr(tax.rate)
if item.get("charges"):
if not summary_data.get(key):
summary_data.setdefault(key, {"taxable_amount": 0.0})
summary_data[key]["taxable_amount"] += item.taxable_amount
if summary_data == {}:
# Implies that Zero VAT has not been set on any item.
update_summary_details(summary_data, tax, 0.0, 0.0, tax.total)
return summary_data
def update_summary_details(summary_data, tax, rate, amount, taxable_amount):
key = cstr(rate)
summary_data.setdefault(
key,
{
"tax_amount": 0.0,
"taxable_amount": 0.0,
"tax_exemption_reason": "",
"tax_exemption_law": "",
},
)
summary_data[key]["tax_amount"] += amount
summary_data[key]["taxable_amount"] += taxable_amount
if key == "0.0":
summary_data[key]["tax_exemption_reason"] = tax.tax_exemption_reason
summary_data[key]["tax_exemption_law"] = tax.tax_exemption_law
def append_row_as_charges(items, tax, reference_row, summary_data):
rate = tax.rate
amount = (reference_row.tax_amount * tax.rate) / 100
taxable_amount = reference_row.tax_amount
items.append(
frappe._dict(
idx=len(items) + 1,
item_code=reference_row.description,
item_name=reference_row.description,
description=reference_row.description,
rate=reference_row.tax_amount,
qty=1.0,
amount=reference_row.tax_amount,
stock_uom=get_default_stock_uom(),
tax_rate=rate,
tax_amount=amount,
net_amount=taxable_amount,
taxable_amount=taxable_amount,
item_tax_rate={tax.account_head: tax.rate},
charges=True,
)
)
update_summary_details(summary_data, tax, rate, amount, taxable_amount)
# Preflight for successful e-invoice export.
def sales_invoice_validate(doc):
# Validate company

View File

@@ -8,7 +8,7 @@ import frappe
from frappe import _
from frappe.utils import formatdate, get_link_to_form
from erpnext.controllers.taxes_and_totals import ItemWiseTaxDetail
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import get_tax_details_query
def execute(filters=None):
@@ -80,93 +80,54 @@ class VATAuditReport:
def get_invoice_items(self, doctype):
self.invoice_items = frappe._dict()
items = frappe.db.sql(
"""
SELECT
item_code, parent, base_net_amount, is_zero_rated
FROM
`tab{} Item`
WHERE
parent in ({})
""".format(doctype, ", ".join(["%s"] * len(self.invoices))),
tuple(self.invoices),
as_dict=1,
item_doctype = frappe.qb.DocType(doctype + " Item")
self.invoice_items = frappe._dict(
frappe.qb.from_(item_doctype)
.select(
item_doctype.name,
item_doctype.is_zero_rated,
)
.where(item_doctype.parent.isin(list(self.invoices.keys())))
.run(as_list=1)
)
for d in items:
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, {"net_amount": 0.0})
self.invoice_items[d.parent][d.item_code]["net_amount"] += d.get("base_net_amount", 0)
self.invoice_items[d.parent][d.item_code]["is_zero_rated"] = d.is_zero_rated
def get_items_based_on_tax_rate(self, doctype):
self.items_based_on_tax_rate = frappe._dict()
self.item_tax_rate = frappe._dict()
self.tax_doctype = (
"Purchase Taxes and Charges" if doctype == "Purchase Invoice" else "Sales Taxes and Charges"
)
self.tax_details = frappe.db.sql(
"""
SELECT
parent, account_head, item_wise_tax_detail
FROM
`tab{}`
WHERE
parenttype = {} and docstatus = 1
and parent in ({})
ORDER BY
account_head
""".format(self.tax_doctype, "%s", ", ".join(["%s"] * len(self.invoices.keys()))),
tuple([doctype, *list(self.invoices.keys())]),
taxes_and_charges = frappe.qb.DocType(self.tax_doctype)
item_wise_tax = frappe.qb.DocType("Item Wise Tax Detail")
invoice_names = list(self.invoices.keys())
if not invoice_names:
return
tax_details = (
get_tax_details_query(doctype, self.tax_doctype)
.where(item_wise_tax.parent.isin(invoice_names))
.where(taxes_and_charges.account_head.isin(self.sa_vat_accounts))
.run(as_dict=True)
)
for parent, account, item_wise_tax_detail in self.tax_details:
if item_wise_tax_detail:
try:
if account in self.sa_vat_accounts:
item_wise_tax_detail = json.loads(item_wise_tax_detail)
else:
continue
for item_code, tax_data in item_wise_tax_detail.items():
tax_data = ItemWiseTaxDetail(**tax_data)
is_zero_rated = self.invoice_items.get(parent).get(item_code).get("is_zero_rated")
# to skip items with non-zero tax rate in multiple rows
if tax_data.tax_rate == 0 and not is_zero_rated:
continue
tax_rate = self.get_item_amount_map(parent, item_code, tax_data)
for row in tax_details:
parent = row.parent
item = row.item_row
is_zero_rated = self.invoice_items.get(item)
if row.rate == 0 and not is_zero_rated:
continue
if tax_rate is not None:
rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {}).setdefault(
tax_rate, []
)
if item_code not in rate_based_dict:
rate_based_dict.append(item_code)
except ValueError:
continue
# TODO: now that tax_data holds net_amount, this method seems almost obsolete and can be removactored,
# gross_amount can be calculated on the file as a list comprehension
def get_item_amount_map(self, parent, item_code, tax_data):
net_amount = self.invoice_items.get(parent).get(item_code).get("net_amount")
tax_rate = tax_data.tax_rate
tax_amount = tax_data.tax_amount
gross_amount = net_amount + tax_amount
self.item_tax_rate.setdefault(parent, {}).setdefault(
item_code,
{
"tax_rate": tax_rate,
"gross_amount": 0.0,
"tax_amount": 0.0,
"net_amount": 0.0,
},
)
self.item_tax_rate[parent][item_code]["net_amount"] += net_amount
self.item_tax_rate[parent][item_code]["tax_amount"] += tax_amount
self.item_tax_rate[parent][item_code]["gross_amount"] += gross_amount
return tax_rate
self.items_based_on_tax_rate.setdefault(parent, {}).setdefault(
row.rate,
{
"gross_amount": 0.0,
"tax_amount": 0.0,
"net_amount": 0.0,
},
)
self.items_based_on_tax_rate[parent][row.rate]["tax_amount"] += row.amount
self.items_based_on_tax_rate[parent][row.rate]["net_amount"] += row.taxable_amount
self.items_based_on_tax_rate[parent][row.rate]["gross_amount"] += row.amount + row.taxable_amount
def get_conditions(self):
conditions = ""
@@ -209,25 +170,30 @@ class VATAuditReport:
def get_consolidated_data(self, doctype):
consolidated_data_map = {}
for inv, inv_data in self.invoices.items():
if self.items_based_on_tax_rate.get(inv):
for rate, items in self.items_based_on_tax_rate.get(inv).items():
row = {"tax_amount": 0.0, "gross_amount": 0.0, "net_amount": 0.0}
rate_details = self.items_based_on_tax_rate.get(inv, {})
if not rate_details:
continue
consolidated_data_map.setdefault(rate, {"data": []})
for item in items:
item_details = self.item_tax_rate.get(inv).get(item)
row["account"] = inv_data.get("account")
row["posting_date"] = formatdate(inv_data.get("posting_date"), "dd-mm-yyyy")
row["voucher_type"] = doctype
row["voucher_no"] = inv
row["party_type"] = "Customer" if doctype == "Sales Invoice" else "Supplier"
row["party"] = inv_data.get("party")
row["remarks"] = inv_data.get("remarks")
row["gross_amount"] += item_details.get("gross_amount")
row["tax_amount"] += item_details.get("tax_amount")
row["net_amount"] += item_details.get("net_amount")
for rate, item_details in rate_details.items():
row = {
"tax_amount": 0.0,
"gross_amount": 0.0,
"net_amount": 0.0,
}
consolidated_data_map[rate]["data"].append(row)
row["account"] = inv_data.get("account")
row["posting_date"] = formatdate(inv_data.get("posting_date"), "dd-mm-yyyy")
row["voucher_type"] = doctype
row["voucher_no"] = inv
row["party_type"] = "Customer" if doctype == "Sales Invoice" else "Supplier"
row["party"] = inv_data.get("party")
row["remarks"] = inv_data.get("remarks")
row["gross_amount"] += item_details.get("gross_amount")
row["tax_amount"] += item_details.get("tax_amount")
row["net_amount"] += item_details.get("net_amount")
consolidated_data_map.setdefault(rate, {"data": []})
consolidated_data_map[rate]["data"].append(row)
return consolidated_data_map

View File

@@ -14,7 +14,7 @@ def update_itemised_tax_data(doc):
if not meta.has_field("tax_rate"):
return
itemised_tax = get_itemised_tax(doc.taxes)
itemised_tax = get_itemised_tax(doc)
def determine_if_export(doc):
if doc.doctype != "Sales Invoice":