diff --git a/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.js b/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.js index c61fe49a58..d788b3c0ed 100644 --- a/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.js +++ b/india_compliance/gst_india/report/gst_sales_register_beta/gst_sales_register_beta.js @@ -1,17 +1,24 @@ // Copyright (c) 2024, Resilient Tech and contributors // For license information, please see license.txt const INVOICE_TYPE = { - "B2B,SEZ,DE": ["B2B Regular", "B2B Reverse Charge", "SEZWP", "SEZWOP", "Deemed Exports"], + "B2B, SEZ, DE": [ + "B2B Regular", + "B2B Reverse Charge", + "SEZWP", + "SEZWOP", + "Deemed Exports", + ], "B2C (Large)": ["B2C (Large)"], - "Exports": ["EXPWP", "EXPWOP"], + Exports: ["EXPWP", "EXPWOP"], "B2C (Others)": ["B2C (Others)"], - "Nil-Rated,Exempted,Non-GST": ["Nil-Rated", "Exempted", "Non-GST"], + "Nil-Rated, Exempted, Non-GST": ["Nil-Rated", "Exempted", "Non-GST"], "Credit/Debit Notes (Registered)": ["CDNR"], "Credit/Debit Notes (Unregistered)": ["CDNUR"], -} +}; frappe.query_reports["GST Sales Register Beta"] = { onload: set_sub_category_options, + filters: [ { fieldname: "company", @@ -46,42 +53,80 @@ frappe.query_reports["GST Sales Register Beta"] = { fieldname: "date_range", label: __("Date Range"), fieldtype: "DateRange", - default: [india_compliance.last_month_start(), india_compliance.last_month_end()], - width: "80" + default: [ + india_compliance.last_month_start(), + india_compliance.last_month_end(), + ], + width: "80", }, { fieldtype: "Select", fieldname: "summary_by", label: __("Summary By"), options: "Overview\nSummary by HSN\nSummary by Item", - default: "Summary by Item" + default: "Summary by Item", }, { fieldtype: "Autocomplete", fieldname: "invoice_category", label: __("Invoice Category"), - options: "B2B,SEZ,DE\nB2C (Large)\nExports\nB2C (Others)\nNil-Rated,Exempted,Non-GST\nCredit/Debit Notes (Registered)\nCredit/Debit Notes (Unregistered)", + options: + "B2B, SEZ, DE\nB2C (Large)\nExports\nB2C (Others)\nNil-Rated, Exempted, Non-GST\nCredit/Debit Notes (Registered)\nCredit/Debit Notes (Unregistered)", on_change(report) { - report.set_filter_value('invoice_sub_category', ""); + report.set_filter_value("invoice_sub_category", ""); set_sub_category_options(report); }, - depends_on: 'eval:doc.summary_by=="Summary by HSN" || doc.summary_by=="Summary by Item"' + depends_on: + 'eval:doc.summary_by=="Summary by HSN" || doc.summary_by=="Summary by Item"', }, { fieldtype: "Autocomplete", fieldname: "invoice_sub_category", label: __("Invoice Sub Category"), - depends_on: 'eval:doc.summary_by=="Summary by HSN" || doc.summary_by=="Summary by Item"' + depends_on: + 'eval:doc.summary_by=="Summary by HSN" || doc.summary_by=="Summary by Item"', + }, + ], + + formatter: (value, row, column, data, default_formatter) => { + value = default_formatter(value, row, column, data); + if (data && data.indent === 0) { + let $value = $(`${value}`).css("font-weight", "bold"); + value = $value.wrap("

").parent().html(); } - ] + + return value; + }, }; function set_sub_category_options(report) { const invoice_category = frappe.query_report.get_filter_value("invoice_category"); - report.get_filter('invoice_sub_category').set_data(INVOICE_TYPE[invoice_category] || []); + report + .get_filter("invoice_sub_category") + .set_data(INVOICE_TYPE[invoice_category] || []); if (invoice_category && INVOICE_TYPE[invoice_category].length === 1) { - report.set_filter_value("invoice_sub_category", INVOICE_TYPE[invoice_category][0]) + report.set_filter_value( + "invoice_sub_category", + INVOICE_TYPE[invoice_category][0] + ); } } +frappe_report_column_total = frappe.utils.report_column_total; + +// Override datatable hook for column total calculation +frappe.utils.report_column_total = function (...args) { + const summary_by = frappe.query_report.get_filter_value("summary_by"); + if (summary_by !== "Overview") return frappe_report_column_total.apply(this, args); + + const column_field = args[1].column.fieldname; + if (column_field === "description") return; + + const total = this.datamanager.data.reduce((acc, row) => { + if (row.indent !== 1) acc += row[column_field] || 0; + return acc; + }, 0); + + return total; +}; diff --git a/india_compliance/gst_india/utils/gstr/gstr_1.py b/india_compliance/gst_india/utils/gstr/gstr_1.py index e6dece6b9d..9ee9c6293a 100644 --- a/india_compliance/gst_india/utils/gstr/gstr_1.py +++ b/india_compliance/gst_india/utils/gstr/gstr_1.py @@ -11,6 +11,8 @@ B2C_LIMIT = 2_50_000 +# TODO: Enum for Invoice Type + class GSTR1_Categories(Enum): """ @@ -18,7 +20,7 @@ class GSTR1_Categories(Enum): """ # Invoice Items Bifurcation - B2B = "B2B,SEZ,DE" + B2B = "B2B, SEZ, DE" B2CL = "B2C (Large)" EXP = "Exports" B2CS = "B2C (Others)" @@ -59,29 +61,61 @@ class GSTR1_SubCategories(Enum): # DOC_ISSUE = "Document Issued" +CATEGORY_SUB_CATEGORY_MAPPING = { + GSTR1_Categories.B2B: ( + GSTR1_SubCategories.B2B_REGULAR, + GSTR1_SubCategories.B2B_REVERSE_CHARGE, + GSTR1_SubCategories.SEZWP, + GSTR1_SubCategories.SEZWOP, + GSTR1_SubCategories.DE, + ), + GSTR1_Categories.B2CL: (GSTR1_SubCategories.B2CL,), + GSTR1_Categories.EXP: (GSTR1_SubCategories.EXPWP, GSTR1_SubCategories.EXPWOP), + GSTR1_Categories.B2CS: (GSTR1_SubCategories.B2CS,), + GSTR1_Categories.NIL_EXEMPT: ( + GSTR1_SubCategories.NIL_RATED, + GSTR1_SubCategories.EXEMPTED, + GSTR1_SubCategories.NON_GST, + ), + GSTR1_Categories.CDNR: (GSTR1_SubCategories.CDNR,), + GSTR1_Categories.CDNUR: (GSTR1_SubCategories.CDNUR,), +} + SUB_CATEGORIES_DESCRIPTION = { - "SEZWP": "SEZ with payment", - "SEZWOP": "SEZ without payment", - "EXPWP": "Exports with payment", - "EXPWOP": "Exports without payment", - "CDNR": "Credit/Debit Notes (Registered)", - "CDNUR": "Credit/Debit Notes (Unregistered)", + GSTR1_SubCategories.SEZWP: "SEZ with payment", + GSTR1_SubCategories.SEZWOP: "SEZ without payment", + GSTR1_SubCategories.EXPWP: "Exports with payment", + GSTR1_SubCategories.EXPWOP: "Exports without payment", + GSTR1_SubCategories.CDNR: "Credit/Debit Notes (Registered)", + GSTR1_SubCategories.CDNUR: "Credit/Debit Notes (Unregistered)", } CATEGORY_CONDITIONS = { - "B2B,SEZ,DE": {"category": "is_b2b_invoice", "sub_category": "set_for_b2b"}, - "B2C (Large)": {"category": "is_b2cl_invoice", "sub_category": "set_for_b2cl"}, - "Exports": {"category": "is_export_invoice", "sub_category": "set_for_exports"}, - "B2C (Others)": {"category": "is_b2cs_invoice", "sub_category": "set_for_b2cs"}, - "Nil-Rated,Exempted,Non-GST": { + GSTR1_Categories.B2B.value: { + "category": "is_b2b_invoice", + "sub_category": "set_for_b2b", + }, + GSTR1_Categories.B2CL.value: { + "category": "is_b2cl_invoice", + "sub_category": "set_for_b2cl", + }, + GSTR1_Categories.EXP.value: { + "category": "is_export_invoice", + "sub_category": "set_for_exports", + }, + GSTR1_Categories.B2CS.value: { + "category": "is_b2cs_invoice", + "sub_category": "set_for_b2cs", + }, + GSTR1_Categories.NIL_EXEMPT.value: { "category": "is_nil_rated_exempted_non_gst_invoice", "sub_category": "set_for_nil_exp_non_gst", }, - "Credit/Debit Notes (Registered)": { + GSTR1_Categories.CDNR.value: { "category": "is_cdnr_invoice", "sub_category": "set_for_cdnr", }, - "Credit/Debit Notes (Unregistered)": { + GSTR1_Categories.CDNUR.value: { "category": "is_cdnur_invoice", "sub_category": "set_for_cdnur", }, @@ -325,20 +359,20 @@ def set_for_b2b(self, invoice): def set_for_b2cl(self, invoice): # NO INVOICE VALUE - invoice.invoice_sub_category = "B2C (Large)" + invoice.invoice_sub_category = GSTR1_SubCategories.B2CL.value def set_for_exports(self, invoice): if invoice.is_export_with_gst: - invoice.invoice_sub_category = "EXPWP" + invoice.invoice_sub_category = GSTR1_SubCategories.EXPWP.value invoice.invoice_type = "WPAY" else: - invoice.invoice_sub_category = "EXPWOP" + invoice.invoice_sub_category = GSTR1_SubCategories.EXPWOP.value invoice.invoice_type = "WOPAY" def set_for_b2cs(self, invoice): # NO INVOICE VALUE - invoice.invoice_sub_category = "B2C (Others)" + invoice.invoice_sub_category = GSTR1_SubCategories.B2CS.value def set_for_nil_exp_non_gst(self, invoice): # INVOICE TYPE @@ -352,20 +386,20 @@ def set_for_nil_exp_non_gst(self, invoice): # INVOICE SUB CATEGORY if self.is_nil_rated(invoice): - invoice.invoice_sub_category = "Nil-Rated" + invoice.invoice_sub_category = GSTR1_SubCategories.NIL_RATED.value elif self.is_exempted(invoice): - invoice.invoice_sub_category = "Exempted" + invoice.invoice_sub_category = GSTR1_SubCategories.EXEMPTED.value elif self.is_non_gst(invoice): - invoice.invoice_sub_category = "Non-GST" + invoice.invoice_sub_category = GSTR1_SubCategories.NON_GST.value def set_for_cdnr(self, invoice): self._set_invoice_type_for_b2b_and_cdnr(invoice) - invoice.invoice_sub_category = "CDNR" + invoice.invoice_sub_category = GSTR1_SubCategories.CDNR.value def set_for_cdnur(self, invoice): - invoice.invoice_sub_category = "CDNUR" + invoice.invoice_sub_category = GSTR1_SubCategories.CDNUR.value if self.is_export(invoice): if invoice.is_export_with_gst: invoice.invoice_type = "EXPWP" @@ -380,27 +414,35 @@ def set_for_cdnur(self, invoice): def _set_invoice_type_for_b2b_and_cdnr(self, invoice): if invoice.gst_category == "Deemed Export": invoice.invoice_type = "Deemed Exp" - invoice.invoice_sub_category = "Deemed Export" + invoice.invoice_sub_category = GSTR1_SubCategories.DE.value elif invoice.gst_category == "SEZ": if invoice.is_export_with_gst: invoice.invoice_type = "SEZ supplies with payment" - invoice.invoice_sub_category = "SEZWP" + invoice.invoice_sub_category = GSTR1_SubCategories.SEZWP.value else: invoice.invoice_type = "SEZ supplies without payment" - invoice.invoice_sub_category = "SEZWOP" + invoice.invoice_sub_category = GSTR1_SubCategories.SEZWOP.value elif invoice.is_reverese_charge: invoice.invoice_type = "Regular B2B" - invoice.invoice_sub_category = "B2B Reverse Charge" + invoice.invoice_sub_category = GSTR1_SubCategories.B2B_REVERSE_CHARGE.value else: invoice.invoice_type = "Regular B2B" - invoice.invoice_sub_category = "B2B Regular" + invoice.invoice_sub_category = GSTR1_SubCategories.B2B_REGULAR.value class GSTR1Invoices(GSTR1Query, GSTR1Subcategory): + AMOUNT_FIELDS = { + "taxable_value": 0, + "igst_amount": 0, + "cgst_amount": 0, + "sgst_amount": 0, + "total_cess_amount": 0, + } + def __init__(self, filters=None): super().__init__(filters) @@ -482,51 +524,71 @@ def get_filtered_invoices( return filtered_invoices def get_overview(self): + final_summary = [] + sub_category_summary = self.get_sub_category_summary() + + for category, sub_categories in CATEGORY_SUB_CATEGORY_MAPPING.items(): + category_summary = { + "description": category.value, + "no_of_records": 0, + "indent": 0, + **self.AMOUNT_FIELDS, + } + final_summary.append(category_summary) + + for sub_category in sub_categories: + sub_category_row = sub_category_summary[sub_category.value] + category_summary["no_of_records"] += sub_category_row["no_of_records"] + + for key in self.AMOUNT_FIELDS: + category_summary[key] += sub_category_row[key] + + final_summary.append(sub_category_row) + + self.update_overlaping_invoice_summary(sub_category_summary, final_summary) + + return final_summary + + def get_sub_category_summary(self): invoices = self.get_invoices_for_item_wise_summary() self.process_invoices(invoices) - amount_fields = { - "taxable_value": 0, - "igst_amount": 0, - "cgst_amount": 0, - "sgst_amount": 0, - "total_cess_amount": 0, - } - summary = {} - subcategories = [category.value for category in GSTR1_SubCategories] - for category in subcategories: - summary[category] = { - "description": SUB_CATEGORIES_DESCRIPTION.get(category, category), + for category in GSTR1_SubCategories: + summary[category.value] = { + "description": SUB_CATEGORIES_DESCRIPTION.get(category, category.value), "no_of_records": 0, + "indent": 1, "unique_records": set(), - **amount_fields, + **self.AMOUNT_FIELDS, } for row in invoices: - category_key = summary[ + summary_row = summary[ row.get("invoice_sub_category", row["invoice_category"]) ] - for key in amount_fields: - category_key[key] += row[key] - - category_key["unique_records"].add(row.invoice_no) + for key in self.AMOUNT_FIELDS: + summary_row[key] += row[key] - for category_key in summary.values(): - category_key["no_of_records"] = len(category_key["unique_records"]) + summary_row["unique_records"].add(row.invoice_no) - self.update_overlaping_invoice_summary(summary) + for summary_row in summary.values(): + summary_row["no_of_records"] = len(summary_row["unique_records"]) - return list(summary.values()) + return summary - def update_overlaping_invoice_summary(self, summary): - nil_exempt_non_gst = ("Nil-Rated", "Exempted", "Non-GST") + def update_overlaping_invoice_summary(self, sub_category_summary, final_summary): + nil_exempt_non_gst = ( + GSTR1_SubCategories.NIL_RATED.value, + GSTR1_SubCategories.EXEMPTED.value, + GSTR1_SubCategories.NON_GST.value, + ) # Get Unique Taxable Invoices unique_invoices = set() - for category, row in summary.items(): + for category, row in sub_category_summary.items(): if category in nil_exempt_non_gst: continue @@ -535,14 +597,16 @@ def update_overlaping_invoice_summary(self, summary): # Get Overlaping Invoices overlaping_invoices = set() for category in nil_exempt_non_gst: - category_invoices = summary[category]["unique_records"] + category_invoices = sub_category_summary[category]["unique_records"] overlaping_invoices.update(category_invoices.intersection(unique_invoices)) unique_invoices.update(category_invoices) # Update Summary if overlaping_invoices: - summary["Overlaping Invoices"] = { - "description": "Overlaping Invoices in Nil-Rated/Exempt/Non-GST", - "no_of_records": -len(overlaping_invoices), - } + final_summary.append( + { + "description": "Overlaping Invoices in Nil-Rated/Exempt/Non-GST", + "no_of_records": -len(overlaping_invoices), + } + )