diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 530bd893c0..a2a47b3a19 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 @@ from __future__ import unicode_literals 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 @@ class TestSalesInvoice(unittest.TestCase): 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 5c319008a8..d18f8e54d8 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 @@ def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters) @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 @@ def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): @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 @@ 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'))) 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 049a837eaf..b52a07dbdf 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -8,6 +8,7 @@ from frappe import _, scrub 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 @@ class calculate_taxes_and_totals(object): 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 @@ class calculate_taxes_and_totals(object): 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 926227b24c..3d4c4a6459 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -107,6 +107,12 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ 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 748e623730..51ab48a3ab 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -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() { 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 fa2cb1299a..86cd4d1545 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 @@ def update_item_document(items, taxes): 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 1c9b30b828..8278745a80 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -84,6 +84,13 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ 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 e3d356f93b..253390ac50 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -49,7 +49,7 @@ frappe.ui.form.on("Item", { 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 @@ frappe.ui.form.on("Item", { 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 37daa2938d..a93e4636ad 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 76644ed846..4e5b933a3f 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 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru 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 @@ def get_item_tax_template(args, item, out): 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