feat: Validity for Item taxes (#20135)
* feat: Validity for Item taxes
* fix: Trigger for gst hsn code
* fix: Sort taxes based on validity
* fix: Validation for item tax template and filters based on validity
* fix: Add missing semicolon
* fix: Validate tax template only if item code available
* fix: Do not validate or filter item tax template if no item taxes applied
* fix: Consider item group for validating taxes
* fix: Test cases for item tax validation
* fix: Item tax template filtering fixes
* fix: Add missing semicolon
* fix: Remove unnecessary query
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 530bd89..a2a47b3 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -5,7 +5,7 @@
import frappe
import unittest, copy, time
-from frappe.utils import nowdate, flt, getdate, cint
+from frappe.utils import nowdate, flt, getdate, cint, add_days
from frappe.model.dynamic_links import get_dynamic_link_map
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
@@ -1847,6 +1847,26 @@
self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
+ def test_item_tax_validity(self):
+ item = frappe.get_doc("Item", "_Test Item 2")
+
+ if item.taxes:
+ item.taxes = []
+ item.save()
+
+ item.append("taxes", {
+ "item_tax_template": "_Test Item Tax Template 1",
+ "valid_from": add_days(nowdate(), 1)
+ })
+
+ item.save()
+
+ sales_invoice = create_sales_invoice(item = "_Test Item 2", do_not_save=1)
+ sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1"
+ self.assertRaises(frappe.ValidationError, sales_invoice.save)
+
+ item.taxes = []
+ item.save()
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 5c31900..d18f8e5 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -4,9 +4,9 @@
from __future__ import unicode_literals
import frappe
from frappe.desk.reportview import get_match_cond, get_filters_cond
-from frappe.utils import nowdate
+from frappe.utils import nowdate, getdate
from collections import defaultdict
-
+from erpnext.stock.get_item_details import _get_item_tax_template
# searches for active employees
def employee_query(doctype, txt, searchfield, start, page_len, filters):
@@ -486,7 +486,7 @@
@frappe.whitelist()
def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters):
query = """
- select pr.name
+ select pr.name
from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pritem
where pr.docstatus = 1 and pritem.parent = pr.name
and pr.name like {txt}""".format(txt = frappe.db.escape('%{0}%'.format(txt)))
@@ -499,7 +499,7 @@
@frappe.whitelist()
def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
query = """
- select pi.name
+ select pi.name
from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` piitem
where pi.docstatus = 1 and piitem.parent = pi.name
and pi.name like {txt}""".format(txt = frappe.db.escape('%{0}%'.format(txt)))
@@ -508,3 +508,27 @@
query += " and piitem.item_code = {item_code}".format(item_code = frappe.db.escape(filters.get('item_code')))
return frappe.db.sql(query, filters)
+
+@frappe.whitelist()
+def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
+
+ item_doc = frappe.get_cached_doc('Item', filters.get('item_code'))
+ item_group = filters.get('item_group')
+ taxes = item_doc.taxes or []
+
+ while item_group:
+ item_group_doc = frappe.get_cached_doc('Item Group', item_group)
+ taxes += item_group_doc.taxes or []
+ item_group = item_group_doc.parent_item_group
+
+ if not taxes:
+ return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """)
+ else:
+ args = {
+ 'item_code': filters.get('item_code'),
+ 'posting_date': filters.get('valid_from'),
+ 'tax_category': filters.get('tax_category')
+ }
+
+ taxes = _get_item_tax_template(args, taxes, for_validate=True)
+ return [(d,) for d in set(taxes)]
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 049a837..b52a07d 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -8,6 +8,7 @@
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
from erpnext.controllers.accounts_controller import validate_conversion_rate, \
validate_taxes_and_charges, validate_inclusive_tax
+from erpnext.stock.get_item_details import _get_item_tax_template
class calculate_taxes_and_totals(object):
def __init__(self, doc):
@@ -34,6 +35,7 @@
def _calculate(self):
self.validate_conversion_rate()
self.calculate_item_values()
+ self.validate_item_tax_template()
self.initialize_taxes()
self.determine_exclusive_rate()
self.calculate_net_total()
@@ -43,6 +45,38 @@
self._cleanup()
self.calculate_total_net_weight()
+ def validate_item_tax_template(self):
+ for item in self.doc.get('items'):
+ if item.item_code and item.get('item_tax_template'):
+ item_doc = frappe.get_cached_doc("Item", item.item_code)
+ args = {
+ 'tax_category': self.doc.get('tax_category'),
+ 'posting_date': self.doc.get('posting_date'),
+ 'bill_date': self.doc.get('bill_date'),
+ 'transaction_date': self.doc.get('transaction_date')
+ }
+
+ item_group = item_doc.item_group
+ item_group_taxes = []
+
+ while item_group:
+ item_group_doc = frappe.get_cached_doc('Item Group', item_group)
+ item_group_taxes += item_group_doc.taxes or []
+ item_group = item_group_doc.parent_item_group
+
+ item_taxes = item_doc.taxes or []
+
+ if not item_group_taxes and (not item_taxes):
+ # No validation if no taxes in item or item group
+ continue
+
+ taxes = _get_item_tax_template(args, item_taxes + item_group_taxes, for_validate=True)
+
+ if item.item_tax_template not in taxes:
+ frappe.throw(_("Row {0}: Invalid Item Tax Template for item {1}").format(
+ item.idx, frappe.bold(item.item_code)
+ ))
+
def validate_conversion_rate(self):
# validate conversion rate
company_currency = erpnext.get_company_currency(self.doc.company)
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 926227b..3d4c4a6 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -107,6 +107,12 @@
filters:{ 'item_code': row.item_code }
}
});
+
+ if(this.frm.fields_dict["items"].grid.get_field('item_code')) {
+ this.frm.set_query("item_tax_template", "items", function(doc, cdt, cdn) {
+ return me.set_query_for_item_tax_template(doc, cdt, cdn)
+ });
+ }
},
refresh: function(doc) {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 748e623..51ab48a 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1700,6 +1700,29 @@
}
},
+ set_query_for_item_tax_template: function(doc, cdt, cdn) {
+
+ var item = frappe.get_doc(cdt, cdn);
+ if(!item.item_code) {
+ frappe.throw(__("Please enter Item Code to get item taxes"));
+ } else {
+
+ let filters = {
+ 'item_code': item.item_code,
+ 'valid_from': doc.transaction_date || doc.bill_date || doc.posting_date,
+ 'item_group': item.item_group,
+ }
+
+ if (doc.tax_category)
+ filters['tax_category'] = doc.tax_category;
+
+ return {
+ query: "erpnext.controllers.queries.get_tax_template",
+ filters: filters
+ }
+ }
+ },
+
payment_terms_template: function() {
var me = this;
const doc = this.frm.doc;
diff --git a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py
index fa2cb12..86cd4d1 100644
--- a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py
+++ b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py
@@ -25,5 +25,9 @@
item_to_be_updated.taxes = []
for tax in taxes:
tax = frappe._dict(tax)
- item_to_be_updated.append("taxes", {'item_tax_template': tax.item_tax_template, 'tax_category': tax.tax_category})
+ item_to_be_updated.append("taxes", {
+ 'item_tax_template': tax.item_tax_template,
+ 'tax_category': tax.tax_category,
+ 'valid_from': tax.valid_from
+ })
item_to_be_updated.save()
\ No newline at end of file
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 1c9b30b..8278745 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -84,6 +84,13 @@
return me.set_query_for_batch(doc, cdt, cdn)
});
}
+
+ if(this.frm.fields_dict["items"].grid.get_field('item_code')) {
+ this.frm.set_query("item_tax_template", "items", function(doc, cdt, cdn) {
+ return me.set_query_for_item_tax_template(doc, cdt, cdn)
+ });
+ }
+
},
refresh: function() {
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index e3d356f..253390a 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -49,7 +49,7 @@
if (!frm.doc.is_fixed_asset) {
erpnext.item.make_dashboard(frm);
}
-
+
if (frm.doc.is_fixed_asset) {
frm.trigger('is_fixed_asset');
frm.trigger('auto_create_assets');
@@ -136,14 +136,14 @@
frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0);
},
- gst_hsn_code: function(frm){
- if(!frm.doc.taxes){
- frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc=>{
- frm.doc.taxes = [];
+ gst_hsn_code: function(frm) {
+ if(!frm.doc.taxes.length) {
+ frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc => {
$.each(hsn_doc.taxes || [], function(i, tax) {
let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes');
a.item_tax_template = tax.item_tax_template;
a.tax_category = tax.tax_category;
+ a.valid_from = tax.valid_from;
frm.refresh_field('taxes');
});
});
diff --git a/erpnext/stock/doctype/item_tax/item_tax.json b/erpnext/stock/doctype/item_tax/item_tax.json
index 37daa29..a93e463 100644
--- a/erpnext/stock/doctype/item_tax/item_tax.json
+++ b/erpnext/stock/doctype/item_tax/item_tax.json
@@ -1,107 +1,50 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2013-02-22 01:28:01",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2013-02-22 01:28:01",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_tax_template",
+ "tax_category",
+ "valid_from"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "item_tax_template",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Item Tax Template",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "tax_type",
- "oldfieldtype": "Link",
- "options": "Item Tax Template",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "item_tax_template",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Tax Template",
+ "oldfieldname": "tax_type",
+ "oldfieldtype": "Link",
+ "options": "Item Tax Template",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "",
- "fieldname": "tax_category",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Tax Category",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "tax_rate",
- "oldfieldtype": "Currency",
- "options": "Tax Category",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "tax_category",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Tax Category",
+ "oldfieldname": "tax_rate",
+ "oldfieldtype": "Currency",
+ "options": "Tax Category"
+ },
+ {
+ "fieldname": "valid_from",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Valid From"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-12-21 23:52:40.798944",
- "modified_by": "Administrator",
- "module": "Stock",
- "name": "Item Tax",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "idx": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2019-12-28 21:54:40.807849",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Item Tax",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 76644ed..4e5b933 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _, throw
-from frappe.utils import flt, cint, add_days, cstr, add_months
+from frappe.utils import flt, cint, add_days, cstr, add_months, getdate
import json, copy
from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item, set_transaction_type
from erpnext.setup.utils import get_exchange_rate
@@ -52,6 +52,16 @@
out = get_basic_details(args, item, overwrite_warehouse)
+ if isinstance(doc, string_types):
+ doc = json.loads(doc)
+
+ if doc and doc.get('doctype') == 'Purchase Invoice':
+ args['bill_date'] = doc.get('bill_date')
+
+ if doc:
+ args['posting_date'] = doc.get('posting_date')
+ args['transaction_date'] = doc.get('transaction_date')
+
get_item_tax_template(args, item, out)
out["item_tax_rate"] = get_item_tax_map(args.company, args.get("item_tax_template") if out.get("item_tax_template") is None \
else out.get("item_tax_template"), as_json=True)
@@ -395,7 +405,34 @@
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
item_group = item_group_doc.parent_item_group
-def _get_item_tax_template(args, taxes, out):
+def _get_item_tax_template(args, taxes, out={}, for_validate=False):
+ taxes_with_validity = []
+ taxes_with_no_validity = []
+
+ for tax in taxes:
+ if tax.valid_from:
+ # In purchase Invoice first preference will be given to supplier invoice date
+ # if supplier date is not present then posting date
+ validation_date = args.get('transaction_date') or args.get('bill_date') or args.get('posting_date')
+
+ if getdate(tax.valid_from) <= getdate(validation_date):
+ taxes_with_validity.append(tax)
+ else:
+ taxes_with_no_validity.append(tax)
+
+ if taxes_with_validity:
+ taxes = sorted(taxes_with_validity, key = lambda i: i.valid_from, reverse=True)
+ else:
+ taxes = taxes_with_no_validity
+
+ if for_validate:
+ return [tax.item_tax_template for tax in taxes if (cstr(tax.tax_category) == cstr(args.get('tax_category')) \
+ and (tax.item_tax_template not in taxes))]
+
+ # all templates have validity and no template is valid
+ if not taxes_with_validity and (not taxes_with_no_validity):
+ return None
+
for tax in taxes:
if cstr(tax.tax_category) == cstr(args.get("tax_category")):
out["item_tax_template"] = tax.item_tax_template