Merge pull request #25961 from deepeshgarg007/dynamic_gst_rates
feat: Item Taxes based on net rate
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 5409a6f..5010fdc 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2001,6 +2001,33 @@
self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
self.assertTrue(einvoice['EwbDtls'])
+ def test_item_tax_net_range(self):
+ item = create_item("T Shirt")
+
+ item.set('taxes', [])
+ item.append("taxes", {
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
+ "minimum_net_rate": 0,
+ "maximum_net_rate": 500
+ })
+
+ item.append("taxes", {
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
+ "minimum_net_rate": 501,
+ "maximum_net_rate": 1000
+ })
+
+ item.save()
+
+ sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
+ self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
+
+ # Apply discount
+ sales_invoice.apply_discount_on = 'Net Total'
+ sales_invoice.discount_amount = 300
+ sales_invoice.save()
+ self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
+
def make_test_address_for_ewaybill():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
address = frappe.get_doc({
@@ -2123,27 +2150,6 @@
doc.assertEqual(expected_gle[i][2], gle.credit)
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
- 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 - _TC",
- "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 - _TC"
- self.assertRaises(frappe.ValidationError, sales_invoice.save)
-
- item.taxes = []
- item.save()
-
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")
args = frappe._dict(args)
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 9fae494..fb22a1d 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -54,6 +54,7 @@
if item.item_code and item.get('item_tax_template'):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
+ 'net_rate': item.net_rate or item.rate,
'tax_category': self.doc.get('tax_category'),
'posting_date': self.doc.get('posting_date'),
'bill_date': self.doc.get('bill_date'),
@@ -77,10 +78,12 @@
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)
- ))
+ if taxes:
+ if item.item_tax_template not in taxes:
+ item.item_tax_template = taxes[0]
+ frappe.msgprint(_("Row {0}: Item Tax template updated as per validity and rate applied").format(
+ item.idx, frappe.bold(item.item_code)
+ ))
def validate_conversion_rate(self):
# validate conversion rate
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 2e133be..0b9d771 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -12,7 +12,7 @@
if (in_list(["Sales Order", "Quotation"], item.parenttype) && item.blanket_order_rate) {
effective_item_rate = item.blanket_order_rate;
}
- if(item.margin_type == "Percentage"){
+ if (item.margin_type == "Percentage") {
item.rate_with_margin = flt(effective_item_rate)
+ flt(effective_item_rate) * ( flt(item.margin_rate_or_amount) / 100);
} else {
@@ -22,7 +22,7 @@
item_rate = flt(item.rate_with_margin , precision("rate", item));
- if(item.discount_percentage){
+ if (item.discount_percentage) {
item.discount_amount = flt(item.rate_with_margin) * flt(item.discount_percentage) / 100;
}
@@ -73,15 +73,18 @@
},
_calculate_taxes_and_totals: function() {
- this.validate_conversion_rate();
- this.calculate_item_values();
- this.initialize_taxes();
- this.determine_exclusive_rate();
- this.calculate_net_total();
- this.calculate_taxes();
- this.manipulate_grand_total_for_inclusive_tax();
- this.calculate_totals();
- this._cleanup();
+ frappe.run_serially([
+ () => this.validate_conversion_rate(),
+ () => this.calculate_item_values(),
+ () => this.update_item_tax_map(),
+ () => this.initialize_taxes(),
+ () => this.determine_exclusive_rate(),
+ () => this.calculate_net_total(),
+ () => this.calculate_taxes(),
+ () => this.manipulate_grand_total_for_inclusive_tax(),
+ () => this.calculate_totals(),
+ () => this._cleanup()
+ ]);
},
validate_conversion_rate: function() {
@@ -263,6 +266,65 @@
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
},
+ update_item_tax_map: function() {
+ let me = this;
+ let item_codes = [];
+ let item_rates = {};
+ $.each(this.frm.doc.items || [], function(i, item) {
+ if (item.item_code) {
+ // Use combination of name and item code in case same item is added multiple times
+ item_codes.push([item.item_code, item.name]);
+ item_rates[item.name] = item.net_rate;
+ }
+ });
+
+ if (item_codes.length) {
+ return this.frm.call({
+ method: "erpnext.stock.get_item_details.get_item_tax_info",
+ args: {
+ company: me.frm.doc.company,
+ tax_category: cstr(me.frm.doc.tax_category),
+ item_codes: item_codes,
+ item_rates: item_rates
+ },
+ callback: function(r) {
+ if (!r.exc) {
+ $.each(me.frm.doc.items || [], function(i, item) {
+ if (item.name && r.message.hasOwnProperty(item.name)) {
+ item.item_tax_template = r.message[item.name].item_tax_template;
+ item.item_tax_rate = r.message[item.name].item_tax_rate;
+ me.add_taxes_from_item_tax_template(item.item_tax_rate);
+ } else {
+ item.item_tax_template = "";
+ item.item_tax_rate = "{}";
+ }
+ });
+ }
+ }
+ });
+ }
+ },
+
+ add_taxes_from_item_tax_template: function(item_tax_map) {
+ let me = this;
+
+ if (item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) {
+ if (typeof (item_tax_map) == "string") {
+ item_tax_map = JSON.parse(item_tax_map);
+ }
+
+ $.each(item_tax_map, function(tax, rate) {
+ let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
+ if (!found) {
+ let child = frappe.model.add_child(me.frm.doc, "taxes");
+ child.charge_type = "On Net Total";
+ child.account_head = tax;
+ child.rate = 0;
+ }
+ });
+ }
+ },
+
calculate_taxes: function() {
var me = this;
this.frm.doc.rounding_adjustment = 0;
@@ -406,6 +468,11 @@
let tax_detail = tax.item_wise_tax_detail;
let key = item.item_code || item.item_name;
+ if(typeof (tax_detail) == "string") {
+ tax.item_wise_tax_detail = JSON.parse(tax.item_wise_tax_detail);
+ tax_detail = tax.item_wise_tax_detail;
+ }
+
let item_wise_tax_amount = current_tax_amount * this.frm.doc.conversion_rate;
if (tax_detail && tax_detail[key])
item_wise_tax_amount += tax_detail[key][1];
@@ -562,6 +629,8 @@
tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail);
});
}
+
+ this.frm.refresh_fields();
},
set_discount_amount: function() {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 982b1fe..89fed3b 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -6,6 +6,7 @@
erpnext.TransactionController = erpnext.taxes_and_totals.extend({
setup: function() {
this._super();
+ let me = this;
frappe.flags.hide_serial_batch_dialog = true;
frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
@@ -43,8 +44,6 @@
cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn);
});
-
-
frappe.ui.form.on(this.frm.cscript.tax_table, "rate", function(frm, cdt, cdn) {
cur_frm.cscript.calculate_taxes_and_totals();
});
@@ -121,7 +120,6 @@
}
});
- var me = this;
if(this.frm.fields_dict["items"].grid.get_field('batch_no')) {
this.frm.set_query("batch_no", "items", function(doc, cdt, cdn) {
return me.set_query_for_batch(doc, cdt, cdn);
@@ -564,6 +562,7 @@
name: me.frm.doc.name,
project: item.project || me.frm.doc.project,
qty: item.qty || 1,
+ net_rate: item.rate,
stock_qty: item.stock_qty,
conversion_factor: item.conversion_factor,
weight_per_unit: item.weight_per_unit,
@@ -720,26 +719,6 @@
});
},
- add_taxes_from_item_tax_template: function(item_tax_map) {
- let me = this;
-
- if(item_tax_map && cint(frappe.defaults.get_default("add_taxes_from_item_tax_template"))) {
- if(typeof (item_tax_map) == "string") {
- item_tax_map = JSON.parse(item_tax_map);
- }
-
- $.each(item_tax_map, function(tax, rate) {
- let found = (me.frm.doc.taxes || []).find(d => d.account_head === tax);
- if(!found) {
- let child = frappe.model.add_child(me.frm.doc, "taxes");
- child.charge_type = "On Net Total";
- child.account_head = tax;
- child.rate = 0;
- }
- });
- }
- },
-
serial_no: function(doc, cdt, cdn) {
var me = this;
var item = frappe.get_doc(cdt, cdn);
@@ -843,9 +822,9 @@
frappe.run_serially([
() => me.frm.script_manager.trigger("currency"),
- () => me.update_item_tax_map(),
() => me.apply_default_taxes(),
- () => me.apply_pricing_rule()
+ () => me.apply_pricing_rule(),
+ () => me.calculate_taxes_and_totals()
]);
}
}
@@ -1824,7 +1803,6 @@
callback: function(r) {
if(!r.exc) {
item.item_tax_rate = r.message;
- me.add_taxes_from_item_tax_template(item.item_tax_rate);
me.calculate_taxes_and_totals();
}
}
@@ -1835,43 +1813,7 @@
}
},
- update_item_tax_map: function() {
- var me = this;
- var item_codes = [];
- $.each(this.frm.doc.items || [], function(i, item) {
- if(item.item_code) {
- item_codes.push(item.item_code);
- }
- });
- if(item_codes.length) {
- return this.frm.call({
- method: "erpnext.stock.get_item_details.get_item_tax_info",
- args: {
- company: me.frm.doc.company,
- tax_category: cstr(me.frm.doc.tax_category),
- item_codes: item_codes
- },
- callback: function(r) {
- if(!r.exc) {
- $.each(me.frm.doc.items || [], function(i, item) {
- if(item.item_code && r.message.hasOwnProperty(item.item_code)) {
- if (!item.item_tax_template) {
- item.item_tax_template = r.message[item.item_code].item_tax_template;
- item.item_tax_rate = r.message[item.item_code].item_tax_rate;
- }
- me.add_taxes_from_item_tax_template(item.item_tax_rate);
- } else {
- item.item_tax_template = "";
- item.item_tax_rate = "{}";
- }
- });
- me.calculate_taxes_and_totals();
- }
- }
- });
- }
- },
is_recurring: function() {
// set default values for recurring documents
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index dd81540..fbd30cf 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -122,6 +122,7 @@
self.validate_auto_reorder_enabled_in_stock_settings()
self.cant_change()
self.update_show_in_website()
+ self.validate_item_tax_net_rate_range()
if not self.is_new():
self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group")
@@ -485,6 +486,11 @@
if self.disabled:
self.show_in_website = False
+ def validate_item_tax_net_rate_range(self):
+ for tax in self.get('taxes'):
+ if flt(tax.maximum_net_rate) < flt(tax.minimum_net_rate):
+ frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate"))
+
def update_template_tables(self):
template = frappe.get_doc("Item", self.variant_of)
diff --git a/erpnext/stock/doctype/item_tax/item_tax.json b/erpnext/stock/doctype/item_tax/item_tax.json
index ae36efc..fb10096 100644
--- a/erpnext/stock/doctype/item_tax/item_tax.json
+++ b/erpnext/stock/doctype/item_tax/item_tax.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2013-02-22 01:28:01",
"doctype": "DocType",
"editable_grid": 1,
@@ -6,7 +7,9 @@
"field_order": [
"item_tax_template",
"tax_category",
- "valid_from"
+ "valid_from",
+ "minimum_net_rate",
+ "maximum_net_rate"
],
"fields": [
{
@@ -33,11 +36,24 @@
"fieldtype": "Date",
"in_list_view": 1,
"label": "Valid From"
+ },
+ {
+ "fieldname": "maximum_net_rate",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Maximum Net Rate"
+ },
+ {
+ "fieldname": "minimum_net_rate",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Minimum Net Rate"
}
],
"idx": 1,
"istable": 1,
- "modified": "2020-06-25 01:40:28.859752",
+ "links": [],
+ "modified": "2021-06-03 13:20:06.982303",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Tax",
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index d1dcdc2..746cbbf 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -436,18 +436,22 @@
return itemwise_barcode
@frappe.whitelist()
-def get_item_tax_info(company, tax_category, item_codes):
+def get_item_tax_info(company, tax_category, item_codes, item_rates=None):
out = {}
if isinstance(item_codes, string_types):
item_codes = json.loads(item_codes)
+ if isinstance(item_rates, string_types):
+ item_rates = json.loads(item_rates)
+
for item_code in item_codes:
- if not item_code or item_code in out:
+ if not item_code or item_code[1] in out:
continue
- out[item_code] = {}
- item = frappe.get_cached_doc("Item", item_code)
- get_item_tax_template({"company": company, "tax_category": tax_category}, item, out[item_code])
- out[item_code]["item_tax_rate"] = get_item_tax_map(company, out[item_code].get("item_tax_template"), as_json=True)
+ out[item_code[1]] = {}
+ item = frappe.get_cached_doc("Item", item_code[0])
+ args = {"company": company, "tax_category": tax_category, "net_rate": item_rates[item_code[1]]}
+ get_item_tax_template(args, item, out[item_code[1]])
+ out[item_code[1]]["item_tax_rate"] = get_item_tax_map(company, out[item_code[1]].get("item_tax_template"), as_json=True)
return out
@@ -478,12 +482,13 @@
for tax in taxes:
tax_company = frappe.get_value("Item Tax Template", tax.item_tax_template, 'company')
- if tax.valid_from and tax_company == args['company']:
+ if (tax.valid_from or tax.maximum_net_rate) and tax_company == args['company']:
# 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):
+ if getdate(tax.valid_from) <= getdate(validation_date) \
+ and is_within_valid_range(args, tax):
taxes_with_validity.append(tax)
else:
if tax_company == args['company']:
@@ -502,12 +507,25 @@
if not taxes_with_validity and (not taxes_with_no_validity):
return None
+ # do not change if already a valid template
+ if args.get('item_tax_template') in taxes:
+ return args.get('item_tax_template')
+
for tax in taxes:
if cstr(tax.tax_category) == cstr(args.get("tax_category")):
out["item_tax_template"] = tax.item_tax_template
return tax.item_tax_template
return None
+def is_within_valid_range(args, tax):
+ if not flt(tax.maximum_net_rate):
+ # No range specified, just ignore
+ return True
+ elif flt(tax.minimum_net_rate) <= flt(args.get('net_rate')) <= flt(tax.maximum_net_rate):
+ return True
+
+ return False
+
@frappe.whitelist()
def get_item_tax_map(company, item_tax_template, as_json=True):
item_tax_map = {}