fix: Add Taxes if missing via Update Items (#23702)
* fix: Add Taxes if missing via Update Items
* chore: PO Test for adding tax row via Update Items
* chore: SO Test for adding tax row via Update Items
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 7c8ae6c..0cf3d24 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -203,9 +203,39 @@
frappe.set_user("Administrator")
def test_update_child_with_tax_template(self):
+ """
+ Test Action: Create a PO with one item having its tax account head already in the PO.
+ Add the same item + new item with tax template via Update Items.
+ Expected result: First Item's tax row is updated. New tax row is added for second Item.
+ """
+ if not frappe.db.exists("Item", "Test Item with Tax"):
+ make_item("Test Item with Tax", {
+ 'is_stock_item': 1,
+ })
+
+ if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}):
+ frappe.get_doc({
+ 'doctype': 'Item Tax Template',
+ 'title': 'Test Update Items Template',
+ 'company': '_Test Company',
+ 'taxes': [
+ {
+ 'tax_type': "_Test Account Service Tax - _TC",
+ 'tax_rate': 10,
+ }
+ ]
+ }).insert()
+
+ new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
+
+ new_item_with_tax.append("taxes", {
+ "item_tax_template": "Test Update Items Template",
+ "valid_from": nowdate()
+ })
+ new_item_with_tax.save()
+
tax_template = "_Test Account Excise Duty @ 10"
item = "_Test Item Home Desktop 100"
-
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
item_doc = frappe.get_doc("Item", item)
item_doc.append("taxes", {
@@ -237,17 +267,25 @@
items = json.dumps([
{'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name},
- {'item_code' : item, 'rate' : 100, 'qty' : 1} # added item
+ {'item_code' : item, 'rate' : 100, 'qty' : 1}, # added item whose tax account head already exists in PO
+ {'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO
])
update_child_qty_rate('Purchase Order', items, po.name)
po.reload()
- self.assertEqual(po.taxes[0].tax_amount, 60)
- self.assertEqual(po.taxes[0].total, 660)
+ self.assertEqual(po.taxes[0].tax_amount, 70)
+ self.assertEqual(po.taxes[0].total, 770)
+ self.assertEqual(po.taxes[1].account_head, "_Test Account Service Tax - _TC")
+ self.assertEqual(po.taxes[1].tax_amount, 70)
+ self.assertEqual(po.taxes[1].total, 840)
+ # teardown
frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL
- where parent = %(item)s and item_tax_template = %(tax)s""",
- {"item": item, "tax": tax_template})
+ where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template})
+ po.cancel()
+ po.delete()
+ new_item_with_tax.delete()
+ frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
def test_update_child_uom_conv_factor_change(self):
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 1818cb6..fc32977 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1168,6 +1168,31 @@
if child_item.get("item_tax_template"):
child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True)
+def add_taxes_from_tax_template(child_item, parent_doc):
+ add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "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"))
+ for tax_type in tax_map:
+ tax_rate = flt(tax_map[tax_type])
+ taxes = parent_doc.get('taxes') or []
+ # add new row for tax head only if missing
+ found = any(tax.account_head == tax_type for tax in taxes)
+ if not found:
+ tax_row = parent_doc.append("taxes", {})
+ tax_row.update({
+ "description" : str(tax_type).split(' - ')[0],
+ "charge_type" : "On Net Total",
+ "account_head" : tax_type,
+ "rate" : tax_rate
+ })
+ if parent_doc.doctype == "Purchase Order":
+ tax_row.update({
+ "category" : "Total",
+ "add_deduct_tax" : "Add"
+ })
+ tax_row.db_insert()
+
def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
"""
Returns a Sales Order Item child item containing the default values
@@ -1183,6 +1208,7 @@
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
set_child_tax_template_and_map(item, child_item, p_doc)
+ add_taxes_from_tax_template(child_item, p_doc)
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
if not child_item.warehouse:
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
@@ -1207,6 +1233,7 @@
child_item.base_rate = 1 # Initiallize value will update in parent validation
child_item.base_amount = 1 # Initiallize value will update in parent validation
set_child_tax_template_and_map(item, child_item, p_doc)
+ add_taxes_from_tax_template(child_item, p_doc)
return child_item
def validate_and_delete_children(parent, data):
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 1ce36dd..2f5f979 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -403,7 +403,7 @@
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name)
-
+
def test_update_child_with_precision(self):
from frappe.model.meta import get_field_precision
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
@@ -437,7 +437,7 @@
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name)
test_user.remove_roles("Accounts User")
frappe.set_user("Administrator")
-
+
def test_update_child_qty_rate_with_workflow(self):
from frappe.model.workflow import apply_workflow
@@ -506,6 +506,95 @@
so.reload()
self.assertEqual(so.packed_items[0].qty, 8)
+ def test_update_child_with_tax_template(self):
+ """
+ Test Action: Create a SO with one item having its tax account head already in the SO.
+ Add the same item + new item with tax template via Update Items.
+ Expected result: First Item's tax row is updated. New tax row is added for second Item.
+ """
+ if not frappe.db.exists("Item", "Test Item with Tax"):
+ make_item("Test Item with Tax", {
+ 'is_stock_item': 1,
+ })
+
+ if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}):
+ frappe.get_doc({
+ 'doctype': 'Item Tax Template',
+ 'title': 'Test Update Items Template',
+ 'company': '_Test Company',
+ 'taxes': [
+ {
+ 'tax_type': "_Test Account Service Tax - _TC",
+ 'tax_rate': 10,
+ }
+ ]
+ }).insert()
+
+ new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
+
+ new_item_with_tax.append("taxes", {
+ "item_tax_template": "Test Update Items Template",
+ "valid_from": nowdate()
+ })
+ new_item_with_tax.save()
+
+ tax_template = "_Test Account Excise Duty @ 10"
+ item = "_Test Item Home Desktop 100"
+ if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
+ item_doc = frappe.get_doc("Item", item)
+ item_doc.append("taxes", {
+ "item_tax_template": tax_template,
+ "valid_from": nowdate()
+ })
+ item_doc.save()
+ else:
+ # update valid from
+ frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE()
+ where parent = %(item)s and item_tax_template = %(tax)s""",
+ {"item": item, "tax": tax_template})
+
+ so = make_sales_order(item_code=item, qty=1, do_not_save=1)
+
+ so.append("taxes", {
+ "account_head": "_Test Account Excise Duty - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Excise Duty",
+ "doctype": "Sales Taxes and Charges",
+ "rate": 10
+ })
+ so.insert()
+ so.submit()
+
+ self.assertEqual(so.taxes[0].tax_amount, 10)
+ self.assertEqual(so.taxes[0].total, 110)
+
+ old_stock_settings_value = frappe.db.get_single_value("Stock Settings", "default_warehouse")
+ frappe.db.set_value("Stock Settings", None, "default_warehouse", "_Test Warehouse - _TC")
+
+ items = json.dumps([
+ {'item_code' : item, 'rate' : 100, 'qty' : 1, 'docname': so.items[0].name},
+ {'item_code' : item, 'rate' : 200, 'qty' : 1}, # added item whose tax account head already exists in PO
+ {'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO
+ ])
+ update_child_qty_rate('Sales Order', items, so.name)
+
+ so.reload()
+ self.assertEqual(so.taxes[0].tax_amount, 40)
+ self.assertEqual(so.taxes[0].total, 440)
+ self.assertEqual(so.taxes[1].account_head, "_Test Account Service Tax - _TC")
+ self.assertEqual(so.taxes[1].tax_amount, 40)
+ self.assertEqual(so.taxes[1].total, 480)
+
+ # teardown
+ frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL
+ where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template})
+ so.cancel()
+ so.delete()
+ new_item_with_tax.delete()
+ frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
+ frappe.db.set_value("Stock Settings", None, "default_warehouse", old_stock_settings_value)
+
def test_warehouse_user(self):
frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com")
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 1a7c15e..8d8dcb7 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -398,6 +398,11 @@
else:
warehouse = args.get('warehouse')
+ if not warehouse:
+ default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse")
+ if frappe.db.get_value("Warehouse", default_warehouse, "company") == args.company:
+ return default_warehouse
+
return warehouse
def update_barcode_value(out):