mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-02 19:59:12 +00:00
feat(regional): Auto state wise taxation for GST India (#19877)
* feat(regional): Auto state wise taxation for GST India * fix: Update gst category on addition of GSTIN * fix: Codacy and travis fixes * fix: Travis * fix(test): Update GST category only if GSTIN field available * fix: Test Cases * fix: Do not skip accounts if place of supply is not present * fix: Auto GST taxation for SEZ Party types * fix: Automatic taxation for multi state * fix: Codacy and travis fixes * fix: Auto GST template selection in Sales Order * fix: Move inter state check and source state to tax category * fix: Remove unique check from tax template * fix: Remove unique check from tax template * fix: Address fetching logic in Sales * fix: fecth tax template on company address change * fix: fetch company gstin on address change * fix: company_gstin set value fix * fix: Mutiple fixes and code refactor * fix: Add missing semicolon * fix: Company address fetching in sales invoice * fix: Remove print statement * fix: Import functools * fix: Naming fixes and code cleanup * fix: Update patches * fix: Remove changes in patches.txt * fix: Iteritems compatibility for python 3
This commit is contained in:
@@ -64,7 +64,8 @@ class TestGSTR3BReport(unittest.TestCase):
|
||||
self.assertEqual(output["inter_sup"]["unreg_details"][0]["iamt"], 18),
|
||||
self.assertEqual(output["sup_details"]["osup_nil_exmp"]["txval"], 100),
|
||||
self.assertEqual(output["inward_sup"]["isup_details"][0]["inter"], 250)
|
||||
self.assertEqual(output["itc_elg"]["itc_avl"][4]["iamt"], 45)
|
||||
self.assertEqual(output["itc_elg"]["itc_avl"][4]["samt"], 22.50)
|
||||
self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50)
|
||||
|
||||
def make_sales_invoice():
|
||||
si = create_sales_invoice(company="_Test Company GST",
|
||||
@@ -158,10 +159,18 @@ def create_purchase_invoices():
|
||||
|
||||
pi.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "IGST - _GST",
|
||||
"account_head": "CGST - _GST",
|
||||
"cost_center": "Main - _GST",
|
||||
"description": "IGST @ 18.0",
|
||||
"rate": 18
|
||||
"description": "CGST @ 9.0",
|
||||
"rate": 9
|
||||
})
|
||||
|
||||
pi.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "SGST - _GST",
|
||||
"cost_center": "Main - _GST",
|
||||
"description": "SGST @ 9.0",
|
||||
"rate": 9
|
||||
})
|
||||
|
||||
pi.submit()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
from six import iteritems
|
||||
|
||||
states = [
|
||||
'',
|
||||
@@ -79,4 +80,6 @@ state_numbers = {
|
||||
"Uttar Pradesh": "09",
|
||||
"Uttarakhand": "05",
|
||||
"West Bengal": "19",
|
||||
}
|
||||
}
|
||||
|
||||
number_state_mapping = {v: k for k, v in iteritems(state_numbers)}
|
||||
@@ -107,7 +107,12 @@ def make_custom_fields(update=True):
|
||||
dict(fieldname='gst_category', label='GST Category',
|
||||
fieldtype='Select', insert_after='gst_section', print_hide=1,
|
||||
options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders',
|
||||
fetch_from='supplier.gst_category', fetch_if_empty=1)
|
||||
fetch_from='supplier.gst_category', fetch_if_empty=1),
|
||||
dict(fieldname='export_type', label='Export Type',
|
||||
fieldtype='Select', insert_after='gst_category', print_hide=1,
|
||||
depends_on='eval:in_list(["SEZ", "Overseas"], doc.gst_category)',
|
||||
options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='supplier.export_type',
|
||||
fetch_if_empty=1),
|
||||
]
|
||||
|
||||
sales_invoice_gst_category = [
|
||||
@@ -116,20 +121,21 @@ def make_custom_fields(update=True):
|
||||
dict(fieldname='gst_category', label='GST Category',
|
||||
fieldtype='Select', insert_after='gst_section', print_hide=1,
|
||||
options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders',
|
||||
fetch_from='customer.gst_category', fetch_if_empty=1)
|
||||
fetch_from='customer.gst_category', fetch_if_empty=1),
|
||||
dict(fieldname='export_type', label='Export Type',
|
||||
fieldtype='Select', insert_after='gst_category', print_hide=1,
|
||||
depends_on='eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)',
|
||||
options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='customer.export_type',
|
||||
fetch_if_empty=1),
|
||||
]
|
||||
|
||||
invoice_gst_fields = [
|
||||
dict(fieldname='invoice_copy', label='Invoice Copy',
|
||||
fieldtype='Select', insert_after='gst_category', print_hide=1, allow_on_submit=1,
|
||||
fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1,
|
||||
options='Original for Recipient\nDuplicate for Transporter\nDuplicate for Supplier\nTriplicate for Supplier'),
|
||||
dict(fieldname='reverse_charge', label='Reverse Charge',
|
||||
fieldtype='Select', insert_after='invoice_copy', print_hide=1,
|
||||
options='Y\nN', default='N'),
|
||||
dict(fieldname='export_type', label='Export Type',
|
||||
fieldtype='Select', insert_after='reverse_charge', print_hide=1,
|
||||
depends_on='eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)',
|
||||
options='\nWith Payment of Tax\nWithout Payment of Tax'),
|
||||
dict(fieldname='ecommerce_gstin', label='E-commerce GSTIN',
|
||||
fieldtype='Data', insert_after='export_type', print_hide=1),
|
||||
dict(fieldname='gst_col_break', fieldtype='Column Break', insert_after='ecommerce_gstin'),
|
||||
@@ -142,13 +148,13 @@ def make_custom_fields(update=True):
|
||||
purchase_invoice_gst_fields = [
|
||||
dict(fieldname='supplier_gstin', label='Supplier GSTIN',
|
||||
fieldtype='Data', insert_after='supplier_address',
|
||||
fetch_from='supplier_address.gstin', print_hide=1),
|
||||
fetch_from='supplier_address.gstin', print_hide=1, read_only=1),
|
||||
dict(fieldname='company_gstin', label='Company GSTIN',
|
||||
fieldtype='Data', insert_after='shipping_address_display',
|
||||
fetch_from='shipping_address.gstin', print_hide=1),
|
||||
fetch_from='shipping_address.gstin', print_hide=1, read_only=1),
|
||||
dict(fieldname='place_of_supply', label='Place of Supply',
|
||||
fieldtype='Data', insert_after='shipping_address',
|
||||
print_hide=1, read_only=0),
|
||||
print_hide=1, read_only=1),
|
||||
]
|
||||
|
||||
purchase_invoice_itc_fields = [
|
||||
@@ -167,17 +173,17 @@ def make_custom_fields(update=True):
|
||||
|
||||
sales_invoice_gst_fields = [
|
||||
dict(fieldname='billing_address_gstin', label='Billing Address GSTIN',
|
||||
fieldtype='Data', insert_after='customer_address',
|
||||
fieldtype='Data', insert_after='customer_address', read_only=1,
|
||||
fetch_from='customer_address.gstin', print_hide=1),
|
||||
dict(fieldname='customer_gstin', label='Customer GSTIN',
|
||||
fieldtype='Data', insert_after='shipping_address_name',
|
||||
fetch_from='shipping_address_name.gstin', print_hide=1),
|
||||
dict(fieldname='place_of_supply', label='Place of Supply',
|
||||
fieldtype='Data', insert_after='customer_gstin',
|
||||
print_hide=1, read_only=0),
|
||||
print_hide=1, read_only=1),
|
||||
dict(fieldname='company_gstin', label='Company GSTIN',
|
||||
fieldtype='Data', insert_after='company_address',
|
||||
fetch_from='company_address.gstin', print_hide=1),
|
||||
fetch_from='company_address.gstin', print_hide=1, read_only=1),
|
||||
]
|
||||
|
||||
sales_invoice_shipping_fields = [
|
||||
@@ -194,7 +200,11 @@ def make_custom_fields(update=True):
|
||||
|
||||
inter_state_gst_field = [
|
||||
dict(fieldname='is_inter_state', label='Is Inter State',
|
||||
fieldtype='Check', insert_after='disabled', print_hide=1)
|
||||
fieldtype='Check', insert_after='disabled', print_hide=1),
|
||||
dict(fieldname='tax_category_column_break', fieldtype='Column Break',
|
||||
insert_after='is_inter_state'),
|
||||
dict(fieldname='gst_state', label='Source State', fieldtype='Select',
|
||||
options='\n'.join(states), insert_after='company')
|
||||
]
|
||||
|
||||
ewaybill_fields = [
|
||||
@@ -374,8 +384,7 @@ def make_custom_fields(update=True):
|
||||
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields,
|
||||
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields,
|
||||
'Sales Order': sales_invoice_gst_fields,
|
||||
'Sales Taxes and Charges Template': inter_state_gst_field,
|
||||
'Purchase Taxes and Charges Template': inter_state_gst_field,
|
||||
'Tax Category': inter_state_gst_field,
|
||||
'Item': [
|
||||
dict(fieldname='gst_hsn_code', label='HSN/SAC',
|
||||
fieldtype='Link', options='GST HSN Code', insert_after='item_group'),
|
||||
@@ -459,6 +468,15 @@ def make_custom_fields(update=True):
|
||||
'insert_after': 'gst_transporter_id',
|
||||
'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders',
|
||||
'default': 'Unregistered'
|
||||
},
|
||||
{
|
||||
'fieldname': 'export_type',
|
||||
'label': 'Export Type',
|
||||
'fieldtype': 'Select',
|
||||
'insert_after': 'gst_category',
|
||||
'default': 'Without Payment of Tax',
|
||||
'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)',
|
||||
'options': '\nWith Payment of Tax\nWithout Payment of Tax'
|
||||
}
|
||||
],
|
||||
'Customer': [
|
||||
@@ -469,6 +487,15 @@ def make_custom_fields(update=True):
|
||||
'insert_after': 'customer_type',
|
||||
'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders',
|
||||
'default': 'Unregistered'
|
||||
},
|
||||
{
|
||||
'fieldname': 'export_type',
|
||||
'label': 'Export Type',
|
||||
'fieldtype': 'Select',
|
||||
'insert_after': 'gst_category',
|
||||
'default': 'Without Payment of Tax',
|
||||
'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)',
|
||||
'options': '\nWith Payment of Tax\nWithout Payment of Tax'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
41
erpnext/regional/india/taxes.js
Normal file
41
erpnext/regional/india/taxes.js
Normal file
@@ -0,0 +1,41 @@
|
||||
erpnext.setup_auto_gst_taxation = (doctype) => {
|
||||
frappe.ui.form.on(doctype, {
|
||||
company_address: function(frm) {
|
||||
frm.trigger('get_tax_template');
|
||||
},
|
||||
shipping_address: function(frm) {
|
||||
frm.trigger('get_tax_template');
|
||||
},
|
||||
tax_category: function(frm) {
|
||||
frm.trigger('get_tax_template');
|
||||
},
|
||||
get_tax_template: function(frm) {
|
||||
let party_details = {
|
||||
'shipping_address': frm.doc.shipping_address || '',
|
||||
'shipping_address_name': frm.doc.shipping_address_name || '',
|
||||
'customer_address': frm.doc.customer_address || '',
|
||||
'customer': frm.doc.customer,
|
||||
'supplier': frm.doc.supplier,
|
||||
'supplier_gstin': frm.doc.supplier_gstin,
|
||||
'company_gstin': frm.doc.company_gstin,
|
||||
'tax_category': frm.doc.tax_category
|
||||
};
|
||||
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.utils.get_regional_address_details',
|
||||
args: {
|
||||
party_details: JSON.stringify(party_details),
|
||||
doctype: frm.doc.doctype,
|
||||
company: frm.doc.company,
|
||||
return_taxes: 1
|
||||
},
|
||||
callback: function(r) {
|
||||
if(r.message) {
|
||||
frm.set_value('taxes_and_charges', r.message.taxes_and_charges);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_
|
||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||
from erpnext.hr.utils import get_salary_assignment
|
||||
from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip
|
||||
from erpnext.regional.india import number_state_mapping
|
||||
from six import string_types
|
||||
|
||||
def validate_gstin_for_india(doc, method):
|
||||
if hasattr(doc, 'gst_state') and doc.gst_state:
|
||||
@@ -46,6 +48,14 @@ def validate_gstin_for_india(doc, method):
|
||||
frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
|
||||
.format(doc.gst_state_number))
|
||||
|
||||
def update_gst_category(doc, method):
|
||||
for link in doc.links:
|
||||
if link.link_doctype in ['Customer', 'Supplier']:
|
||||
if doc.get('gstin'):
|
||||
frappe.db.sql("""
|
||||
UPDATE `tab{0}` SET gst_category = %s WHERE name = %s AND gst_category = 'Unregistered'
|
||||
""".format(link.link_doctype), ("Registered Regular", link.link_name)) #nosec
|
||||
|
||||
def set_gst_state_and_state_number(doc):
|
||||
if not doc.gst_state:
|
||||
if not doc.state:
|
||||
@@ -122,44 +132,106 @@ def test_method():
|
||||
'''test function'''
|
||||
return 'overridden'
|
||||
|
||||
def get_place_of_supply(out, doctype):
|
||||
def get_place_of_supply(party_details, doctype):
|
||||
if not frappe.get_meta('Address').has_field('gst_state'): return
|
||||
|
||||
if doctype in ("Sales Invoice", "Delivery Note"):
|
||||
address_name = out.shipping_address_name or out.customer_address
|
||||
elif doctype == "Purchase Invoice":
|
||||
address_name = out.shipping_address or out.supplier_address
|
||||
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
|
||||
address_name = party_details.shipping_address_name or party_details.customer_address
|
||||
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
|
||||
address_name = party_details.shipping_address or party_details.supplier_address
|
||||
|
||||
if address_name:
|
||||
address = frappe.db.get_value("Address", address_name, ["gst_state", "gst_state_number"], as_dict=1)
|
||||
if address and address.gst_state and address.gst_state_number:
|
||||
return cstr(address.gst_state_number) + "-" + cstr(address.gst_state)
|
||||
|
||||
def get_regional_address_details(out, doctype, company):
|
||||
out.place_of_supply = get_place_of_supply(out, doctype)
|
||||
@frappe.whitelist()
|
||||
def get_regional_address_details(party_details, doctype, company, return_taxes=None):
|
||||
|
||||
if not out.place_of_supply: return
|
||||
if isinstance(party_details, string_types):
|
||||
party_details = json.loads(party_details)
|
||||
party_details = frappe._dict(party_details)
|
||||
|
||||
if doctype in ("Sales Invoice", "Delivery Note"):
|
||||
party_details.place_of_supply = get_place_of_supply(party_details, doctype)
|
||||
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
|
||||
master_doctype = "Sales Taxes and Charges Template"
|
||||
if not out.company_gstin:
|
||||
return
|
||||
elif doctype == "Purchase Invoice":
|
||||
master_doctype = "Purchase Taxes and Charges Template"
|
||||
if not out.supplier_gstin:
|
||||
|
||||
get_tax_template_for_sez(party_details, master_doctype, company, 'Customer')
|
||||
get_tax_template_based_on_category(master_doctype, company, party_details)
|
||||
|
||||
if party_details.get('taxes_and_charges') and return_taxes:
|
||||
return party_details
|
||||
|
||||
if not party_details.company_gstin:
|
||||
return
|
||||
|
||||
if ((doctype in ("Sales Invoice", "Delivery Note") and out.company_gstin
|
||||
and out.company_gstin[:2] != out.place_of_supply[:2]) or (doctype == "Purchase Invoice"
|
||||
and out.supplier_gstin and out.supplier_gstin[:2] != out.place_of_supply[:2])):
|
||||
default_tax = frappe.db.get_value(master_doctype, {"company": company, "is_inter_state":1, "disabled":0})
|
||||
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
|
||||
master_doctype = "Purchase Taxes and Charges Template"
|
||||
|
||||
get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier')
|
||||
get_tax_template_based_on_category(master_doctype, company, party_details)
|
||||
|
||||
if party_details.get('taxes_and_charges') and return_taxes:
|
||||
return party_details
|
||||
|
||||
if not party_details.supplier_gstin:
|
||||
return
|
||||
|
||||
if not party_details.place_of_supply: return
|
||||
|
||||
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
|
||||
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
|
||||
"Purchase Order", "Purchase Receipt") and party_details.supplier_gstin and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2])):
|
||||
default_tax = get_tax_template(master_doctype, company, 1, party_details.company_gstin[:2])
|
||||
else:
|
||||
default_tax = frappe.db.get_value(master_doctype, {"company": company, "disabled":0, "is_default": 1})
|
||||
default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2])
|
||||
|
||||
if not default_tax:
|
||||
return
|
||||
out["taxes_and_charges"] = default_tax
|
||||
out.taxes = get_taxes_and_charges(master_doctype, default_tax)
|
||||
party_details["taxes_and_charges"] = default_tax
|
||||
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
|
||||
|
||||
if return_taxes:
|
||||
return party_details
|
||||
|
||||
def get_tax_template_based_on_category(master_doctype, company, party_details):
|
||||
if not party_details.get('tax_category'):
|
||||
return
|
||||
|
||||
default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')},
|
||||
'name')
|
||||
|
||||
if default_tax:
|
||||
party_details["taxes_and_charges"] = default_tax
|
||||
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
|
||||
|
||||
def get_tax_template(master_doctype, company, is_inter_state, state_code):
|
||||
tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'],
|
||||
filters = {'is_inter_state': is_inter_state})
|
||||
|
||||
default_tax = ''
|
||||
|
||||
for tax_category in tax_categories:
|
||||
if tax_category.gst_state == number_state_mapping[state_code] or \
|
||||
(not default_tax and not tax_category.gst_state):
|
||||
default_tax = frappe.db.get_value(master_doctype,
|
||||
{'disabled': 0, 'tax_category': tax_category.name}, 'name')
|
||||
|
||||
return default_tax
|
||||
|
||||
def get_tax_template_for_sez(party_details, master_doctype, company, party_type):
|
||||
|
||||
gst_details = frappe.db.get_value(party_type, {'name': party_details.get(frappe.scrub(party_type))},
|
||||
['gst_category', 'export_type'], as_dict=1)
|
||||
|
||||
if gst_details:
|
||||
if gst_details.gst_category == 'SEZ' and gst_details.export_type == 'With Payment of Tax':
|
||||
default_tax = frappe.db.get_value(master_doctype, {"company": company, "is_inter_state":1, "disabled":0,
|
||||
"gst_state": number_state_mapping[party_details.company_gstin[:2]]})
|
||||
|
||||
party_details["taxes_and_charges"] = default_tax
|
||||
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
|
||||
|
||||
|
||||
def calculate_annual_eligible_hra_exemption(doc):
|
||||
basic_component = frappe.get_cached_value('Company', doc.company, "basic_component")
|
||||
@@ -555,7 +627,7 @@ def get_gst_accounts(company, account_wise=False):
|
||||
filters={"parent": "GST Settings", "company": company},
|
||||
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"])
|
||||
|
||||
if not gst_settings_accounts:
|
||||
if not gst_settings_accounts and not frappe.flags.in_test:
|
||||
frappe.throw(_("Please set GST Accounts in GST Settings"))
|
||||
|
||||
for d in gst_settings_accounts:
|
||||
|
||||
Reference in New Issue
Block a user