mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-29 18:04:46 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user