From 6b24008bf4debe6e402869ccc724901273b9ac11 Mon Sep 17 00:00:00 2001 From: Lakshit Jain <108322669+ljain112@users.noreply.github.com> Date: Fri, 26 Apr 2024 19:50:21 +0530 Subject: [PATCH] fix: correct outward supply details using gst treatment (#2067) * fix: correct outward supply details using gst treatment * test: gstr-3b report refactor * test: change settings for test case * fix: changes as per review * fix: modify test cases * refactor: remove duplication and rename variable * fix: query --------- Co-authored-by: Smit Vora (cherry picked from commit f6e97b404a2f8dbbab3d2536eccfd0c45a907953) --- .../doctype/gstr_3b_report/gstr_3b_report.py | 426 +++++++++--------- .../gstr_3b_report/test_gstr_3b_report.py | 164 ++++++- 2 files changed, 359 insertions(+), 231 deletions(-) diff --git a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py index 641ab857d..1957199f4 100644 --- a/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py +++ b/india_compliance/gst_india/doctype/gstr_3b_report/gstr_3b_report.py @@ -13,13 +13,13 @@ from frappe.utils import cstr, flt, get_date_str, get_first_day, get_last_day from india_compliance.gst_india.constants import INVOICE_DOCTYPES +from india_compliance.gst_india.overrides.transaction import is_inter_state_supply from india_compliance.gst_india.report.gstr_3b_details.gstr_3b_details import ( IneligibleITC, ) -from india_compliance.gst_india.utils import ( - get_gst_accounts_by_type, - is_overseas_transaction, -) +from india_compliance.gst_india.utils import get_gst_accounts_by_type + +VALUES_TO_UPDATE = ["iamt", "camt", "samt", "csamt"] class GSTR3BReport(Document): @@ -60,6 +60,7 @@ def get_data(self): self.set_inward_nil_exempt(inward_nil_exempt) self.missing_field_invoices = self.get_missing_field_invoices() + self.report_dict = format_values(self.report_dict) self.json_output = frappe.as_json(self.report_dict) self.generation_status = "Generated" @@ -110,9 +111,9 @@ def set_itc_details(self, itc_details): for d in self.report_dict["itc_elg"]["itc_avl"]: itc_type = itc_eligible_type_map.get(d["ty"]) - for key in ["iamt", "camt", "samt", "csamt"]: + for key in VALUES_TO_UPDATE: d[key] = flt(itc_details.get(itc_type, {}).get(key)) - net_itc[key] += flt(d[key], 2) + net_itc[key] += d[key] def get_itc_reversal_entries(self): self.update_itc_reversal_from_journal_entry() @@ -137,7 +138,7 @@ def process_ineligible_credit(self, ineligible_credit): if not ineligible_credit: return - tax_amounts = ["camt", "samt", "iamt", "csamt"] + tax_amounts = VALUES_TO_UPDATE for row in ineligible_credit: if row.itc_classification == "Ineligible As Per Section 17(5)": @@ -145,15 +146,15 @@ def process_ineligible_credit(self, ineligible_credit): if key not in row: continue - self.report_dict["itc_elg"]["itc_rev"][0][key] += flt(row[key]) - self.report_dict["itc_elg"]["itc_net"][key] -= flt(row[key]) + self.report_dict["itc_elg"]["itc_rev"][0][key] += row[key] + self.report_dict["itc_elg"]["itc_net"][key] -= row[key] elif row.itc_classification == "ITC restricted due to PoS rules": for key in tax_amounts: if key not in row: continue - self.report_dict["itc_elg"]["itc_inelg"][1][key] += flt(row[key]) + self.report_dict["itc_elg"]["itc_inelg"][1][key] += row[key] def update_itc_reversal_from_journal_entry(self): reversal_entries = frappe.db.sql( @@ -179,12 +180,11 @@ def update_itc_reversal_from_journal_entry(self): else: index = 1 - for key in ["camt", "samt", "iamt", "csamt"]: + for key in VALUES_TO_UPDATE: if entry.account in self.account_heads.get(key): - self.report_dict["itc_elg"]["itc_rev"][index][key] += flt( - entry.amount - ) - net_itc[key] -= flt(entry.amount) + self.report_dict["itc_elg"]["itc_rev"][index][key] += entry.amount + + net_itc[key] -= entry.amount def get_itc_details(self): itc_amounts = frappe.db.sql( @@ -296,8 +296,97 @@ def get_inward_nil_exempt(self, state): def get_outward_supply_details(self, doctype, reverse_charge=None): self.get_outward_tax_invoices(doctype, reverse_charge=reverse_charge) - self.get_outward_items(doctype) - self.get_outward_tax_details(doctype) + self.get_invoice_item_wise_tax_details(doctype) + + def get_invoice_item_wise_tax_details(self, doctype): + item_details = self.get_outward_items(doctype) + tax_details = self.get_outward_tax_details(doctype) + docs = self.combine_item_and_taxes(doctype, item_details, tax_details) + + self.set_item_wise_tax_details(docs) + + def set_item_wise_tax_details(self, docs): + self.invoice_item_wise_tax_details = {} + item_wise_details = {} + account_head_gst_map = {} + + for key, values in self.account_heads.items(): + for value in values: + if value is not None: + account_head_gst_map[value] = key + + item_defaults = frappe._dict( + { + "camt": 0, + "samt": 0, + "iamt": 0, + "csamt": 0, + } + ) + + # Process tax and item details + for doc, details in docs.items(): + item_wise_details[doc] = {} + invoice_items = {} + item_code_gst_treatment_map = {} + + # Initialize invoice items with default values + for item in details["items"]: + item_code_gst_treatment_map[item.item_code or item.item_name] = ( + item.gst_treatment + ) + invoice_items.setdefault( + item.gst_treatment, + { + "taxable_value": 0, + **item_defaults, + }, + ) + + invoice_items[item.gst_treatment]["taxable_value"] += item.get( + "taxable_value", 0 + ) + + # Process tax details + for tax in details["taxes"]: + gst_tax_type = account_head_gst_map.get(tax.account_head) + + if not gst_tax_type: + continue + + if tax.item_wise_tax_detail: + try: + item_wise_detail = json.loads(tax.item_wise_tax_detail) + for item_code, tax_amounts in item_wise_detail.items(): + gst_treatment = item_code_gst_treatment_map.get(item_code) + invoice_items[gst_treatment][gst_tax_type] += tax_amounts[1] + + except ValueError: + continue + + item_wise_details[doc].update(invoice_items) + + self.invoice_item_wise_tax_details = item_wise_details + + def combine_item_and_taxes(self, doctype, item_details, tax_details): + response = frappe._dict() + + # Group tax details by parent document + for tax in tax_details: + if tax.parent not in response: + response[tax.parent] = frappe._dict(taxes=[], items=[], doctype=doctype) + + response[tax.parent]["taxes"].append(tax) + + # Group item details by parent document + for item in item_details: + if item.parent not in response: + response[item.parent] = frappe._dict( + taxes=[], items=[], doctype=doctype + ) + + response[item.parent]["items"].append(item) + return response def get_outward_tax_invoices(self, doctype, reverse_charge=None): self.invoice_map = {} @@ -326,18 +415,13 @@ def get_outward_tax_invoices(self, doctype, reverse_charge=None): self.invoice_map = {d.name: d for d in invoice_details} def get_outward_items(self, doctype): - self.invoice_items = frappe._dict() - self.is_nil_or_exempt = [] - self.is_non_gst = [] - if not self.invoice_map: - return + return {} item_details = frappe.db.sql( f""" SELECT - item_code, parent, taxable_value, item_tax_rate, - gst_treatment + item_code, item_name, parent, taxable_value, gst_treatment FROM `tab{doctype} Item` WHERE parent in ({", ".join(["%s"] * len(self.invoice_map))}) @@ -346,39 +430,17 @@ def get_outward_items(self, doctype): as_dict=1, ) - for d in item_details: - self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0) - self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) - - is_nil_rated = d.gst_treatment == "Nil-Rated" - is_exempted = d.gst_treatment == "Exempted" - is_non_gst = d.gst_treatment == "Non-GST" - - if ( - is_nil_rated or is_exempted - ) and d.item_code not in self.is_nil_or_exempt: - self.is_nil_or_exempt.append(d.item_code) - - if is_non_gst and d.item_code not in self.is_non_gst: - self.is_non_gst.append(d.item_code) + return item_details def get_outward_tax_details(self, doctype): - if doctype == "Sales Invoice": - tax_template = "Sales Taxes and Charges" - elif doctype == "Purchase Invoice": - tax_template = "Purchase Taxes and Charges" - - self.items_based_on_tax_rate = {} - self.invoice_cess = frappe._dict() - self.cgst_sgst_invoices = [] - if not self.invoice_map: - return + return {} + tax_template = f"{doctype.split()[0]} Taxes and Charges" tax_details = frappe.db.sql( f""" SELECT - parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount + parent, account_head, item_wise_tax_detail FROM `tab{tax_template}` WHERE parenttype = %s and docstatus = 1 @@ -386,196 +448,103 @@ def get_outward_tax_details(self, doctype): ORDER BY account_head """, (doctype, *self.invoice_map.keys()), + as_dict=1, ) - for parent, account, item_wise_tax_detail, tax_amount in tax_details: - if account in self.account_heads.get("csamt"): - 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.account_heads.get( - "camt" - ) or account in self.account_heads.get("samt"): - cgst_or_sgst = True - - for item_code, tax_amounts in item_wise_tax_detail.items(): - if not ( - cgst_or_sgst - or account in self.account_heads.get("iamt") - or ( - item_code in self.is_non_gst + self.is_nil_or_exempt - ) - ): - continue - - tax_rate = tax_amounts[0] - if tax_rate: - if cgst_or_sgst: - tax_rate *= 2 - if parent not in self.cgst_sgst_invoices: - self.cgst_sgst_invoices.append(parent) - - 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 - - # Build itemised tax for export invoices, nil and exempted where tax table is blank - for invoice, items in self.invoice_items.items(): - invoice_details = self.invoice_map.get(invoice, {}) - if ( - invoice not in self.items_based_on_tax_rate - and not invoice_details.get("is_export_with_gst") - and is_overseas_transaction( - "Sales Invoice", - invoice_details.get("gst_category"), - invoice_details.get("place_of_supply"), - ) - ): - self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault( - 0, items.keys() - ) - else: - for item in items.keys(): - if ( - item in self.is_nil_or_exempt + self.is_non_gst - and item - not in self.items_based_on_tax_rate.get(invoice, {}).get(0, []) - ): - self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault( - 0, [] - ) - self.items_based_on_tax_rate[invoice][0].append(item) + return tax_details def set_outward_taxable_supplies(self): inter_state_supply_details = {} + gst_treatment_map = { + "Nil-Rated": "osup_nil_exmp", + "Exempted": "osup_nil_exmp", + "Zero-Rated": "osup_zero", + "Non-GST": "osup_nongst", + "Taxable": "osup_det", + } - for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): - invoice_details = self.invoice_map.get(inv, {}) + for inv, invoice_details in self.invoice_map.items(): + gst_treatment_details = self.invoice_item_wise_tax_details.get(inv, {}) gst_category = invoice_details.get("gst_category") place_of_supply = ( invoice_details.get("place_of_supply") or "00-Other Territory" ) - is_overseas_invoice = is_overseas_transaction( - "Sales Invoice", gst_category, place_of_supply + + doc = frappe._dict( + { + "gst_category": gst_category, + "place_of_supply": place_of_supply, + "company_gstin": self.gst_details.get("gstin"), + } ) - for rate, items in items_based_on_rate.items(): - for item_code, taxable_value in self.invoice_items.get(inv).items(): - if item_code in items: - if item_code in self.is_nil_or_exempt: - self.report_dict["sup_details"]["osup_nil_exmp"][ - "txval" - ] += taxable_value - elif item_code in self.is_non_gst: - self.report_dict["sup_details"]["osup_nongst"][ - "txval" - ] += taxable_value - elif rate == 0 or (is_overseas_invoice): - self.report_dict["sup_details"]["osup_zero"][ - "txval" - ] += taxable_value - - self.report_dict["sup_details"]["osup_zero"]["iamt"] += flt( - taxable_value * rate / 100, 2 - ) - else: - if inv in self.cgst_sgst_invoices: - tax_rate = rate / 2 - self.report_dict["sup_details"]["osup_det"][ - "camt" - ] += flt(taxable_value * tax_rate / 100, 2) - self.report_dict["sup_details"]["osup_det"][ - "samt" - ] += flt(taxable_value * tax_rate / 100, 2) - self.report_dict["sup_details"]["osup_det"][ - "txval" - ] += flt(taxable_value, 2) - else: - self.report_dict["sup_details"]["osup_det"][ - "iamt" - ] += flt(taxable_value * rate / 100, 2) - self.report_dict["sup_details"]["osup_det"][ - "txval" - ] += flt(taxable_value, 2) - - if ( - gst_category - in [ - "Unregistered", - "Registered Composition", - "UIN Holders", - ] - and self.gst_details.get("gst_state") - != place_of_supply.split("-")[1] - ): - inter_state_supply_details.setdefault( - (gst_category, place_of_supply), - { - "txval": 0.0, - "pos": place_of_supply.split("-")[0], - "iamt": 0.0, - }, - ) - inter_state_supply_details[ - (gst_category, place_of_supply) - ]["txval"] += flt(taxable_value, 2) - inter_state_supply_details[ - (gst_category, place_of_supply) - ]["iamt"] += flt(taxable_value * rate / 100, 2) - - if self.invoice_cess.get(inv): - - invoice_category = "osup_zero" if is_overseas_invoice else "osup_det" - - self.report_dict["sup_details"][invoice_category]["csamt"] += flt( - self.invoice_cess.get(inv), 2 - ) + is_inter_state = is_inter_state_supply(doc) + + for gst_treatment, details in gst_treatment_details.items(): + gst_treatment_section = gst_treatment_map.get(gst_treatment) + section = self.report_dict["sup_details"][gst_treatment_section] + + taxable_value = details.get("taxable_value") + + # updating taxable value and tax value + section["txval"] += taxable_value + for key in section: + if key in VALUES_TO_UPDATE: + section[key] += details.get(key, 0) + + # section 3.2 details + if not gst_treatment == "Taxable": + continue + + if ( + gst_category + in [ + "Unregistered", + "Registered Composition", + "UIN Holders", + ] + and is_inter_state + ): + inter_state_supply_details.setdefault( + (gst_category, place_of_supply), + { + "txval": 0.0, + "pos": place_of_supply.split("-")[0], + "iamt": 0.0, + }, + ) + + inter_state_supply_details[(gst_category, place_of_supply)][ + "txval" + ] += taxable_value + inter_state_supply_details[(gst_category, place_of_supply)][ + "iamt" + ] += details.get("iamt") self.set_inter_state_supply(inter_state_supply_details) def set_supplies_liable_to_reverse_charge(self): - for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): - for rate, items in items_based_on_rate.items(): - for item_code, taxable_value in self.invoice_items.get(inv).items(): - if item_code in items: - if inv in self.cgst_sgst_invoices: - tax_rate = rate / 2 - self.report_dict["sup_details"]["isup_rev"]["camt"] += flt( - taxable_value * tax_rate / 100, 2 - ) - self.report_dict["sup_details"]["isup_rev"]["samt"] += flt( - taxable_value * tax_rate / 100, 2 - ) - self.report_dict["sup_details"]["isup_rev"]["txval"] += flt( - taxable_value, 2 - ) - else: - self.report_dict["sup_details"]["isup_rev"]["iamt"] += flt( - taxable_value * rate / 100, 2 - ) - self.report_dict["sup_details"]["isup_rev"]["txval"] += flt( - taxable_value, 2 - ) + section = self.report_dict["sup_details"]["isup_rev"] + for inv, invoice_details in self.invoice_map.items(): + gst_treatment_section = self.invoice_item_wise_tax_details.get(inv, {}) + for item in gst_treatment_section.values(): + section["txval"] += item.get("taxable_value") + for key in section: + if key in VALUES_TO_UPDATE: + section[key] += item.get(key, 0) def set_inter_state_supply(self, inter_state_supply): - for key, value in inter_state_supply.items(): - if key[0] == "Unregistered": - self.report_dict["inter_sup"]["unreg_details"].append(value) + inter_state_supply_map = { + "Unregistered": "unreg_details", + "Registered Composition": "comp_details", + "UIN Holders": "uin_details", + } - if key[0] == "Registered Composition": - self.report_dict["inter_sup"]["comp_details"].append(value) + for key, value in inter_state_supply.items(): + section = inter_state_supply_map.get(key[0]) - if key[0] == "UIN Holders": - self.report_dict["inter_sup"]["uin_details"].append(value) + if section: + self.report_dict["inter_sup"][section].append(value) def get_company_gst_details(self): gst_details = frappe.get_all( @@ -678,6 +647,23 @@ def get_period(month, year=None): return month_no +def format_values(data, precision=2): + if isinstance(data, dict): + for key, value in data.items(): + if isinstance(value, (int, float)): + data[key] = flt(value, precision) + elif isinstance(value, dict) or isinstance(value, list): + format_values(value) + elif isinstance(data, list): + for i, item in enumerate(data): + if isinstance(item, (int, float)): + data[i] = flt(item, precision) + elif isinstance(item, dict) or isinstance(item, list): + format_values(item) + + return data + + @frappe.whitelist() def view_report(name): frappe.has_permission("GSTR 3B Report", throw=True) diff --git a/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py b/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py index 73b01f2ca..2bb1784f1 100644 --- a/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py +++ b/india_compliance/gst_india/doctype/gstr_3b_report/test_gstr_3b_report.py @@ -2,9 +2,9 @@ # See license.txt import json -import unittest import frappe +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import getdate from india_compliance.gst_india.utils.tests import ( @@ -13,11 +13,12 @@ ) -class TestGSTR3BReport(unittest.TestCase): +class TestGSTR3BReport(FrappeTestCase): def setUp(self): frappe.set_user("Administrator") filters = {"company": "_Test Indian Registered Company"} + self.maxDiff = None for doctype in ("Sales Invoice", "Purchase Invoice", "GSTR 3B Report"): frappe.db.delete(doctype, filters=filters) @@ -25,6 +26,7 @@ def setUp(self): def tearDownClass(cls): frappe.db.rollback() + @change_settings("GST Settings", {"enable_overseas_transactions": 1}) def test_gstr_3b_report(self): month_number_mapping = { 1: "January", @@ -41,28 +43,143 @@ def test_gstr_3b_report(self): 12: "December", } + gst_settings = frappe.get_cached_doc("GST Settings") + gst_settings.round_off_gst_values = 0 + gst_settings.save() + create_sales_invoices() create_purchase_invoices() + today = getdate() + ret_period = f"{today.month:02}{today.year}" + report = frappe.get_doc( { "doctype": "GSTR 3B Report", "company": "_Test Indian Registered Company", "company_address": "_Test Indian Registered Company-Billing", - "year": getdate().year, - "month": month_number_mapping.get(getdate().month), + "year": today.year, + "month": month_number_mapping.get(today.month), } ).insert() output = json.loads(report.json_output) - self.assertEqual(output["sup_details"]["osup_det"]["iamt"], 18) - self.assertEqual(output["sup_details"]["osup_det"]["txval"], 300) - self.assertEqual(output["sup_details"]["isup_rev"]["txval"], 100) - self.assertEqual(output["sup_details"]["isup_rev"]["camt"], 9) - self.assertEqual(output["itc_elg"]["itc_net"]["samt"], 40) + self.assertDictEqual( + output, + { + "gstin": "24AAQCA8719H1ZC", + "ret_period": ret_period, + # 3.1 + "sup_details": { + "isup_rev": { + "camt": 9.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 9.0, + "txval": 100.0, + }, + "osup_det": { + "camt": 18.0, + "csamt": 0.0, + "iamt": 37.98, + "samt": 18.0, + "txval": 411.0, + }, + "osup_nil_exmp": {"txval": 100.0}, + "osup_nongst": {"txval": 222.0}, + "osup_zero": {"csamt": 0.0, "iamt": 99.9, "txval": 999.0}, + }, + # 3.2 + "inter_sup": { + "comp_details": [{"iamt": 18.0, "pos": "29", "txval": 100.0}], + "uin_details": [], + "unreg_details": [{"iamt": 19.98, "pos": "06", "txval": 111.0}], + }, + # 4 + "itc_elg": { + "itc_avl": [ + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "IMPG", + }, + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "IMPS", + }, + { + "camt": 9.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 9.0, + "ty": "ISRC", + }, + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "ISD", + }, + { + "camt": 31.5, + "csamt": 0.0, + "iamt": 0.0, + "samt": 31.5, + "ty": "OTH", + }, + ], + "itc_inelg": [ + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "RUL", + }, + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "OTH", + }, + ], + "itc_net": {"camt": 40.5, "csamt": 0.0, "iamt": 0.0, "samt": 40.5}, + "itc_rev": [ + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "RUL", + }, + { + "camt": 0.0, + "csamt": 0.0, + "iamt": 0.0, + "samt": 0.0, + "ty": "OTH", + }, + ], + }, + # 5 + "inward_sup": { + "isup_details": [ + {"inter": 100.0, "intra": 0.0, "ty": "GST"}, + {"inter": 0.0, "intra": 0.0, "ty": "NONGST"}, + ] + }, + }, + ) def test_gst_rounding(self): - gst_settings = frappe.get_doc("GST Settings") + gst_settings = frappe.get_cached_doc("GST Settings") gst_settings.round_off_gst_values = 1 gst_settings.save() @@ -81,7 +198,6 @@ def test_gst_rounding(self): def create_sales_invoices(): create_sales_invoice(is_in_state=True) - create_sales_invoice(item_code="_Test Nil Rated Item") create_sales_invoice( customer="_Test Registered Composition Customer", is_out_state=True, @@ -90,6 +206,32 @@ def create_sales_invoices(): customer="_Test Unregistered Customer", is_in_state=True, ) + # Unregistered Out of state + create_sales_invoice( + customer="_Test Unregistered Customer", + is_out_state=True, + place_of_supply="06-Haryana", + rate=111, + ) + + # Same Item Nil-Rated + create_sales_invoice(item_tax_template="Nil-Rated - _TIRC") + + # Non Gst item + create_sales_invoice(item_code="_Test Non GST Item", rate=222) + + # Zero Rated + create_sales_invoice( + customer_address="_Test Registered Customer-Billing-1", + is_export_with_gst=False, + rate=444, + ) + create_sales_invoice( + customer_address="_Test Registered Customer-Billing-1", + is_export_with_gst=True, + is_out_state=True, + rate=555, + ) def create_purchase_invoices():