diff --git a/india_compliance/gst_india/utils/gstr_1/gstr_1_json_map.py b/india_compliance/gst_india/utils/gstr_1/gstr_1_json_map.py index fb1ae0282..597eaf486 100644 --- a/india_compliance/gst_india/utils/gstr_1/gstr_1_json_map.py +++ b/india_compliance/gst_india/utils/gstr_1/gstr_1_json_map.py @@ -3,7 +3,7 @@ import frappe from frappe.utils import flt -from india_compliance.gst_india.constants import STATE_NUMBERS, UOM_MAP +from india_compliance.gst_india.constants import UOM_MAP from india_compliance.gst_india.report.gstr_1.gstr_1 import ( GSTR1DocumentIssuedSummary, GSTR11A11BData, @@ -26,21 +26,20 @@ GSTR1_SubCategory, ) from india_compliance.gst_india.utils.gstr_1.gstr_1_data import GSTR1Invoices -from india_compliance.gst_india.utils.gstr_mapper_utils import GSTRDataMapper +from india_compliance.gst_india.utils.gstr_mapper_utils import GovDataMapper ############################################################################################################ ### Map Govt JSON to Internal Data Structure ############################################################### ############################################################################################################ -class GovDataMapper: +class GSTR1DataMapper(GovDataMapper): """ GST Developer API Documentation for Returns - https://developer.gst.gov.in/apiportal/taxpayer/returns GSTR-1 JSON format - https://developer.gst.gov.in/pages/apiportal/data/Returns/GSTR1%20-%20Save%20GSTR1%20data/v4.0/GSTR1%20-%20Save%20GSTR1%20data%20attributes.xlsx """ - KEY_MAPPING = {} # default item amounts DEFAULT_ITEM_AMOUNTS = { GSTR1_ItemField.TAXABLE_VALUE.value: 0, @@ -71,106 +70,10 @@ class GovDataMapper: } def __init__(self): - self.set_total_defaults() - - self.value_formatters_for_internal = {} - self.value_formatters_for_gov = {} + super().__init__() self.gstin_party_map = {} - # value formatting constants - - self.STATE_NUMBERS = self.reverse_dict(STATE_NUMBERS) - - def format_data( - self, data: dict, default_data: dict = None, for_gov: bool = False - ) -> dict: - """ - Objective: Convert Object from one format to another. - eg: Govt JSON to Internal Data Structure - - Args: - data (dict): Data to be converted - default_data (dict, optional): Default Data to be added. Hardcoded values. - for_gov (bool, optional): If the data is to be converted to Govt JSON. Defaults to False. - else it will be converted to Internal Data Structure. - - Steps: - 1. Use key mapping to map the keys from one format to another. - 2. Use value formatters to format the values of the keys. - 3. Round values - """ - output = {} - - if default_data: - output.update(default_data) - - key_mapping = self.KEY_MAPPING.copy() - - if for_gov: - key_mapping = self.reverse_dict(key_mapping) - - value_formatters = ( - self.value_formatters_for_gov - if for_gov - else self.value_formatters_for_internal - ) - - for old_key, new_key in key_mapping.items(): - invoice_data_value = data.get(old_key, "") - - if not for_gov and old_key == "flag": - continue - - if new_key in self.DISCARD_IF_ZERO_FIELDS and not invoice_data_value: - continue - - if not (invoice_data_value or invoice_data_value == 0): - # continue if value is None or empty object - continue - - value_formatter = value_formatters.get(old_key) - - if callable(value_formatter): - output[new_key] = value_formatter(invoice_data_value, data) - else: - output[new_key] = invoice_data_value - - if new_key in self.FLOAT_FIELDS: - output[new_key] = flt(output[new_key], 2) - - return output - - # common utils - - def update_totals(self, invoice, items): - """ - Update item totals to the invoice row - """ - total_data = self.TOTAL_DEFAULTS.copy() - - for item in items: - for field, value in item.items(): - total_field = f"total_{field}" - - if total_field not in total_data: - continue - - invoice[total_field] = invoice.setdefault(total_field, 0) + value - - def set_total_defaults(self): - self.TOTAL_DEFAULTS = { - f"total_{key}": 0 for key in self.DEFAULT_ITEM_AMOUNTS.keys() - } - - def reverse_dict(self, data): - return {v: k for k, v in data.items()} # common value formatters - def map_place_of_supply(self, pos, *args): - if pos.isnumeric(): - return f"{pos}-{self.STATE_NUMBERS.get(pos)}" - - return pos.split("-")[0] - def format_item_for_internal(self, items, *args): return [ { @@ -204,7 +107,7 @@ def format_date_for_gov(self, date, *args): return datetime.strptime(date, "%Y-%m-%d").strftime("%d-%m-%Y") -class B2B(GovDataMapper): +class B2B(GSTR1DataMapper): """ GST API Version - v4.0 @@ -379,7 +282,7 @@ def document_category_mapping(self, sub_category, data): return self.DOCUMENT_CATEGORIES.get(sub_category, sub_category) -class B2CL(GovDataMapper): +class B2CL(GSTR1DataMapper): """ GST API Version - v4.0 @@ -505,7 +408,7 @@ def convert_to_gov_data_format(self, input_data, **kwargs): return list(pos_data.values()) -class Exports(GovDataMapper): +class Exports(GSTR1DataMapper): """ GST API Version - v4.0 @@ -651,7 +554,7 @@ def format_item_for_gov(self, items, *args): return [self.format_data(item, for_gov=True) for item in items] -class B2CS(GovDataMapper): +class B2CS(GSTR1DataMapper): """ GST API Version - v4.0 @@ -747,7 +650,7 @@ def format_data(self, data, default_data=None, for_gov=False): return data -class NilRated(GovDataMapper): +class NilRated(GSTR1DataMapper): """ GST API Version - v4.0 @@ -852,7 +755,7 @@ def document_category_mapping(self, doc_category, data): return self.DOCUMENT_CATEGORIES.get(doc_category, doc_category) -class CDNR(GovDataMapper): +class CDNR(GSTR1DataMapper): """ GST API Version - v4.0 @@ -1040,7 +943,7 @@ def format_doc_value(self, value, data): return value * -1 if data[GovDataField.NOTE_TYPE.value] == "C" else value -class CDNUR(GovDataMapper): +class CDNUR(GSTR1DataMapper): """ GST API Version - v4.0 @@ -1178,7 +1081,7 @@ def format_doc_value(self, value, data): return value * -1 if data[GovDataField.NOTE_TYPE.value] == "C" else value -class HSNSUM(GovDataMapper): +class HSNSUM(GSTR1DataMapper): """ GST API Version - v4.0 @@ -1298,7 +1201,7 @@ def map_uom(self, uom, data=None): return f"OTH-{UOM_MAP.get('OTH')}" -class AT(GovDataMapper): +class AT(GSTR1DataMapper): """ GST API Version - v4.0 @@ -1479,7 +1382,7 @@ class TXPD(AT): MULTIPLIER = -1 -class DOC_ISSUE(GovDataMapper): +class DOC_ISSUE(GSTR1DataMapper): """ GST API Version - v4.0 @@ -1615,7 +1518,7 @@ def get_document_nature(self, doc_nature, *args): return self.DOCUMENT_NATURE.get(doc_nature, doc_nature) -class SUPECOM(GovDataMapper): +class SUPECOM(GSTR1DataMapper): """ GST API Version - v4.0 @@ -1692,7 +1595,7 @@ def convert_to_gov_data_format(self, input_data, **kwargs): return output -class RETSUM(GovDataMapper): +class RETSUM(GSTR1DataMapper): """ Convert GSTR-1 Summary as returned by the API to the internal format @@ -1837,35 +1740,179 @@ def map_document_types(self, doc_type, *args): return self.SECTION_NAMES.get(doc_type, doc_type) -class GSTR1DataMapper(GSTRDataMapper): - CLASS_MAP = { - GovJsonKey.B2B.value: B2B, - GovJsonKey.B2CL.value: B2CL, - GovJsonKey.EXP.value: Exports, - GovJsonKey.B2CS.value: B2CS, - GovJsonKey.NIL_EXEMPT.value: NilRated, - GovJsonKey.CDNR.value: CDNR, - GovJsonKey.CDNUR.value: CDNUR, - GovJsonKey.HSN.value: HSNSUM, - GovJsonKey.DOC_ISSUE.value: DOC_ISSUE, - GovJsonKey.AT.value: AT, - GovJsonKey.TXP.value: TXPD, - GovJsonKey.SUPECOM.value: SUPECOM, - GovJsonKey.RET_SUM.value: RETSUM, - } +CLASS_MAP = { + GovJsonKey.B2B.value: B2B, + GovJsonKey.B2CL.value: B2CL, + GovJsonKey.EXP.value: Exports, + GovJsonKey.B2CS.value: B2CS, + GovJsonKey.NIL_EXEMPT.value: NilRated, + GovJsonKey.CDNR.value: CDNR, + GovJsonKey.CDNUR.value: CDNUR, + GovJsonKey.HSN.value: HSNSUM, + GovJsonKey.DOC_ISSUE.value: DOC_ISSUE, + GovJsonKey.AT.value: AT, + GovJsonKey.TXP.value: TXPD, + GovJsonKey.SUPECOM.value: SUPECOM, + GovJsonKey.RET_SUM.value: RETSUM, +} + + +def convert_to_internal_data_format(gov_data): + """ + Converts Gov data format to internal data format for all categories + """ + output = {} + + for category, mapper_class in CLASS_MAP.items(): + if not gov_data.get(category): + continue + + output.update( + mapper_class().convert_to_internal_data_format(gov_data.get(category)) + ) + + return output + + +def get_category_wise_data( + subcategory_wise_data: dict, + mapping: dict = SUB_CATEGORY_GOV_CATEGORY_MAPPING, +) -> dict: + """ + returns category wise data from subcategory wise data + + Args: + subcategory_wise_data (dict): subcategory wise data + mapping (dict): subcategory to category mapping + with_subcategory (bool): include subcategory level data + + Returns: + dict: category wise data + + Example (with_subcategory=True): + { + "B2B, SEZ, DE": { + "B2B": data, + ... + } + ... + } + + Example (with_subcategory=False): + { + "B2B, SEZ, DE": data, + ... + } + """ + category_wise_data = {} + for subcategory, category in mapping.items(): + if not subcategory_wise_data.get(subcategory.value): + continue + + category_wise_data.setdefault(category.value, []).extend( + subcategory_wise_data.get(subcategory.value, []) + ) + + return category_wise_data + + +def convert_to_gov_data_format(internal_data: dict, company_gstin: str) -> dict: + """ + converts internal data format to Gov data format for all categories + """ + + category_wise_data = get_category_wise_data(internal_data) + + output = {} + for category, mapper_class in CLASS_MAP.items(): + if not category_wise_data.get(category): + continue + + output[category] = mapper_class().convert_to_gov_data_format( + category_wise_data.get(category), company_gstin=company_gstin + ) + + return output + + +def summarize_retsum_data(input_data): + if not input_data: + return [] + + summarized_data = [] + total_values_keys = [ + "total_igst_amount", + "total_cgst_amount", + "total_sgst_amount", + "total_cess_amount", + "total_taxable_value", + ] + amended_data = {key: 0 for key in total_values_keys} + + input_data = {row.get("description"): row for row in input_data} + + def _sum(row): + return flt(sum([row.get(key, 0) for key in total_values_keys]), 2) + + for category, sub_categories in CATEGORY_SUB_CATEGORY_MAPPING.items(): + category = category.value + if category not in input_data: + continue + + # compute total liability and total amended data + amended_category_data = input_data.get(f"{category} (Amended)", {}) + for key in total_values_keys: + amended_data[key] += amended_category_data.get(key, 0) + + # add category data + if _sum(input_data[category]) == 0: + continue + + summarized_data.append({**input_data.get(category), "indent": 0}) + + # add subcategory data + for sub_category in sub_categories: + sub_category = sub_category.value + if sub_category not in input_data: + continue + + if _sum(input_data[sub_category]) == 0: + continue + + summarized_data.append( + { + **input_data.get(sub_category), + "indent": 1, + "consider_in_total_taxable_value": ( + False + if sub_category + in SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAXABLE_VALUE + else True + ), + "consider_in_total_tax": ( + False + if sub_category in SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAX + else True + ), + } + ) + + # add total amendment liability + if _sum(amended_data) != 0: + summarized_data.extend( + [ + { + "description": "Net Liability from Amendments", + **amended_data, + "indent": 0, + "consider_in_total_taxable_value": True, + "consider_in_total_tax": True, + "no_of_records": 0, + } + ] + ) - category_sub_category_mapping = CATEGORY_SUB_CATEGORY_MAPPING - subcategories_not_considered_in_total_tax = ( - SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAX - ) - subcategories_not_considered_in_total_taxable_value = ( - SUBCATEGORIES_NOT_CONSIDERED_IN_TOTAL_TAXABLE_VALUE - ) - mapping = SUB_CATEGORY_GOV_CATEGORY_MAPPING - - def convert_to_gov_data_format(self, internal_data, company_gstin): - category_wise_data = self.get_category_wise_data(internal_data) - return super().convert_to_gov_data_format(category_wise_data, company_gstin) + return summarized_data #################################################################################################### diff --git a/india_compliance/gst_india/utils/gstr_mapper_utils.py b/india_compliance/gst_india/utils/gstr_mapper_utils.py index 318240346..48171c454 100644 --- a/india_compliance/gst_india/utils/gstr_mapper_utils.py +++ b/india_compliance/gst_india/utils/gstr_mapper_utils.py @@ -1,161 +1,116 @@ from frappe.utils import flt +from india_compliance.gst_india.constants import STATE_NUMBERS -class GSTRDataMapper: - def convert_to_internal_data_format(self, gov_data): - """ - Converts Gov data format to internal data format for all categories - """ - output = {} - for category, mapper_class in self.CLASS_MAP.items(): - if not gov_data.get(category): - continue +class GovDataMapper: + """ + GST Developer API Documentation for Returns - https://developer.gst.gov.in/apiportal/taxpayer/returns - output.update( - mapper_class().convert_to_internal_data_format(gov_data.get(category)) - ) + GSTR-1 JSON format - https://developer.gst.gov.in/pages/apiportal/data/Returns/GSTR1%20-%20Save%20GSTR1%20data/v4.0/GSTR1%20-%20Save%20GSTR1%20data%20attributes.xlsx + """ - return output + KEY_MAPPING = {} + FLOAT_FIELDS = {} + DISCARD_IF_ZERO_FIELDS = {} + TOTAL_DEFAULTS = {} + DEFAULT_ITEM_AMOUNTS = {} + + def __init__(self): + # value formatting constants + self.value_formatters_for_internal = {} + self.value_formatters_for_gov = {} + + self.STATE_NUMBERS = self.reverse_dict(STATE_NUMBERS) + self.set_total_defaults() - def get_category_wise_data( - self, - subcategory_wise_data: dict, + def format_data( + self, data: dict, default_data: dict = None, for_gov: bool = False ) -> dict: """ - returns category wise data from subcategory wise data + Objective: Convert Object from one format to another. + eg: Govt JSON to Internal Data Structure Args: - subcategory_wise_data (dict): subcategory wise data - mapping (dict): subcategory to category mapping - with_subcategory (bool): include subcategory level data - - Returns: - dict: category wise data - - Example (with_subcategory=True): - { - "B2B, SEZ, DE": { - "B2B": data, - ... - } - ... - } - - Example (with_subcategory=False): - { - "B2B, SEZ, DE": data, - ... - } + data (dict): Data to be converted + default_data (dict, optional): Default Data to be added. Hardcoded values. + for_gov (bool, optional): If the data is to be converted to Govt JSON. Defaults to False. + else it will be converted to Internal Data Structure. + + Steps: + 1. Use key mapping to map the keys from one format to another. + 2. Use value formatters to format the values of the keys. + 3. Round values """ - category_wise_data = {} - for subcategory, category in self.mapping.items(): - if not subcategory_wise_data.get(subcategory.value): - continue + output = {} - category_wise_data.setdefault(category.value, []).extend( - subcategory_wise_data.get(subcategory.value, []) - ) + if default_data: + output.update(default_data) - return category_wise_data + key_mapping = self.KEY_MAPPING.copy() - def convert_to_gov_data_format( - self, internal_data: dict, company_gstin: str - ) -> dict: - """ - converts internal data format to Gov data format for all categories - """ + if for_gov: + key_mapping = self.reverse_dict(key_mapping) - output = {} - for category, mapper_class in self.CLASS_MAP.items(): - if not internal_data.get(category): + value_formatters = ( + self.value_formatters_for_gov + if for_gov + else self.value_formatters_for_internal + ) + + for old_key, new_key in key_mapping.items(): + invoice_data_value = data.get(old_key, "") + + if not for_gov and old_key == "flag": + continue + + if new_key in self.DISCARD_IF_ZERO_FIELDS and not invoice_data_value: continue - output[category] = mapper_class().convert_to_gov_data_format( - internal_data.get(category), company_gstin=company_gstin - ) + if not (invoice_data_value or invoice_data_value == 0): + # continue if value is None or empty object + continue + + value_formatter = value_formatters.get(old_key) + + if callable(value_formatter): + output[new_key] = value_formatter(invoice_data_value, data) + else: + output[new_key] = invoice_data_value + + if new_key in self.FLOAT_FIELDS: + output[new_key] = flt(output[new_key], 2) return output - def summarize_retsum_data( - self, - input_data, - ): - if not input_data: - return [] - - summarized_data = [] - total_values_keys = [ - "total_igst_amount", - "total_cgst_amount", - "total_sgst_amount", - "total_cess_amount", - "total_taxable_value", - ] - amended_data = {key: 0 for key in total_values_keys} - - input_data = {row.get("description"): row for row in input_data} - - def _sum(row): - return flt(sum([row.get(key, 0) for key in total_values_keys]), 2) - - for category, sub_categories in self.category_sub_category_mapping.items(): - category = category.value - if category not in input_data: - continue + # common utils - # compute total liability and total amended data - amended_category_data = input_data.get(f"{category} (Amended)", {}) - for key in total_values_keys: - amended_data[key] += amended_category_data.get(key, 0) + def reverse_dict(self, data): + return {v: k for k, v in data.items()} - # add category data - if _sum(input_data[category]) == 0: - continue + # common value formatters + def map_place_of_supply(self, pos, *args): + if pos.isnumeric(): + return f"{pos}-{self.STATE_NUMBERS.get(pos)}" - summarized_data.append({**input_data.get(category), "indent": 0}) + return pos.split("-")[0] - # add subcategory data - for sub_category in sub_categories: - sub_category = sub_category.value - if sub_category not in input_data: - continue + def update_totals(self, invoice, items): + """ + Update item totals to the invoice row + """ + total_data = self.TOTAL_DEFAULTS.copy() + + for item in items: + for field, value in item.items(): + total_field = f"total_{field}" - if _sum(input_data[sub_category]) == 0: + if total_field not in total_data: continue - summarized_data.append( - { - **input_data.get(sub_category), - "indent": 1, - "consider_in_total_taxable_value": ( - False - if sub_category - in self.subcategory_not_considered_in_total_taxable_value - else True - ), - "consider_in_total_tax": ( - False - if sub_category - in self.subcategory_not_considered_in_total_tax - else True - ), - } - ) - - # add total amendment liability - if _sum(amended_data) != 0: - summarized_data.extend( - [ - { - "description": "Net Liability from Amendments", - **amended_data, - "indent": 0, - "consider_in_total_taxable_value": True, - "consider_in_total_tax": True, - "no_of_records": 0, - } - ] - ) - - return summarized_data + invoice[total_field] = invoice.setdefault(total_field, 0) + value + + def set_total_defaults(self): + self.TOTAL_DEFAULTS = { + f"total_{key}": 0 for key in self.DEFAULT_ITEM_AMOUNTS.keys() + } diff --git a/india_compliance/gst_india/utils/itc_04/__init__.py b/india_compliance/gst_india/utils/itc_04/__init__.py index c344402ad..0e515f1b9 100644 --- a/india_compliance/gst_india/utils/itc_04/__init__.py +++ b/india_compliance/gst_india/utils/itc_04/__init__.py @@ -7,36 +7,65 @@ class GovJsonKey(Enum): """ TABLE5A = "table5A" - TABLE5B = "table5B" + STOCK_ENTRY = "m2jw" + + +class ITC04JsonKey(Enum): + """ + Categories / Keys as per Internal JSON file + """ + + TABLE5A = "Table 5A" + STOCK_ENTRY = "Stock Entry" class GovDataField(Enum): COMPANY_GSTIN = "ctin" JOB_WORKER_STATE_CODE = "jw_stcd" ITEMS = "items" - ORIGINAL_CAHLLAN_NUMBER = "o_chnum" + ORIGINAL_CHALLAN_NUMBER = "o_chnum" ORIGINAL_CHALLAN_DATE = "o_chdt" - CHALLAN_NUMBER = "jw2_chnum" - CHALLAN_DATE = "jw2_chdt" + JOB_WORK_CHALLAN_NUMBER = "jw2_chnum" + JOB_WORK_CHALLAN_DATE = "jw2_chdt" NATURE_OF_JOB = "nat_jw" UOM = "uqc" - QTY = "qty" + QUANTITY = "qty" DESCRIPTION = "desc" LOSS_UOM = "lwuqc" LOSS_QTY = "lwqty" + TAXABLE_VALUE = "txval" + GOODS_TYPE = "goods_ty" + IGST = "tx_i" + CGST = "tx_c" + SGST = "tx_s" + CESS_AMOUNT = "tx_cs" + FLAG = "flag" + + +class GovDataField_SE(Enum): + ITEMS = "itms" + ORIGINAL_CHALLAN_NUMBER = "chnum" + ORIGINAL_CHALLAN_DATE = "chdt" class ITC04_DataField(Enum): COMPANY_GSTIN = "company_gstin" JOB_WORKER_STATE_CODE = "jw_state_code" ITEMS = "items" - ORIGINAL_CAHLLAN_NUMBER = "original_challan_number" + ORIGINAL_CHALLAN_NUMBER = "original_challan_number" ORIGINAL_CHALLAN_DATE = "original_challan_date" - CHALLAN_NUMBER = "jw_challan_number" - CHALLAN_DATE = "jw_challan_date" + JOB_WORK_CHALLAN_NUMBER = "jw_challan_number" + JOB_WORK_CHALLAN_DATE = "jw_challan_date" NATURE_OF_JOB = "nature_of_job" UOM = "uom" - QTY = "qty" + QUANTITY = "qty" DESCRIPTION = "desc" LOSS_UOM = "loss_uom" LOSS_QTY = "loss_qty" + TAXABLE_VALUE = "taxable_value" + GOODS_TYPE = "goods_type" + IGST = "igst_rate" + CGST = "cgst_rate" + SGST = "sgst_rate" + CESS_AMOUNT = "cess_amount" + FLAG = "flag" diff --git a/india_compliance/gst_india/utils/itc_04/itc_04_json_map.py b/india_compliance/gst_india/utils/itc_04/itc_04_json_map.py index e50f78922..4e01b2fbe 100644 --- a/india_compliance/gst_india/utils/itc_04/itc_04_json_map.py +++ b/india_compliance/gst_india/utils/itc_04/itc_04_json_map.py @@ -1,9 +1,11 @@ -from india_compliance.gst_india.constants import STATE_NUMBERS, UOM_MAP -from india_compliance.gst_india.utils.gstr_mapper_utils import GSTRDataMapper +from india_compliance.gst_india.constants import UOM_MAP +from india_compliance.gst_india.utils.gstr_mapper_utils import GovDataMapper from india_compliance.gst_india.utils.itc_04 import ( GovDataField, + GovDataField_SE, GovJsonKey, ITC04_DataField, + ITC04JsonKey, ) ############################################################################################################ @@ -11,74 +13,34 @@ ############################################################################################################ -class GovDataMapper: +class ITC04DataMapper(GovDataMapper): """ GST Developer API Documentation for Returns - https://developer.gst.gov.in/apiportal/taxpayer/returns ITC-04 JSON format - https://developer.gst.gov.in/pages/apiportal/data/Returns/ITC04%20-%20Save/v1.2/ITC04%20-%20Save%20attributes.xlsx """ - KEY_MAPPING = {} - - def __init__(self): - self.value_formatters_for_internal = {} - self.value_formatters_for_gov = {} - self.STATE_NUMBERS = self.reverse_dict(STATE_NUMBERS) - - def format_data( - self, data: dict, default_data: dict = None, for_gov: bool = False - ) -> dict: - """ - Objective: Convert Object from one format to another. - eg: Govt JSON to Internal Data Structure - - Args: - data (dict): Data to be converted - default_data (dict, optional): Default Data to be added. Hardcoded values. - for_gov (bool, optional): If the data is to be converted to Govt JSON. Defaults to False. - else it will be converted to Internal Data Structure. - - Steps: - 1. Use key mapping to map the keys from one format to another. - 2. Use value formatters to format the values of the keys. - 3. Round values - """ - output = {} - - if default_data: - output.update(default_data) - - key_mapping = self.KEY_MAPPING.copy() - - if for_gov: - key_mapping = self.reverse_dict(key_mapping) - - value_formatters = ( - self.value_formatters_for_gov - if for_gov - else self.value_formatters_for_internal - ) - - for old_key, new_key in key_mapping.items(): - invoice_data_value = data.get(old_key, "") - - if not for_gov and old_key == "flag": - continue - - if not (invoice_data_value or invoice_data_value == 0): - # continue if value is None or empty object - continue - - value_formatter = value_formatters.get(old_key) + DEFAULT_ITEM_AMOUNTS = { + ITC04_DataField.TAXABLE_VALUE.value: 0, + ITC04_DataField.IGST.value: 0, + ITC04_DataField.CGST.value: 0, + ITC04_DataField.SGST.value: 0, + ITC04_DataField.CESS_AMOUNT.value: 0, + } - if callable(value_formatter): - output[new_key] = value_formatter(invoice_data_value) - else: - output[new_key] = invoice_data_value + FLOAT_FIELDS = { + GovDataField.TAXABLE_VALUE.value, + GovDataField.IGST.value, + GovDataField.CGST.value, + GovDataField.SGST.value, + GovDataField.CESS_AMOUNT.value, + GovDataField.QUANTITY.value, + } - return output + def __init__(self): + super().__init__() - def map_uom(self, uom): + def map_uom(self, uom, *args): uom = uom.upper() if "-" in uom: @@ -89,33 +51,25 @@ def map_uom(self, uom): return f"OTH-{UOM_MAP.get('OTH')}" - def map_place_of_supply(self, state_code): - if state_code.isnumeric(): - return f"{state_code}-{self.STATE_NUMBERS.get(state_code)}" - - return state_code.split("-")[0] - - def reverse_dict(self, data): - return {v: k for k, v in data.items()} - -class TABLE5A(GovDataMapper): - CATEGORY = GovJsonKey.TABLE5A.value +class TABLE5A(ITC04DataMapper): + CATEGORY = ITC04JsonKey.TABLE5A.value KEY_MAPPING = { GovDataField.COMPANY_GSTIN.value: ITC04_DataField.COMPANY_GSTIN.value, GovDataField.JOB_WORKER_STATE_CODE.value: ITC04_DataField.JOB_WORKER_STATE_CODE.value, GovDataField.ITEMS.value: ITC04_DataField.ITEMS.value, - GovDataField.ORIGINAL_CAHLLAN_NUMBER.value: ITC04_DataField.ORIGINAL_CAHLLAN_NUMBER.value, + GovDataField.ORIGINAL_CHALLAN_NUMBER.value: ITC04_DataField.ORIGINAL_CHALLAN_NUMBER.value, GovDataField.ORIGINAL_CHALLAN_DATE.value: ITC04_DataField.ORIGINAL_CHALLAN_DATE.value, - GovDataField.CHALLAN_NUMBER.value: ITC04_DataField.CHALLAN_NUMBER.value, - GovDataField.CHALLAN_DATE.value: ITC04_DataField.CHALLAN_DATE.value, + GovDataField.JOB_WORK_CHALLAN_NUMBER.value: ITC04_DataField.JOB_WORK_CHALLAN_NUMBER.value, + GovDataField.JOB_WORK_CHALLAN_DATE.value: ITC04_DataField.JOB_WORK_CHALLAN_DATE.value, GovDataField.NATURE_OF_JOB.value: ITC04_DataField.NATURE_OF_JOB.value, GovDataField.UOM.value: ITC04_DataField.UOM.value, - GovDataField.QTY.value: ITC04_DataField.QTY.value, + GovDataField.QUANTITY.value: ITC04_DataField.QUANTITY.value, GovDataField.DESCRIPTION.value: ITC04_DataField.DESCRIPTION.value, GovDataField.LOSS_UOM.value: ITC04_DataField.LOSS_UOM.value, GovDataField.LOSS_QTY.value: ITC04_DataField.LOSS_QTY.value, + GovDataField.FLAG.value: ITC04_DataField.FLAG.value, } def __init__(self): @@ -140,9 +94,14 @@ def convert_to_internal_data_format(self, input_data): for invoice in input_data: original_challan_number = invoice.get(GovDataField.ITEMS.value)[0].get( - GovDataField.ORIGINAL_CAHLLAN_NUMBER.value + GovDataField.ORIGINAL_CHALLAN_NUMBER.value + ) + job_work_challan_number = invoice.get(GovDataField.ITEMS.value)[0].get( + GovDataField.JOB_WORK_CHALLAN_NUMBER.value + ) + output[f"{original_challan_number} - {job_work_challan_number}"] = ( + self.format_data(invoice) ) - output[original_challan_number] = self.format_data(invoice) return {self.CATEGORY: output} @@ -156,7 +115,7 @@ def convert_to_gov_data_format(self, input_data, **kwargs): return output - def format_item_for_internal(self, items): + def format_item_for_internal(self, items, *args): return [ { **self.format_data(item), @@ -165,23 +124,124 @@ def format_item_for_internal(self, items): ] def format_item_for_gov(self, items, *args): + return [self.format_data(item, for_gov=True) for item in items] + + +class STOCK_ENTRY(ITC04DataMapper): + CATEGORY = ITC04JsonKey.STOCK_ENTRY.value + + KEY_MAPPING = { + GovDataField.COMPANY_GSTIN.value: ITC04_DataField.COMPANY_GSTIN.value, + GovDataField.JOB_WORKER_STATE_CODE.value: ITC04_DataField.JOB_WORKER_STATE_CODE.value, + GovDataField_SE.ITEMS.value: ITC04_DataField.ITEMS.value, + GovDataField_SE.ORIGINAL_CHALLAN_NUMBER.value: ITC04_DataField.ORIGINAL_CHALLAN_NUMBER.value, + GovDataField_SE.ORIGINAL_CHALLAN_DATE.value: ITC04_DataField.ORIGINAL_CHALLAN_DATE.value, + GovDataField.UOM.value: ITC04_DataField.UOM.value, + GovDataField.QUANTITY.value: ITC04_DataField.QUANTITY.value, + GovDataField.DESCRIPTION.value: ITC04_DataField.DESCRIPTION.value, + GovDataField.TAXABLE_VALUE.value: ITC04_DataField.TAXABLE_VALUE.value, + GovDataField.GOODS_TYPE.value: ITC04_DataField.GOODS_TYPE.value, + GovDataField.IGST.value: ITC04_DataField.IGST.value, + GovDataField.CGST.value: ITC04_DataField.CGST.value, + GovDataField.SGST.value: ITC04_DataField.SGST.value, + GovDataField.CESS_AMOUNT.value: ITC04_DataField.CESS_AMOUNT.value, + GovDataField.FLAG.value: ITC04_DataField.FLAG.value, + } + + def __init__(self): + super().__init__() + + self.value_formatters_for_internal = { + GovDataField_SE.ITEMS.value: self.format_item_for_internal, + GovDataField.UOM.value: self.map_uom, + GovDataField.JOB_WORKER_STATE_CODE.value: self.map_place_of_supply, + } + + self.value_formatters_for_gov = { + ITC04_DataField.ITEMS.value: self.format_item_for_gov, + ITC04_DataField.UOM.value: self.map_uom, + ITC04_DataField.JOB_WORKER_STATE_CODE.value: self.map_place_of_supply, + } + + def convert_to_internal_data_format(self, input_data): + output = {} + + for invoice in input_data: + original_challan_number = invoice.get( + GovDataField_SE.ORIGINAL_CHALLAN_NUMBER.value + ) + + invoice_level_data = self.format_data(invoice) + + self.update_totals( + invoice_level_data, + invoice_level_data.get(ITC04_DataField.ITEMS.value), + ) + output[str(original_challan_number)] = invoice_level_data + + return {self.CATEGORY: output} + + def convert_to_gov_data_format(self, input_data, **kwargs): + output = [] + + for invoice in input_data: + output.append(self.format_data(invoice, for_gov=True)) + + return output + + def format_item_for_internal(self, items, *args): return [ { - **self.format_data(item, for_gov=True), + **self.DEFAULT_ITEM_AMOUNTS.copy(), + **self.format_data(item), } for item in items ] + def format_item_for_gov(self, items, *args): + return [self.format_data(item, for_gov=True) for item in items] -class TABLE5B(TABLE5A): - CATEGORY = GovJsonKey.TABLE5B.value - def __init__(self): - super().__init__() +CLASS_MAP = { + GovJsonKey.TABLE5A.value: TABLE5A, + GovJsonKey.STOCK_ENTRY.value: STOCK_ENTRY, +} +CATEGORY_MAP = { + GovJsonKey.TABLE5A.value: ITC04JsonKey.TABLE5A.value, + GovJsonKey.STOCK_ENTRY.value: ITC04JsonKey.STOCK_ENTRY.value, +} -class ITC04DataMapper(GSTRDataMapper): - CLASS_MAP = { - GovJsonKey.TABLE5A.value: TABLE5A, - GovJsonKey.TABLE5B.value: TABLE5B, - } + +def convert_to_internal_data_format(gov_data): + """ + Converts Gov data format to internal data format for all categories + """ + output = {} + + for category, mapper_class in CLASS_MAP.items(): + if not gov_data.get(category): + continue + + output.update( + mapper_class().convert_to_internal_data_format(gov_data.get(category)) + ) + + return output + + +def convert_to_gov_data_format(internal_data: dict, company_gstin: str) -> dict: + """ + converts internal data format to Gov data format for all categories + """ + + output = {} + for category, mapper_class in CLASS_MAP.items(): + if not internal_data.get(CATEGORY_MAP.get(category)): + continue + + output[category] = mapper_class().convert_to_gov_data_format( + internal_data.get(CATEGORY_MAP.get(category)), company_gstin=company_gstin + ) + + return output