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
This commit is contained in:
Deepesh Garg 2020-01-06 15:34:15 +05:30 committed by Nabin Hait
parent b5f91bea90
commit ef0d26c161
10 changed files with 212 additions and 114 deletions

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest, copy, time 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 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.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 from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
@ -1847,6 +1847,26 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) 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): def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice") si = frappe.new_doc("Sales Invoice")

View File

@ -4,9 +4,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.desk.reportview import get_match_cond, get_filters_cond 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 collections import defaultdict
from erpnext.stock.get_item_details import _get_item_tax_template
# searches for active employees # searches for active employees
def employee_query(doctype, txt, searchfield, start, page_len, filters): def employee_query(doctype, txt, searchfield, start, page_len, filters):
@ -508,3 +508,27 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
query += " and piitem.item_code = {item_code}".format(item_code = frappe.db.escape(filters.get('item_code'))) query += " and piitem.item_code = {item_code}".format(item_code = frappe.db.escape(filters.get('item_code')))
return frappe.db.sql(query, filters) 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)]

View File

@ -8,6 +8,7 @@ from frappe import _, scrub
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
from erpnext.controllers.accounts_controller import validate_conversion_rate, \ from erpnext.controllers.accounts_controller import validate_conversion_rate, \
validate_taxes_and_charges, validate_inclusive_tax validate_taxes_and_charges, validate_inclusive_tax
from erpnext.stock.get_item_details import _get_item_tax_template
class calculate_taxes_and_totals(object): class calculate_taxes_and_totals(object):
def __init__(self, doc): def __init__(self, doc):
@ -34,6 +35,7 @@ class calculate_taxes_and_totals(object):
def _calculate(self): def _calculate(self):
self.validate_conversion_rate() self.validate_conversion_rate()
self.calculate_item_values() self.calculate_item_values()
self.validate_item_tax_template()
self.initialize_taxes() self.initialize_taxes()
self.determine_exclusive_rate() self.determine_exclusive_rate()
self.calculate_net_total() self.calculate_net_total()
@ -43,6 +45,38 @@ class calculate_taxes_and_totals(object):
self._cleanup() self._cleanup()
self.calculate_total_net_weight() 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): def validate_conversion_rate(self):
# validate conversion rate # validate conversion rate
company_currency = erpnext.get_company_currency(self.doc.company) company_currency = erpnext.get_company_currency(self.doc.company)

View File

@ -107,6 +107,12 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
filters:{ 'item_code': row.item_code } 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) { refresh: function(doc) {

View File

@ -1700,6 +1700,29 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
}, },
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() { payment_terms_template: function() {
var me = this; var me = this;
const doc = this.frm.doc; const doc = this.frm.doc;

View File

@ -25,5 +25,9 @@ def update_item_document(items, taxes):
item_to_be_updated.taxes = [] item_to_be_updated.taxes = []
for tax in taxes: for tax in taxes:
tax = frappe._dict(tax) 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() item_to_be_updated.save()

View File

@ -84,6 +84,13 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
return me.set_query_for_batch(doc, cdt, cdn) 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() { refresh: function() {

View File

@ -136,14 +136,14 @@ frappe.ui.form.on("Item", {
frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0); frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0);
}, },
gst_hsn_code: function(frm){ gst_hsn_code: function(frm) {
if(!frm.doc.taxes){ if(!frm.doc.taxes.length) {
frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc=>{ frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc => {
frm.doc.taxes = [];
$.each(hsn_doc.taxes || [], function(i, tax) { $.each(hsn_doc.taxes || [], function(i, tax) {
let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes'); let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes');
a.item_tax_template = tax.item_tax_template; a.item_tax_template = tax.item_tax_template;
a.tax_category = tax.tax_category; a.tax_category = tax.tax_category;
a.valid_from = tax.valid_from;
frm.refresh_field('taxes'); frm.refresh_field('taxes');
}); });
}); });

View File

@ -1,107 +1,50 @@
{ {
"allow_copy": 0, "actions": [],
"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", "creation": "2013-02-22 01:28:01",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_tax_template",
"tax_category",
"valid_from"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_tax_template", "fieldname": "item_tax_template",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Tax Template", "label": "Item Tax Template",
"length": 0,
"no_copy": 0,
"oldfieldname": "tax_type", "oldfieldname": "tax_type",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Item Tax Template", "options": "Item Tax Template",
"permlevel": 0, "reqd": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "",
"fieldname": "tax_category", "fieldname": "tax_category",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Tax Category", "label": "Tax Category",
"length": 0,
"no_copy": 0,
"oldfieldname": "tax_rate", "oldfieldname": "tax_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "Tax Category", "options": "Tax Category"
"permlevel": 0, },
"print_hide": 0, {
"print_hide_if_no_value": 0, "fieldname": "valid_from",
"read_only": 0, "fieldtype": "Date",
"remember_last_selected_value": 0, "in_list_view": 1,
"report_hide": 0, "label": "Valid From"
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1, "idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "links": [],
"modified": "2018-12-21 23:52:40.798944", "modified": "2019-12-28 21:54:40.807849",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Tax", "name": "Item Tax",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0, "sort_order": "DESC"
"read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 0,
"track_seen": 0,
"track_views": 0
} }

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _, throw 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 import json, copy
from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item, set_transaction_type 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 from erpnext.setup.utils import get_exchange_rate
@ -52,6 +52,16 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
out = get_basic_details(args, item, overwrite_warehouse) 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) 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 \ 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) else out.get("item_tax_template"), as_json=True)
@ -395,7 +405,34 @@ def get_item_tax_template(args, item, out):
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out) item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
item_group = item_group_doc.parent_item_group 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: for tax in taxes:
if cstr(tax.tax_category) == cstr(args.get("tax_category")): if cstr(tax.tax_category) == cstr(args.get("tax_category")):
out["item_tax_template"] = tax.item_tax_template out["item_tax_template"] = tax.item_tax_template