From a49f720ee3c271eb648896d8f5082d05052558f2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 12 Jan 2018 11:59:59 +0530 Subject: [PATCH] GSTR1 for B2B, B2CL and B2CS (#12459) --- erpnext/regional/india/utils.py | 9 +- erpnext/regional/report/gstr_1/gstr_1.py | 408 +++++++++++++++-------- erpnext/regional/report/gstr_1/utils.py | 0 3 files changed, 274 insertions(+), 143 deletions(-) delete mode 100644 erpnext/regional/report/gstr_1/utils.py diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 8088bfc87d..58df053ec4 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -1,5 +1,6 @@ import frappe, re from frappe import _ +from frappe.utils import cstr from erpnext.regional.india import states, state_numbers from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount @@ -61,12 +62,10 @@ def get_itemised_tax_breakup_data(doc): return hsn_tax, hsn_taxable_amount def set_place_of_supply(doc, method): - if not hasattr(doc, 'customer_gstin'): - return - address_name = doc.shipping_address_name or doc.customer_address - address = frappe.db.get_value("Address", address_name, ["gst_state", "gst_state_number"], as_dict=1) - doc.place_of_supply = str(address.gst_state_number) + "-" + address.gst_state + if address_name: + address = frappe.db.get_value("Address", address_name, ["gst_state", "gst_state_number"], as_dict=1) + doc.place_of_supply = cstr(address.gst_state_number) + "-" + address.gst_state # don't remove this function it is used in tests def test_method(): diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 982c409978..65b1b89523 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -6,167 +6,299 @@ import frappe, json from frappe import _ def execute(filters=None): - columns, data = get_columns(filters), get_data(filters) - return columns, data + return Gstr1Report(filters).run() -def get_columns(filters): - return [ - "GSTIN/UIN of Recipient::150", - "Receiver Name::120", - "Invoice Number:Link/Sales Invoice:120", - "Invoice date:Date:120", - "Invoice Value:Currency:120", - "Place of Supply::120", - "Reverse Charge::120", - "Invoice Type::120", - "E-Commerce GSTIN::120", - "Rate:Int:80", - "Taxable Value:Currency:120", - "Cess Amount:Currency:120" - ] +class Gstr1Report(object): + def __init__(self, filters=None): + self.filters = frappe._dict(filters or {}) + self.customer_type = "Company" if self.filters.get("type_of_business") == "B2B" else "Individual" + + def run(self): + self.get_columns() + self.get_data() + return self.columns, self.data -def get_data(filters): - gst_accounts = get_gst_accounts(filters) - invoices = get_invoice_data(filters) - invoice_items = get_invoice_items(invoices) - items_based_on_tax_rate, invoice_cess = get_items_based_on_tax_rate(invoices.keys(), gst_accounts) + def get_data(self): + self.data = [] + self.get_gst_accounts() + self.get_invoice_data() - data = [] - for inv, items_based_on_rate in items_based_on_tax_rate.items(): - invoice_details = invoices.get(inv) - for rate, items in items_based_on_rate.items(): - row = [ - invoice_details.customer_gstin, - invoice_details.customer_name, - inv, - invoice_details.posting_date, - invoice_details.base_rounded_total or invoice_details.base_grand_total, - invoice_details.place_of_supply, - invoice_details.reverse_charge, - invoice_details.invoice_type, - invoice_details.ecommerce_gstin, - rate, - sum([net_amount for item_code, net_amount in invoice_items.get(inv).items() - if item_code in items]), - invoice_cess.get(inv) - ] - data.append(row) + if not self.invoices: return - return data + self.get_invoice_items() + self.get_items_based_on_tax_rate() + invoice_fields = [d["fieldname"] for d in self.invoice_columns] -def get_gst_accounts(filters): - gst_accounts = frappe._dict() - gst_settings_accounts = frappe.get_list("GST Account", - filters={"parent": "GST Settings", "company": filters.company}, - fields=["cgst_account", "sgst_account", "igst_account", "cess_account"]) - if not gst_settings_accounts: - frappe.throw(_("Please set GST Accounts in GST Settings")) + for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): + invoice_details = self.invoices.get(inv) + for rate, items in items_based_on_rate.items(): + row = [] + for fieldname in invoice_fields: + if fieldname == "invoice_value": + row.append(invoice_details.base_rounded_total or invoice_details.base_grand_total) + else: + row.append(invoice_details.get(fieldname)) - for d in gst_settings_accounts: - for acc, val in d.items(): - gst_accounts.setdefault(acc, []).append(val) + row += [rate, + sum([net_amount for item_code, net_amount in self.invoice_items.get(inv).items() + if item_code in items]), + self.invoice_cess.get(inv) + ] - return gst_accounts + if self.filters.get("type_of_business") == "B2C Small": + row.append("E" if invoice_details.ecommerce_gstin else "OE") -def get_invoice_data(filters): - invoices = frappe._dict() - conditions = get_conditions(filters) - match_conditions = frappe.build_match_conditions("Sales Invoice") + self.data.append(row) - if match_conditions: - match_conditions = " and {0} ".format(match_conditions) + def get_invoice_data(self): + self.invoices = frappe._dict() + conditions = self.get_conditions() - invoice_data = frappe.db.sql(""" - select - `tabSales Invoice`.name, - `tabSales Invoice`.customer_name, - `tabSales Invoice`.posting_date, - `tabSales Invoice`.base_grand_total, - `tabSales Invoice`.base_rounded_total, - `tabSales Invoice`.customer_gstin, - `tabSales Invoice`.place_of_supply, - `tabSales Invoice`.ecommerce_gstin, - `tabSales Invoice`.reverse_charge, - `tabSales Invoice`.invoice_type - from `tabSales Invoice` - where `tabSales Invoice`.docstatus = 1 %s %s - order by `tabSales Invoice`.posting_date desc - """ % (conditions, match_conditions), filters, as_dict=1) + invoice_data = frappe.db.sql(""" + select + name as invoice_number, + customer_name, + posting_date, + base_grand_total, + base_rounded_total, + customer_gstin, + place_of_supply, + ecommerce_gstin, + reverse_charge, + invoice_type + from `tabSales Invoice` + where docstatus = 1 %s + order by posting_date desc + """ % (conditions), self.filters, as_dict=1) - for d in invoice_data: - invoices.setdefault(d.name, d) - return invoices + for d in invoice_data: + self.invoices.setdefault(d.invoice_number, d) -def get_conditions(filters): - conditions = "" + def get_conditions(self): + conditions = "" - for opts in (("company", " and company=%(company)s"), - ("from_date", " and `tabSales Invoice`.posting_date>=%(from_date)s"), - ("to_date", " and `tabSales Invoice`.posting_date<=%(to_date)s")): - if filters.get(opts[0]): - conditions += opts[1] + for opts in (("company", " and company=%(company)s"), + ("from_date", " and posting_date>=%(from_date)s"), + ("to_date", " and posting_date<=%(to_date)s")): + if self.filters.get(opts[0]): + conditions += opts[1] - return conditions + customers = frappe.get_all("Customer", filters={"customer_type": self.customer_type}) + conditions += " and customer in ('{0}')".format("', '".join([frappe.db.escape(c.name) + for c in customers])) -def get_invoice_items(invoices): - invoice_items = frappe._dict() - items = frappe.db.sql(""" - select item_code, parent, base_net_amount - from `tabSales Invoice Item` - where parent in (%s) - """ % (', '.join(['%s']*len(invoices))), tuple(invoices), as_dict=1) + if self.filters.get("type_of_business") == "B2C Large": + conditions += """ and SUBSTR(place_of_supply, 1, 2) != SUBSTR(company_gstin, 1, 2) + and grand_total > 250000""" + elif self.filters.get("type_of_business") == "B2C Small": + conditions += """ and ( + SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2) + or grand_total <= 250000 + )""" - for d in items: - invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, d.base_net_amount) - return invoice_items + return conditions -def get_items_based_on_tax_rate(invoices, gst_accounts): - tax_details = frappe.db.sql(""" - select - parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount - from `tabSales Taxes and Charges` - where - parenttype = 'Sales Invoice' and docstatus = 1 - and parent in (%s) - and tax_amount_after_discount_amount > 0 - order by account_head - """ % (', '.join(['%s']*len(invoices))), tuple(invoices)) + def get_invoice_items(self): + self.invoice_items = frappe._dict() + items = frappe.db.sql(""" + select item_code, parent, base_net_amount + from `tabSales Invoice Item` + where parent in (%s) + """ % (', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) - items_based_on_tax_rate = {} - invoice_cess = frappe._dict() - unidentified_gst_accounts = [] + for d in items: + self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, d.base_net_amount) - for parent, account, item_wise_tax_detail, tax_amount in tax_details: - if account in gst_accounts.cess_account: - invoice_cess.setdefault(parent, tax_amount) - else: - if item_wise_tax_detail: - try: - item_wise_tax_detail = json.loads(item_wise_tax_detail) - cgst_or_sgst = False - if account in gst_accounts.cgst_account or account in gst_accounts.sgst_account: - cgst_or_sgst = True + def get_items_based_on_tax_rate(self): + tax_details = frappe.db.sql(""" + select + parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount + from `tabSales Taxes and Charges` + where + parenttype = 'Sales Invoice' and docstatus = 1 + and parent in (%s) + and tax_amount_after_discount_amount > 0 + order by account_head + """ % (', '.join(['%s']*len(self.invoices.keys()))), tuple(self.invoices.keys())) - if not (cgst_or_sgst or account in gst_accounts.igst_account): - if "gst" in account.lower() and account not in unidentified_gst_accounts: - unidentified_gst_accounts.append(account) + self.items_based_on_tax_rate = {} + self.invoice_cess = frappe._dict() + unidentified_gst_accounts = [] + + for parent, account, item_wise_tax_detail, tax_amount in tax_details: + if account in self.gst_accounts.cess_account: + self.invoice_cess.setdefault(parent, tax_amount) + else: + if item_wise_tax_detail: + try: + item_wise_tax_detail = json.loads(item_wise_tax_detail) + cgst_or_sgst = False + if account in self.gst_accounts.cgst_account \ + or account in self.gst_accounts.sgst_account: + cgst_or_sgst = True + + if not (cgst_or_sgst or account in self.gst_accounts.igst_account): + if "gst" in account.lower() and account not in unidentified_gst_accounts: + unidentified_gst_accounts.append(account) + continue + + for item_code, tax_amounts in item_wise_tax_detail.items(): + tax_rate = tax_amounts[0] + if cgst_or_sgst: + tax_rate *= 2 + + rate_based_dict = self.items_based_on_tax_rate.setdefault(parent, {})\ + .setdefault(tax_rate, []) + if item_code not in rate_based_dict: + rate_based_dict.append(item_code) + + except ValueError: continue + if unidentified_gst_accounts: + frappe.msgprint(_("Following accounts might be selected in GST Settings:") + + "
" + "
".join(unidentified_gst_accounts), alert=True) - for item_code, tax_amounts in item_wise_tax_detail.items(): - tax_rate = tax_amounts[0] - if cgst_or_sgst: - tax_rate *= 2 + def get_gst_accounts(self): + self.gst_accounts = frappe._dict() + gst_settings_accounts = frappe.get_list("GST Account", + filters={"parent": "GST Settings", "company": self.filters.company}, + fields=["cgst_account", "sgst_account", "igst_account", "cess_account"]) - rate_based_dict = items_based_on_tax_rate.setdefault(parent, {})\ - .setdefault(tax_rate, []) - if item_code not in rate_based_dict: - rate_based_dict.append(item_code) + if not gst_settings_accounts: + frappe.throw(_("Please set GST Accounts in GST Settings")) - except ValueError: - continue - if unidentified_gst_accounts: - frappe.msgprint(_("Following accounts might be selected in GST Settings:") - + "
" + "
".join(unidentified_gst_accounts), alert=True) + for d in gst_settings_accounts: + for acc, val in d.items(): + self.gst_accounts.setdefault(acc, []).append(val) - return items_based_on_tax_rate, invoice_cess + def get_columns(self): + self.tax_columns = [ + { + "fieldname": "rate", + "label": "Rate", + "fieldtype": "Int", + "width": 60 + }, + { + "fieldname": "taxable_value", + "label": "Taxable Value", + "fieldtype": "Currency", + "width": 100 + }, + { + "fieldname": "cess_amount", + "label": "Cess Amount", + "fieldtype": "Currency", + "width": 100 + } + ] + self.other_columns = [] + + if self.filters.get("type_of_business") == "B2B": + self.invoice_columns = [ + { + "fieldname": "customer_gstin", + "label": "GSTIN/UIN of Recipient", + "fieldtype": "Data" + }, + { + "fieldname": "customer_name", + "label": "Receiver Name", + "fieldtype": "Data" + }, + { + "fieldname": "invoice_number", + "label": "Invoice Number", + "fieldtype": "Link", + "options": "Sales Invoice" + }, + { + "fieldname": "posting_date", + "label": "Invoice date", + "fieldtype": "Date" + }, + { + "fieldname": "invoice_value", + "label": "Invoice Value", + "fieldtype": "Currency" + }, + { + "fieldname": "place_of_supply", + "label": "Place of Supply", + "fieldtype": "Data" + }, + { + "fieldname": "reverse_charge", + "label": "Reverse Charge", + "fieldtype": "Data" + }, + { + "fieldname": "invoice_type", + "label": "Invoice Type", + "fieldtype": "Data" + }, + { + "fieldname": "ecommerce_gstin", + "label": "E-Commerce GSTIN", + "fieldtype": "Data" + } + ] + elif self.filters.get("type_of_business") == "B2C Large": + self.invoice_columns = [ + { + "fieldname": "invoice_number", + "label": "Invoice Number", + "fieldtype": "Link", + "options": "Sales Invoice", + "width": 120 + }, + { + "fieldname": "posting_date", + "label": "Invoice date", + "fieldtype": "Date", + "width": 100 + }, + { + "fieldname": "invoice_value", + "label": "Invoice Value", + "fieldtype": "Currency", + "width": 100 + }, + { + "fieldname": "place_of_supply", + "label": "Place of Supply", + "fieldtype": "Data", + "width": 120 + }, + { + "fieldname": "ecommerce_gstin", + "label": "E-Commerce GSTIN", + "fieldtype": "Data", + "width": 130 + } + ] + elif self.filters.get("type_of_business") == "B2C Small": + self.invoice_columns = [ + { + "fieldname": "place_of_supply", + "label": "Place of Supply", + "fieldtype": "Data", + "width": 120 + }, + { + "fieldname": "ecommerce_gstin", + "label": "E-Commerce GSTIN", + "fieldtype": "Data", + "width": 130 + } + ] + self.other_columns = [ + { + "fieldname": "type", + "label": "Type", + "fieldtype": "Data", + "width": 50 + } + ] + self.columns = self.invoice_columns + self.tax_columns + self.other_columns \ No newline at end of file diff --git a/erpnext/regional/report/gstr_1/utils.py b/erpnext/regional/report/gstr_1/utils.py deleted file mode 100644 index e69de29bb2..0000000000