mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-29 22:58:25 +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:
@@ -137,6 +137,11 @@ class AccountsController(TransactionBase):
|
||||
if self.doctype in relevant_docs:
|
||||
self.set_payment_schedule()
|
||||
|
||||
def on_update(self):
|
||||
from erpnext.controllers.taxes_and_totals import process_item_wise_tax_details
|
||||
|
||||
process_item_wise_tax_details(self)
|
||||
|
||||
def remove_bundle_for_non_stock_invoices(self):
|
||||
has_sabb = False
|
||||
if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.update_stock:
|
||||
@@ -1161,7 +1166,6 @@ class AccountsController(TransactionBase):
|
||||
if self.get("taxes_and_charges"):
|
||||
if not tax_master_doctype:
|
||||
tax_master_doctype = self.meta.get_field("taxes_and_charges").options
|
||||
|
||||
self.extend("taxes", get_taxes_and_charges(tax_master_doctype, self.get("taxes_and_charges")))
|
||||
|
||||
def append_taxes_from_item_tax_template(self):
|
||||
@@ -4102,35 +4106,47 @@ def check_if_child_table_updated(child_table_before_update, child_table_after_up
|
||||
return False
|
||||
|
||||
|
||||
def merge_taxes(source_taxes, target_doc):
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
|
||||
update_item_wise_tax_detail,
|
||||
)
|
||||
|
||||
existing_taxes = target_doc.get("taxes") or []
|
||||
idx = 1
|
||||
for tax in source_taxes:
|
||||
def merge_taxes(source_doc, target_doc):
|
||||
tax_map = {}
|
||||
for tax in source_doc.get("taxes") or []:
|
||||
found = False
|
||||
for t in existing_taxes:
|
||||
for t in target_doc.get("taxes") or []:
|
||||
if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
|
||||
t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
|
||||
t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
|
||||
update_item_wise_tax_detail(t, tax)
|
||||
tax_map[tax.name] = t
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
tax.charge_type = "Actual"
|
||||
tax.idx = idx
|
||||
idx += 1
|
||||
tax.included_in_print_rate = 0
|
||||
tax.dont_recompute_tax = 1
|
||||
tax.row_id = ""
|
||||
tax.row_id = None
|
||||
tax.idx = None
|
||||
tax.tax_amount = tax.tax_amount_after_discount_amount
|
||||
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
|
||||
tax.item_wise_tax_detail = tax.item_wise_tax_detail
|
||||
existing_taxes.append(tax)
|
||||
tax_map[tax.name] = target_doc.append("taxes", tax)
|
||||
|
||||
target_doc.set("taxes", existing_taxes)
|
||||
item_map = {d._old_name: d for d in target_doc.get("items") if d.get("_old_name")}
|
||||
|
||||
item_tax_details = target_doc.get("_item_wise_tax_details") or []
|
||||
for row in source_doc.get("item_wise_tax_details"):
|
||||
item = item_map.get(row.item_row)
|
||||
tax = tax_map.get(row.tax_row)
|
||||
if not (item and tax):
|
||||
continue
|
||||
|
||||
item_tax_details.append(
|
||||
frappe._dict(
|
||||
item=item,
|
||||
tax=tax,
|
||||
amount=row.amount,
|
||||
rate=row.rate,
|
||||
taxable_amount=row.taxable_amount,
|
||||
)
|
||||
)
|
||||
|
||||
target_doc._item_wise_tax_details = item_tax_details
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
|
||||
@@ -71,6 +71,7 @@ class StockController(AccountsController):
|
||||
self.reset_conversion_factor()
|
||||
|
||||
def on_update(self):
|
||||
super().on_update()
|
||||
self.check_zero_rate()
|
||||
|
||||
def reset_conversion_factor(self):
|
||||
|
||||
@@ -6,12 +6,13 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.document import Document, bulk_insert
|
||||
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules
|
||||
from erpnext.accounts.utils import get_zero_cutoff
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
validate_conversion_rate,
|
||||
validate_inclusive_tax,
|
||||
@@ -21,8 +22,6 @@ from erpnext.deprecation_dumpster import deprecated
|
||||
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template, get_item_tax_map
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
ItemWiseTaxDetail = frappe._dict
|
||||
|
||||
|
||||
class calculate_taxes_and_totals:
|
||||
def __init__(self, doc: Document):
|
||||
@@ -36,7 +35,6 @@ class calculate_taxes_and_totals:
|
||||
)
|
||||
|
||||
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
|
||||
|
||||
get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
|
||||
self.calculate()
|
||||
|
||||
@@ -83,7 +81,6 @@ class calculate_taxes_and_totals:
|
||||
self.calculate_taxes()
|
||||
self.adjust_grand_total_for_inclusive_tax()
|
||||
self.calculate_totals()
|
||||
self._cleanup()
|
||||
self.calculate_total_net_weight()
|
||||
|
||||
def calculate_tax_withholding_net_total(self):
|
||||
@@ -251,14 +248,12 @@ class calculate_taxes_and_totals:
|
||||
doc.set("base_" + f, val)
|
||||
|
||||
def initialize_taxes(self):
|
||||
self.reset_item_wise_tax_details()
|
||||
for tax in self.doc.get("taxes"):
|
||||
if not self.discount_amount_applied:
|
||||
validate_taxes_and_charges(tax)
|
||||
validate_inclusive_tax(tax, self.doc)
|
||||
|
||||
if not (self.doc.get("is_consolidated") or tax.get("dont_recompute_tax")):
|
||||
tax.item_wise_tax_detail = {}
|
||||
|
||||
tax_fields = [
|
||||
"net_amount",
|
||||
"total",
|
||||
@@ -278,6 +273,22 @@ class calculate_taxes_and_totals:
|
||||
|
||||
self.doc.round_floats_in(tax)
|
||||
|
||||
def reset_item_wise_tax_details(self):
|
||||
# Setting flag for adding rows
|
||||
self.doc.update_item_wise_tax_details = True
|
||||
dont_recompute_taxes = [d for d in self.doc.get("taxes") if d.get("dont_recompute_tax")]
|
||||
|
||||
# Identify taxes that shouldn't be recomputed
|
||||
item_wise_tax_details = []
|
||||
# retain tax_breakup for dont_recompute_taxes
|
||||
for row in self.doc.get("_item_wise_tax_details") or []:
|
||||
tax = row.get("tax")
|
||||
if tax in dont_recompute_taxes:
|
||||
item_wise_tax_details.append(row)
|
||||
|
||||
self.doc._item_wise_tax_details = item_wise_tax_details
|
||||
self.doc.item_wise_tax_details = []
|
||||
|
||||
def determine_exclusive_rate(self):
|
||||
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
|
||||
return
|
||||
@@ -476,6 +487,60 @@ class calculate_taxes_and_totals:
|
||||
|
||||
self._set_in_company_currency(tax, ["total"])
|
||||
|
||||
self.adjust_rounding_in_item_wise_tax_details()
|
||||
|
||||
def adjust_rounding_in_item_wise_tax_details(self):
|
||||
if ignore_item_wise_tax_details(self.doc):
|
||||
return
|
||||
|
||||
if not self.doc.get("_item_wise_tax_details"):
|
||||
return
|
||||
|
||||
invalid_rows = []
|
||||
|
||||
# reset temporary attributes
|
||||
for tax in self.doc.taxes:
|
||||
tax._total_tax_breakup = 0
|
||||
tax._last_row_idx = None
|
||||
|
||||
for idx, d in enumerate(self.doc._item_wise_tax_details):
|
||||
tax = d.get("tax")
|
||||
if not tax:
|
||||
continue
|
||||
tax._total_tax_breakup += d.amount or 0
|
||||
tax._last_row_idx = idx
|
||||
|
||||
# Apply rounding difference to the last row
|
||||
for tax in self.doc.taxes:
|
||||
last_idx = tax._last_row_idx
|
||||
if last_idx is None:
|
||||
continue
|
||||
|
||||
multiplier = -1 if tax.get("add_deduct_tax") == "Deduct" else 1
|
||||
expected_amount = tax.base_tax_amount_after_discount_amount * multiplier
|
||||
actual_breakup = tax._total_tax_breakup
|
||||
diff = flt(expected_amount - actual_breakup, 5)
|
||||
|
||||
# TODO: fix rounding difference issues
|
||||
if abs(diff) <= 0.5:
|
||||
detail_row = self.doc._item_wise_tax_details[last_idx]
|
||||
detail_row["amount"] = flt(detail_row["amount"] + diff, 5)
|
||||
|
||||
else:
|
||||
invalid_rows.append(f"Row {tax.idx} (Difference: {diff})")
|
||||
|
||||
if self.doc.flags.ignore_validate:
|
||||
return
|
||||
|
||||
if invalid_rows:
|
||||
message = (
|
||||
_("Item Wise Tax Details do not match with Taxes and Charges at the following rows:")
|
||||
+ "<br>"
|
||||
+ "<br>".join(invalid_rows)
|
||||
)
|
||||
|
||||
frappe.throw(_(message))
|
||||
|
||||
def get_tax_amount_if_for_valuation_or_deduction(self, tax_amount, tax):
|
||||
# if just for valuation, do not add the tax amount in total
|
||||
# if tax/charges is for deduction, multiply by -1
|
||||
@@ -533,41 +598,35 @@ class calculate_taxes_and_totals:
|
||||
# don't sum current net amount due to the field being a currency field
|
||||
current_tax_amount = tax_rate * item.qty
|
||||
|
||||
if not (self.doc.get("is_consolidated") or tax.get("dont_recompute_tax")):
|
||||
if not tax.get("dont_recompute_tax"):
|
||||
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount, current_net_amount)
|
||||
|
||||
return current_net_amount, current_tax_amount
|
||||
|
||||
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount, current_net_amount):
|
||||
# store tax breakup for each item
|
||||
key = item.item_code or item.item_name
|
||||
item_wise_tax_amount = current_tax_amount * self.doc.conversion_rate
|
||||
if tax.charge_type != "On Item Quantity":
|
||||
item_wise_net_amount = current_net_amount * self.doc.conversion_rate
|
||||
else:
|
||||
item_wise_net_amount = 0.0
|
||||
if frappe.flags.round_row_wise_tax:
|
||||
item_wise_tax_amount = flt(item_wise_tax_amount, tax.precision("tax_amount"))
|
||||
item_wise_net_amount = flt(item_wise_net_amount, tax.precision("net_amount"))
|
||||
if tax_data := tax.item_wise_tax_detail.get(key):
|
||||
item_wise_tax_amount += flt(tax_data.tax_amount, tax.precision("tax_amount"))
|
||||
item_wise_net_amount += flt(tax_data.net_amount, tax.precision("net_amount"))
|
||||
else:
|
||||
tax.item_wise_tax_detail[key] = ItemWiseTaxDetail(
|
||||
tax_rate=tax_rate,
|
||||
tax_amount=flt(item_wise_tax_amount, tax.precision("tax_amount")),
|
||||
net_amount=flt(item_wise_net_amount, tax.precision("net_amount")),
|
||||
)
|
||||
else:
|
||||
if tax_data := tax.item_wise_tax_detail.get(key):
|
||||
item_wise_tax_amount += tax_data.tax_amount
|
||||
item_wise_net_amount += tax_data.net_amount
|
||||
multiplier = -1 if tax.get("add_deduct_tax") == "Deduct" else 1
|
||||
item_wise_tax_amount = flt(
|
||||
current_tax_amount * self.doc.conversion_rate * multiplier, tax.precision("tax_amount")
|
||||
)
|
||||
|
||||
tax.item_wise_tax_detail[key] = ItemWiseTaxDetail(
|
||||
tax_rate=tax_rate,
|
||||
tax_amount=item_wise_tax_amount,
|
||||
net_amount=item_wise_net_amount,
|
||||
if tax.charge_type != "On Item Quantity":
|
||||
item_wise_taxable_amount = flt(
|
||||
current_net_amount * self.doc.conversion_rate * multiplier, tax.precision("tax_amount")
|
||||
)
|
||||
else:
|
||||
item_wise_taxable_amount = 0.0
|
||||
|
||||
# maintaining a temp object with item and tax object because correct name will be available after insertion.
|
||||
self.doc._item_wise_tax_details.append(
|
||||
frappe._dict(
|
||||
item=item,
|
||||
tax=tax,
|
||||
rate=tax_rate,
|
||||
amount=item_wise_tax_amount,
|
||||
taxable_amount=item_wise_taxable_amount,
|
||||
)
|
||||
)
|
||||
|
||||
def round_off_totals(self, tax):
|
||||
if tax.account_head in frappe.flags.round_off_applicable_accounts:
|
||||
@@ -704,12 +763,6 @@ class calculate_taxes_and_totals:
|
||||
|
||||
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
|
||||
|
||||
def _cleanup(self):
|
||||
if not self.doc.get("is_consolidated"):
|
||||
for tax in self.doc.get("taxes"):
|
||||
if not tax.get("dont_recompute_tax"):
|
||||
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail)
|
||||
|
||||
def set_discount_amount(self):
|
||||
if self.doc.additional_discount_percentage:
|
||||
self.doc.discount_amount = flt(
|
||||
@@ -1150,30 +1203,45 @@ def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_itemised_tax_breakup_data(doc):
|
||||
itemised_tax = get_itemised_tax(doc.taxes)
|
||||
itemised_tax = get_itemised_tax(doc)
|
||||
itemised_tax_data = []
|
||||
for item_code, taxes in itemised_tax.items():
|
||||
taxable_amount = next(iter(taxes.values())).get("net_amount")
|
||||
taxable_amount = next(iter(taxes.values())).get("taxable_amount")
|
||||
itemised_tax_data.append(frappe._dict({"item": item_code, "taxable_amount": taxable_amount, **taxes}))
|
||||
|
||||
return itemised_tax_data
|
||||
|
||||
|
||||
def get_itemised_tax(taxes, with_tax_account=False):
|
||||
def get_itemised_tax(doc, with_tax_account=False):
|
||||
itemised_tax = {}
|
||||
for tax in taxes:
|
||||
precision = doc.precision("tax_amount", "taxes")
|
||||
|
||||
for row in doc.get("_item_wise_tax_details"):
|
||||
item = row.get("item")
|
||||
tax = row.get("tax")
|
||||
if not item or not tax:
|
||||
continue
|
||||
|
||||
item_code = item.item_code or item.item_name
|
||||
if getattr(tax, "category", None) and tax.category == "Valuation":
|
||||
continue
|
||||
|
||||
item_tax_map = json.loads(tax.item_wise_tax_detail) if tax.item_wise_tax_detail else {}
|
||||
if item_tax_map:
|
||||
for item_code, tax_data in item_tax_map.items():
|
||||
tax_data = ItemWiseTaxDetail(**tax_data)
|
||||
itemised_tax.setdefault(item_code, frappe._dict())
|
||||
itemised_tax[item_code][tax.description] = tax_data
|
||||
tax_info = itemised_tax.setdefault(item_code, frappe._dict()).setdefault(
|
||||
tax.description,
|
||||
frappe._dict(
|
||||
{
|
||||
"tax_amount": 0.0,
|
||||
"taxable_amount": 0.0,
|
||||
"tax_rate": row.rate,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
if with_tax_account:
|
||||
itemised_tax[item_code][tax.description].tax_account = tax.account_head
|
||||
tax_info.tax_amount += flt(row.amount, precision)
|
||||
tax_info.taxable_amount += flt(row.taxable_amount, precision)
|
||||
|
||||
if with_tax_account:
|
||||
tax_info.tax_account = tax.account_head
|
||||
|
||||
return itemised_tax
|
||||
|
||||
@@ -1196,6 +1264,39 @@ def get_rounding_tax_settings():
|
||||
return frappe.get_single_value("Accounts Settings", "round_row_wise_tax")
|
||||
|
||||
|
||||
def ignore_item_wise_tax_details(doc):
|
||||
"""Ignore item wise tax details if the doctype does not have item_wise_tax_details field."""
|
||||
if not doc.meta.get_field("item_wise_tax_details"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def process_item_wise_tax_details(doc):
|
||||
if ignore_item_wise_tax_details(doc):
|
||||
return
|
||||
|
||||
if not (doc.get("update_item_wise_tax_details") and doc.get("_item_wise_tax_details")):
|
||||
return
|
||||
|
||||
docs = []
|
||||
for row in doc.get("_item_wise_tax_details"):
|
||||
tax_details = doc.append(
|
||||
"item_wise_tax_details",
|
||||
{
|
||||
**row,
|
||||
"docstatus": doc.docstatus,
|
||||
"item_row": row.item.name,
|
||||
"tax_row": row.tax.name,
|
||||
},
|
||||
)
|
||||
tax_details.set_new_name()
|
||||
docs.append(tax_details)
|
||||
|
||||
bulk_insert("Item Wise Tax Detail", docs)
|
||||
doc.update_item_wise_tax_details = False
|
||||
|
||||
|
||||
class init_landed_taxes_and_totals:
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
@@ -73,32 +73,54 @@ class TestTaxesAndTotals(FrappeTestCase):
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Item Quantity",
|
||||
"account_head": "_Test Account Shipping - _TC",
|
||||
"account_head": "_Test Account Shipping Charges - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Shipping",
|
||||
"rate": 50,
|
||||
},
|
||||
)
|
||||
self.doc.set_missing_item_details()
|
||||
calculate_taxes_and_totals(self.doc)
|
||||
self.doc.save()
|
||||
|
||||
expected_values = {
|
||||
"VAT": {"tax_rate": 10, "tax_amount": 10, "net_amount": 100},
|
||||
"Service Tax": {"tax_rate": 14, "tax_amount": 1.4, "net_amount": 10},
|
||||
"Customs Duty": {"tax_rate": 5, "tax_amount": 5.57, "net_amount": 111.4},
|
||||
"Shipping": {"tax_rate": 50, "tax_amount": 50, "net_amount": 0.0}, # net_amount: here qty
|
||||
}
|
||||
expected_values = [
|
||||
{
|
||||
"item_row": self.doc.items[0].name,
|
||||
"tax_row": self.doc.taxes[0].name,
|
||||
"rate": 10.0,
|
||||
"amount": 10.0,
|
||||
"taxable_amount": 100.0,
|
||||
},
|
||||
{
|
||||
"item_row": self.doc.items[0].name,
|
||||
"tax_row": self.doc.taxes[1].name,
|
||||
"rate": 14.0,
|
||||
"amount": 1.4,
|
||||
"taxable_amount": 10.0,
|
||||
},
|
||||
{
|
||||
"item_row": self.doc.items[0].name,
|
||||
"tax_row": self.doc.taxes[2].name,
|
||||
"rate": 5.0,
|
||||
"amount": 5.57,
|
||||
"taxable_amount": 111.4,
|
||||
},
|
||||
{
|
||||
"item_row": self.doc.items[0].name,
|
||||
"tax_row": self.doc.taxes[3].name,
|
||||
"rate": 50.0,
|
||||
"amount": 50.0,
|
||||
"taxable_amount": 0.0,
|
||||
},
|
||||
]
|
||||
|
||||
for tax in self.doc.taxes:
|
||||
self.assertIn(tax.description, expected_values)
|
||||
item_wise_tax_detail = json.loads(tax.item_wise_tax_detail)
|
||||
tax_detail = item_wise_tax_detail[self.doc.items[0].item_code]
|
||||
self.assertAlmostEqual(tax_detail.get("tax_rate"), expected_values[tax.description]["tax_rate"])
|
||||
self.assertAlmostEqual(
|
||||
tax_detail.get("tax_amount"), expected_values[tax.description]["tax_amount"]
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
tax_detail.get("net_amount"), expected_values[tax.description]["net_amount"]
|
||||
)
|
||||
# Check if net_total is set for each tax
|
||||
self.assertEqual(tax.net_amount, expected_values[tax.description]["net_amount"])
|
||||
actual_values = [
|
||||
{
|
||||
"item_row": row.item_row,
|
||||
"tax_row": row.tax_row,
|
||||
"rate": row.rate,
|
||||
"amount": row.amount,
|
||||
"taxable_amount": row.taxable_amount,
|
||||
}
|
||||
for row in self.doc.item_wise_tax_details
|
||||
]
|
||||
|
||||
self.assertEqual(actual_values, expected_values)
|
||||
|
||||
Reference in New Issue
Block a user