Files
erpnext/erpnext/regional/united_arab_emirates/utils.py
Lakshit Jain 91f3c82bdf 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
2025-11-17 19:02:31 +05:30

195 lines
6.3 KiB
Python

import frappe
from frappe import _
from frappe.utils import flt, money_in_words, round_based_on_smallest_currency_fraction
import erpnext
from erpnext.controllers.taxes_and_totals import get_itemised_tax
def update_itemised_tax_data(doc):
if not doc.items:
return
meta = frappe.get_meta(doc.items[0].doctype)
if not meta.has_field("tax_rate"):
return
itemised_tax = get_itemised_tax(doc)
def determine_if_export(doc):
if doc.doctype != "Sales Invoice":
return False
if not doc.customer_address:
if not doc.total_taxes_and_charges:
frappe.msgprint(
_("Please set Customer Address to determine if the transaction is an export."),
alert=True,
)
return False
company_country = frappe.get_cached_value("Company", doc.company, "country")
customer_country = frappe.db.get_value("Address", doc.customer_address, "country")
if company_country != customer_country:
return True
return False
is_export = determine_if_export(doc)
for row in doc.items:
tax_rate, tax_amount = 0.0, 0.0
# dont even bother checking in item tax template as it contains both input and output accounts - double the tax rate
item_code = row.item_code or row.item_name
if itemised_tax.get(item_code):
for tax in itemised_tax.get(item_code).values():
_tax_rate = flt(tax.get("tax_rate", 0), row.precision("tax_rate"))
tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount"))
tax_rate += _tax_rate
if not tax_rate or row.get("is_zero_rated"):
row.is_zero_rated = is_export or frappe.get_cached_value("Item", row.item_code, "is_zero_rated")
row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
row.tax_amount = flt(tax_amount, row.precision("tax_amount"))
row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
def get_account_currency(account):
"""Helper function to get account currency."""
if not account:
return
def generator():
account_currency, company = frappe.get_cached_value(
"Account", account, ["account_currency", "company"]
)
if not account_currency:
account_currency = frappe.get_cached_value("Company", company, "default_currency")
return account_currency
return frappe.local_cache("account_currency", account, generator)
def get_tax_accounts(company):
"""Get the list of tax accounts for a specific company."""
tax_accounts_dict = frappe._dict()
tax_accounts_list = frappe.get_all("UAE VAT Account", filters={"parent": company}, fields=["Account"])
if not tax_accounts_list and not frappe.in_test:
frappe.throw(_('Please set Vat Accounts for Company: "{0}" in UAE VAT Settings').format(company))
for tax_account in tax_accounts_list:
for _account, name in tax_account.items():
tax_accounts_dict[name] = name
return tax_accounts_dict
def update_grand_total_for_rcm(doc, method):
"""If the Reverse Charge is Applicable subtract the tax amount from the grand total and update in the form."""
country = frappe.get_cached_value("Company", doc.company, "country")
if country != "United Arab Emirates":
return
if not doc.total_taxes_and_charges:
return
if doc.reverse_charge == "Y":
tax_accounts = get_tax_accounts(doc.company)
base_vat_tax = 0
vat_tax = 0
for tax in doc.get("taxes"):
if tax.category not in ("Total", "Valuation and Total"):
continue
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts:
base_vat_tax += tax.base_tax_amount_after_discount_amount
vat_tax += tax.tax_amount_after_discount_amount
doc.taxes_and_charges_added -= vat_tax
doc.total_taxes_and_charges -= vat_tax
doc.base_taxes_and_charges_added -= base_vat_tax
doc.base_total_taxes_and_charges -= base_vat_tax
update_totals(vat_tax, base_vat_tax, doc)
def update_totals(vat_tax, base_vat_tax, doc):
"""Update the grand total values in the form."""
doc.base_grand_total -= base_vat_tax
doc.grand_total -= vat_tax
if doc.meta.get_field("rounded_total"):
if doc.is_rounded_total_disabled():
doc.outstanding_amount = doc.grand_total
else:
doc.rounded_total = round_based_on_smallest_currency_fraction(
doc.grand_total, doc.currency, doc.precision("rounded_total")
)
doc.rounding_adjustment = flt(
doc.rounded_total - doc.grand_total, doc.precision("rounding_adjustment")
)
doc.outstanding_amount = doc.rounded_total or doc.grand_total
doc.in_words = money_in_words(doc.grand_total, doc.currency)
doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company))
doc.set_payment_schedule()
def make_regional_gl_entries(gl_entries, doc):
"""Hooked to make_regional_gl_entries in Purchase Invoice.It appends the region specific general ledger entries to the list of GL Entries."""
country = frappe.get_cached_value("Company", doc.company, "country")
if country != "United Arab Emirates":
return gl_entries
if doc.reverse_charge == "Y":
tax_accounts = get_tax_accounts(doc.company)
for tax in doc.get("taxes"):
if tax.category not in ("Total", "Valuation and Total"):
continue
gl_entries = make_gl_entry(tax, gl_entries, doc, tax_accounts)
return gl_entries
def make_gl_entry(tax, gl_entries, doc, tax_accounts):
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts:
account_currency = get_account_currency(tax.account_head)
gl_entries.append(
doc.get_gl_dict(
{
"account": tax.account_head,
"cost_center": tax.cost_center,
"posting_date": doc.posting_date,
"against": doc.supplier,
dr_or_cr: tax.base_tax_amount_after_discount_amount,
dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount
if account_currency == doc.company_currency
else tax.tax_amount_after_discount_amount,
},
account_currency,
item=tax,
)
)
return gl_entries
def validate_returns(doc, method):
"""Standard Rated expenses should not be set when Reverse Charge Applicable is set."""
country = frappe.get_cached_value("Company", doc.company, "country")
if country != "United Arab Emirates":
return
if doc.reverse_charge == "Y" and flt(doc.recoverable_standard_rated_expenses) != 0:
frappe.throw(
_("Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y")
)