Show hsn code in tax breakup for India and render via template (#9866)
* Show hsn code in tax breakup for India and render via template
* tax breakup if gst_tax_field does not exists
* Fixed tax-breakup test cases
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 800e6a9..90b36e0 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.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 @@
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 @@
"rate": 10
})
si.insert()
-
- tax_breakup_html = '''\n<div class="tax-break-up" style="overflow-x: auto;">\n\t<table class="table table-bordered table-hover">\n\t\t<thead><tr><th class="text-left" style="min-width: 120px;">Item Name</th><th class="text-right" style="min-width: 80px;">Taxable Amount</th><th class="text-right" style="min-width: 80px;">_Test Account Service Tax - _TC</th></tr></thead>\n\t\t<tbody><tr><td>_Test Item</td><td class="text-right">\u20b9 10,000.00</td><td class="text-right">(10.0%) \u20b9 1,000.00</td></tr></tbody>\n\t</table>\n</div>'''
-
- 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 @@
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 506427e..a5f9b3c 100644
--- a/erpnext/accounts/page/pos/pos.js
+++ b/erpnext/accounts/page/pos/pos.js
@@ -1398,10 +1398,6 @@
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 8b96152..f1e95ec 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -5,7 +5,7 @@
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 @@
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 = '''
-<div class="tax-break-up" style="overflow-x: auto;">
- <table class="table table-bordered table-hover">
- <thead><tr>{headings}</tr></thead>
- <tbody>{rows}</tbody>
- </table>
-</div>'''.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('<th style="min-width: 120px;" class="text-left">' + (head or "") + "</th>")
- else:
- headings.append('<th style="min-width: 80px;" class="text-right">' + (head or "") + "</th>")
-
- 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("<td class='text-right'>(" + item_tax_record[head[0]][0] + ") "
- + item_tax_record[head[0]][1] + "</td>")
+ 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("<td></td>")
+ 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("<tr><td>{item_name}</td><td class='text-right'>{taxable_amount}</td>{taxes}</tr>".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 cb65724..6543518 100644
--- a/erpnext/docs/assets/img/regional/india/sample-gst-tax-invoice.png
+++ b/erpnext/docs/assets/img/regional/india/sample-gst-tax-invoice.png
Binary files differ
diff --git a/erpnext/docs/license.html b/erpnext/docs/license.html
index 4740c5c..1d50b78 100644
--- a/erpnext/docs/license.html
+++ b/erpnext/docs/license.html
@@ -640,8 +640,8 @@
the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.</p>
-<pre><code> <one line to give the program's name and a brief idea of what it does.>
- Copyright (C) <year> <name of author>
+<pre><code> <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 fedc6d5..173531d 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -209,6 +209,8 @@
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 837097b..3a010c6 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -54,7 +54,6 @@
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 @@
}
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 '<th style="min-width: 100px;" class="text-left">' + (head || "") + "</th>";
- } else {
- return '<th style="min-width: 80px;" class="text-right">' + (head || "") + "</th>";
- }
- }
- ).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("<tr><td>%(item_name)s</td><td class='text-right'>%(taxable_amount)s</td>%(taxes)s</tr>", {
- 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]] ?
- "<td class='text-right'>(" + item_tax_record[head[0]][0] + ") " + item_tax_record[head[0]][1] + "</td>" :
- "<td></td>";
- }).join("")
- });
- }).join("");
-
- if(!rows) return "";
- return '<div class="tax-break-up" style="overflow-x: auto;">\
- <table class="table table-bordered table-hover">\
- <thead><tr>' + headings + '</tr></thead> \
- <tbody>' + rows + '</tbody> \
- </table></div>';
}
})
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 9ed1de2..1e8353b 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -210,7 +210,6 @@
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 c35ff0a..0369487 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -80,7 +80,7 @@
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 437465a..8f2dacd 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 @@
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 2a1520e..7c5ad4f 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 0000000..342ce6b
--- /dev/null
+++ b/erpnext/templates/includes/itemised_tax_breakup.html
@@ -0,0 +1,38 @@
+<div class="tax-break-up" style="overflow-x: auto;">
+ <table class="table table-bordered table-hover">
+ <thead>
+ <tr>
+ {% set i = 0 %}
+ {% for key in headers %}
+ {% if i==0 %}
+ <th style="min-width: 120px;" class="text-left">{{ key }}</th>
+ {% else %}
+ <th style="min-width: 80px;" class="text-right">{{ key }}</th>
+ {% endif %}
+ {% set i = i + 1 %}
+ {% endfor%}
+ </tr>
+ </thead>
+ <tbody>
+ {% for item, taxes in itemised_tax.items() %}
+ <tr>
+ <td>{{ item }}</td>
+ <td class='text-right'>
+ {{ frappe.utils.fmt_money(itemised_taxable_amount.get(item), None, company_currency) }}
+ </td>
+ {% for tax_account in tax_accounts %}
+ {% set tax_details = taxes.get(tax_account) %}
+ {% if tax_details %}
+ <td class='text-right'>
+ ({{ tax_details.tax_rate }})
+ {{ frappe.utils.fmt_money(tax_details.tax_amount, None, company_currency) }}
+ </td>
+ {% else %}
+ <td></td>
+ {% endif %}
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+</div>
\ No newline at end of file