diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 800e6a91b81..90b36e06883 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -13,6 +13,7 @@ from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError from frappe.model.naming import make_autoname from erpnext.accounts.doctype.account.test_account import get_inventory_account +from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data class TestSalesInvoice(unittest.TestCase): def make(self): @@ -1105,10 +1106,75 @@ class TestSalesInvoice(unittest.TestCase): for i, k in enumerate(expected_values["keys"]): self.assertEquals(d.get(k), expected_values[d.item_code][i]) - def test_item_wise_tax_breakup(self): + def test_item_wise_tax_breakup_india(self): + frappe.flags.country = "India" + + si = self.create_si_to_test_tax_breakup() + itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si) + + expected_itemised_tax = { + "999800": { + "Service Tax": { + "tax_rate": 10.0, + "tax_amount": 1500.0 + } + } + } + expected_itemised_taxable_amount = { + "999800": 15000.0 + } + + self.assertEqual(itemised_tax, expected_itemised_tax) + self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount) + + frappe.flags.country = None + + def test_item_wise_tax_breakup_outside_india(self): + frappe.flags.country = "United States" + + si = self.create_si_to_test_tax_breakup() + + itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si) + + expected_itemised_tax = { + "_Test Item": { + "Service Tax": { + "tax_rate": 10.0, + "tax_amount": 1000.0 + } + }, + "_Test Item 2": { + "Service Tax": { + "tax_rate": 10.0, + "tax_amount": 500.0 + } + } + } + expected_itemised_taxable_amount = { + "_Test Item": 10000.0, + "_Test Item 2": 5000.0 + } + + self.assertEqual(itemised_tax, expected_itemised_tax) + self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount) + + frappe.flags.country = None + + def create_si_to_test_tax_breakup(self): si = create_sales_invoice(qty=100, rate=50, do_not_save=True) si.append("items", { "item_code": "_Test Item", + "gst_hsn_code": "999800", + "warehouse": "_Test Warehouse - _TC", + "qty": 100, + "rate": 50, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC" + }) + si.append("items", { + "item_code": "_Test Item 2", + "gst_hsn_code": "999800", "warehouse": "_Test Warehouse - _TC", "qty": 100, "rate": 50, @@ -1125,11 +1191,7 @@ class TestSalesInvoice(unittest.TestCase): "rate": 10 }) si.insert() - - tax_breakup_html = '''\n
\n\t\n\t\t\n\t\t\n\t
Item NameTaxable Amount_Test Account Service Tax - _TC
_Test Item\u20b9 10,000.00(10.0%) \u20b9 1,000.00
\n
''' - - self.assertEqual(si.other_charges_calculation, tax_breakup_html) - + return si def create_sales_invoice(**args): si = frappe.new_doc("Sales Invoice") @@ -1150,6 +1212,7 @@ def create_sales_invoice(**args): si.append("items", { "item_code": args.item or args.item_code or "_Test Item", + "gst_hsn_code": "999800", "warehouse": args.warehouse or "_Test Warehouse - _TC", "qty": args.qty or 1, "rate": args.rate or 100, diff --git a/erpnext/accounts/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js index 506427e240f..a5f9b3c2863 100644 --- a/erpnext/accounts/page/pos/pos.js +++ b/erpnext/accounts/page/pos/pos.js @@ -1398,10 +1398,6 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({ return erpnext.get_currency(this.frm.doc.company); }, - show_item_wise_taxes: function () { - return null; - }, - show_items_in_item_cart: function () { var me = this; var $items = this.wrapper.find(".items").empty(); diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 8b96152bb2f..f1e95ec0be7 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import json import frappe, erpnext from frappe import _, scrub -from frappe.utils import cint, flt, cstr, fmt_money, 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, \ validate_taxes_and_charges, validate_inclusive_tax @@ -509,108 +509,72 @@ class calculate_taxes_and_totals(object): return rate_with_margin def set_item_wise_tax_breakup(self): - item_tax = {} - tax_accounts = [] - company_currency = erpnext.get_company_currency(self.doc.company) + if not self.doc.taxes: + return + frappe.flags.company = self.doc.company - item_tax, tax_accounts = self.get_item_tax(item_tax, tax_accounts, company_currency) + # get headers + tax_accounts = list(set([d.description for d in self.doc.taxes])) + headers = get_itemised_tax_breakup_header(self.doc.doctype + " Item", tax_accounts) - headings = get_table_column_headings(tax_accounts) + # get tax breakup data + itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(self.doc) - distinct_items, taxable_amount = self.get_distinct_items() + frappe.flags.company = None - rows = get_table_rows(distinct_items, item_tax, tax_accounts, company_currency, taxable_amount) - - if not rows: - self.doc.other_charges_calculation = "" - else: - self.doc.other_charges_calculation = ''' -
- - {headings} - {rows} -
-
'''.format(**{ - "headings": "".join(headings), - "rows": "".join(rows) -}) + self.doc.other_charges_calculation = frappe.render_template( + "templates/includes/itemised_tax_breakup.html", dict( + headers=headers, + itemised_tax=itemised_tax, + itemised_taxable_amount=itemised_taxable_amount, + tax_accounts=tax_accounts, + company_currency=erpnext.get_company_currency(self.doc.company) + ) + ) - def get_item_tax(self, item_tax, tax_accounts, company_currency): - for tax in self.doc.taxes: - tax_amount_precision = tax.precision("tax_amount") - tax_rate_precision = tax.precision("rate"); +@erpnext.allow_regional +def get_itemised_tax_breakup_header(item_doctype, tax_accounts): + return [_("Item"), _("Taxable Amount")] + tax_accounts + +@erpnext.allow_regional +def get_itemised_tax_breakup_data(doc): + itemised_tax = get_itemised_tax(doc.taxes) + + itemised_taxable_amount = get_itemised_taxable_amount(doc.items) + + return itemised_tax, itemised_taxable_amount + +def get_itemised_tax(taxes): + itemised_tax = {} + for tax in taxes: + tax_amount_precision = tax.precision("tax_amount") + tax_rate_precision = tax.precision("rate") + + item_tax_map = json.loads(tax.item_wise_tax_detail) if tax.item_wise_tax_detail else {} + + for item_code, tax_data in item_tax_map.items(): + itemised_tax.setdefault(item_code, frappe._dict()) - item_tax_map = self._load_item_tax_rate(tax.item_wise_tax_detail) - for item_code, tax_data in item_tax_map.items(): - if not item_tax.get(item_code): - item_tax[item_code] = {} - - if isinstance(tax_data, list): - tax_rate = "" - if tax_data[0]: - if tax.charge_type == "Actual": - tax_rate = fmt_money(flt(tax_data[0], tax_amount_precision), - tax_amount_precision, company_currency) - else: - tax_rate = cstr(flt(tax_data[0], tax_rate_precision)) + "%" - - tax_amount = fmt_money(flt(tax_data[1], tax_amount_precision), - tax_amount_precision, company_currency) - - item_tax[item_code][tax.name] = [tax_rate, tax_amount] - else: - item_tax[item_code][tax.name] = [cstr(flt(tax_data, tax_rate_precision)) + "%", "0.00"] - tax_accounts.append([tax.name, tax.account_head]) - - return item_tax, tax_accounts - - - def get_distinct_items(self): - distinct_item_names = [] - distinct_items = [] - taxable_amount = {} - for item in self.doc.items: - item_code = item.item_code or item.item_name - if item_code not in distinct_item_names: - distinct_item_names.append(item_code) - distinct_items.append(item) - taxable_amount[item_code] = item.net_amount - else: - taxable_amount[item_code] = taxable_amount.get(item_code, 0) + item.net_amount + if isinstance(tax_data, list) and tax_data[0]: + precision = tax_amount_precision if tax.charge_type == "Actual" else tax_rate_precision - return distinct_items, taxable_amount - -def get_table_column_headings(tax_accounts): - headings_name = [_("Item Name"), _("Taxable Amount")] + [d[1] for d in tax_accounts] - headings = [] - for head in headings_name: - if head == _("Item Name"): - headings.append('' + (head or "") + "") - else: - headings.append('' + (head or "") + "") - - return headings - -def get_table_rows(distinct_items, item_tax, tax_accounts, company_currency, taxable_amount): - rows = [] - for item in distinct_items: - item_tax_record = item_tax.get(item.item_code or item.item_name) - if not item_tax_record: - continue - - taxes = [] - for head in tax_accounts: - if item_tax_record[head[0]]: - taxes.append("(" + item_tax_record[head[0]][0] + ") " - + item_tax_record[head[0]][1] + "") + itemised_tax[item_code][tax.description] = frappe._dict(dict( + tax_rate=flt(tax_data[0], precision), + tax_amount=flt(tax_data[1], tax_amount_precision) + )) else: - taxes.append("") + itemised_tax[item_code][tax.description] = frappe._dict(dict( + tax_rate=flt(tax_data, tax_rate_precision), + tax_amount=0.0 + )) + return itemised_tax + +def get_itemised_taxable_amount(items): + itemised_taxable_amount = frappe._dict() + for item in items: item_code = item.item_code or item.item_name - rows.append("{item_name}{taxable_amount}{taxes}".format(**{ - "item_name": item.item_name, - "taxable_amount": fmt_money(taxable_amount.get(item_code, 0), item.precision("net_amount"), company_currency), - "taxes": "".join(taxes) - })) - - return rows \ No newline at end of file + itemised_taxable_amount.setdefault(item_code, 0) + itemised_taxable_amount[item_code] += item.net_amount + + return itemised_taxable_amount \ No newline at end of file diff --git a/erpnext/docs/assets/img/regional/india/sample-gst-tax-invoice.png b/erpnext/docs/assets/img/regional/india/sample-gst-tax-invoice.png index cb657248178..654351880a5 100644 Binary files a/erpnext/docs/assets/img/regional/india/sample-gst-tax-invoice.png and b/erpnext/docs/assets/img/regional/india/sample-gst-tax-invoice.png differ diff --git a/erpnext/docs/license.html b/erpnext/docs/license.html index 4740c5c1455..1d50b78b305 100644 --- a/erpnext/docs/license.html +++ b/erpnext/docs/license.html @@ -640,8 +640,8 @@ attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.

-
    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
+
    <one line="" to="" give="" the="" program's="" name="" and="" a="" brief="" idea="" of="" what="" it="" does.="">
+    Copyright (C) <year>  <name of="" author="">
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index fedc6d5caed..173531d94b7 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -209,6 +209,8 @@ payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account
 
 regional_overrides = {
 	'India': {
-		'erpnext.tests.test_regional.test_method': 'erpnext.regional.india.utils.test_method'
+		'erpnext.tests.test_regional.test_method': 'erpnext.regional.india.utils.test_method',
+		'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_header': 'erpnext.regional.india.utils.get_itemised_tax_breakup_header',
+		'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_data': 'erpnext.regional.india.utils.get_itemised_tax_breakup_data'
 	}
 }
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 837097b4907..3a010c60229 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -54,7 +54,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
 		this.manipulate_grand_total_for_inclusive_tax();
 		this.calculate_totals();
 		this._cleanup();
-		this.show_item_wise_taxes();
 	},
 
 	validate_conversion_rate: function() {
@@ -634,99 +633,5 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
 		}
 		
 		this.calculate_outstanding_amount(false)
-	},
-	
-	show_item_wise_taxes: function() {
-		if(this.frm.fields_dict.other_charges_calculation) {
-			this.frm.toggle_display("other_charges_calculation", this.frm.doc.other_charges_calculation);
-		}
-	},
-	
-	set_item_wise_tax_breakup: function() {
-		if(this.frm.fields_dict.other_charges_calculation) {
-			var html = this.get_item_wise_taxes_html();
-			// console.log(html);
-			this.frm.set_value("other_charges_calculation", html);
-			this.show_item_wise_taxes();
-		}
-	},
-		
-	get_item_wise_taxes_html: function() {
-		var item_tax = {};
-		var tax_accounts = [];
-		var company_currency = this.get_company_currency();
-
-		$.each(this.frm.doc["taxes"] || [], function(i, tax) {
-			var tax_amount_precision = precision("tax_amount", tax);
-			var tax_rate_precision = precision("rate", tax);
-			$.each(JSON.parse(tax.item_wise_tax_detail || '{}'),
-				function(item_code, tax_data) {
-					if(!item_tax[item_code]) item_tax[item_code] = {};
-					if($.isArray(tax_data)) {
-						var tax_rate = "";
-						if(tax_data[0] != null) {
-							tax_rate = (tax.charge_type === "Actual") ?
-								format_currency(flt(tax_data[0], tax_amount_precision),
-									company_currency, tax_amount_precision) :
-								(flt(tax_data[0], tax_rate_precision) + "%");
-						}
-						var tax_amount = format_currency(flt(tax_data[1], tax_amount_precision),
-							company_currency, tax_amount_precision);
-
-						item_tax[item_code][tax.name] = [tax_rate, tax_amount];
-					} else {
-						item_tax[item_code][tax.name] = [flt(tax_data, tax_rate_precision) + "%", "0.00"];
-					}
-				});
-			tax_accounts.push([tax.name, tax.account_head]);
-		});
-		
-		var headings = $.map([__("Item Name"), __("Taxable Amount")].concat($.map(tax_accounts, 
-			function(head) { return head[1]; })), function(head) {
-				if(head==__("Item Name")) {
-					return '' + (head || "") + "";
-				} else {
-					return '' + (head || "") + "";
-				}	
-			}
-		).join("");
-
-		var distinct_item_names = [];
-		var distinct_items = [];
-		var taxable_amount = {};
-		$.each(this.frm.doc["items"] || [], function(i, item) {
-			var item_code = item.item_code || item.item_name;
-			if(distinct_item_names.indexOf(item_code)===-1) {
-				distinct_item_names.push(item_code);
-				distinct_items.push(item);
-				taxable_amount[item_code] = item.net_amount;
-			} else {
-				taxable_amount[item_code] = taxable_amount[item_code] + item.net_amount;
-			}
-		});
-
-		var rows = $.map(distinct_items, function(item) {
-			var item_code = item.item_code || item.item_name;
-			var item_tax_record = item_tax[item_code];
-			if(!item_tax_record) { return null; }
-
-			return repl("%(item_name)s%(taxable_amount)s%(taxes)s", {
-				item_name: item.item_name,
-				taxable_amount: format_currency(taxable_amount[item_code],
-					company_currency, precision("net_amount", item)),
-				taxes: $.map(tax_accounts, function(head) {
-					return item_tax_record[head[0]] ?
-						"(" + item_tax_record[head[0]][0] + ") " + item_tax_record[head[0]][1] + "" :
-						"";
-				}).join("")
-			});
-		}).join("");
-
-		if(!rows) return "";
-		return '
\ - \ - ' + headings + ' \ - ' + rows + ' \ -
'; } }) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 9ed1de22f76..1e8353bc925 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -210,7 +210,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ refresh: function() { erpnext.toggle_naming_series(); erpnext.hide_company(); - this.show_item_wise_taxes(); this.set_dynamic_labels(); this.setup_sms(); }, diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index c35ff0a6f00..0369487e1af 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -80,7 +80,7 @@ def add_print_formats(): def make_custom_fields(): hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', - fieldtype='Data', options='item_code.gst_hsn_code', insert_after='description') + fieldtype='Data', options='item_code.gst_hsn_code', insert_after='description', print_hide=1) custom_fields = { 'Address': [ diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 437465a6a26..8f2dacda707 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -1,6 +1,7 @@ import frappe, re from frappe import _ from erpnext.regional.india import states, state_numbers +from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount def validate_gstin_for_india(doc, method): if not hasattr(doc, 'gstin'): @@ -23,6 +24,42 @@ def validate_gstin_for_india(doc, method): frappe.throw(_("First 2 digits of GSTIN should match with State number {0}") .format(doc.gst_state_number)) +def get_itemised_tax_breakup_header(item_doctype, tax_accounts): + if frappe.get_meta(item_doctype).has_field('gst_hsn_code'): + return [_("HSN/SAC"), _("Taxable Amount")] + tax_accounts + else: + return [_("Item"), _("Taxable Amount")] + tax_accounts + +def get_itemised_tax_breakup_data(doc): + itemised_tax = get_itemised_tax(doc.taxes) + + itemised_taxable_amount = get_itemised_taxable_amount(doc.items) + + if not frappe.get_meta(doc.doctype + " Item").has_field('gst_hsn_code'): + return itemised_tax, itemised_taxable_amount + + item_hsn_map = frappe._dict() + for d in doc.items: + item_hsn_map.setdefault(d.item_code or d.item_name, d.get("gst_hsn_code")) + + hsn_tax = {} + for item, taxes in itemised_tax.items(): + hsn_code = item_hsn_map.get(item) + hsn_tax.setdefault(hsn_code, frappe._dict()) + for tax_account, tax_detail in taxes.items(): + hsn_tax[hsn_code].setdefault(tax_account, {"tax_rate": 0, "tax_amount": 0}) + hsn_tax[hsn_code][tax_account]["tax_rate"] = tax_detail.get("tax_rate") + hsn_tax[hsn_code][tax_account]["tax_amount"] += tax_detail.get("tax_amount") + + # set taxable amount + hsn_taxable_amount = frappe._dict() + for item, taxable_amount in itemised_taxable_amount.items(): + hsn_code = item_hsn_map.get(item) + hsn_taxable_amount.setdefault(hsn_code, 0) + hsn_taxable_amount[hsn_code] += itemised_taxable_amount.get(item) + + return hsn_tax, hsn_taxable_amount + # don't remove this function it is used in tests def test_method(): '''test function''' diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json index 2a1520e8ca3..7c5ad4fb0d6 100644 --- a/erpnext/stock/doctype/item/test_records.json +++ b/erpnext/stock/doctype/item/test_records.json @@ -15,6 +15,7 @@ "item_group": "_Test Item Group", "item_name": "_Test Item", "apply_warehouse_wise_reorder_level": 1, + "gst_hsn_code": "999800", "valuation_rate": 100, "reorder_levels": [ { diff --git a/erpnext/templates/includes/itemised_tax_breakup.html b/erpnext/templates/includes/itemised_tax_breakup.html new file mode 100644 index 00000000000..342ce6b292a --- /dev/null +++ b/erpnext/templates/includes/itemised_tax_breakup.html @@ -0,0 +1,38 @@ +
+ + + + {% set i = 0 %} + {% for key in headers %} + {% if i==0 %} + + {% else %} + + {% endif %} + {% set i = i + 1 %} + {% endfor%} + + + + {% for item, taxes in itemised_tax.items() %} + + + + {% for tax_account in tax_accounts %} + {% set tax_details = taxes.get(tax_account) %} + {% if tax_details %} + + {% else %} + + {% endif %} + {% endfor %} + + {% endfor %} + +
{{ key }}{{ key }}
{{ item }} + {{ frappe.utils.fmt_money(itemised_taxable_amount.get(item), None, company_currency) }} + + ({{ tax_details.tax_rate }}) + {{ frappe.utils.fmt_money(tax_details.tax_amount, None, company_currency) }} +
+
\ No newline at end of file