|
2 | 2 | # For license information, please see license.txt |
3 | 3 |
|
4 | 4 |
|
| 5 | +import calendar |
5 | 6 | import json |
6 | 7 | import os |
7 | 8 | from collections import defaultdict |
8 | 9 |
|
| 10 | +from openpyxl.cell.cell import MergedCell |
| 11 | + |
9 | 12 | import frappe |
10 | 13 | from frappe import _ |
11 | 14 | from frappe.model.document import Document |
|
17 | 20 | from india_compliance.gst_india.report.gstr_3b_details.gstr_3b_details import ( |
18 | 21 | IneligibleITC, |
19 | 22 | ) |
20 | | -from india_compliance.gst_india.utils import get_gst_accounts_by_type, get_period |
| 23 | +from india_compliance.gst_india.utils import ( |
| 24 | + get_data_file_path, |
| 25 | + get_gst_accounts_by_type, |
| 26 | + get_period, |
| 27 | +) |
| 28 | +from india_compliance.gst_india.utils.exporter import ExcelExporter |
21 | 29 | from india_compliance.gst_india.utils.gstr_1.gstr_1_data import GSTR11A11BData |
22 | 30 |
|
23 | 31 | VALUES_TO_UPDATE = ["iamt", "camt", "samt", "csamt"] |
@@ -749,3 +757,296 @@ def make_json(name): |
749 | 757 | frappe.local.response.filename = file_name |
750 | 758 | frappe.local.response.filecontent = json_data |
751 | 759 | frappe.local.response.type = "download" |
| 760 | + |
| 761 | + |
| 762 | +@frappe.whitelist() |
| 763 | +def download_gstr3b_as_excel(name): |
| 764 | + """Download GSTR 3B report as Excel file""" |
| 765 | + frappe.has_permission("GSTR 3B Report", throw=True) |
| 766 | + json_data = frappe.get_value("GSTR 3B Report", name, "json_output") |
| 767 | + |
| 768 | + if not json_data: |
| 769 | + frappe.throw(_("Report data not found. Please generate the report.")) |
| 770 | + |
| 771 | + data = json.loads(json_data) |
| 772 | + exporter = GSTR3BExcelExporter(data) |
| 773 | + exporter.generate_excel() |
| 774 | + |
| 775 | + |
| 776 | +class GSTR3BExcelExporter: |
| 777 | + """ |
| 778 | + Export GSTR-3B data to Excel format using the official template. |
| 779 | +
|
| 780 | + This class handles data transformation and mapping from JSON to Excel cells |
| 781 | + following the official GSTR-3B offline utility format. |
| 782 | + """ |
| 783 | + |
| 784 | + TEMPLATE_FILE = get_data_file_path("gstr3b_excel_utility_v5.7.xlsx") |
| 785 | + WORKSHEET_NAME = "GSTR-3B" |
| 786 | + |
| 787 | + _STATE_CODE_TO_NAME = {code: state for state, code in STATE_NUMBERS.items()} |
| 788 | + |
| 789 | + # Row mappings for each section (consistent with JSON keys) |
| 790 | + ROWS = { |
| 791 | + # Header info |
| 792 | + "gstin": 5, |
| 793 | + "year": 5, |
| 794 | + "month": 6, |
| 795 | + # Section 3.1 - Outward supplies |
| 796 | + "osup_det": 11, |
| 797 | + "osup_zero": 12, |
| 798 | + "osup_nil_exmp": 13, |
| 799 | + "isup_rev": 14, |
| 800 | + "osup_nongst": 15, |
| 801 | + "eco_reg_sup": 23, |
| 802 | + # Section 3.2 - Inter-state |
| 803 | + "inter_state_start": 88, |
| 804 | + # Section 4 - ITC |
| 805 | + "itc_import_goods": 31, |
| 806 | + "itc_import_services": 32, |
| 807 | + "itc_reverse_charge": 33, |
| 808 | + "itc_isd": 34, |
| 809 | + "itc_others": 35, |
| 810 | + "itc_reversed_rules": 37, |
| 811 | + "itc_reversed_others": 38, |
| 812 | + # Section 5 - Inward supplies |
| 813 | + "inward_gst": 48, |
| 814 | + "inward_non_gst": 49, |
| 815 | + } |
| 816 | + |
| 817 | + HEADER_COLUMNS = { |
| 818 | + "gstin": 3, |
| 819 | + "year": 7, |
| 820 | + "month": 7, |
| 821 | + } |
| 822 | + |
| 823 | + # Section 3.1 - Tax columns |
| 824 | + TAX_COLUMNS = { |
| 825 | + "txval": 3, |
| 826 | + "iamt": 4, |
| 827 | + "camt": 5, |
| 828 | + "csamt": 7, |
| 829 | + } |
| 830 | + |
| 831 | + # Section 4 - ITC columns |
| 832 | + ITC_COLUMNS = { |
| 833 | + "iamt": 3, |
| 834 | + "camt": 4, |
| 835 | + "csamt": 6, |
| 836 | + } |
| 837 | + |
| 838 | + # Section 5 - Inward supplies columns |
| 839 | + INWARD_COLUMNS = { |
| 840 | + "inter": 4, |
| 841 | + "intra": 5, |
| 842 | + } |
| 843 | + |
| 844 | + # ITC type mappings based on 'ty' field in JSON |
| 845 | + ITC_AVAILABLE_TYPES = { |
| 846 | + "IMPG": "itc_import_goods", |
| 847 | + "IMPS": "itc_import_services", |
| 848 | + "ISRC": "itc_reverse_charge", |
| 849 | + "ISD": "itc_isd", |
| 850 | + "OTH": "itc_others", |
| 851 | + } |
| 852 | + |
| 853 | + ITC_REVERSED_TYPES = { |
| 854 | + "RUL": "itc_reversed_rules", |
| 855 | + "OTH": "itc_reversed_others", |
| 856 | + } |
| 857 | + |
| 858 | + INWARD_SUPPLY_TYPES = { |
| 859 | + "GST": "inward_gst", |
| 860 | + "NONGST": "inward_non_gst", |
| 861 | + } |
| 862 | + |
| 863 | + COLUMN_SETS = { |
| 864 | + "tax": ["txval", "iamt", "camt", "csamt"], |
| 865 | + "itc": ["iamt", "camt", "csamt"], |
| 866 | + "import_itc": ["iamt", "csamt"], |
| 867 | + "inward": ["inter", "intra"], |
| 868 | + "zero_rated": ["txval", "iamt", "csamt"], |
| 869 | + "taxable_only": ["txval"], |
| 870 | + } |
| 871 | + |
| 872 | + def __init__(self, data): |
| 873 | + self.data = data |
| 874 | + self.gstin = data.get("gstin") |
| 875 | + self.worksheet = None |
| 876 | + self.month = None |
| 877 | + self.fiscal_year = None |
| 878 | + |
| 879 | + def generate_excel(self): |
| 880 | + """Generate and export Excel file""" |
| 881 | + if not os.path.exists(self.TEMPLATE_FILE): |
| 882 | + frappe.throw(_("GSTR 3B Excel template not found")) |
| 883 | + |
| 884 | + excel = ExcelExporter(file=self.TEMPLATE_FILE) |
| 885 | + self._update_worksheet(excel) |
| 886 | + |
| 887 | + file_name = self._get_filename() |
| 888 | + excel.export(file_name) |
| 889 | + |
| 890 | + def _get_filename(self): |
| 891 | + return f"GSTR-3B-{self.gstin}-{self.month}-{self.fiscal_year}.xlsx" |
| 892 | + |
| 893 | + def _update_worksheet(self, excel): |
| 894 | + self.worksheet = excel.wb[self.WORKSHEET_NAME] |
| 895 | + |
| 896 | + self._set_header_info() |
| 897 | + self._set_outward_supplies() |
| 898 | + self._set_ecommerce_supplies() |
| 899 | + self._set_inter_state_supplies() |
| 900 | + self._set_itc_details() |
| 901 | + self._set_inward_supplies() |
| 902 | + |
| 903 | + def _set_header_info(self): |
| 904 | + """Set header information""" |
| 905 | + period = self.data.get("ret_period", "") |
| 906 | + if not period or len(period) < 6: |
| 907 | + return |
| 908 | + |
| 909 | + month_num = int(period[:2]) |
| 910 | + calendar_year = int(period[2:6]) |
| 911 | + |
| 912 | + self.month = calendar.month_name[month_num] |
| 913 | + self.fiscal_year = self._get_fiscal_year(month_num, calendar_year) |
| 914 | + |
| 915 | + self._set_cell(self.ROWS["gstin"], self.HEADER_COLUMNS["gstin"], self.gstin) |
| 916 | + self._set_cell(self.ROWS["year"], self.HEADER_COLUMNS["year"], self.fiscal_year) |
| 917 | + self._set_cell(self.ROWS["month"], self.HEADER_COLUMNS["month"], self.month) |
| 918 | + |
| 919 | + def _get_fiscal_year(self, month_num, calendar_year): |
| 920 | + if month_num >= 4: |
| 921 | + fiscal_year_start = str(calendar_year) |
| 922 | + fiscal_year_end = str(calendar_year + 1)[2:] |
| 923 | + else: |
| 924 | + fiscal_year_start = str(calendar_year - 1) |
| 925 | + fiscal_year_end = str(calendar_year)[2:] |
| 926 | + |
| 927 | + return f"{fiscal_year_start}-{fiscal_year_end}" |
| 928 | + |
| 929 | + def _set_outward_supplies(self): |
| 930 | + sup_details = self.data.get("sup_details", {}) |
| 931 | + |
| 932 | + section_mappings = [ |
| 933 | + ("osup_det", "tax"), |
| 934 | + ("osup_zero", "zero_rated"), |
| 935 | + ("osup_nil_exmp", "taxable_only"), |
| 936 | + ("isup_rev", "tax"), |
| 937 | + ("osup_nongst", "taxable_only"), |
| 938 | + ] |
| 939 | + |
| 940 | + for json_key, column_set in section_mappings: |
| 941 | + data = sup_details.get(json_key, {}) |
| 942 | + self._set_section_data(json_key, data, column_set) |
| 943 | + |
| 944 | + def _set_ecommerce_supplies(self): |
| 945 | + eco_dtls = self.data.get("eco_dtls", {}) |
| 946 | + self._set_section_data( |
| 947 | + "eco_reg_sup", eco_dtls.get("eco_reg_sup", {}), "taxable_only" |
| 948 | + ) |
| 949 | + |
| 950 | + def _set_inter_state_supplies(self): |
| 951 | + inter_sup = self.data.get("inter_sup", {}) |
| 952 | + pos_data = self._group_by_place_of_supply(inter_sup) |
| 953 | + |
| 954 | + if not pos_data: |
| 955 | + return |
| 956 | + |
| 957 | + for i, (pos, data) in enumerate(sorted(pos_data.items())): |
| 958 | + row = self.ROWS["inter_state_start"] + i |
| 959 | + self._set_inter_state_row(row, pos, data) |
| 960 | + |
| 961 | + def _group_by_place_of_supply(self, inter_sup): |
| 962 | + pos_data = {} |
| 963 | + categories = { |
| 964 | + "unreg_details": "unreg", |
| 965 | + "comp_details": "comp", |
| 966 | + "uin_details": "uin", |
| 967 | + } |
| 968 | + |
| 969 | + for category_key, category_name in categories.items(): |
| 970 | + for item in inter_sup.get(category_key, []): |
| 971 | + state_code = item.get("pos", "00") |
| 972 | + state_name = self._format_place_of_supply(state_code) |
| 973 | + |
| 974 | + if state_name not in pos_data: |
| 975 | + pos_data[state_name] = { |
| 976 | + "unreg": {"txval": 0, "iamt": 0}, |
| 977 | + "comp": {"txval": 0, "iamt": 0}, |
| 978 | + "uin": {"txval": 0, "iamt": 0}, |
| 979 | + } |
| 980 | + |
| 981 | + pos_data[state_name][category_name]["txval"] += flt( |
| 982 | + item.get("txval", 0), 2 |
| 983 | + ) |
| 984 | + pos_data[state_name][category_name]["iamt"] += flt( |
| 985 | + item.get("iamt", 0), 2 |
| 986 | + ) |
| 987 | + |
| 988 | + return pos_data |
| 989 | + |
| 990 | + def _set_inter_state_row(self, row, pos, data): |
| 991 | + self._set_cell(row, 2, pos) |
| 992 | + |
| 993 | + categories = [ |
| 994 | + ("unreg", 3, 4), |
| 995 | + ("comp", 5, 6), |
| 996 | + ("uin", 7, 8), |
| 997 | + ] |
| 998 | + |
| 999 | + for category, val_col, tax_col in categories: |
| 1000 | + category_data = data.get(category, {"txval": 0, "iamt": 0}) |
| 1001 | + self._set_cell(row, val_col, category_data["txval"]) |
| 1002 | + self._set_cell(row, tax_col, category_data["iamt"]) |
| 1003 | + |
| 1004 | + def _set_itc_details(self): |
| 1005 | + itc_elg = self.data.get("itc_elg", {}) |
| 1006 | + self._populate_itc_sections( |
| 1007 | + itc_elg.get("itc_avl", []), self.ITC_AVAILABLE_TYPES |
| 1008 | + ) |
| 1009 | + self._populate_itc_sections(itc_elg.get("itc_rev", []), self.ITC_REVERSED_TYPES) |
| 1010 | + |
| 1011 | + def _populate_itc_sections(self, itc_entries, type_mapping): |
| 1012 | + for itc_entry in itc_entries: |
| 1013 | + itc_type = itc_entry.get("ty", "") |
| 1014 | + if itc_type not in type_mapping: |
| 1015 | + continue |
| 1016 | + |
| 1017 | + row_key = type_mapping[itc_type] |
| 1018 | + column_set = "import_itc" if itc_type in ["IMPG", "IMPS"] else "itc" |
| 1019 | + self._set_section_data(row_key, itc_entry, column_set, self.ITC_COLUMNS) |
| 1020 | + |
| 1021 | + def _set_inward_supplies(self): |
| 1022 | + inward_sup = self.data.get("inward_sup", {}) |
| 1023 | + isup_details = inward_sup.get("isup_details", []) |
| 1024 | + |
| 1025 | + for supply_data in isup_details: |
| 1026 | + supply_type = supply_data.get("ty") |
| 1027 | + if supply_type in self.INWARD_SUPPLY_TYPES: |
| 1028 | + row_key = self.INWARD_SUPPLY_TYPES[supply_type] |
| 1029 | + self._set_section_data( |
| 1030 | + row_key, supply_data, "inward", self.INWARD_COLUMNS |
| 1031 | + ) |
| 1032 | + |
| 1033 | + def _set_section_data(self, row_key, data, column_set, columns_dict=None): |
| 1034 | + row = self.ROWS[row_key] |
| 1035 | + columns = self.COLUMN_SETS[column_set] |
| 1036 | + mapping = columns_dict or self.TAX_COLUMNS |
| 1037 | + |
| 1038 | + for key in columns: |
| 1039 | + if key in mapping: |
| 1040 | + value = flt(data.get(key, 0), 2) |
| 1041 | + self._set_cell(row, mapping[key], value) |
| 1042 | + |
| 1043 | + def _set_cell(self, row, column, value): |
| 1044 | + cell = self.worksheet.cell(row, column) |
| 1045 | + if not isinstance(cell, MergedCell): |
| 1046 | + cell.value = value |
| 1047 | + |
| 1048 | + @classmethod |
| 1049 | + def _format_place_of_supply(cls, state_code): |
| 1050 | + formatted_code = state_code.zfill(2) |
| 1051 | + state_name = cls._STATE_CODE_TO_NAME.get(formatted_code, "Other Territory") |
| 1052 | + return f"{formatted_code}-{state_name}" |
0 commit comments