diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f096c513f..e646da466 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,8 +20,8 @@ repos: hooks: - id: black - - repo: https://github.com/timothycrosley/isort - rev: 5.10.1 + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 hooks: - id: isort diff --git a/README.md b/README.md index 2f2ada991..92a22fea4 100644 --- a/README.md +++ b/README.md @@ -32,17 +32,26 @@ For a detailed overview of these features, please [refer to the documentation](h Once you've [set up a Frappe site](https://frappeframework.com/docs/v14/user/en/installation/), installing India Compliance is simple: -1. Download the app using the Bench CLI +1. Download the app using the Bench CLI. - ```bash - bench get-app https://github.com/resilient-tech/india-compliance.git - ``` + ```bash + bench get-app --branch [branch name] https://github.com/resilient-tech/india-compliance.git + ``` -2. Install the app on your site + Replace `[branch name]` with the appropriate branch as per your setup: - ```bash - bench --site [site name] install-app india_compliance - ``` + | Frappe Branch | India Compliance Branch | + |---------------|-------------------------| + | version-14 | version-14 | + | develop | next | + + If it isn't specified, the `--branch` option will default to `next`. + +2. Install the app on your site. + + ```bash + bench --site [site name] install-app india_compliance + ``` ## In-app Purchases diff --git a/india_compliance/gst_india/api_classes/base.py b/india_compliance/gst_india/api_classes/base.py index 77ad6f64a..239b98d34 100644 --- a/india_compliance/gst_india/api_classes/base.py +++ b/india_compliance/gst_india/api_classes/base.py @@ -171,6 +171,13 @@ def _make_request( log.output = response_json enqueue_integration_request(**log) + if self.sandbox_mode and not frappe.flags.ic_sandbox_message_shown: + frappe.msgprint( + _("GST API request was made in Sandbox Mode"), + alert=True, + ) + frappe.flags.ic_sandbox_message_shown = True + def handle_failed_response(self, response_json): # Override in subclass, return truthy value to stop frappe.throw pass diff --git a/india_compliance/gst_india/api_classes/e_invoice.py b/india_compliance/gst_india/api_classes/e_invoice.py index 5e44657b4..f7d16a17e 100644 --- a/india_compliance/gst_india/api_classes/e_invoice.py +++ b/india_compliance/gst_india/api_classes/e_invoice.py @@ -11,6 +11,12 @@ class EInvoiceAPI(BaseAPI): API_NAME = "e-Invoice" BASE_PATH = "ei/api" SENSITIVE_HEADERS = BaseAPI.SENSITIVE_HEADERS + ("password",) + IGNORED_ERROR_CODES = { + "2150": "Duplicate IRN", + "2283": ( + "IRN details cannot be provided as it is generated more than 2 days ago" + ), + } def setup(self, doc=None, *, company_gstin=None): if not self.settings.enable_e_invoice: @@ -43,9 +49,12 @@ def setup(self, doc=None, *, company_gstin=None): ) def handle_failed_response(self, response_json): - # Don't fail in case of Duplicate IRN - if response_json.get("message").startswith("2150"): - return True + message = response_json.get("message", "").strip() + + for error_code in self.IGNORED_ERROR_CODES: + if message.startswith(error_code): + response_json.error_code = error_code + return True def get_e_invoice_by_irn(self, irn): return self.get(endpoint="invoice/irn", params={"irn": irn}) diff --git a/india_compliance/gst_india/api_classes/returns.py b/india_compliance/gst_india/api_classes/returns.py index eedf3ee2a..d0baaccdf 100644 --- a/india_compliance/gst_india/api_classes/returns.py +++ b/india_compliance/gst_india/api_classes/returns.py @@ -28,11 +28,14 @@ def setup(self, company_gstin): ) def handle_failed_response(self, response_json): - if response_json.get("errorCode") in self.IGNORED_ERROR_CODES: + error_code = response_json.get("errorCode") + + if error_code in self.IGNORED_ERROR_CODES: + response_json.error_type = self.IGNORED_ERROR_CODES[error_code] return True def get(self, action, return_period, otp=None, params=None): - response = super().get( + return super().get( params={"action": action, "gstin": self.company_gstin, **(params or {})}, headers={ "requestid": self.generate_request_id(), @@ -41,11 +44,6 @@ def get(self, action, return_period, otp=None, params=None): }, ) - if error_type := self.IGNORED_ERROR_CODES.get(response.errorCode): - response.error_type = error_type - - return response - class GSTR2bAPI(ReturnsAPI): API_NAME = "GSTR-2B" diff --git a/india_compliance/gst_india/client_scripts/item.js b/india_compliance/gst_india/client_scripts/item.js index cceb1ec89..1bc374f7f 100644 --- a/india_compliance/gst_india/client_scripts/item.js +++ b/india_compliance/gst_india/client_scripts/item.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Item', { if ((!frm.doc.taxes || !frm.doc.taxes.length) && frm.doc.gst_hsn_code) { frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc => { $.each(hsn_doc.taxes || [], function(i, tax) { - let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes'); + let a = frappe.model.add_child(frm.doc, 'Item Tax', 'taxes'); a.item_tax_template = tax.item_tax_template; a.tax_category = tax.tax_category; a.valid_from = tax.valid_from; diff --git a/india_compliance/gst_india/client_scripts/sales_invoice_list.js b/india_compliance/gst_india/client_scripts/sales_invoice_list.js index feda7ae94..426b2d648 100644 --- a/india_compliance/gst_india/client_scripts/sales_invoice_list.js +++ b/india_compliance/gst_india/client_scripts/sales_invoice_list.js @@ -1,29 +1,111 @@ -const erpnext_onload = frappe.listview_settings["Sales Invoice"].onload; -frappe.listview_settings["Sales Invoice"].onload = function (list_view) { +const DOCTYPE = "Sales Invoice"; +const erpnext_onload = frappe.listview_settings[DOCTYPE].onload; +frappe.listview_settings[DOCTYPE].onload = function (list_view) { if (erpnext_onload) { erpnext_onload(list_view); } - const action = async () => { + if (!frappe.perm.has_perm(DOCTYPE, 0, "submit")) return; + + if (gst_settings.enable_e_waybill) + add_bulk_action_for_submitted_invoices( + list_view, + __("Generate e-Waybill JSON"), + generate_e_waybill_json + ); + + if (ic.is_e_invoice_enabled()) + add_bulk_action_for_submitted_invoices( + list_view, + __("Enqueue Bulk e-Invoice Generation"), + enqueue_bulk_e_invoice_generation + ); +}; + +function add_bulk_action_for_submitted_invoices(list_view, label, callback) { + list_view.page.add_actions_menu_item(label, async () => { const selected_docs = list_view.get_checked_items(); - const docnames = list_view.get_checked_items(true); - - for (let doc of selected_docs) { - if (doc.docstatus !== 1) { - frappe.throw( - __("e-Waybill JSON can only be generated from a submitted document") - ); - } + const submitted_docs = await validate_if_submitted(selected_docs); + if (submitted_docs) callback(submitted_docs); + }); +} + +async function generate_e_waybill_json(docnames) { + const ewb_data = await frappe.xcall( + "india_compliance.gst_india.utils.e_waybill.generate_e_waybill_json", + { doctype: DOCTYPE, docnames } + ); + + trigger_file_download(ewb_data, get_e_waybill_file_name()); +} + +async function enqueue_bulk_e_invoice_generation(docnames) { + const now = frappe.datetime.system_datetime(); + + const job_id = await frappe.xcall( + "india_compliance.gst_india.utils.e_invoice.enqueue_bulk_e_invoice_generation", + { docnames } + ); + + const creation_filter = `[">", "${now}"]`; + const api_requests_link = frappe.utils.generate_route({ + type: "doctype", + name: "Integration Request", + route_options: { + integration_request_service: "India Compliance API", + creation: creation_filter, + }, + }); + const error_logs_link = frappe.utils.generate_route({ + type: "doctype", + name: "Error Log", + route_options: { + creation: creation_filter, + }, + }); + + frappe.msgprint( + __( + `Bulk e-Invoice Generation has been queued. You can track the + Background Job, + API Request(s), + and Error Log(s).`, + [ + frappe.utils.get_form_link("RQ Job", job_id), + api_requests_link, + error_logs_link, + ] + ) + ); +} + +async function validate_if_submitted(selected_docs) { + const valid_docs = []; + const invalid_docs = []; + + for (const doc of selected_docs) { + if (doc.docstatus != 1) { + invalid_docs.push(doc.name); + } else { + valid_docs.push(doc.name); } + } - const ewb_data = await frappe.xcall( - "india_compliance.gst_india.utils.e_waybill.generate_e_waybill_json", - { doctype: list_view.doctype, docnames } - ); + if (!invalid_docs.length) return valid_docs; - trigger_file_download(ewb_data, get_e_waybill_file_name()); - }; + if (!valid_docs.length) { + frappe.throw(__("This action can only be performed on submitted documents")); + } - list_view.page.add_actions_menu_item(__("Generate e-Waybill JSON"), action, false); + const confirmed = await new Promise(resolve => { + frappe.confirm( + __( + "This action can only be performed on submitted documents. Do you want to continue without the following documents?

{0}", + [invalid_docs.join("
")] + ), + () => resolve(true) + ); + }); -}; + return confirmed ? valid_docs : false; +} diff --git a/india_compliance/gst_india/constants/custom_fields.py b/india_compliance/gst_india/constants/custom_fields.py index 08771dbd8..5ea162de5 100644 --- a/india_compliance/gst_india/constants/custom_fields.py +++ b/india_compliance/gst_india/constants/custom_fields.py @@ -116,7 +116,7 @@ "fieldname": "gst_section", "label": "GST Details", "fieldtype": "Section Break", - "insert_after": "language", + "insert_after": "gst_vehicle_type", "print_hide": 1, "collapsible": 1, }, @@ -554,6 +554,7 @@ "fieldtype": "Link", "options": "GST HSN Code", "insert_after": "item_group", + "allow_in_quick_entry": 1, }, { "fieldname": "is_nil_exempt", @@ -600,10 +601,7 @@ "insert_after": "customer", "no_copy": 1, "print_hide": 1, - "depends_on": ( - 'eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed' - ' Export"], doc.gst_category)' - ), + "depends_on": 'eval:doc.gst_category != "Unregistered"', "translatable": 0, }, { @@ -681,7 +679,7 @@ "fieldname": "transporter_info", "label": "Transporter Info", "fieldtype": "Section Break", - "insert_after": "terms", + "insert_after": "language", "collapsible": 1, "collapsible_depends_on": "transporter", "print_hide": 1, diff --git a/india_compliance/gst_india/data/test_e_invoice.json b/india_compliance/gst_india/data/test_e_invoice.json new file mode 100644 index 000000000..75879fc09 --- /dev/null +++ b/india_compliance/gst_india/data/test_e_invoice.json @@ -0,0 +1,533 @@ +{ + "goods_item_with_ewaybill": { + "kwargs": { + "vehicle_no": "GJ07DL9009", + "customer_address": "_Test Registered Customer-Billing", + "shipping_address_name": "_Test Registered Customer-Billing" + }, + "request_data": { + "BuyerDtls": { + "Addr1": "Test Address - 3", + "Gstin": "36AMBPG7773M002", + "LglNm": "_Test Registered Customer", + "Loc": "Test City", + "Pin": 500055, + "Pos": "01", + "Stcd": "36", + "TrdNm": "Test Registered Customer" + }, + "DispDtls": { + "Addr1": "Test Address - 1", + "Loc": "Test City", + "Nm": "Test Indian Registered Company", + "Pin": 193501, + "Stcd": "01" + }, + "DocDtls": { + "Dt": "16/09/2022", + "No": "test_invoice_no", + "Typ": "INV" + }, + "EwbDtls": { + "Distance": 0, + "TransMode": "1", + "VehNo": "GJ07DL9009", + "VehType": "R" + }, + "ItemList": [ + { + "AssAmt": 100, + "CesAmt": 0, + "CesNonAdvlAmt": 0, + "CesRt": 0, + "CgstAmt": 0, + "Discount": 0, + "GstRt": 0, + "HsnCd": "61149090", + "IgstAmt": 0, + "IsServc": "N", + "PrdDesc": "Test Trading Goods 1", + "Qty": 1, + "SgstAmt": 0, + "SlNo": "1", + "TotAmt": 100, + "TotItemVal": 100, + "Unit": "NOS", + "UnitPrice": 100 + } + ], + "PayDtls": { + "CrDay": 0, + "PaidAmt": 0, + "PaymtDue": 100 + }, + "SellerDtls": { + "Addr1": "Test Address - 1", + "Gstin": "01AMBPG7773M002", + "LglNm": "_Test Indian Registered Company", + "Loc": "Test City", + "Pin": 193501, + "Stcd": "01", + "TrdNm": "Test Indian Registered Company" + }, + "ShipDtls": { + "Addr1": "Test Address - 3", + "Gstin": "36AMBPG7773M002", + "LglNm": "Test Registered Customer", + "Loc": "Test City", + "Pin": 500055, + "Stcd": "36", + "TrdNm": "Test Registered Customer" + }, + "TranDtls": { + "RegRev": "N", + "SupTyp": "B2B", + "TaxSch": "GST" + }, + "ValDtls": { + "AssVal": 100, + "CesVal": 0, + "CgstVal": 0, + "Discount": 0, + "IgstVal": 0, + "OthChrg": 0, + "RndOffAmt": 0, + "SgstVal": 0, + "TotInvVal": 100 + }, + "Version": "1.1" + }, + "response_data": { + "info": [ + { + "InfCd": "EWBPPD", + "Desc": "Pin-Pin calc distance: 2467KM" + } + ], + "message": "IRN generated successfully", + "result": { + "AckDt": "2022-09-16 19:29:00", + "AckNo": 232210036743849, + "EwbDt": "2022-09-16 19:29:00", + "EwbNo": 391009149369, + "EwbValidTill": "2022-09-29 23:59:00", + "Irn": "706daeccda0ef6f818da78f3a2a05a1288731057373002289b46c3229289a2e7", + "Remarks": null, + "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoyMzIyMTAwMzY3NDM4NDksXCJBY2tEdFwiOlwiMjAyMi0wOS0xNiAxOToyOTowMFwiLFwiSXJuXCI6XCI3MDZkYWVjY2RhMGVmNmY4MThkYTc4ZjNhMmEwNWExMjg4NzMxMDU3MzczMDAyMjg5YjQ2YzMyMjkyODlhMmU3XCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJJTlZcIixcIk5vXCI6XCJTSU5WLUNGWS0wMDA2N1wiLFwiRHRcIjpcIjE2LzA5LzIwMjJcIn0sXCJTZWxsZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIlRyZE5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAxXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6MTkzNTAxLFwiU3RjZFwiOlwiMDFcIn0sXCJCdXllckR0bHNcIjp7XCJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJMZ2xObVwiOlwiX1Rlc3QgUmVnaXN0ZXJlZCBDdXN0b21lclwiLFwiVHJkTm1cIjpcIlRlc3QgUmVnaXN0ZXJlZCBDdXN0b21lclwiLFwiUG9zXCI6XCIwMVwiLFwiQWRkcjFcIjpcIlRlc3QgQWRkcmVzcyAtIDNcIixcIkxvY1wiOlwiVGVzdCBDaXR5XCIsXCJQaW5cIjo1MDAwNTUsXCJTdGNkXCI6XCIzNlwifSxcIkRpc3BEdGxzXCI6e1wiTm1cIjpcIlRlc3QgSW5kaWFuIFJlZ2lzdGVyZWQgQ29tcGFueVwiLFwiQWRkcjFcIjpcIlRlc3QgQWRkcmVzcyAtIDFcIixcIkxvY1wiOlwiVGVzdCBDaXR5XCIsXCJQaW5cIjoxOTM1MDEsXCJTdGNkXCI6XCIwMVwifSxcIlNoaXBEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIlRlc3QgUmVnaXN0ZXJlZCBDdXN0b21lclwiLFwiVHJkTm1cIjpcIlRlc3QgUmVnaXN0ZXJlZCBDdXN0b21lclwiLFwiQWRkcjFcIjpcIlRlc3QgQWRkcmVzcyAtIDNcIixcIkxvY1wiOlwiVGVzdCBDaXR5XCIsXCJQaW5cIjo1MDAwNTUsXCJTdGNkXCI6XCIzNlwifSxcIkl0ZW1MaXN0XCI6W3tcIkl0ZW1Ob1wiOjAsXCJTbE5vXCI6XCIxXCIsXCJJc1NlcnZjXCI6XCJOXCIsXCJQcmREZXNjXCI6XCJUZXN0IFRyYWRpbmcgR29vZHMgMVwiLFwiSHNuQ2RcIjpcIjYxMTQ5MDkwXCIsXCJRdHlcIjoxLjAsXCJVbml0XCI6XCJOT1NcIixcIlVuaXRQcmljZVwiOjEwMC4wLFwiVG90QW10XCI6MTAwLjAsXCJEaXNjb3VudFwiOjAsXCJBc3NBbXRcIjoxMDAuMCxcIkdzdFJ0XCI6MC4wLFwiSWdzdEFtdFwiOjAsXCJDZ3N0QW10XCI6MCxcIlNnc3RBbXRcIjowLFwiQ2VzUnRcIjowLFwiQ2VzQW10XCI6MCxcIkNlc05vbkFkdmxBbXRcIjowLFwiVG90SXRlbVZhbFwiOjEwMC4wfV0sXCJWYWxEdGxzXCI6e1wiQXNzVmFsXCI6MTAwLjAsXCJDZ3N0VmFsXCI6MCxcIlNnc3RWYWxcIjowLFwiSWdzdFZhbFwiOjAsXCJDZXNWYWxcIjowLFwiRGlzY291bnRcIjowLFwiT3RoQ2hyZ1wiOjAuMCxcIlJuZE9mZkFtdFwiOjAuMCxcIlRvdEludlZhbFwiOjEwMC4wfSxcIlBheUR0bHNcIjp7XCJDckRheVwiOjAsXCJQYWlkQW10XCI6MCxcIlBheW10RHVlXCI6MTAwLjB9LFwiRXdiRHRsc1wiOntcIlRyYW5zTW9kZVwiOlwiMVwiLFwiRGlzdGFuY2VcIjowLFwiVmVoTm9cIjpcIkdKMDdETDkwMDlcIixcIlZlaFR5cGVcIjpcIlJcIn19IiwiaXNzIjoiTklDIn0.ZOqrLJLsoXHf1QMRPBJoBesVluRB0a0ISsBGn6gqLuiJLfsAG1Oxmimqi9c7dboRnsW1eEj78Yps5D2A05WPMXwdkOy9Ahb_t4jXSGH-ijq_ed8z-xAtyiWH16YfIc9Zg020VkrlZiHdbfkx53hOwEA3aUhHIdPwQE5Kk-O9KWES3cttl9r5lrtzueTlTKB0GqXqiNlmuuQnCAJpWe34Coko1__kyPLLMdKgpOSB0EX2j7NjaZ5KPhu-GZHBtTvKczuSXvli6lwSQLaKpBm1IGvwMo2IzGW62pXp4XdMlcncLuc8wLTExSlKwHhsSspOxhMNBRx3NqcU0PQZOq050Q", + "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiU0lOVi1DRlktMDAwNjdcIixcIkRvY1R5cFwiOlwiSU5WXCIsXCJEb2NEdFwiOlwiMTYvMDkvMjAyMlwiLFwiVG90SW52VmFsXCI6MTAwLjAsXCJJdGVtQ250XCI6MSxcIk1haW5Ic25Db2RlXCI6XCI2MTE0OTA5MFwiLFwiSXJuXCI6XCI3MDZkYWVjY2RhMGVmNmY4MThkYTc4ZjNhMmEwNWExMjg4NzMxMDU3MzczMDAyMjg5YjQ2YzMyMjkyODlhMmU3XCIsXCJJcm5EdFwiOlwiMjAyMi0wOS0xNiAxOToyOTowMFwifSIsImlzcyI6Ik5JQyJ9.j7Fpl3ol0G6akp1-ukVzOK-8Dqoey3iKLf9SCaXGfb3crIcpniezqevH1qBTgCtUDYnOa0tRk5Nhyi-ER-W8Hu2a4Ug28AJFp3S8Xv2RwdMe9HvJN1b8KBKz6N4_WcO7wD2VcXyoEKDuTP2KFlXRjuZx7tBh5ttjQ4vRNtVwpR2qy-lRtMquEbZsJ-JOPBLUTXimdpVwt9EW8xKUxRKT_7-8kwK-DGHePADVBUjD6kv-GSpbxgfM4UAPg1TRlRz_BHMbbi9adZVZn5l9GA-WRuSP_7C-Qd_ucYg1cmP2zswh1XClMEjwjmxpuFkhqdDsRfl8unnEi--FxA0lvn4nmw", + "Status": "ACT", + "distance": 2467 + }, + "success": true + } + }, + "service_item": { + "kwargs": { + "item_code": "_Test Service Item", + "customer_address": "_Test Registered Customer-Billing", + "shipping_address_name": "_Test Registered Customer-Billing" + }, + "request_data": { + "BuyerDtls": { + "Addr1": "Test Address - 3", + "Gstin": "36AMBPG7773M002", + "LglNm": "_Test Registered Customer", + "Loc": "Test City", + "Pin": 500055, + "Pos": "01", + "Stcd": "36", + "TrdNm": "Test Registered Customer" + }, + "DispDtls": { + "Addr1": "Test Address - 1", + "Loc": "Test City", + "Nm": "Test Indian Registered Company", + "Pin": 193501, + "Stcd": "01" + }, + "DocDtls": { + "Dt": "17/09/2022", + "No": "test_invoice_no", + "Typ": "INV" + }, + "EwbDtls": { + "Distance": 0 + }, + "ItemList": [ + { + "AssAmt": 100.0, + "CesAmt": 0, + "CesNonAdvlAmt": 0, + "CesRt": 0, + "CgstAmt": 0, + "Discount": 0, + "GstRt": 0.0, + "HsnCd": "999900", + "IgstAmt": 0, + "IsServc": "Y", + "PrdDesc": "Test Service Item", + "Qty": 1.0, + "SgstAmt": 0, + "SlNo": "1", + "TotAmt": 100.0, + "TotItemVal": 100.0, + "Unit": "NOS", + "UnitPrice": 100.0 + } + ], + "PayDtls": { + "CrDay": 0, + "PaidAmt": 0, + "PaymtDue": 100.0 + }, + "SellerDtls": { + "Addr1": "Test Address - 1", + "Gstin": "01AMBPG7773M002", + "LglNm": "_Test Indian Registered Company", + "Loc": "Test City", + "Pin": 193501, + "Stcd": "01", + "TrdNm": "Test Indian Registered Company" + }, + "ShipDtls": { + "Addr1": "Test Address - 3", + "Gstin": "36AMBPG7773M002", + "LglNm": "Test Registered Customer", + "Loc": "Test City", + "Pin": 500055, + "Stcd": "36", + "TrdNm": "Test Registered Customer" + }, + "TranDtls": { + "RegRev": "N", + "SupTyp": "B2B", + "TaxSch": "GST" + }, + "ValDtls": { + "AssVal": 100.0, + "CesVal": 0, + "CgstVal": 0, + "Discount": 0, + "IgstVal": 0, + "OthChrg": 0.0, + "RndOffAmt": 0.0, + "SgstVal": 0, + "TotInvVal": 100.0 + }, + "Version": "1.1" + }, + "response_data": { + "success": true, + "message": "IRN generated successfully", + "result": { + "AckNo": 232210036754863, + "AckDt": "2022-09-17 16:26:00", + "Irn": "68fb4fab44aee99fb23292478c4bd838e664837c9f1b04e3d9134ffed0b40b60", + "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoyMzIyMTAwMzY3NTQ4NjMsXCJBY2tEdFwiOlwiMjAyMi0wOS0xNyAxNjoyNjowMFwiLFwiSXJuXCI6XCI2OGZiNGZhYjQ0YWVlOTlmYjIzMjkyNDc4YzRiZDgzOGU2NjQ4MzdjOWYxYjA0ZTNkOTEzNGZmZWQwYjQwYjYwXCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJJTlZcIixcIk5vXCI6XCJnMnF4aFlcIixcIkR0XCI6XCIxNy8wOS8yMDIyXCJ9LFwiU2VsbGVyRHRsc1wiOntcIkdzdGluXCI6XCIwMUFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJfVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJUcmRObVwiOlwiVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJBZGRyMVwiOlwiVGVzdCBBZGRyZXNzIC0gMVwiLFwiTG9jXCI6XCJUZXN0IENpdHlcIixcIlBpblwiOjE5MzUwMSxcIlN0Y2RcIjpcIjAxXCJ9LFwiQnV5ZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlBvc1wiOlwiMDFcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJEaXNwRHRsc1wiOntcIk5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAxXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6MTkzNTAxLFwiU3RjZFwiOlwiMDFcIn0sXCJTaGlwRHRsc1wiOntcIkdzdGluXCI6XCIzNkFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJJdGVtTGlzdFwiOlt7XCJJdGVtTm9cIjowLFwiU2xOb1wiOlwiMVwiLFwiSXNTZXJ2Y1wiOlwiWVwiLFwiUHJkRGVzY1wiOlwiVGVzdCBTZXJ2aWNlIEl0ZW1cIixcIkhzbkNkXCI6XCI5OTU0MTFcIixcIlF0eVwiOjEuMCxcIlVuaXRcIjpcIk5PU1wiLFwiVW5pdFByaWNlXCI6MTAwLjAsXCJUb3RBbXRcIjoxMDAuMCxcIkRpc2NvdW50XCI6MCxcIkFzc0FtdFwiOjEwMC4wLFwiR3N0UnRcIjowLjAsXCJJZ3N0QW10XCI6MCxcIkNnc3RBbXRcIjowLFwiU2dzdEFtdFwiOjAsXCJDZXNSdFwiOjAsXCJDZXNBbXRcIjowLFwiQ2VzTm9uQWR2bEFtdFwiOjAsXCJUb3RJdGVtVmFsXCI6MTAwLjB9XSxcIlZhbER0bHNcIjp7XCJBc3NWYWxcIjoxMDAuMCxcIkNnc3RWYWxcIjowLFwiU2dzdFZhbFwiOjAsXCJJZ3N0VmFsXCI6MCxcIkNlc1ZhbFwiOjAsXCJEaXNjb3VudFwiOjAsXCJPdGhDaHJnXCI6MC4wLFwiUm5kT2ZmQW10XCI6MC4wLFwiVG90SW52VmFsXCI6MTAwLjB9LFwiUGF5RHRsc1wiOntcIkNyRGF5XCI6MCxcIlBhaWRBbXRcIjowLFwiUGF5bXREdWVcIjoxMDAuMH0sXCJFd2JEdGxzXCI6e1wiRGlzdGFuY2VcIjowfX0iLCJpc3MiOiJOSUMifQ.rcfXkciqDJypX-xCqaUU3xAk2gccHK_qBD_FBIUsEr-SyWVs4LStgXwQEWUhTYnEfcGGm_sWX15ewC0jn9iWVFCNNFnjKc8vsFQqnbzvi-bnr6CWkjRXOxVqQTfis6PbtTrblojBq2hhBaT1B_ZlgLePi5qFNWnxxaHItjYtBEeBzW5JxzXWQTqESrBy02iLQgQMOexmQ6jKGdUR3tRRG5MVB7QfXbL9BpQr-DXbHhbDGllT2S_xyj9SBsN6ICeluCG-ZJdHE_kCoIxc8iY_nXneb2PsBciij14cHb96h4ddNZtapxTPTh4CumVDmAgLBSVsBTugp8vm0L-jd7n3dg", + "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiZzJxeGhZXCIsXCJEb2NUeXBcIjpcIklOVlwiLFwiRG9jRHRcIjpcIjE3LzA5LzIwMjJcIixcIlRvdEludlZhbFwiOjEwMC4wLFwiSXRlbUNudFwiOjEsXCJNYWluSHNuQ29kZVwiOlwiOTk1NDExXCIsXCJJcm5cIjpcIjY4ZmI0ZmFiNDRhZWU5OWZiMjMyOTI0NzhjNGJkODM4ZTY2NDgzN2M5ZjFiMDRlM2Q5MTM0ZmZlZDBiNDBiNjBcIixcIklybkR0XCI6XCIyMDIyLTA5LTE3IDE2OjI2OjAwXCJ9IiwiaXNzIjoiTklDIn0.A99BPXfKiGSNjnEcqmxc7RGutWakaeW0NMan9oC5yMw6zAoTNcVc34GtQKV7iajBZQhyiFrNwn5n6QtYOXafpitHcI_yrUWSojQBPPpPlslqj4hbnbCy7kmOZZ8mOKISHJrsIZJxpjRlSquIzfDN4aP1aT_qHDqwFqyA8RyJM-id5EpDaTrUFK12HwjKfAXHn4shEUBBgEWrHYOKK6VdpCNi6F_5I5bbJRivrvUJxMLjk3Ux9fyylqnEeyE2NThs9hFuV9EgoVzGE3FhfPZsooToAuG_npYEv3f6Q9KbOw3pNQ3NkqFwvmFjfNJLXdbxZIPe9fe9F1c-CRrIoNo_9w", + "Status": "ACT", + "EwbNo": null, + "EwbDt": null, + "EwbValidTill": null, + "Remarks": null + }, + "info": [ + { + "InfCd": "EWBERR", + "Desc": [ + { + "ErrorCode": "4019", + "ErrorMessage": "Provide Transporter ID in order to generate Part A of e-Way Bill" + } + ] + } + ] + } + }, + "return_invoice": { + "kwargs": { + "qty": -1, + "is_return": 1, + "customer_address": "_Test Registered Customer-Billing", + "shipping_address_name": "_Test Registered Customer-Billing" + }, + "request_data": { + "BuyerDtls": { + "Addr1": "Test Address - 3", + "Gstin": "36AMBPG7773M002", + "LglNm": "_Test Registered Customer", + "Loc": "Test City", + "Pin": 500055, + "Pos": "01", + "Stcd": "36", + "TrdNm": "Test Registered Customer" + }, + "DispDtls": { + "Addr1": "Test Address - 3", + "Loc": "Test City", + "Nm": "Test Registered Customer", + "Pin": 500055, + "Stcd": "36" + }, + "DocDtls": { + "Dt": "17/09/2022", + "No": "test_invoice_no", + "Typ": "CRN" + }, + "EwbDtls": { + "Distance": 0 + }, + "ItemList": [ + { + "AssAmt": 100.0, + "CesAmt": 0, + "CesNonAdvlAmt": 0, + "CesRt": 0, + "CgstAmt": 0, + "Discount": 0, + "GstRt": 0.0, + "HsnCd": "61149090", + "IgstAmt": 0, + "IsServc": "N", + "PrdDesc": "Test Trading Goods 1", + "Qty": 1.0, + "SgstAmt": 0, + "SlNo": "1", + "TotAmt": 100.0, + "TotItemVal": 100.0, + "Unit": "NOS", + "UnitPrice": 100.0 + } + ], + "PayDtls": { + "CrDay": 0, + "PaidAmt": 0, + "PaymtDue": 0.0 + }, + "RefDtls": { + "PrecDocDtls": [ + { + "InvDt": "17/09/2022", + "InvNo": "SINV-CFY-00092" + } + ] + }, + "SellerDtls": { + "Addr1": "Test Address - 1", + "Gstin": "01AMBPG7773M002", + "LglNm": "_Test Indian Registered Company", + "Loc": "Test City", + "Pin": 193501, + "Stcd": "01", + "TrdNm": "Test Indian Registered Company" + }, + "ShipDtls": { + "Addr1": "Test Address - 1", + "Gstin": "01AMBPG7773M002", + "LglNm": "Test Indian Registered Company", + "Loc": "Test City", + "Pin": 193501, + "Stcd": "01", + "TrdNm": "Test Indian Registered Company" + }, + "TranDtls": { + "RegRev": "N", + "SupTyp": "B2B", + "TaxSch": "GST" + }, + "ValDtls": { + "AssVal": 100.0, + "CesVal": 0, + "CgstVal": 0, + "Discount": 0, + "IgstVal": 0, + "OthChrg": 0.0, + "RndOffAmt": -0.0, + "SgstVal": 0, + "TotInvVal": 100.0 + }, + "Version": "1.1" + }, + "response_data": { + "success": true, + "message": "IRN generated successfully", + "result": { + "AckNo": 232210036755145, + "AckDt": "2022-09-17 17:05:00", + "Irn": "1c96258af085e45da556494ea5e5a7b401a598ab80af4136309c2dac7b54d795", + "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoyMzIyMTAwMzY3NTUxNDUsXCJBY2tEdFwiOlwiMjAyMi0wOS0xNyAxNzowNTowMFwiLFwiSXJuXCI6XCIxYzk2MjU4YWYwODVlNDVkYTU1NjQ5NGVhNWU1YTdiNDAxYTU5OGFiODBhZjQxMzYzMDljMmRhYzdiNTRkNzk1XCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJDUk5cIixcIk5vXCI6XCJnMnF4aFlcIixcIkR0XCI6XCIxNy8wOS8yMDIyXCJ9LFwiU2VsbGVyRHRsc1wiOntcIkdzdGluXCI6XCIwMUFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJfVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJUcmRObVwiOlwiVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJBZGRyMVwiOlwiVGVzdCBBZGRyZXNzIC0gMVwiLFwiTG9jXCI6XCJUZXN0IENpdHlcIixcIlBpblwiOjE5MzUwMSxcIlN0Y2RcIjpcIjAxXCJ9LFwiQnV5ZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlBvc1wiOlwiMDFcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJEaXNwRHRsc1wiOntcIk5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJTaGlwRHRsc1wiOntcIkdzdGluXCI6XCIwMUFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIlRyZE5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAxXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6MTkzNTAxLFwiU3RjZFwiOlwiMDFcIn0sXCJJdGVtTGlzdFwiOlt7XCJJdGVtTm9cIjowLFwiU2xOb1wiOlwiMVwiLFwiSXNTZXJ2Y1wiOlwiTlwiLFwiUHJkRGVzY1wiOlwiVGVzdCBUcmFkaW5nIEdvb2RzIDFcIixcIkhzbkNkXCI6XCI2MTE0OTA5MFwiLFwiUXR5XCI6MS4wLFwiVW5pdFwiOlwiTk9TXCIsXCJVbml0UHJpY2VcIjoxMDAuMCxcIlRvdEFtdFwiOjEwMC4wLFwiRGlzY291bnRcIjowLFwiQXNzQW10XCI6MTAwLjAsXCJHc3RSdFwiOjAuMCxcIklnc3RBbXRcIjowLFwiQ2dzdEFtdFwiOjAsXCJTZ3N0QW10XCI6MCxcIkNlc1J0XCI6MCxcIkNlc0FtdFwiOjAsXCJDZXNOb25BZHZsQW10XCI6MCxcIlRvdEl0ZW1WYWxcIjoxMDAuMH1dLFwiVmFsRHRsc1wiOntcIkFzc1ZhbFwiOjEwMC4wLFwiQ2dzdFZhbFwiOjAsXCJTZ3N0VmFsXCI6MCxcIklnc3RWYWxcIjowLFwiQ2VzVmFsXCI6MCxcIkRpc2NvdW50XCI6MCxcIk90aENocmdcIjowLjAsXCJSbmRPZmZBbXRcIjowLjAsXCJUb3RJbnZWYWxcIjoxMDAuMH0sXCJQYXlEdGxzXCI6e1wiQ3JEYXlcIjowLFwiUGFpZEFtdFwiOjAsXCJQYXltdER1ZVwiOjAuMH0sXCJSZWZEdGxzXCI6e1wiUHJlY0RvY0R0bHNcIjpbe1wiSW52Tm9cIjpcIlNJTlYtQ0ZZLTAwMDkyXCIsXCJJbnZEdFwiOlwiMTcvMDkvMjAyMlwifV19LFwiRXdiRHRsc1wiOntcIkRpc3RhbmNlXCI6MH19IiwiaXNzIjoiTklDIn0.OZpYYN2pXIPJmLTi79NuVxVWWWGwdUzdnIEKXpgXyCEtXqgfwm9opf6EbyEt6wjwiYV4b2HWAfmkOXYJ-TO3dMc_98nOW8SeupGC6k-aV5kcDK9DNiRNWZBCiTMCRhlfmYC3oEZtd5f6yVoHvESl-QKeBlXcK2L8M1C0Guuq3QYqyhFp_gFqz_xY6ImKi65hEMDBghCaKINatRvPtFiwmtIspxUaHid5YcyqXdBxerEDjz-D2RjHOnhq1K_JX07X9xqcncLa106XltEeVFswJiEfqwtZTKDzkd4klymGJL5je7L1tgO8sNTKXGtFETKZfKmFSvxsMrBy1Dlp0LXXtg", + "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiZzJxeGhZXCIsXCJEb2NUeXBcIjpcIkNSTlwiLFwiRG9jRHRcIjpcIjE3LzA5LzIwMjJcIixcIlRvdEludlZhbFwiOjEwMC4wLFwiSXRlbUNudFwiOjEsXCJNYWluSHNuQ29kZVwiOlwiNjExNDkwOTBcIixcIklyblwiOlwiMWM5NjI1OGFmMDg1ZTQ1ZGE1NTY0OTRlYTVlNWE3YjQwMWE1OThhYjgwYWY0MTM2MzA5YzJkYWM3YjU0ZDc5NVwiLFwiSXJuRHRcIjpcIjIwMjItMDktMTcgMTc6MDU6MDBcIn0iLCJpc3MiOiJOSUMifQ.fgLb2ILeWK-MHxs61jZeo3kgy6w-sFXkVSh9GbzH0yWCeVx8MTsRwkKEXklPlmdwudG1lUPVjWsOioCgLL1Q8EE_wqnpGY00JEWr5X74GduBLe0lo5ZKtsSIVf10REy1E7_JJV8qitSZa3JeSqdjPlTUowFpxvPiw-nu3fyBP92IdVFbGO6oMvodI66kanqEyKj26Rn0nfnHP3KT3u61RBUSaVm_gch79cnPQanDPrJtd_0Ra2Vn7FoopdfcNIEdASB71IDRHsMFCNs8LyHTtFoJVI2LqU8wic_A6oWZkswOTBHdsBauMa_CMF9-2QbwTHFv60yvS7KuS2HvBw-oyQ", + "Status": "ACT", + "EwbNo": null, + "EwbDt": null, + "EwbValidTill": null, + "Remarks": null + }, + "info": [ + { + "InfCd": "EWBERR", + "Desc": [ + { + "ErrorCode": "4019", + "ErrorMessage": "Provide Transporter ID in order to generate Part A of e-Way Bill" + } + ] + } + ] + } + }, + "debit_invoice": { + "kwargs": { + "is_debit_note": 1, + "qty": 0, + "customer_address": "_Test Registered Customer-Billing", + "shipping_address_name": "_Test Registered Customer-Billing" + }, + "request_data": { + "BuyerDtls": { + "Addr1": "Test Address - 3", + "Gstin": "36AMBPG7773M002", + "LglNm": "_Test Registered Customer", + "Loc": "Test City", + "Pin": 500055, + "Pos": "01", + "Stcd": "36", + "TrdNm": "Test Registered Customer" + }, + "DispDtls": { + "Addr1": "Test Address - 1", + "Loc": "Test City", + "Nm": "Test Indian Registered Company", + "Pin": 193501, + "Stcd": "01" + }, + "DocDtls": { + "Dt": "17/09/2022", + "No": "test_invoice_no", + "Typ": "DBN" + }, + "EwbDtls": { + "Distance": 0 + }, + "ItemList": [ + { + "AssAmt": 100.0, + "CesAmt": 0, + "CesNonAdvlAmt": 0, + "CesRt": 0, + "CgstAmt": 0, + "Discount": 0, + "GstRt": 0.0, + "HsnCd": "61149090", + "IgstAmt": 0, + "IsServc": "N", + "PrdDesc": "Test Trading Goods 1", + "Qty": 0.0, + "SgstAmt": 0, + "SlNo": "1", + "TotAmt": 100.0, + "TotItemVal": 100.0, + "Unit": "NOS", + "UnitPrice": 100.0 + } + ], + "PayDtls": { + "CrDay": 0, + "PaidAmt": 0, + "PaymtDue": 100.0 + }, + "SellerDtls": { + "Addr1": "Test Address - 1", + "Gstin": "01AMBPG7773M002", + "LglNm": "_Test Indian Registered Company", + "Loc": "Test City", + "Pin": 193501, + "Stcd": "01", + "TrdNm": "Test Indian Registered Company" + }, + "ShipDtls": { + "Addr1": "Test Address - 3", + "Gstin": "36AMBPG7773M002", + "LglNm": "Test Registered Customer", + "Loc": "Test City", + "Pin": 500055, + "Stcd": "36", + "TrdNm": "Test Registered Customer" + }, + "TranDtls": { + "RegRev": "N", + "SupTyp": "B2B", + "TaxSch": "GST" + }, + "ValDtls": { + "AssVal": 100.0, + "CesVal": 0, + "CgstVal": 0, + "Discount": 0, + "IgstVal": 0, + "OthChrg": 0.0, + "RndOffAmt": 0.0, + "SgstVal": 0, + "TotInvVal": 100.0 + }, + "Version": "1.1" + }, + "response_data": { + "success": true, + "message": "IRN generated successfully", + "result": { + "AckNo": 232210036755701, + "AckDt": "2022-09-17 17:50:00", + "Irn": "24f37c80532583c6894d8153e2b12494daa80ddbb197f0fc2c1bac07db67f933", + "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoyMzIyMTAwMzY3NTU3MDEsXCJBY2tEdFwiOlwiMjAyMi0wOS0xNyAxNzo1MDowMFwiLFwiSXJuXCI6XCIyNGYzN2M4MDUzMjU4M2M2ODk0ZDgxNTNlMmIxMjQ5NGRhYTgwZGRiYjE5N2YwZmMyYzFiYWMwN2RiNjdmOTMzXCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJEQk5cIixcIk5vXCI6XCJnMnF4aFlcIixcIkR0XCI6XCIxNy8wOS8yMDIyXCJ9LFwiU2VsbGVyRHRsc1wiOntcIkdzdGluXCI6XCIwMUFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJfVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJUcmRObVwiOlwiVGVzdCBJbmRpYW4gUmVnaXN0ZXJlZCBDb21wYW55XCIsXCJBZGRyMVwiOlwiVGVzdCBBZGRyZXNzIC0gMVwiLFwiTG9jXCI6XCJUZXN0IENpdHlcIixcIlBpblwiOjE5MzUwMSxcIlN0Y2RcIjpcIjAxXCJ9LFwiQnV5ZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QU1CUEc3NzczTTAwMlwiLFwiTGdsTm1cIjpcIl9UZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlBvc1wiOlwiMDFcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJEaXNwRHRsc1wiOntcIk5tXCI6XCJUZXN0IEluZGlhbiBSZWdpc3RlcmVkIENvbXBhbnlcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAxXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6MTkzNTAxLFwiU3RjZFwiOlwiMDFcIn0sXCJTaGlwRHRsc1wiOntcIkdzdGluXCI6XCIzNkFNQlBHNzc3M00wMDJcIixcIkxnbE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIlRyZE5tXCI6XCJUZXN0IFJlZ2lzdGVyZWQgQ3VzdG9tZXJcIixcIkFkZHIxXCI6XCJUZXN0IEFkZHJlc3MgLSAzXCIsXCJMb2NcIjpcIlRlc3QgQ2l0eVwiLFwiUGluXCI6NTAwMDU1LFwiU3RjZFwiOlwiMzZcIn0sXCJJdGVtTGlzdFwiOlt7XCJJdGVtTm9cIjowLFwiU2xOb1wiOlwiMVwiLFwiSXNTZXJ2Y1wiOlwiTlwiLFwiUHJkRGVzY1wiOlwiVGVzdCBUcmFkaW5nIEdvb2RzIDFcIixcIkhzbkNkXCI6XCI2MTE0OTA5MFwiLFwiUXR5XCI6MS4wLFwiVW5pdFwiOlwiTk9TXCIsXCJVbml0UHJpY2VcIjoxMDAuMCxcIlRvdEFtdFwiOjEwMC4wLFwiRGlzY291bnRcIjowLFwiQXNzQW10XCI6MTAwLjAsXCJHc3RSdFwiOjAuMCxcIklnc3RBbXRcIjowLFwiQ2dzdEFtdFwiOjAsXCJTZ3N0QW10XCI6MCxcIkNlc1J0XCI6MCxcIkNlc0FtdFwiOjAsXCJDZXNOb25BZHZsQW10XCI6MCxcIlRvdEl0ZW1WYWxcIjoxMDAuMH1dLFwiVmFsRHRsc1wiOntcIkFzc1ZhbFwiOjEwMC4wLFwiQ2dzdFZhbFwiOjAsXCJTZ3N0VmFsXCI6MCxcIklnc3RWYWxcIjowLFwiQ2VzVmFsXCI6MCxcIkRpc2NvdW50XCI6MCxcIk90aENocmdcIjowLjAsXCJSbmRPZmZBbXRcIjowLjAsXCJUb3RJbnZWYWxcIjoxMDAuMH0sXCJQYXlEdGxzXCI6e1wiQ3JEYXlcIjowLFwiUGFpZEFtdFwiOjAsXCJQYXltdER1ZVwiOjEwMC4wfSxcIkV3YkR0bHNcIjp7XCJEaXN0YW5jZVwiOjB9fSIsImlzcyI6Ik5JQyJ9.ad1NfDA-H8FgBHr_kaTeiVWUj-f8T6NXuLFWa1gprxGhACXoI9h6sU47U9PBxHcZ7qVXcYAKzA9CbNvAfxWCLKtjur5p85uIrksZYDD494lodGn3QeXbyjXMJeh7eM0mcKKN3Chp0TxaUfi9C7mA9W0R8HYKNnXIOT1CVlD-brrAw09_QiDsNgMhLBX5QfpHKIHPKCIEl_DgmWlnMzduy1iKYPpNreNPCV-J-ZaVQjxl93LjKBUb5AF1XWyWvPw_e8ePZEttviX_bU_Nnm1M4zCj-QWYzj8A0bauzl7kjp5UajEM7_7CLAI4sjZnqonGKYFfR5rf2Qj76exbs_pWGw", + "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBTUJQRzc3NzNNMDAyXCIsXCJEb2NOb1wiOlwiZzJxeGhZXCIsXCJEb2NUeXBcIjpcIkRCTlwiLFwiRG9jRHRcIjpcIjE3LzA5LzIwMjJcIixcIlRvdEludlZhbFwiOjEwMC4wLFwiSXRlbUNudFwiOjEsXCJNYWluSHNuQ29kZVwiOlwiNjExNDkwOTBcIixcIklyblwiOlwiMjRmMzdjODA1MzI1ODNjNjg5NGQ4MTUzZTJiMTI0OTRkYWE4MGRkYmIxOTdmMGZjMmMxYmFjMDdkYjY3ZjkzM1wiLFwiSXJuRHRcIjpcIjIwMjItMDktMTcgMTc6NTA6MDBcIn0iLCJpc3MiOiJOSUMifQ.TUE7iIvF9Orc1bEuOaxtSuj0D6vP1MwV-hcoh3IZqV7EKXwMmm0PkVrN87vRzSu97NMKfQIHJDRgPYv4prhuT0dshWJ9A_kC4jiRSm5Naj_R1egBKsv5ykTojOKKrkGy35DtdUcJK_FyiD0qFfmMmInFD8u6D8W83eEo93i99RONgVKUyCkd_uqs-cz1P-PTlsi2xWbeDVVIcRoAmf-lcsbwkl2Hn6ECHgorKJHPJC1FGo1jRQ2Ktq0ODiJdncplbxbdYN19vUz61JJB4DPzWtf8wOkX11N0fDhdUdEINfJURWEOIGQRYip5GIJuA2qdqxZieVk0CetnsckcGgfE8g", + "Status": "ACT", + "EwbNo": null, + "EwbDt": null, + "EwbValidTill": null, + "Remarks": null + }, + "info": [ + { + "InfCd": "EWBERR", + "Desc": [ + { + "ErrorCode": "4019", + "ErrorMessage": "Provide Transporter ID in order to generate Part A of e-Way Bill" + } + ] + } + ] + } + }, + "cancel_e_invoice": { + "request_data": { + "Cnlrem": "Data Entry Mistake", + "Cnlrsn": "2", + "Irn": "706daeccda0ef6f818da78f3a2a05a1288731057373002289b46c3229289a2e7" + }, + "response_data": { + "message": "E-Invoice is cancelled successfully", + "result": { + "CancelDate": "2022-09-17 18:09:00", + "Irn": "706daeccda0ef6f818da78f3a2a05a1288731057373002289b46c3229289a2e7" + }, + "success": true + } + }, + "cancel_e_waybill": { + "request_data": { + "cancelRmrk": "Data Entry Mistake", + "cancelRsnCode": "3", + "ewbNo": "391009149369" + }, + "response_data": { + "message": "E-Way bill cancelled successfully", + "result": { + "cancelDate": "20/09/2022 12:10:00 PM", + "ewayBillNo": "391009149369" + }, + "success": true + } + } +} \ No newline at end of file diff --git a/india_compliance/gst_india/doctype/gst_settings/gst_settings.py b/india_compliance/gst_india/doctype/gst_settings/gst_settings.py index 1b9107175..46336c4ad 100644 --- a/india_compliance/gst_india/doctype/gst_settings/gst_settings.py +++ b/india_compliance/gst_india/doctype/gst_settings/gst_settings.py @@ -16,7 +16,8 @@ _disable_api_promo, post_login, ) -from india_compliance.gst_india.utils import can_enable_api, toggle_custom_fields +from india_compliance.gst_india.utils import can_enable_api +from india_compliance.gst_india.utils.custom_fields import toggle_custom_fields class GSTSettings(Document): @@ -56,7 +57,6 @@ def validate_gst_accounts(self): company_wise_account_types = {} for row in self.gst_accounts: - # Validate Duplicate Accounts for fieldname in GST_ACCOUNT_FIELDS: account = row.get(fieldname) diff --git a/india_compliance/gst_india/print_format/e_invoice/e_invoice.html b/india_compliance/gst_india/print_format/e_invoice/e_invoice.html index 0dc5f6533..7ebbf3764 100644 --- a/india_compliance/gst_india/print_format/e_invoice/e_invoice.html +++ b/india_compliance/gst_india/print_format/e_invoice/e_invoice.html @@ -1,30 +1,3 @@ -{% set ITEM_FIELDS_MAP = { - "SlNo": "Sr.", - "PrdDesc": "Product Description", - "HsnCd": "HSN Code", - "Qty": "Qty", - "Unit": "UOM", - "UnitPrice": "Rate", - "Discount": "Discount", - "AssAmt": "Taxable Amount", - "GstRt": "Tax Rate", - "CesRt": "Cess Rate", - "TotItemVal": "Total", -} %} - -{% set AMOUNT_FIELDS_MAP = { - "AssVal": "Taxable Value", - "CgstVal": "CGST", - "SgstVal": "SGST", - "IgstVal": "IGST", - "CessVal": "CESS", - "Discount": "Discount", - "OthChrg": "Other Charges", - "RndOffAmt": "Round Off", - "TotInvVal": "Total Value", -} %} - - @@ -51,6 +24,14 @@ {% else %} +{% if not e_invoice_log.invoice_data %} + +
+ Invoice Data is not available in the e-Invoice Log linked to this invoice. +
+ +{% else %} + {%- set invoice_data = _dict(json.loads(e_invoice_log.invoice_data)) -%} {% macro get_address_html(address) %} @@ -89,20 +70,23 @@
1. Transaction Details
{% set transaction_details = { "IRN": invoice_data.Irn, - "Ack. No": invoice_data.AckNo, + "Ack. No.": invoice_data.AckNo, "Ack. Date": frappe.utils.format_datetime( invoice_data.AckDt, "dd/MM/yyyy hh:mm:ss" ), "Category": invoice_data.TranDtls.SupTyp, "Document Type": invoice_data.DocDtls.Typ, - "Document No": invoice_data.DocDtls.No, + "Document No.": invoice_data.DocDtls.No, + "e-Waybill No.": doc.ewaybill, } %} {% for key, value in transaction_details.items() %} -
-
-
{{ value }}
-
+ {% if value %} +
+
+
{{ value }}
+
+ {% endif %} {% endfor %}
@@ -155,19 +139,19 @@
Shipped To
{% set items = invoice_data.ItemList %} -{% set item_fields = get_non_zero_fields(items, ITEM_FIELDS_MAP) %} +{% set item_fields = get_e_invoice_item_fields(items) %}
3. Item Details
- {% for field in item_fields %} + {% for field, label in item_fields.items() %} + >{{ label }} {% endfor %} @@ -205,15 +189,15 @@
3. Item Details
{%- set amounts = invoice_data.ValDtls -%} -{% set amount_fields = get_non_zero_fields(amounts, AMOUNT_FIELDS_MAP) %} +{% set amount_fields = get_e_invoice_amount_fields(amounts, doc) %}
4. Value Details
{{ ITEM_FIELDS_MAP[field] }}
- {% for field in amount_fields %} - + {% for field, label in amount_fields.items() %} + {% endfor %} @@ -232,3 +216,4 @@
4. Value Details
{% endif %} {% endif %} +{% endif %} diff --git a/india_compliance/gst_india/print_format/e_waybill/e_waybill.html b/india_compliance/gst_india/print_format/e_waybill/e_waybill.html index f98ec6f34..6bc95f65e 100644 --- a/india_compliance/gst_india/print_format/e_waybill/e_waybill.html +++ b/india_compliance/gst_india/print_format/e_waybill/e_waybill.html @@ -3,6 +3,7 @@ {% set data = _dict(json.loads(doc.data)) %} +{%- set irn = frappe.db.get_value("Sales Invoice", {"ewaybill": data.ewbNo}, "irn") -%} @@ -81,6 +82,18 @@

e-Waybill

+ {% if irn %} + + + + + {% endif %}
{{ AMOUNT_FIELDS_MAP[field] }}{{ label }}
+ IRN: + + + {{ irn }} + +
diff --git a/india_compliance/gst_india/report/gst_itemised_sales_register/gst_itemised_sales_register.py b/india_compliance/gst_india/report/gst_itemised_sales_register/gst_itemised_sales_register.py index 2b74dfe76..b05c3d53d 100644 --- a/india_compliance/gst_india/report/gst_itemised_sales_register/gst_itemised_sales_register.py +++ b/india_compliance/gst_india/report/gst_itemised_sales_register/gst_itemised_sales_register.py @@ -1,70 +1,30 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - +from frappe import _ from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import ( _execute, ) +from india_compliance.gst_india.report.gst_sales_register.gst_sales_register import ( + get_additional_table_columns, + get_column_names, +) + def execute(filters=None): + additional_table_columns = get_additional_table_columns() + additional_table_columns.append( + { + "fieldtype": "Data", + "label": _("HSN Code"), + "fieldname": "gst_hsn_code", + "width": 120, + } + ) + return _execute( filters, - additional_table_columns=[ - dict( - fieldtype="Data", - label="Billing Address GSTIN", - fieldname="billing_address_gstin", - width=140, - ), - dict( - fieldtype="Data", - label="Company GSTIN", - fieldname="company_gstin", - width=120, - ), - dict( - fieldtype="Data", - label="Place of Supply", - fieldname="place_of_supply", - width=120, - ), - dict( - fieldtype="Check", - label="Is Reverse Charge", - fieldname="is_reverse_charge", - width=120, - ), - dict( - fieldtype="Data", - label="GST Category", - fieldname="gst_category", - width=120, - ), - dict( - fieldtype="Check", - label="Is Export With GST", - fieldname="is_export_with_gst", - width=120, - ), - dict( - fieldtype="Data", - label="E-Commerce GSTIN", - fieldname="ecommerce_gstin", - width=130, - ), - dict( - fieldtype="Data", label="HSN Code", fieldname="gst_hsn_code", width=120 - ), - ], - additional_query_columns=[ - "billing_address_gstin", - "company_gstin", - "place_of_supply", - "is_reverse_charge", - "gst_category", - "is_export_with_gst", - "ecommerce_gstin", - "gst_hsn_code", - ], + additional_table_columns, + get_column_names(additional_table_columns), ) diff --git a/india_compliance/gst_india/report/gst_sales_register/gst_sales_register.py b/india_compliance/gst_india/report/gst_sales_register/gst_sales_register.py index ea24afb39..45589c67d 100644 --- a/india_compliance/gst_india/report/gst_sales_register/gst_sales_register.py +++ b/india_compliance/gst_india/report/gst_sales_register/gst_sales_register.py @@ -1,64 +1,84 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import frappe +from frappe import _ +from erpnext.accounts.report.sales_register.sales_register import _execute -from erpnext.accounts.report.sales_register.sales_register import _execute +def get_additional_table_columns(): + overseas_enabled, reverse_charge_enabled = frappe.get_cached_value( + "GST Settings", + "GST Settings", + ("enable_overseas_transactions", "enable_reverse_charge_in_sales"), + ) + + additional_table_columns = [ + { + "fieldtype": "Data", + "label": _("Billing Address GSTIN"), + "fieldname": "billing_address_gstin", + "width": 140, + }, + { + "fieldtype": "Data", + "label": _("Company GSTIN"), + "fieldname": "company_gstin", + "width": 120, + }, + { + "fieldtype": "Data", + "label": _("Place of Supply"), + "fieldname": "place_of_supply", + "width": 120, + }, + { + "fieldtype": "Data", + "label": _("GST Category"), + "fieldname": "gst_category", + "width": 120, + }, + { + "fieldtype": "Data", + "label": _("E-Commerce GSTIN"), + "fieldname": "ecommerce_gstin", + "width": 130, + }, + ] + + if reverse_charge_enabled: + additional_table_columns.insert( + -2, + { + "fieldtype": "Check", + "label": _("Is Reverse Charge"), + "fieldname": "is_reverse_charge", + "width": 120, + }, + ) + + if overseas_enabled: + additional_table_columns.insert( + -2, + { + "fieldtype": "Check", + "label": _("Is Export With GST"), + "fieldname": "is_export_with_gst", + "width": 120, + }, + ) + + return additional_table_columns + + +def get_column_names(additional_table_columns): + return [column["fieldname"] for column in additional_table_columns] def execute(filters=None): + additional_table_columns = get_additional_table_columns() + return _execute( filters, - additional_table_columns=[ - dict( - fieldtype="Data", - label="Billing Address GSTIN", - fieldname="billing_address_gstin", - width=140, - ), - dict( - fieldtype="Data", - label="Company GSTIN", - fieldname="company_gstin", - width=120, - ), - dict( - fieldtype="Data", - label="Place of Supply", - fieldname="place_of_supply", - width=120, - ), - dict( - fieldtype="Check", - label="Is Reverse Charge", - fieldname="is_reverse_charge", - width=120, - ), - dict( - fieldtype="Data", - label="GST Category", - fieldname="gst_category", - width=120, - ), - dict( - fieldtype="Check", - label="Is Export With GST", - fieldname="is_export_with_gst", - width=120, - ), - dict( - fieldtype="Data", - label="E-Commerce GSTIN", - fieldname="ecommerce_gstin", - width=130, - ), - ], - additional_query_columns=[ - "billing_address_gstin", - "company_gstin", - "place_of_supply", - "is_reverse_charge", - "gst_category", - "is_export_with_gst", - "ecommerce_gstin", - ], + additional_table_columns, + get_column_names(additional_table_columns), ) diff --git a/india_compliance/gst_india/report/gstr_1/gstr_1.py b/india_compliance/gst_india/report/gstr_1/gstr_1.py index 4459ddc87..46199eb3c 100644 --- a/india_compliance/gst_india/report/gstr_1/gstr_1.py +++ b/india_compliance/gst_india/report/gstr_1/gstr_1.py @@ -1090,20 +1090,20 @@ def get_advances_json(data, gstin): for item in items: itms = { "rt": item["rate"], - "ad_amount": flt(item.get("taxable_value")), - "csamt": flt(item.get("cess_amount")), + "ad_amount": flt(item.get("taxable_value"), 2), + "csamt": flt(item.get("cess_amount"), 2), } if supply_type == "INTRA": itms.update( { - "samt": flt((itms["ad_amount"] * itms["rt"]) / 100), - "camt": flt((itms["ad_amount"] * itms["rt"]) / 100), + "samt": flt((itms["ad_amount"] * itms["rt"]) / 100, 2), + "camt": flt((itms["ad_amount"] * itms["rt"]) / 100, 2), "rt": itms["rt"] * 2, } ) else: - itms.update({"iamt": flt((itms["ad_amount"] * itms["rt"]) / 100)}) + itms["iamt"] = flt((itms["ad_amount"] * itms["rt"]) / 100, 2) row["itms"].append(itms) out.append(row) @@ -1193,7 +1193,7 @@ def get_cdnr_reg_json(res, gstin): inv_item = { "nt_num": invoice[0]["invoice_number"], "nt_dt": getdate(invoice[0]["posting_date"]).strftime("%d-%m-%Y"), - "val": abs(flt(invoice[0]["invoice_value"])), + "val": abs(flt(invoice[0]["invoice_value"], 2)), "ntty": invoice[0]["document_type"], "pos": "%02d" % int(invoice[0]["place_of_supply"].split("-")[0]), "rchrg": invoice[0]["is_reverse_charge"], @@ -1221,7 +1221,7 @@ def get_cdnr_unreg_json(res, gstin): inv_item = { "nt_num": items[0]["invoice_number"], "nt_dt": getdate(items[0]["posting_date"]).strftime("%d-%m-%Y"), - "val": abs(flt(items[0]["invoice_value"])), + "val": abs(flt(items[0]["invoice_value"], 2)), "ntty": items[0]["document_type"], "pos": "%02d" % int(items[0]["place_of_supply"].split("-")[0]), "typ": get_invoice_type(items[0]), diff --git a/india_compliance/gst_india/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/india_compliance/gst_india/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py index 63a7186e6..20028bac9 100644 --- a/india_compliance/gst_india/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py +++ b/india_compliance/gst_india/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py @@ -16,12 +16,9 @@ def execute(filters=None): - return _execute(filters) - - -def _execute(filters=None): if not filters: filters = {} + columns = get_columns() output_gst_accounts = [ @@ -40,25 +37,39 @@ def _execute(filters=None): data = [] added_item = [] for d in item_list: - if (d.parent, d.gst_hsn_code, d.item_code) not in added_item: - row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty] - total_tax = 0 - tax_rate = 0 - for tax in tax_columns: - item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) - tax_rate += flt(item_tax.get("tax_rate", 0)) - total_tax += flt(item_tax.get("tax_amount", 0)) - - row += [tax_rate, d.taxable_value + total_tax, d.taxable_value] - - for tax in tax_columns: - item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) - row += [item_tax.get("tax_amount", 0)] - - data.append(row) - added_item.append((d.parent, d.gst_hsn_code, d.item_code)) + if (d.parent, d.gst_hsn_code, d.item_code) in added_item: + continue + + if d.gst_hsn_code.startswith("99"): + # service item doesnt have qty / uom + d.stock_qty = 0 + d.uqc = "NA" + + else: + d.uqc = d.get("uqc", "").upper() + if d.uqc not in UOMS: + d.uqc = "OTH" + + row = [d.gst_hsn_code, d.description, d.uqc, d.stock_qty] + total_tax = 0 + tax_rate = 0 + for tax in tax_columns: + item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) + tax_rate += flt(item_tax.get("tax_rate", 0)) + total_tax += flt(item_tax.get("tax_amount", 0)) + + row += [tax_rate, d.taxable_value + total_tax, d.taxable_value] + + for tax in tax_columns: + item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) + row += [item_tax.get("tax_amount", 0)] + + data.append(row) + added_item.append((d.parent, d.gst_hsn_code, d.item_code)) + if data: data = get_merged_data(columns, data) # merge same hsn code data + return columns, data @@ -78,8 +89,8 @@ def get_columns(): "width": 300, }, { - "fieldname": "stock_uom", - "label": _("Stock UOM"), + "fieldname": "uqc", + "label": _("UQC"), "fieldtype": "Data", "width": 100, }, @@ -138,7 +149,7 @@ def get_items(filters): f""" SELECT `tabSales Invoice Item`.gst_hsn_code, - `tabSales Invoice Item`.stock_uom, + `tabSales Invoice Item`.stock_uom as uqc, sum(`tabSales Invoice Item`.stock_qty) AS stock_qty, sum(`tabSales Invoice Item`.taxable_value) AS taxable_value, sum(`tabSales Invoice Item`.base_price_list_rate) AS base_price_list_rate, @@ -308,31 +319,28 @@ def download_json_file(): def get_hsn_wise_json_data(filters, report_data): - filters = frappe._dict(filters) gst_accounts = get_gst_accounts_by_type(filters.company, "Output") data = [] count = 1 for hsn in report_data: - uom = hsn.get("stock_uom", "").upper() - if uom not in UOMS: - uom = "OTH" - row = { "num": count, "hsn_sc": hsn.get("gst_hsn_code"), - "desc": hsn.get("description")[:30], - "uqc": uom, + "uqc": hsn.get("uqc"), "qty": hsn.get("stock_qty"), "rt": flt(hsn.get("tax_rate"), 2), - "txval": flt(hsn.get("taxable_amount", 2)), + "txval": flt(hsn.get("taxable_amount"), 2), "iamt": 0.0, "camt": 0.0, "samt": 0.0, "csamt": 0.0, } + if hsn_description := hsn.get("description"): + row["desc"] = hsn_description[:30] + row["iamt"] += flt( hsn.get(frappe.scrub(cstr(gst_accounts.get("igst_account"))), 0.0), 2 ) diff --git a/india_compliance/gst_india/setup/__init__.py b/india_compliance/gst_india/setup/__init__.py index 28ec72239..4d339d626 100644 --- a/india_compliance/gst_india/setup/__init__.py +++ b/india_compliance/gst_india/setup/__init__.py @@ -13,7 +13,10 @@ SALES_REVERSE_CHARGE_FIELDS, ) from india_compliance.gst_india.setup.property_setters import get_property_setters -from india_compliance.gst_india.utils import get_data_file_path, toggle_custom_fields +from india_compliance.gst_india.utils import get_data_file_path +from india_compliance.gst_india.utils.custom_fields import toggle_custom_fields + +ITEM_VARIANT_FIELDNAMES = frozenset(("gst_hsn_code", "is_nil_exempt", "is_non_gst")) def after_install(): @@ -23,21 +26,14 @@ def after_install(): set_default_gst_settings() set_default_accounts_settings() create_hsn_codes() + add_fields_to_item_variant_settings() def create_custom_fields(): # Validation ignored for faster creation # Will not fail if a core field with same name already exists (!) # Will update a custom field if it already exists - _create_custom_fields( - _get_custom_fields_to_create( - CUSTOM_FIELDS, - SALES_REVERSE_CHARGE_FIELDS, - E_INVOICE_FIELDS, - E_WAYBILL_FIELDS, - ), - ignore_validate=True, - ) + _create_custom_fields(get_all_custom_fields(), ignore_validate=True) def create_property_setters(): @@ -99,6 +95,18 @@ def create_hsn_codes(): ) +def add_fields_to_item_variant_settings(): + settings = frappe.get_doc("Item Variant Settings") + fields_to_add = ITEM_VARIANT_FIELDNAMES - { + row.field_name for row in settings.fields + } + + for fieldname in fields_to_add: + settings.append("fields", {"field_name": fieldname}) + + settings.save() + + def set_default_gst_settings(): settings = frappe.get_doc("GST Settings") default_settings = { @@ -181,16 +189,23 @@ def show_accounts_settings_override_warning(): ) click.secho( - "This is being set as Billing Address, since that's the correct " - "address for determining GST applicablility.", + ( + "This is being set as Billing Address, since that's the correct " + "address for determining GST applicablility." + ), fg="yellow", ) -def _get_custom_fields_to_create(*custom_fields_list): +def get_all_custom_fields(): result = {} - for custom_fields in custom_fields_list: + for custom_fields in ( + CUSTOM_FIELDS, + SALES_REVERSE_CHARGE_FIELDS, + E_INVOICE_FIELDS, + E_WAYBILL_FIELDS, + ): for doctypes, fields in custom_fields.items(): if isinstance(fields, dict): fields = [fields] diff --git a/india_compliance/gst_india/uninstall.py b/india_compliance/gst_india/uninstall.py new file mode 100644 index 000000000..f24ab940e --- /dev/null +++ b/india_compliance/gst_india/uninstall.py @@ -0,0 +1,36 @@ +import frappe + +from india_compliance.gst_india.setup import ( + ITEM_VARIANT_FIELDNAMES, + get_all_custom_fields, + get_property_setters, +) +from india_compliance.gst_india.utils.custom_fields import delete_custom_fields + + +def before_uninstall(): + delete_custom_fields(get_all_custom_fields()) + delete_property_setters() + remove_fields_from_item_variant_settings() + + +def delete_property_setters(): + field_map = { + "doctype": "doc_type", + "fieldname": "field_name", + } + + for property_setter in get_property_setters(): + for key, fieldname in field_map.items(): + if key in property_setter: + property_setter[fieldname] = property_setter.pop(key) + + frappe.db.delete("Property Setter", property_setter) + + +def remove_fields_from_item_variant_settings(): + settings = frappe.get_doc("Item Variant Settings") + settings.fields = [ + row for row in settings.fields if row.field_name not in ITEM_VARIANT_FIELDNAMES + ] + settings.save() diff --git a/india_compliance/gst_india/utils/__init__.py b/india_compliance/gst_india/utils/__init__.py index 099da6a09..14bb0e1d5 100644 --- a/india_compliance/gst_india/utils/__init__.py +++ b/india_compliance/gst_india/utils/__init__.py @@ -26,7 +26,7 @@ def get_state(state_number): """Get state from State Number""" - state_number = str(state_number) + state_number = str(state_number).zfill(2) for state, code in STATE_NUMBERS.items(): if code == state_number: @@ -318,37 +318,6 @@ def get_all_gst_accounts(company): return accounts_list -def toggle_custom_fields(custom_fields, show): - """ - Show / hide custom fields - - :param custom_fields: a dict like `{'Sales Invoice': [{fieldname: 'test', ...}]}` - :param show: True to show fields, False to hide - """ - - for doctypes, fields in custom_fields.items(): - if isinstance(fields, dict): - # only one field - fields = [fields] - - if isinstance(doctypes, str): - # only one doctype - doctypes = (doctypes,) - - for doctype in doctypes: - frappe.db.set_value( - "Custom Field", - { - "dt": doctype, - "fieldname": ["in", [field["fieldname"] for field in fields]], - }, - "hidden", - int(not show), - ) - - frappe.clear_cache(doctype=doctype) - - def parse_datetime(value, day_first=False): """Convert IST string to offset-naive system time""" @@ -407,22 +376,6 @@ def get_titlecase_version(word, all_caps=False, **kwargs): return word -def delete_old_fields(fields, doctypes): - if isinstance(fields, str): - fields = (fields,) - - if isinstance(doctypes, str): - doctypes = (doctypes,) - - frappe.db.delete( - "Custom Field", - { - "fieldname": ("in", fields), - "dt": ("in", doctypes), - }, - ) - - def is_api_enabled(settings=None): if not settings: settings = frappe.get_cached_value( diff --git a/india_compliance/gst_india/utils/api.py b/india_compliance/gst_india/utils/api.py index 2efb0d385..8ea61b165 100644 --- a/india_compliance/gst_india/utils/api.py +++ b/india_compliance/gst_india/utils/api.py @@ -3,7 +3,8 @@ def enqueue_integration_request(**kwargs): frappe.enqueue( - "india_compliance.gst_india.utils.api.create_integration_request", **kwargs + "india_compliance.gst_india.utils.api.create_integration_request", + **kwargs, ) diff --git a/india_compliance/gst_india/utils/custom_fields.py b/india_compliance/gst_india/utils/custom_fields.py new file mode 100644 index 000000000..26adba3e7 --- /dev/null +++ b/india_compliance/gst_india/utils/custom_fields.py @@ -0,0 +1,74 @@ +import frappe + + +def toggle_custom_fields(custom_fields, show): + """ + Show / hide custom fields + + :param custom_fields: a dict like `{'Sales Invoice': [{fieldname: 'test', ...}]}` + :param show: True to show fields, False to hide + """ + + for doctypes, fields in custom_fields.items(): + if isinstance(fields, dict): + # only one field + fields = [fields] + + if isinstance(doctypes, str): + # only one doctype + doctypes = (doctypes,) + + for doctype in doctypes: + frappe.db.set_value( + "Custom Field", + { + "dt": doctype, + "fieldname": ["in", [field["fieldname"] for field in fields]], + }, + "hidden", + int(not show), + ) + + frappe.clear_cache(doctype=doctype) + + +def delete_old_fields(fieldnames, doctypes): + if isinstance(fieldnames, str): + fields = (fieldnames,) + + if isinstance(doctypes, str): + doctypes = (doctypes,) + + frappe.db.delete( + "Custom Field", + { + "fieldname": ("in", fields), + "dt": ("in", doctypes), + }, + ) + + +def delete_custom_fields(custom_fields): + """ + :param custom_fields: a dict like `{'Sales Invoice': [{fieldname: 'test', ...}]}` + """ + + for doctypes, fields in custom_fields.items(): + if isinstance(fields, dict): + # only one field + fields = [fields] + + if isinstance(doctypes, str): + # only one doctype + doctypes = (doctypes,) + + for doctype in doctypes: + frappe.db.delete( + "Custom Field", + { + "fieldname": ("in", [field["fieldname"] for field in fields]), + "dt": doctype, + }, + ) + + frappe.clear_cache(doctype=doctype) diff --git a/india_compliance/gst_india/utils/e_invoice.py b/india_compliance/gst_india/utils/e_invoice.py index 7ad4e5b6d..c70281ff5 100644 --- a/india_compliance/gst_india/utils/e_invoice.py +++ b/india_compliance/gst_india/utils/e_invoice.py @@ -1,3 +1,5 @@ +import json + import jwt import frappe @@ -22,6 +24,7 @@ ITEM_LIMIT, ) from india_compliance.gst_india.utils import ( + is_api_enabled, load_doc, parse_datetime, send_updated_doc, @@ -37,6 +40,53 @@ ) +@frappe.whitelist() +def enqueue_bulk_e_invoice_generation(docnames): + """ + Enqueue bulk generation of e-Invoices for the given Sales Invoices. + """ + + frappe.has_permission("Sales Invoice", "submit", throw=True) + + gst_settings = frappe.get_cached_doc("GST Settings") + if not is_api_enabled(gst_settings) or not gst_settings.enable_e_invoice: + frappe.throw(_("Please enable e-Invoicing in GST Settings first")) + + docnames = frappe.parse_json(docnames) if docnames.startswith("[") else [docnames] + rq_job = frappe.enqueue( + "india_compliance.gst_india.utils.e_invoice.generate_e_invoices", + queue="long", + timeout=len(docnames) * 240, # 4 mins per e-Invoice + docnames=docnames, + ) + + return rq_job.id + + +def generate_e_invoices(docnames): + """ + Bulk generate e-Invoices for the given Sales Invoices. + Permission checks are done in the `generate_e_invoice` function. + """ + + for docname in docnames: + try: + generate_e_invoice(docname) + + except Exception: + frappe.log_error( + title=_("e-Invoice generation failed for Sales Invoice {0}").format( + docname + ), + message=frappe.get_traceback(), + ) + + finally: + # each e-Invoice needs to be committed individually + # nosemgrep + frappe.db.commit() + + @frappe.whitelist() def generate_e_invoice(docname, throw=True): doc = load_doc("Sales Invoice", docname, "submit") @@ -47,7 +97,11 @@ def generate_e_invoice(docname, throw=True): # Handle Duplicate IRN if result.InfCd == "DUPIRN": - result = api.get_e_invoice_by_irn(result.Desc.get("Irn")) + response = api.get_e_invoice_by_irn(result.Desc.Irn) + + # Handle error 2283: + # IRN details cannot be provided as it is generated more than 2 days ago + result = result.Desc if response.error_code == "2283" else response except frappe.ValidationError as e: if throw: @@ -71,9 +125,14 @@ def generate_e_invoice(docname, throw=True): } ) - decoded_invoice = frappe.parse_json( - jwt.decode(result.SignedInvoice, options={"verify_signature": False})["data"] - ) + invoice_data = None + if result.SignedInvoice: + decoded_invoice = json.loads( + jwt.decode(result.SignedInvoice, options={"verify_signature": False})[ + "data" + ] + ) + invoice_data = frappe.as_json(decoded_invoice, indent=4) log_e_invoice( doc, @@ -84,7 +143,7 @@ def generate_e_invoice(docname, throw=True): "acknowledged_on": parse_datetime(result.AckDt), "signed_invoice": result.SignedInvoice, "signed_qr_code": result.SignedQRCode, - "invoice_data": frappe.as_json(decoded_invoice, indent=4), + "invoice_data": invoice_data, }, ) @@ -142,7 +201,12 @@ def cancel_e_invoice(docname, values): def log_e_invoice(doc, log_data): - frappe.enqueue(_log_e_invoice, queue="short", at_front=True, log_data=log_data) + frappe.enqueue( + _log_e_invoice, + queue="short", + at_front=True, + log_data=log_data, + ) update_onload(doc, "e_invoice_info", log_data) @@ -310,7 +374,9 @@ def update_payment_details(self): credit_days = 0 paid_amount = 0 - if self.doc.due_date: + if self.doc.due_date and getdate(self.doc.due_date) > getdate( + self.doc.posting_date + ): credit_days = ( getdate(self.doc.due_date) - getdate(self.doc.posting_date) ).days @@ -398,6 +464,9 @@ def get_invoice_data(self): self.dispatch_address.update(seller) self.transaction_details.name = random_string(6).lstrip("0") + if frappe.flags.in_test: + self.transaction_details.name = "test_invoice_no" + # For overseas transactions, dummy GSTIN is not needed if self.doc.gst_category != "Overseas": buyer = { diff --git a/india_compliance/gst_india/utils/e_waybill.py b/india_compliance/gst_india/utils/e_waybill.py index 5e41478fe..c48b22423 100644 --- a/india_compliance/gst_india/utils/e_waybill.py +++ b/india_compliance/gst_india/utils/e_waybill.py @@ -102,7 +102,6 @@ def _generate_e_waybill(doc, throw=True): indicator="green", alert=True, ) - return send_updated_doc(doc) @@ -478,6 +477,8 @@ def __init__(self, *args, **kwargs): def get_data(self, *, with_irn=False): self.validate_transaction() self.set_transporter_details() + self.set_party_address_details() + self.update_distance_if_zero() if with_irn: return self.sanitize_data( @@ -496,7 +497,6 @@ def get_data(self, *, with_irn=False): self.set_transaction_details() self.set_item_list() - self.set_party_address_details() return self.get_transaction_data() @@ -529,7 +529,7 @@ def get_update_vehicle_data(self, values): "fromPlace": dispatch_address.city, "fromState": dispatch_address.state_number, "reasonCode": UPDATE_VEHICLE_REASON_CODES[values.reason], - "reasonRem": self.sanitize_value(values.remark, 3), + "reasonRem": self.sanitize_value(values.remark, regex=3), "transDocNo": self.transaction_details.lr_no, "transDocDate": self.transaction_details.lr_date, "transMode": self.transaction_details.mode_of_transport, @@ -551,7 +551,11 @@ def validate_transaction(self): super().validate_transaction() if self.doc.ewaybill: - frappe.throw(_("e-Waybill already generated for this document")) + frappe.throw( + _("e-Waybill already generated for {0} {1}").format( + _(self.doc.doctype), frappe.bold(self.doc.name) + ) + ) self.validate_applicability() @@ -769,6 +773,19 @@ def get_address_details(self, *args, **kwargs): return address_details + def update_distance_if_zero(self): + """ + e-Waybill portal doesn't return distance where from and to pincode is same. + Hardcode distance to 1 km to simplify and automate this. + Accuracy of distance is immaterial and used only for e-Waybill validity determination. + """ + + if ( + self.transaction_details.distance == 0 + and self.dispatch_address.pincode == self.shipping_address.pincode + ): + self.transaction_details.distance = 1 + def get_transaction_data(self): if self.sandbox_mode: self.transaction_details.update( diff --git a/india_compliance/gst_india/utils/jinja.py b/india_compliance/gst_india/utils/jinja.py index d05126a4b..c96089363 100644 --- a/india_compliance/gst_india/utils/jinja.py +++ b/india_compliance/gst_india/utils/jinja.py @@ -12,8 +12,35 @@ TRANSPORT_MODES, TRANSPORT_TYPES, ) +from india_compliance.gst_india.overrides.transaction import is_inter_state_supply from india_compliance.gst_india.utils import as_ist +E_INVOICE_ITEM_FIELDS = { + "SlNo": "Sr.", + "PrdDesc": "Product Description", + "HsnCd": "HSN Code", + "Qty": "Qty", + "Unit": "UOM", + "UnitPrice": "Rate", + "Discount": "Discount", + "AssAmt": "Taxable Amount", + "GstRt": "Tax Rate", + "CesRt": "Cess Rate", + "TotItemVal": "Total", +} + +E_INVOICE_AMOUNT_FIELDS = { + "AssVal": "Taxable Value", + "CgstVal": "CGST", + "SgstVal": "SGST", + "IgstVal": "IGST", + "CesVal": "CESS", + "Discount": "Discount", + "OthChrg": "Other Charges", + "RndOffAmt": "Round Off", + "TotInvVal": "Total Value", +} + def add_spacing(string, interval): """ @@ -82,16 +109,40 @@ def get_ewaybill_barcode(ewaybill): def get_non_zero_fields(data, fields): - """Returns a list of fields with non-zero values in order of fields specified""" + """Returns a list of fields with non-zero values""" if isinstance(data, dict): data = [data] - non_zero_fields = [] + non_zero_fields = set() + for row in data: for field in fields: - if row.get(field, 0) != 0 and field not in non_zero_fields: - non_zero_fields.append(field) - continue + if field not in non_zero_fields and row.get(field, 0) != 0: + non_zero_fields.add(field) return non_zero_fields + + +def get_fields_to_display(data, field_map, mandatory_fields=None): + fields_to_display = get_non_zero_fields(data, field_map) + if mandatory_fields: + fields_to_display.update(mandatory_fields) + + return { + field: label for field, label in field_map.items() if field in fields_to_display + } + + +def get_e_invoice_item_fields(data): + return get_fields_to_display(data, E_INVOICE_ITEM_FIELDS, {"GstRt"}) + + +def get_e_invoice_amount_fields(data, doc): + mandatory_fields = set() + if is_inter_state_supply(doc): + mandatory_fields.add("IgstVal") + else: + mandatory_fields.update(("CgstVal", "SgstVal")) + + return get_fields_to_display(data, E_INVOICE_AMOUNT_FIELDS, mandatory_fields) diff --git a/india_compliance/gst_india/utils/test_e_invoice.py b/india_compliance/gst_india/utils/test_e_invoice.py new file mode 100644 index 000000000..d9e662bf3 --- /dev/null +++ b/india_compliance/gst_india/utils/test_e_invoice.py @@ -0,0 +1,456 @@ +import json +import re + +import responses +from responses import matchers + +import frappe +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import add_to_date, get_datetime, getdate, now_datetime +from frappe.utils.data import format_date + +from india_compliance.gst_india.api_classes.base import BASE_URL +from india_compliance.gst_india.utils import load_doc +from india_compliance.gst_india.utils.e_invoice import ( + EInvoiceData, + cancel_e_invoice, + generate_e_invoice, + validate_e_invoice_applicability, + validate_if_e_invoice_can_be_cancelled, +) +from india_compliance.gst_india.utils.e_waybill import EWaybillData +from india_compliance.gst_india.utils.tests import create_sales_invoice + + +class TestEInvoice(FrappeTestCase): + @classmethod + def setUpClass(cls): + frappe.db.set_value( + "GST Settings", + "GST Settings", + { + "enable_api": 1, + "enable_e_invoice": 1, + "auto_generate_e_invoice": 0, + "enable_e_waybill": 1, + "fetch_e_waybill_data": 0, + }, + ) + cls.e_invoice_test_data = frappe._dict( + frappe.get_file_json( + frappe.get_app_path( + "india_compliance", "gst_india", "data", "test_e_invoice.json" + ) + ) + ) + update_dates_for_test_data(cls.e_invoice_test_data) + + @change_settings("Selling Settings", {"allow_multiple_items": 1}) + def test_get_data(self): + """Validation test for more than 1000 items in sales invoice""" + si = create_sales_invoice(do_not_submit=True) + item_row = si.get("items")[0] + + for index in range(0, 1000): + si.append( + "items", + { + "item_code": item_row.item_code, + "qty": item_row.qty, + "rate": item_row.rate, + }, + ) + si.save() + + self.assertRaisesRegex( + frappe.exceptions.ValidationError, + re.compile(r"^(e-Invoice can only be generated.*)$"), + EInvoiceData(si).get_data, + ) + + @responses.activate + def test_generate_e_invoice_with_goods_item(self): + """Generate test e-Invoice for goods item""" + test_data = self.e_invoice_test_data.get("goods_item_with_ewaybill") + si = create_sales_invoice(**test_data.get("kwargs")) + + # Assert if request data given in Json + self.assertDictEqual(test_data.get("request_data"), EInvoiceData(si).get_data()) + + # Mock response for generating irn + self._mock_e_invoice_response(data=test_data) + + generate_e_invoice(si.name) + + # Assert if Integration Request Log generated + self.assertDocumentEqual( + { + "output": frappe.as_json(test_data.get("response_data"), indent=4), + }, + frappe.get_doc( + "Integration Request", + {"reference_doctype": "Sales Invoice", "reference_docname": si.name}, + ), + ) + + # Assert if Sales Doc updated + self.assertDocumentEqual( + { + "irn": test_data.get("response_data").get("result").get("Irn"), + "ewaybill": test_data.get("response_data").get("result").get("EwbNo"), + "einvoice_status": "Generated", + }, + frappe.get_doc("Sales Invoice", si.name), + ) + + self.assertDocumentEqual( + {"name": test_data.get("response_data").get("result").get("Irn")}, + frappe.get_doc("e-Invoice Log", {"sales_invoice": si.name}), + ) + self.assertDocumentEqual( + {"name": test_data.get("response_data").get("result").get("EwbNo")}, + frappe.get_doc("e-Waybill Log", {"reference_name": si.name}), + ) + + @responses.activate + def test_generate_e_invoice_with_service_item(self): + """Generate test e-Invoice for Service Item""" + test_data = self.e_invoice_test_data.get("service_item") + si = create_sales_invoice(**test_data.get("kwargs")) + + # Assert if request data given in Json + self.assertDictEqual(test_data.get("request_data"), EInvoiceData(si).get_data()) + + # Mock response for generating irn + self._mock_e_invoice_response(data=test_data) + + generate_e_invoice(si.name) + + # Assert if Integration Request Log generated + self.assertDocumentEqual( + { + "output": frappe.as_json(test_data.get("response_data"), indent=4), + }, + frappe.get_doc( + "Integration Request", + {"reference_doctype": "Sales Invoice", "reference_docname": si.name}, + ), + ) + + # Assert if Sales Doc updated + self.assertDocumentEqual( + { + "irn": test_data.get("response_data").get("result").get("Irn"), + "einvoice_status": "Generated", + }, + frappe.get_doc("Sales Invoice", si.name), + ) + + self.assertDocumentEqual( + {"name": test_data.get("response_data").get("result").get("Irn")}, + frappe.get_doc("e-Invoice Log", {"sales_invoice": si.name}), + ) + + self.assertFalse( + frappe.db.get_value("e-Waybill Log", {"reference_name": si.name}, "name") + ) + + @responses.activate + def test_return_e_invoice_with_goods_item(self): + """Generate test e-Invoice for returned Sales Invoices""" + test_data = self.e_invoice_test_data.get("return_invoice") + + si = create_sales_invoice( + customer_address="_Test Registered Customer-Billing", + shipping_address_name="_Test Registered Customer-Billing", + ) + + test_data.get("kwargs").update({"return_against": si.name}) + + for data in test_data.get("request_data").get("RefDtls").get("PrecDocDtls"): + data.update( + { + "InvDt": format_date(si.posting_date, "dd/mm/yyyy"), + "InvNo": si.name, + } + ) + + return_si = create_sales_invoice( + **test_data.get("kwargs"), + ) + + # Assert if request data given in Json + self.assertDictEqual( + test_data.get("request_data"), + EInvoiceData(frappe.get_doc("Sales Invoice", return_si.name)).get_data(), + ) + + # Mock response for generating irn + self._mock_e_invoice_response(data=test_data) + + generate_e_invoice(return_si.name) + + # Assert if Integration Request Log generated + self.assertDocumentEqual( + { + "output": frappe.as_json(test_data.get("response_data"), indent=4), + }, + frappe.get_doc( + "Integration Request", + { + "reference_doctype": "Sales Invoice", + "reference_docname": return_si.name, + }, + ), + ) + + # Assert if Sales Doc updated + self.assertDocumentEqual( + { + "irn": test_data.get("response_data").get("result").get("Irn"), + "einvoice_status": "Generated", + }, + frappe.get_doc("Sales Invoice", return_si.name), + ) + + self.assertDocumentEqual( + {"name": test_data.get("response_data").get("result").get("Irn")}, + frappe.get_doc("e-Invoice Log", {"sales_invoice": return_si.name}), + ) + + self.assertFalse( + frappe.db.get_value( + "e-Waybill Log", {"reference_name": return_si.name}, "name" + ) + ) + + @responses.activate + def test_debit_note_e_invoice_with_goods_item(self): + """Generate test e-Invoice for debit note with zero quantity""" + test_data = self.e_invoice_test_data.get("debit_invoice") + si = create_sales_invoice( + customer_address="_Test Registered Customer-Billing", + shipping_address_name="_Test Registered Customer-Billing", + ) + + test_data.get("kwargs").update({"return_against": si.name}) + debit_note = create_sales_invoice(**test_data.get("kwargs"), do_not_submit=True) + + debit_note.items[0].qty = 0 + debit_note.save() + debit_note.submit() + + # Assert if request data given in Json + self.assertDictEqual( + test_data.get("request_data"), EInvoiceData(debit_note).get_data() + ) + + # Mock response for generating irn + self._mock_e_invoice_response(data=test_data) + + generate_e_invoice(debit_note.name) + + # Assert if Integration Request Log generated + self.assertDocumentEqual( + { + "output": frappe.as_json(test_data.get("response_data"), indent=4), + }, + frappe.get_doc( + "Integration Request", + { + "reference_doctype": "Sales Invoice", + "reference_docname": debit_note.name, + }, + ), + ) + + # Assert if Sales Doc updated + self.assertDocumentEqual( + { + "irn": test_data.get("response_data").get("result").get("Irn"), + "einvoice_status": "Generated", + }, + frappe.get_doc("Sales Invoice", debit_note.name), + ) + + self.assertDocumentEqual( + {"name": test_data.get("response_data").get("result").get("Irn")}, + frappe.get_doc("e-Invoice Log", {"sales_invoice": debit_note.name}), + ) + + self.assertFalse( + frappe.db.get_value( + "e-Waybill Log", {"reference_name": debit_note.name}, "name" + ) + ) + + @responses.activate + def test_cancel_e_invoice(self): + """Test for generate and cancel e-Invoice + - Test function `validate_if_e_invoice_can_be_cancelled` + """ + + test_data = self.e_invoice_test_data.get("goods_item_with_ewaybill") + si = create_sales_invoice(**test_data.get("kwargs")) + + self.assertRaisesRegex( + frappe.exceptions.ValidationError, + re.compile(r"^(IRN not found)$"), + validate_if_e_invoice_can_be_cancelled, + si, + ) + + test_data.get("response_data").get("result").update( + {"AckDt": str(now_datetime())} + ) + + # Assert if request data given in Json + self.assertDictEqual(test_data.get("request_data"), EInvoiceData(si).get_data()) + + # Mock response for generating irn + self._mock_e_invoice_response(data=test_data) + + generate_e_invoice(si.name) + + si_doc = load_doc("Sales Invoice", si.name, "cancel") + si_doc.get_onload().get("e_invoice_info", {}).update({"acknowledged_on": None}) + + self.assertRaisesRegex( + frappe.exceptions.ValidationError, + re.compile(r"^(e-Invoice can only be cancelled.*)$"), + validate_if_e_invoice_can_be_cancelled, + si_doc, + ) + + cancelled_doc = self._cancel_e_invoice(si.name) + + self.assertDocumentEqual( + {"einvoice_status": "Cancelled", "irn": ""}, + cancelled_doc, + ) + self.assertDocumentEqual({"ewaybill": ""}, cancelled_doc) + + def test_validate_e_invoice_applicability(self): + """Test if e_invoicing is applicable""" + + si = create_sales_invoice( + customer="_Test Unregistered Customer", + gst_category="Unregistered", + do_not_submit=True, + ) + self.assertRaisesRegex( + frappe.exceptions.ValidationError, + re.compile(r"^(e-Invoice is not applicable for invoices.*)$"), + validate_e_invoice_applicability, + si, + ) + + si.update( + { + "gst_category": "Registered Regular", + "customer": "_Test Registered Customer", + } + ) + si.save(ignore_permissions=True) + frappe.db.set_single_value( + "GST Settings", "e_invoice_applicable_from", "2045-05-18" + ) + + self.assertRaisesRegex( + frappe.exceptions.ValidationError, + re.compile(r"^(e-Invoice is not applicable for invoices before.*)$"), + validate_e_invoice_applicability, + si, + ) + + frappe.db.set_single_value( + "GST Settings", "e_invoice_applicable_from", get_datetime() + ) + + def _cancel_e_invoice(self, invoice_no): + values = frappe._dict( + {"reason": "Data Entry Mistake", "remark": "Data Entry Mistake"} + ) + doc = load_doc("Sales Invoice", invoice_no, "cancel") + + # Prepared e_waybill cancel data + cancel_e_waybill = self.e_invoice_test_data.get("cancel_e_waybill") + cancel_e_waybill.get("response_data").get("result").update( + {"ewayBillNo": doc.ewaybill} + ) + + # Assert for Mock request data + self.assertDictEqual( + cancel_e_waybill.get("request_data"), + EWaybillData(doc).get_data_for_cancellation(values), + ) + + # Prepared e_invoice cancel data + cancel_irn_test_data = self.e_invoice_test_data.get("cancel_e_invoice") + cancel_irn_test_data.get("response_data").get("result").update({"Irn": doc.irn}) + + # Assert for Mock request data + self.assertTrue( + cancel_e_waybill.get("request_data"), + ) + + # Mock response for cancel e_waybill + self._mock_e_invoice_response( + data=cancel_e_waybill, + api="ei/api/ewayapi", + ) + + # Mock response for cancel e_invoice + self._mock_e_invoice_response( + data=cancel_irn_test_data, + api="ei/api/invoice/cancel", + ) + + cancel_e_invoice(doc.name, values=values) + return frappe.get_doc("Sales Invoice", doc.name) + + def _mock_e_invoice_response(self, data, api="ei/api/invoice"): + """Mock response for e-Invoice API""" + url = BASE_URL + "/test/" + api + + responses.add( + responses.POST, + url, + body=json.dumps(data.get("response_data")), + match=[matchers.json_params_matcher(data.get("request_data"))], + status=200, + ) + + +def update_dates_for_test_data(test_data): + """Update test data for e-invoice and e-waybill""" + today = format_date(frappe.utils.today(), "dd/mm/yyyy") + now = now_datetime().strftime("%Y-%m-%d %H:%M:%S") + validity = add_to_date(getdate(), days=1).strftime("%Y-%m-%d %I:%M:%S %p") + + # Update test data for goods_item_with_ewaybill + goods_item = test_data.get("goods_item_with_ewaybill") + goods_item.get("response_data").get("result").update( + { + "EwbDt": now, + "EwbValidTill": validity, + } + ) + + # Update Document Date in given test data + for key in ( + "goods_item_with_ewaybill", + "service_item", + "return_invoice", + "debit_invoice", + ): + test_data.get(key).get("request_data").get("DocDtls")["Dt"] = today + test_data.get(key).get("response_data").get("result")["AckDt"] = now + + response = test_data.cancel_e_waybill.get("response_data") + response.get("result")["cancelDate"] = now_datetime().strftime( + "%d/%m/%Y %I:%M:%S %p" + ) + + response = test_data.cancel_e_invoice.get("response_data") + response.get("result")["CancelDate"] = now diff --git a/india_compliance/gst_india/utils/transaction_data.py b/india_compliance/gst_india/utils/transaction_data.py index 591e8fa01..ed6de7a1c 100644 --- a/india_compliance/gst_india/utils/transaction_data.py +++ b/india_compliance/gst_india/utils/transaction_data.py @@ -2,7 +2,7 @@ import frappe from frappe import _ -from frappe.utils import format_date, getdate, rounded +from frappe.utils import format_date, get_link_to_form, getdate, rounded from india_compliance.gst_india.constants import GST_TAX_TYPES, PINCODE_FORMAT from india_compliance.gst_india.constants.e_waybill import ( @@ -148,8 +148,10 @@ def set_transporter_details(self): self.doc.mode_of_transport ), "vehicle_type": VEHICLE_TYPES.get(self.doc.gst_vehicle_type) or "R", - "vehicle_no": self.sanitize_value(self.doc.vehicle_no, 1), - "lr_no": self.sanitize_value(self.doc.lr_no, 2, max_length=15), + "vehicle_no": self.sanitize_value(self.doc.vehicle_no, regex=1), + "lr_no": self.sanitize_value( + self.doc.lr_no, regex=2, max_length=15 + ), "lr_date": ( format_date(self.doc.lr_date, self.DATE_FORMAT) if self.doc.lr_no @@ -157,7 +159,9 @@ def set_transporter_details(self): ), "gst_transporter_id": self.doc.gst_transporter_id or "", "transporter_name": ( - self.sanitize_value(self.doc.transporter_name, 3, max_length=25) + self.sanitize_value( + self.doc.transporter_name, regex=3, max_length=25 + ) if self.doc.transporter_name else "" ), @@ -213,7 +217,9 @@ def get_all_item_details(self): "qty": abs(self.rounded(row.qty, 3)), "taxable_value": abs(self.rounded(row.taxable_value)), "hsn_code": row.gst_hsn_code, - "item_name": self.sanitize_value(row.item_name, 3, max_length=300), + "item_name": self.sanitize_value( + row.item_name, regex=3, max_length=300 + ), "uom": uom if uom in UOMS else "OTH", } ) @@ -311,16 +317,36 @@ def get_address_details(self, address_name, validate_gstin=False): self.check_missing_address_fields(address, validate_gstin) + error_context = { + "reference_doctype": "Address", + "reference_name": address.name, + } + return frappe._dict( { "gstin": address.get("gstin") or "URP", "state_number": address.gst_state_number, - "address_title": self.sanitize_value(address.address_title, 2), + "address_title": self.sanitize_value( + address.address_title, + regex=2, + fieldname="address_title", + **error_context, + ), "address_line1": self.sanitize_value( - address.address_line1, 3, min_length=1 + address.address_line1, + regex=3, + min_length=1, + fieldname="address_line1", + **error_context, + ), + "address_line2": self.sanitize_value(address.address_line2, regex=3), + "city": self.sanitize_value( + address.city, + regex=3, + max_length=50, + fieldname="city", + **error_context, ), - "address_line2": self.sanitize_value(address.address_line2, 3), - "city": self.sanitize_value(address.city, 3, max_length=50), "pincode": int(address.pincode), } ) @@ -392,18 +418,81 @@ def rounded(value, precision=2): @staticmethod def sanitize_value( - value, + value: str, regex=None, min_length=3, max_length=100, truncate=True, + *, + fieldname=None, + reference_doctype=None, + reference_name=None, ): + """ + Sanitize value to make it suitable for GST JSON sent for e-Waybill and e-Invoice. + + If fieldname, reference doctype and reference name are present, + error will be thrown for invalid values instead of sanitizing them. + + Parameters: + ---------- + @param value: Value to be sanitized + @param regex: Regex Key (from REGEX_MAP) to substitute unacceptable characters + @param min_length (default: 3): Minimum length of the value that is acceptable + @param max_length (default: 100): Maximum length of the value that is acceptable + @param truncate (default: True): Truncate the value if it exceeds max_length + @param fieldname: Fieldname for which the value is being sanitized + @param reference_doctype: Doctype of the document that contains the field + @param reference_name: Name of the document that contains the field + + Returns: + ---------- + @return: Sanitized value + + """ + + def _throw(message, **format_args): + if not (fieldname and reference_doctype and reference_name): + return + + message = message.format( + field=_(frappe.get_meta(reference_doctype).get_label(fieldname)), + **format_args, + ) + + frappe.throw( + _("{reference_doctype} {reference_link}: {message}").format( + reference_doctype=_(reference_doctype), + reference_link=frappe.bold( + get_link_to_form(reference_doctype, reference_name) + ), + message=message, + ), + title=_("Invalid Data for GST Upload"), + ) + if not value or len(value) < min_length: - return + return _throw( + _("{field} must be at least {min_length} characters long"), + min_length=min_length, + ) + + original_value = value if regex: value = re.sub(REGEX_MAP[regex], "", value) + if len(value) < min_length: + if not original_value.isascii(): + return _throw(_("{field} must only consist of ASCII characters")) + + return _throw( + _("{field} consists of invalid characters: {invalid_chars}"), + invalid_chars=frappe.bold( + "".join(set(original_value).difference(value)) + ), + ) + if not truncate and len(value) > max_length: return diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index 262a54010..071acf88a 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -8,12 +8,14 @@ app_color = "grey" app_email = "hello@indiacompliance.app" app_license = "GNU General Public License (v3)" -required_apps = ["erpnext"] +required_apps = ["frappe/erpnext"] after_install = "india_compliance.install.after_install" before_tests = "india_compliance.tests.before_tests" boot_session = "india_compliance.boot.set_bootinfo" +before_uninstall = "india_compliance.uninstall.before_uninstall" + app_include_js = "gst_india.bundle.js" doctype_js = { @@ -157,7 +159,8 @@ "india_compliance.gst_india.utils.jinja.get_transport_type", "india_compliance.gst_india.utils.jinja.get_transport_mode", "india_compliance.gst_india.utils.jinja.get_ewaybill_barcode", - "india_compliance.gst_india.utils.jinja.get_non_zero_fields", + "india_compliance.gst_india.utils.jinja.get_e_invoice_item_fields", + "india_compliance.gst_india.utils.jinja.get_e_invoice_amount_fields", ], } diff --git a/india_compliance/income_tax_india/setup.py b/india_compliance/income_tax_india/setup.py index a09c7c656..3ca11115c 100644 --- a/india_compliance/income_tax_india/setup.py +++ b/india_compliance/income_tax_india/setup.py @@ -4,4 +4,4 @@ def after_install(): - create_custom_fields(CUSTOM_FIELDS, update=True) + create_custom_fields(CUSTOM_FIELDS, ignore_validate=True) diff --git a/india_compliance/income_tax_india/uninstall.py b/india_compliance/income_tax_india/uninstall.py new file mode 100644 index 000000000..230852591 --- /dev/null +++ b/india_compliance/income_tax_india/uninstall.py @@ -0,0 +1,6 @@ +from india_compliance.gst_india.utils.custom_fields import delete_custom_fields +from india_compliance.income_tax_india.constants.custom_fields import CUSTOM_FIELDS + + +def before_uninstall(): + delete_custom_fields(CUSTOM_FIELDS) diff --git a/india_compliance/patches.txt b/india_compliance/patches.txt index 3eadb659a..81b6124a4 100644 --- a/india_compliance/patches.txt +++ b/india_compliance/patches.txt @@ -3,8 +3,9 @@ [post_model_sync] india_compliance.patches.v14.set_default_for_overridden_accounts_setting -execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #2 +execute:from india_compliance.gst_india.setup import create_custom_fields; create_custom_fields() #5 execute:from india_compliance.gst_india.setup import create_property_setters; create_property_setters() india_compliance.patches.post_install.update_custom_role_for_e_invoice_summary india_compliance.patches.v14.remove_ecommerce_gstin_from_purchase_invoice india_compliance.patches.v14.set_sandbox_mode_in_gst_settings +execute:from india_compliance.gst_india.setup import add_fields_to_item_variant_settings; add_fields_to_item_variant_settings() diff --git a/india_compliance/patches/post_install/migrate_e_invoice_settings_to_gst_settings.py b/india_compliance/patches/post_install/migrate_e_invoice_settings_to_gst_settings.py index a000b44ca..244fe7f3d 100644 --- a/india_compliance/patches/post_install/migrate_e_invoice_settings_to_gst_settings.py +++ b/india_compliance/patches/post_install/migrate_e_invoice_settings_to_gst_settings.py @@ -5,7 +5,7 @@ from frappe.utils.password import decrypt from india_compliance.gst_india.constants.custom_fields import E_INVOICE_FIELDS -from india_compliance.gst_india.utils import toggle_custom_fields +from india_compliance.gst_india.utils.custom_fields import toggle_custom_fields def execute(): @@ -39,8 +39,10 @@ def execute(): if sbool(old_settings.enable): toggle_custom_fields(E_INVOICE_FIELDS, True) click.secho( - "Your e-Invoice Settings have been migrated to GST Settings." - " Please enable the e-Invoice API in GST Settings manually.\n", + ( + "Your e-Invoice Settings have been migrated to GST Settings." + " Please enable the e-Invoice API in GST Settings manually.\n" + ), fg="yellow", ) diff --git a/india_compliance/patches/post_install/remove_old_fields.py b/india_compliance/patches/post_install/remove_old_fields.py index d3d31af7a..25ff2c237 100644 --- a/india_compliance/patches/post_install/remove_old_fields.py +++ b/india_compliance/patches/post_install/remove_old_fields.py @@ -1,4 +1,4 @@ -from india_compliance.gst_india.utils import delete_old_fields +from india_compliance.gst_india.utils.custom_fields import delete_old_fields def execute(): diff --git a/india_compliance/patches/post_install/set_default_gst_settings.py b/india_compliance/patches/post_install/set_default_gst_settings.py index de094e1f0..6c287aa22 100644 --- a/india_compliance/patches/post_install/set_default_gst_settings.py +++ b/india_compliance/patches/post_install/set_default_gst_settings.py @@ -4,7 +4,7 @@ from india_compliance.gst_india.constants.custom_fields import ( SALES_REVERSE_CHARGE_FIELDS, ) -from india_compliance.gst_india.utils import toggle_custom_fields +from india_compliance.gst_india.utils.custom_fields import toggle_custom_fields # Enable setting only if transaction exists in last 3 years. POSTING_DATE_CONDITION = { diff --git a/india_compliance/patches/post_install/set_gst_category.py b/india_compliance/patches/post_install/set_gst_category.py index f0fb5d45a..50440d7d9 100644 --- a/india_compliance/patches/post_install/set_gst_category.py +++ b/india_compliance/patches/post_install/set_gst_category.py @@ -1,6 +1,6 @@ import frappe -from india_compliance.gst_india.utils import delete_old_fields +from india_compliance.gst_india.utils.custom_fields import delete_old_fields def execute(): diff --git a/india_compliance/patches/post_install/setup_custom_fields_for_gst.py b/india_compliance/patches/post_install/setup_custom_fields_for_gst.py index 9f5884560..51d4db1f7 100644 --- a/india_compliance/patches/post_install/setup_custom_fields_for_gst.py +++ b/india_compliance/patches/post_install/setup_custom_fields_for_gst.py @@ -1,6 +1,6 @@ import frappe -from india_compliance.gst_india.utils import delete_old_fields +from india_compliance.gst_india.utils.custom_fields import delete_old_fields def execute(): diff --git a/india_compliance/patches/post_install/update_e_invoice_fields_and_logs.py b/india_compliance/patches/post_install/update_e_invoice_fields_and_logs.py index 97c9b57cb..cea3efb95 100644 --- a/india_compliance/patches/post_install/update_e_invoice_fields_and_logs.py +++ b/india_compliance/patches/post_install/update_e_invoice_fields_and_logs.py @@ -1,6 +1,7 @@ import frappe from india_compliance.gst_india.utils import parse_datetime +from india_compliance.gst_india.utils.custom_fields import delete_custom_fields user = None @@ -317,32 +318,3 @@ def delete_e_invoice_fields(): ] } delete_custom_fields(FIELDS_TO_DELETE) - - -### Helper Function - - -def delete_custom_fields(custom_fields): - """ - :param custom_fields: a dict like `{'Sales Invoice': [{fieldname: 'test', ...}]}` - """ - - for doctypes, fields in custom_fields.items(): - if isinstance(fields, dict): - # only one field - fields = [fields] - - if isinstance(doctypes, str): - # only one doctype - doctypes = (doctypes,) - - for doctype in doctypes: - frappe.db.delete( - "Custom Field", - { - "fieldname": ("in", [field["fieldname"] for field in fields]), - "dt": doctype, - }, - ) - - frappe.clear_cache(doctype=doctype) diff --git a/india_compliance/patches/post_install/update_reverse_charge_and_export_type.py b/india_compliance/patches/post_install/update_reverse_charge_and_export_type.py index 33764deab..c3174bcb0 100644 --- a/india_compliance/patches/post_install/update_reverse_charge_and_export_type.py +++ b/india_compliance/patches/post_install/update_reverse_charge_and_export_type.py @@ -1,6 +1,6 @@ import frappe -from india_compliance.gst_india.utils import delete_old_fields +from india_compliance.gst_india.utils.custom_fields import delete_old_fields DOCTYPES = ("Purchase Invoice", "Sales Invoice") diff --git a/india_compliance/patches/v14/remove_ecommerce_gstin_from_purchase_invoice.py b/india_compliance/patches/v14/remove_ecommerce_gstin_from_purchase_invoice.py index eb3538ea1..95b7b2b5c 100644 --- a/india_compliance/patches/v14/remove_ecommerce_gstin_from_purchase_invoice.py +++ b/india_compliance/patches/v14/remove_ecommerce_gstin_from_purchase_invoice.py @@ -1,4 +1,4 @@ -from india_compliance.gst_india.utils import delete_old_fields +from india_compliance.gst_india.utils.custom_fields import delete_old_fields def execute(): diff --git a/india_compliance/public/js/quick_entry.js b/india_compliance/public/js/quick_entry.js index f5042e83b..7ccfd2214 100644 --- a/india_compliance/public/js/quick_entry.js +++ b/india_compliance/public/js/quick_entry.js @@ -256,15 +256,16 @@ class AddressQuickEntryForm extends GSTQuickEntryForm { get_default_party() { const doc = cur_frm && cur_frm.doc; - if (!doc) return; - - const { doctype, name } = doc; - if (in_list(frappe.boot.gst_party_types, doctype)) - return { party_type: doctype, party: name }; - - const party_type = ic.get_party_type(doctype); - const party = doc[party_type.toLowerCase()]; - return { party_type, party }; + if ( + doc && + frappe.dynamic_link && + frappe.dynamic_link.doc === doc + ) { + return { + party_type: frappe.dynamic_link.doctype, + party: frappe.dynamic_link.doc[frappe.dynamic_link.fieldname] + }; + } } } diff --git a/india_compliance/public/js/transaction.js b/india_compliance/public/js/transaction.js index 9d457bf1d..93cbb36bb 100644 --- a/india_compliance/public/js/transaction.js +++ b/india_compliance/public/js/transaction.js @@ -40,29 +40,35 @@ function fetch_gst_details(doctype) { async function update_gst_details(frm) { if (frm.__gst_update_triggered || frm.updating_party_details || !frm.doc.company) return; - const party_type = ic.get_party_type(frm.doc.doctype).toLowerCase(); - if (!frm.doc[party_type]) return; + const party = frm.doc[ic.get_party_fieldname(frm.doc.doctype)]; + if (!party) return; frm.__gst_update_triggered = true; + // wait for GSTINs to get fetched await frappe.after_ajax().then(() => frm.__gst_update_triggered = false); - const party_fields = ["tax_category", "gst_category", "company_gstin", party_type]; + const party_details = {}; + + // fieldname may be "party_name" for Quotation, but "customer" is expected by get_gst_details + party_details[ic.get_party_type(frm.doc.doctype).toLowerCase()] = party; + + const fieldnames_to_set = ["tax_category", "gst_category", "company_gstin"]; if (in_list(frappe.boot.sales_doctypes, frm.doc.doctype)) { - party_fields.push( + fieldnames_to_set.push( "customer_address", "billing_address_gstin", "is_export_with_gst", "is_reverse_charge" ); } else { - party_fields.push("supplier_address", "supplier_gstin"); + fieldnames_to_set.push("supplier_address", "supplier_gstin"); } - const party_details = Object.fromEntries( - party_fields.map(field => [field, frm.doc[field]]) - ); + for (const fieldname of fieldnames_to_set) { + party_details[fieldname] = frm.doc[fieldname]; + } frappe.call({ method: "india_compliance.gst_india.overrides.transaction.get_gst_details", diff --git a/india_compliance/public/js/utils.js b/india_compliance/public/js/utils.js index a5ce2b393..86e1c927a 100644 --- a/india_compliance/public/js/utils.js +++ b/india_compliance/public/js/utils.js @@ -39,6 +39,11 @@ Object.assign(ic, { return in_list(frappe.boot.sales_doctypes, doctype) ? "Customer" : "Supplier"; }, + get_party_fieldname(doctype) { + if (doctype == "Quotation") return "party_name"; + return ic.get_party_type(doctype).toLowerCase(); + }, + set_state_options(frm) { const state_field = frm.get_field("state"); const country = frm.get_field("country").value; diff --git a/india_compliance/tests/__init__.py b/india_compliance/tests/__init__.py index a91d8b05d..c86cb8a0c 100644 --- a/india_compliance/tests/__init__.py +++ b/india_compliance/tests/__init__.py @@ -1,3 +1,5 @@ +from functools import partial + import frappe from frappe.desk.page.setup_wizard.setup_wizard import setup_complete from frappe.test_runner import make_test_objects @@ -38,6 +40,7 @@ def before_tests(): frappe.flags.country = "India" frappe.flags.skip_test_records = True + frappe.enqueue = partial(frappe.enqueue, now=True) def set_default_settings_for_tests(): diff --git a/india_compliance/tests/test_records.json b/india_compliance/tests/test_records.json index b6c82b8bc..dd82d1495 100644 --- a/india_compliance/tests/test_records.json +++ b/india_compliance/tests/test_records.json @@ -89,6 +89,32 @@ "income_account": "Sales - _TIRC" } ] + }, + { + "description": "_Test Service Item", + "doctype": "Item", + "item_code": "_Test Service Item", + "item_name": "_Test Service Item", + "valuation_rate": 100, + "item_group": "Services", + "gst_hsn_code": "999900", + "uoms": [ + { + "conversion_factor": 1, + "uom": "Nos", + "name": "_Test Service Item" + } + ], + "item_defaults": [ + { + "name": "_Test Service Item", + "company": "_Test Indian Registered Company", + "default_warehouse": "Stores - _TIRC", + "buying_cost_center": "Main - _TIRC", + "selling_cost_center": "Main - _TIRC", + "income_account": "Service - _TIRC" + } + ] } ], "Customer": [ @@ -123,21 +149,21 @@ "gst_category": "Registered Regular" }, { - "name":"_Test Registered Composition Supplier", + "name": "_Test Registered Composition Supplier", "supplier_name": "_Test Registered Composition Supplier", "supplier_type": "Individual", "gstin": "33AAAAR6720M1ZG", "gst_category": "Registered Composition" }, { - "name":"_Test Registered InterState Supplier", + "name": "_Test Registered InterState Supplier", "supplier_name": "_Test Registered InterState Supplier", "supplier_type": "Individual", "gstin": "33AAAAR6720M1ZG", "gst_category": "Registered Regular" }, { - "name":"_Test Unregistered Supplier", + "name": "_Test Unregistered Supplier", "supplier_name": "_Test Unregistered Supplier", "supplier_type": "Individual", "gstin": "", @@ -198,7 +224,10 @@ "is_primary_address": 1, "is_shipping_address": 1, "links": [ - { "link_doctype": "Customer", "link_name": "_Test Registered Customer" } + { + "link_doctype": "Customer", + "link_name": "_Test Registered Customer" + } ] }, { @@ -212,7 +241,10 @@ "gstin": "24AANCA4892J1Z8", "gst_category": "SEZ", "links": [ - { "link_doctype": "Customer", "link_name": "_Test Registered Customer" } + { + "link_doctype": "Customer", + "link_name": "_Test Registered Customer" + } ] }, { @@ -247,7 +279,10 @@ "is_primary_address": 1, "is_shipping_address": 1, "links": [ - { "link_doctype": "Supplier", "link_name": "_Test Registered Supplier" } + { + "link_doctype": "Supplier", + "link_name": "_Test Registered Supplier" + } ] }, { @@ -261,7 +296,10 @@ "gstin": "", "gst_category": "Overseas", "links": [ - { "link_doctype": "Supplier", "link_name": "_Test Registered Supplier" } + { + "link_doctype": "Supplier", + "link_name": "_Test Registered Supplier" + } ] }, { @@ -327,7 +365,5 @@ } ] } - ] - } \ No newline at end of file diff --git a/india_compliance/uninstall.py b/india_compliance/uninstall.py new file mode 100644 index 000000000..bf35019db --- /dev/null +++ b/india_compliance/uninstall.py @@ -0,0 +1,27 @@ +import click + +from india_compliance.gst_india.constants import BUG_REPORT_URL +from india_compliance.gst_india.uninstall import before_uninstall as remove_gst +from india_compliance.income_tax_india.uninstall import ( + before_uninstall as remove_income_tax, +) + + +def before_uninstall(): + try: + print("Removing Income Tax customizations...") + remove_income_tax() + + print("Removing GST customizations...") + remove_gst() + + except Exception as e: + click.secho( + ( + "Removing customizations for India Compliance failed due to an error." + " Please try again or" + f" report the issue on {BUG_REPORT_URL} if not resolved." + ), + fg="bright_red", + ) + raise e