├── requirements.txt ├── license.txt ├── erpnext_gst_compliance ├── www │ └── __init__.py ├── config │ ├── __init__.py │ ├── desktop.py │ └── docs.py ├── templates │ ├── __init__.py │ └── pages │ │ └── __init__.py ├── adequare_integration │ ├── __init__.py │ ├── doctype │ │ ├── __init__.py │ │ ├── adequare_credential │ │ │ ├── __init__.py │ │ │ ├── adequare_credential.py │ │ │ └── adequare_credential.json │ │ └── adequare_settings │ │ │ ├── __init__.py │ │ │ ├── adequare_settings.js │ │ │ ├── adequare_settings.py │ │ │ ├── adequare_settings.json │ │ │ └── test_adequare_settings.py │ └── adequare_connector.py ├── cleartax_integration │ ├── __init__.py │ ├── doctype │ │ ├── __init__.py │ │ ├── cleartax_credential │ │ │ ├── __init__.py │ │ │ ├── cleartax_credential.py │ │ │ └── cleartax_credential.json │ │ └── cleartax_settings │ │ │ ├── __init__.py │ │ │ ├── cleartax_settings.js │ │ │ ├── cleartax_settings.json │ │ │ ├── cleartax_settings.py │ │ │ └── test_cleartax_settings.py │ └── cleartax_connector.py ├── erpnext_gst_compliance │ ├── __init__.py │ ├── doctype │ │ ├── __init__.py │ │ ├── e_invoice │ │ │ ├── __init__.py │ │ │ ├── e_invoice.js │ │ │ ├── e_invoice_list.js │ │ │ ├── test_e_invoice.py │ │ │ ├── e_invoice.json │ │ │ └── e_invoice.py │ │ ├── e_invoice_item │ │ │ ├── __init__.py │ │ │ ├── e_invoice_item.py │ │ │ └── e_invoice_item.json │ │ ├── e_invoice_request_log │ │ │ ├── __init__.py │ │ │ ├── test_e_invoice_request_log.py │ │ │ ├── e_invoice_request_log.js │ │ │ ├── e_invoice_request_log.py │ │ │ └── e_invoice_request_log.json │ │ └── e_invoicing_settings │ │ │ ├── __init__.py │ │ │ ├── e_invoicing_settings.py │ │ │ ├── test_e_invoicing_settings.py │ │ │ ├── e_invoicing_settings.json │ │ │ └── e_invoicing_settings.js │ ├── report │ │ ├── __init__.py │ │ └── e_invoice_summary │ │ │ ├── __init__.py │ │ │ ├── e_invoice_summary.json │ │ │ ├── e_invoice_summary.js │ │ │ └── e_invoice_summary.py │ ├── print_format │ │ ├── __init__.py │ │ └── gst_e_invoice │ │ │ ├── __init__.py │ │ │ ├── gst_e_invoice.html │ │ │ └── gst_e_invoice.json │ ├── e_invoicing_controller.py │ └── setup.py ├── __init__.py ├── modules.txt ├── patches.txt ├── patches │ ├── setup_einvoice_fields.py │ └── copy_adequare_credentials.py ├── utils.py ├── hooks.py └── public │ └── js │ └── sales_invoice.js ├── .gitignore ├── .flake8 ├── .github ├── helper │ ├── site_config.json │ └── install.sh └── workflows │ └── ci.yml ├── setup.py ├── MANIFEST.in └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | frappe -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | License: GNU GPL v3.0 -------------------------------------------------------------------------------- /erpnext_gst_compliance/www/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/templates/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/templates/pages/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/adequare_integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/cleartax_integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/adequare_integration/doctype/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/cleartax_integration/doctype/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/report/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = '1.0.0' 3 | 4 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/print_format/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/adequare_integration/doctype/adequare_credential/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/adequare_integration/doctype/adequare_settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/cleartax_integration/doctype/cleartax_credential/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/cleartax_integration/doctype/cleartax_settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice_item/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/report/e_invoice_summary/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice_request_log/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoicing_settings/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/print_format/gst_e_invoice/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.egg-info 4 | *.swp 5 | tags 6 | erpnext_gst_compliance/docs/current -------------------------------------------------------------------------------- /erpnext_gst_compliance/modules.txt: -------------------------------------------------------------------------------- 1 | ERPNext GST Compliance 2 | Cleartax Integration 3 | Adequare Integration -------------------------------------------------------------------------------- /erpnext_gst_compliance/patches.txt: -------------------------------------------------------------------------------- 1 | erpnext_gst_compliance.patches.setup_einvoice_fields 2 | erpnext_gst_compliance.patches.copy_adequare_credentials -------------------------------------------------------------------------------- /erpnext_gst_compliance/patches/setup_einvoice_fields.py: -------------------------------------------------------------------------------- 1 | from erpnext_gst_compliance.erpnext_gst_compliance.setup import setup_custom_fields 2 | 3 | def execute(): 4 | setup_custom_fields() -------------------------------------------------------------------------------- /erpnext_gst_compliance/patches/copy_adequare_credentials.py: -------------------------------------------------------------------------------- 1 | from erpnext_gst_compliance.erpnext_gst_compliance.setup import copy_adequare_credentials 2 | 3 | def execute(): 4 | copy_adequare_credentials() -------------------------------------------------------------------------------- /erpnext_gst_compliance/cleartax_integration/doctype/cleartax_settings/cleartax_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Frappe and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('Cleartax Settings', { 5 | // refresh: function(frm) { 6 | 7 | // } 8 | }); 9 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice_request_log/test_e_invoice_request_log.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe Technologied Pvt. Ltd. and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | import unittest 6 | 7 | class TestEInvoiceRequestLog(unittest.TestCase): 8 | pass 9 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/adequare_integration/doctype/adequare_settings/adequare_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Frappe Technologied Pvt. Ltd. and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('Adequare Settings', { 5 | // refresh: function(frm) { 6 | 7 | // } 8 | }); 9 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/config/desktop.py: -------------------------------------------------------------------------------- 1 | from frappe import _ 2 | 3 | def get_data(): 4 | return [ 5 | { 6 | "module_name": "ERPNext GST Compliance", 7 | "color": "grey", 8 | "icon": "octicon octicon-file-directory", 9 | "type": "module", 10 | "label": _("ERPNext GST Compliance") 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice_request_log/e_invoice_request_log.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Frappe Technologied Pvt. Ltd. and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('E Invoice Request Log', { 5 | // refresh: function(frm) { 6 | 7 | // } 8 | }); 9 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/adequare_integration/doctype/adequare_credential/adequare_credential.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe Technologied Pvt. Ltd. and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | class AdequareCredential(Document): 8 | pass 9 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice_request_log/e_invoice_request_log.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe Technologied Pvt. Ltd. and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | class EInvoiceRequestLog(Document): 8 | pass 9 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice/e_invoice.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Frappe and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('E Invoice', { 5 | refresh(frm) { 6 | }, 7 | 8 | invoice(frm) { 9 | frm.call({ 10 | 'doc': frm.doc, 11 | 'method': 'fetch_invoice_details' 12 | }) 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice_item/e_invoice_item.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2021, Frappe and contributors 3 | # For license information, please see license.txt 4 | 5 | from __future__ import unicode_literals 6 | # import frappe 7 | from frappe.model.document import Document 8 | 9 | class EInvoiceItem(Document): 10 | pass 11 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/cleartax_integration/doctype/cleartax_credential/cleartax_credential.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2021, Frappe and contributors 3 | # For license information, please see license.txt 4 | 5 | from __future__ import unicode_literals 6 | # import frappe 7 | from frappe.model.document import Document 8 | 9 | class CleartaxCredential(Document): 10 | pass 11 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/config/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for docs 3 | """ 4 | 5 | # source_link = "https://github.com/[org_name]/erpnext_gst_compliance" 6 | # docs_base_url = "https://[org_name].github.io/erpnext_gst_compliance" 7 | # headline = "App that does everything" 8 | # sub_heading = "Yes, you got that right the first time, everything" 9 | 10 | def get_context(context): 11 | context.brand_html = "ERPNext GST Compliance" 12 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | E121, 4 | E126, 5 | E127, 6 | E128, 7 | E203, 8 | E225, 9 | E226, 10 | E231, 11 | E241, 12 | E251, 13 | E261, 14 | E265, 15 | E302, 16 | E303, 17 | E305, 18 | E402, 19 | E501, 20 | E741, 21 | W291, 22 | W292, 23 | W293, 24 | W391, 25 | W503, 26 | W504, 27 | F403, 28 | B007, 29 | B950, 30 | W191, 31 | 32 | max-line-length = 200 -------------------------------------------------------------------------------- /.github/helper/site_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_host": "127.0.0.1", 3 | "db_port": 3306, 4 | "db_name": "test_frappe", 5 | "db_password": "test_frappe", 6 | "auto_email_id": "test@example.com", 7 | "mail_server": "smtp.example.com", 8 | "mail_login": "test@example.com", 9 | "mail_password": "test", 10 | "admin_password": "admin", 11 | "root_login": "root", 12 | "root_password": "travis", 13 | "host_name": "http://test_site:8000", 14 | "install_apps": ["erpnext"], 15 | "throttle_user_limit": 100 16 | } 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open('requirements.txt') as f: 4 | install_requires = f.read().strip().split('\n') 5 | 6 | # get version from __version__ variable in erpnext_gst_compliance/__init__.py 7 | from erpnext_gst_compliance import __version__ as version 8 | 9 | setup( 10 | name='erpnext_gst_compliance', 11 | version=version, 12 | description='Manage GST Compliance of ERPNext', 13 | author='Frappe Technologies Pvt. Ltd.', 14 | author_email='developers@frappe.io', 15 | packages=find_packages(), 16 | zip_safe=False, 17 | include_package_data=True, 18 | install_requires=install_requires 19 | ) 20 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice/e_invoice_list.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors 2 | // License: GNU General Public License v3. See license.txt 3 | 4 | // render 5 | frappe.listview_settings['E Invoice'] = { 6 | has_indicator_for_draft: 1, 7 | get_indicator: function(doc) { 8 | var status_color = { 9 | "IRN Pending": "yellow", 10 | "IRN Generated": "green", 11 | "E-Way Bill Generated": "green", 12 | "IRN Cancelled": "red", 13 | "E-Way Bill Cancelled": "red", 14 | "Cancelled": "red" 15 | }; 16 | return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include requirements.txt 3 | include *.json 4 | include *.md 5 | include *.py 6 | include *.txt 7 | recursive-include erpnext_gst_compliance *.css 8 | recursive-include erpnext_gst_compliance *.csv 9 | recursive-include erpnext_gst_compliance *.html 10 | recursive-include erpnext_gst_compliance *.ico 11 | recursive-include erpnext_gst_compliance *.js 12 | recursive-include erpnext_gst_compliance *.json 13 | recursive-include erpnext_gst_compliance *.md 14 | recursive-include erpnext_gst_compliance *.png 15 | recursive-include erpnext_gst_compliance *.py 16 | recursive-include erpnext_gst_compliance *.svg 17 | recursive-include erpnext_gst_compliance *.txt 18 | recursive-exclude erpnext_gst_compliance *.pyc -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/report/e_invoice_summary/e_invoice_summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_total_row": 0, 3 | "columns": [], 4 | "creation": "2021-03-12 11:23:37.312294", 5 | "disable_prepared_report": 0, 6 | "disabled": 0, 7 | "docstatus": 0, 8 | "doctype": "Report", 9 | "filters": [], 10 | "idx": 0, 11 | "is_standard": "Yes", 12 | "json": "{}", 13 | "letter_head": "Logo", 14 | "modified": "2021-08-13 13:32:34.858952", 15 | "modified_by": "Administrator", 16 | "module": "ERPNext GST Compliance", 17 | "name": "E-Invoice Summary", 18 | "owner": "Administrator", 19 | "prepared_report": 0, 20 | "ref_doctype": "Sales Invoice", 21 | "report_name": "E-Invoice Summary", 22 | "report_type": "Script Report", 23 | "roles": [ 24 | { 25 | "role": "Administrator" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ERPNext GST Compliance 2 | 3 | Manage GST Compliance of ERPNext for India 4 | 5 | ## Features 6 | ### E Invoicing 7 | 8 | E-Invoicing in ERPNext is automated using API integration with two GST Suvidha Providers (GSP): 9 | 1. Adequare GSP 10 | 2. Cleartax 11 | 12 | Both of these providers enables one-click e-invoice generation & cancellation. 13 | 14 | **Installation** 15 | 1. You must have a working ERPNext site either on a local bench setup or hosted on Frappe Cloud. 16 | 2. For a local bench setup, install this app with Bench CLI 17 | ``` 18 | bench get-app https://github.com/frappe/erpnext_gst_compliance.git 19 | bench --site site_name install-app erpnext_gst_compliance 20 | ``` 21 | 3. For a site hosted on Frappe Cloud, follow [this](https://frappecloud.com/docs/bench/install-custom-app) guide to add this app to your custom bench. Then simply install this app on to your hosted site. 22 | 4. Once you have this app install on your site, you can follow [this](https://docs.erpnext.com/docs/v13/user/manual/en/regional/india/setup-e-invoicing) guide to configure API integration. 23 | 24 | #### License 25 | 26 | GNU GPL v3.0 -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoicing_settings/e_invoicing_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe import _ 6 | from frappe.model.document import Document 7 | from frappe.utils.data import get_link_to_form 8 | 9 | class EInvoicingSettings(Document): 10 | def validate(self): 11 | if self.service_provider and not frappe.db.get_single_value(self.service_provider, 'enabled'): 12 | settings_form = get_link_to_form(self.service_provider, self.service_provider) 13 | frappe.throw(_('Selected Service Provider is disabled. Please enable it by visitng {} Form.') 14 | .format(settings_form)) 15 | 16 | if self.service_provider: 17 | service_provider_doc = frappe.get_single(self.service_provider) 18 | if not service_provider_doc.credentials: 19 | msg = _("Selected Service Provider doesn't have credentials setup.") + ' ' 20 | msg += _("Please add atleast one credential to enable e-invoicing.") 21 | frappe.throw(msg) 22 | self.companies = ', '.join((d.company for d in service_provider_doc.credentials)) 23 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoicing_settings/test_e_invoicing_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe and Contributors 2 | # See license.txt 3 | 4 | import frappe 5 | import unittest 6 | 7 | class TestEInvoicingSettings(unittest.TestCase): 8 | def test_service_provider_is_disabled(self): 9 | e_invoicing_settings = frappe.get_doc('E Invoicing Settings') 10 | e_invoicing_settings.service_provider = 'Adequare Settings' 11 | self.assertRaises(frappe.ValidationError, e_invoicing_settings.save) 12 | 13 | adequare_settings = frappe.get_single('Adequare Settings') 14 | adequare_settings.enabled = 1 15 | adequare_settings.flags.ignore_validate = True 16 | adequare_settings.save() 17 | 18 | e_invoicing_settings.reload() 19 | e_invoicing_settings.service_provider = 'Adequare Settings' 20 | e_invoicing_settings.save() 21 | 22 | adequare_settings.reload() 23 | adequare_settings.enabled = 0 24 | adequare_settings.flags.ignore_validate = True 25 | adequare_settings.save() 26 | 27 | e_invoicing_settings.reload() 28 | e_invoicing_settings.service_provider = None 29 | e_invoicing_settings.flags.ignore_mandatory = True 30 | e_invoicing_settings.save() -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoicing_settings/e_invoicing_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "creation": "2021-05-11 18:36:59.370091", 4 | "doctype": "DocType", 5 | "editable_grid": 1, 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "service_provider", 9 | "companies" 10 | ], 11 | "fields": [ 12 | { 13 | "fieldname": "service_provider", 14 | "fieldtype": "Link", 15 | "in_list_view": 1, 16 | "label": "Service Provider Settings", 17 | "options": "DocType", 18 | "reqd": 1 19 | }, 20 | { 21 | "fieldname": "companies", 22 | "fieldtype": "Small Text", 23 | "hidden": 1, 24 | "label": "Companies", 25 | "read_only": 1 26 | } 27 | ], 28 | "index_web_pages_for_search": 1, 29 | "issingle": 1, 30 | "links": [], 31 | "modified": "2021-08-13 15:59:55.025514", 32 | "modified_by": "Administrator", 33 | "module": "ERPNext GST Compliance", 34 | "name": "E Invoicing Settings", 35 | "owner": "Administrator", 36 | "permissions": [ 37 | { 38 | "create": 1, 39 | "delete": 1, 40 | "email": 1, 41 | "print": 1, 42 | "read": 1, 43 | "role": "System Manager", 44 | "share": 1, 45 | "write": 1 46 | } 47 | ], 48 | "sort_field": "modified", 49 | "sort_order": "DESC", 50 | "track_changes": 1 51 | } -------------------------------------------------------------------------------- /erpnext_gst_compliance/adequare_integration/doctype/adequare_credential/adequare_credential.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "creation": "2021-06-21 16:37:39.070094", 4 | "doctype": "DocType", 5 | "editable_grid": 1, 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "company", 9 | "gstin", 10 | "username", 11 | "password" 12 | ], 13 | "fields": [ 14 | { 15 | "fieldname": "company", 16 | "fieldtype": "Link", 17 | "in_list_view": 1, 18 | "label": "Company", 19 | "options": "Company", 20 | "reqd": 1 21 | }, 22 | { 23 | "fieldname": "gstin", 24 | "fieldtype": "Data", 25 | "in_list_view": 1, 26 | "label": "GSTIN", 27 | "reqd": 1 28 | }, 29 | { 30 | "fieldname": "username", 31 | "fieldtype": "Data", 32 | "in_list_view": 1, 33 | "label": "Username", 34 | "reqd": 1 35 | }, 36 | { 37 | "fieldname": "password", 38 | "fieldtype": "Password", 39 | "in_list_view": 1, 40 | "label": "Password", 41 | "reqd": 1 42 | } 43 | ], 44 | "index_web_pages_for_search": 1, 45 | "istable": 1, 46 | "links": [], 47 | "modified": "2021-06-21 16:37:39.070094", 48 | "modified_by": "Administrator", 49 | "module": "Adequare Integration", 50 | "name": "Adequare Credential", 51 | "owner": "Administrator", 52 | "permissions": [], 53 | "quick_entry": 1, 54 | "sort_field": "modified", 55 | "sort_order": "DESC", 56 | "track_changes": 1 57 | } -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoicing_settings/e_invoicing_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2021, Frappe and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('E Invoicing Settings', { 5 | onload: async (frm) => { 6 | let einvoicing_modules = await frappe.db.get_list('Module Def', { 7 | filters: { 8 | app_name: 'erpnext_gst_compliance' 9 | } 10 | }); 11 | if (einvoicing_modules && einvoicing_modules.length) { 12 | einvoicing_modules = einvoicing_modules.map(m => m.name) 13 | } else { 14 | return; 15 | } 16 | 17 | let service_providers = await frappe.db.get_list('DocType', { 18 | filters: { 19 | module: ['in', einvoicing_modules], 20 | name: ['!=', frm.doc.doctype], 21 | issingle: 1 22 | } 23 | }); 24 | 25 | if (service_providers && service_providers.length) { 26 | service_providers = service_providers.map(p => p.name); 27 | } 28 | 29 | frm.set_query('service_provider', () => { 30 | return { 31 | filters: { 32 | issingle: 1, 33 | name: ['in', service_providers] 34 | } 35 | } 36 | }); 37 | }, 38 | 39 | refresh(frm) { 40 | if (frm.doc.service_provider) { 41 | const label = __('Go to {0}', [frm.doc.service_provider]) 42 | frm.add_custom_button(label, function() { 43 | frappe.set_route('Form', frm.doc.service_provider); 44 | }); 45 | } 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/cleartax_integration/doctype/cleartax_credential/cleartax_credential.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "creation": "2021-05-07 16:16:21.804888", 4 | "doctype": "DocType", 5 | "editable_grid": 1, 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "company", 9 | "gstin", 10 | "username", 11 | "password", 12 | "owner_id" 13 | ], 14 | "fields": [ 15 | { 16 | "columns": 3, 17 | "fieldname": "company", 18 | "fieldtype": "Link", 19 | "in_list_view": 1, 20 | "label": "Company", 21 | "options": "Company", 22 | "reqd": 1 23 | }, 24 | { 25 | "columns": 2, 26 | "fieldname": "gstin", 27 | "fieldtype": "Data", 28 | "in_list_view": 1, 29 | "label": "GSTIN", 30 | "reqd": 1 31 | }, 32 | { 33 | "columns": 2, 34 | "fieldname": "username", 35 | "fieldtype": "Data", 36 | "label": "Username" 37 | }, 38 | { 39 | "columns": 2, 40 | "fieldname": "password", 41 | "fieldtype": "Password", 42 | "label": "Password" 43 | }, 44 | { 45 | "columns": 5, 46 | "fieldname": "owner_id", 47 | "fieldtype": "Password", 48 | "in_list_view": 1, 49 | "label": "Owner ID", 50 | "reqd": 1 51 | } 52 | ], 53 | "index_web_pages_for_search": 1, 54 | "istable": 1, 55 | "links": [], 56 | "modified": "2021-05-22 19:05:58.624184", 57 | "modified_by": "Administrator", 58 | "module": "Cleartax Integration", 59 | "name": "Cleartax Credential", 60 | "owner": "Administrator", 61 | "permissions": [], 62 | "sort_field": "modified", 63 | "sort_order": "DESC", 64 | "track_changes": 1 65 | } -------------------------------------------------------------------------------- /erpnext_gst_compliance/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import json 3 | import frappe 4 | import traceback 5 | from frappe import _ 6 | 7 | class HandledException(frappe.ValidationError): pass 8 | 9 | def log_exception(fn): 10 | '''Decorator to catch & log exceptions''' 11 | 12 | def wrapper(*args, **kwargs): 13 | return_value = None 14 | try: 15 | return_value = fn(*args, **kwargs) 16 | except HandledException: 17 | # exception has been logged 18 | # so just continue raising HandledException to stop futher logging 19 | raise 20 | except Exception: 21 | log_error() 22 | show_request_failed_error() 23 | 24 | return return_value 25 | 26 | return wrapper 27 | 28 | def show_request_failed_error(): 29 | frappe.clear_messages() 30 | message = _('There was an error while making the request.') + ' ' 31 | message += _('Please try once again and if the issue persists, please contact ERPNext Support.') 32 | frappe.throw(message, title=_('Request Failed'), exc=HandledException) 33 | 34 | def log_error(): 35 | frappe.db.rollback() 36 | seperator = "--" * 50 37 | err_tb = traceback.format_exc() 38 | err_msg = str(sys.exc_info()[1]) 39 | # data = json.dumps(data, indent=4) 40 | 41 | message = "\n".join([ 42 | "Error: " + err_msg, seperator, 43 | # "Data:", data, seperator, 44 | "Exception:", err_tb 45 | ]) 46 | frappe.log_error( 47 | title=_('E-Invoicing Exception'), 48 | message=message 49 | ) 50 | frappe.db.commit() 51 | 52 | def safe_load_json(message): 53 | try: 54 | json_message = json.loads(message) 55 | except Exception: 56 | json_message = message 57 | 58 | return json_message 59 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/cleartax_integration/doctype/cleartax_settings/cleartax_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "creation": "2021-05-07 16:10:09.136237", 4 | "doctype": "DocType", 5 | "editable_grid": 1, 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "enabled", 9 | "sandbox_mode", 10 | "auth_token", 11 | "credentials" 12 | ], 13 | "fields": [ 14 | { 15 | "depends_on": "enabled", 16 | "fieldname": "credentials", 17 | "fieldtype": "Table", 18 | "label": "Credentials", 19 | "mandatory_depends_on": "enabled", 20 | "options": "Cleartax Credential" 21 | }, 22 | { 23 | "default": "0", 24 | "fieldname": "enabled", 25 | "fieldtype": "Check", 26 | "label": "Enable" 27 | }, 28 | { 29 | "depends_on": "enabled", 30 | "fieldname": "auth_token", 31 | "fieldtype": "Password", 32 | "label": "Authorization Token", 33 | "mandatory_depends_on": "enabled" 34 | }, 35 | { 36 | "default": "0", 37 | "depends_on": "enabled", 38 | "fieldname": "sandbox_mode", 39 | "fieldtype": "Check", 40 | "label": "Sandbox Mode", 41 | "mandatory_depends_on": "enabled" 42 | } 43 | ], 44 | "index_web_pages_for_search": 1, 45 | "issingle": 1, 46 | "links": [], 47 | "modified": "2021-05-09 14:39:34.015257", 48 | "modified_by": "Administrator", 49 | "module": "Cleartax Integration", 50 | "name": "Cleartax Settings", 51 | "owner": "Administrator", 52 | "permissions": [ 53 | { 54 | "create": 1, 55 | "delete": 1, 56 | "email": 1, 57 | "print": 1, 58 | "read": 1, 59 | "role": "System Manager", 60 | "share": 1, 61 | "write": 1 62 | } 63 | ], 64 | "sort_field": "modified", 65 | "sort_order": "DESC", 66 | "track_changes": 1 67 | } -------------------------------------------------------------------------------- /.github/helper/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | cd ~ || exit 6 | 7 | sudo apt-get -y install redis-server nodejs npm -qq 8 | 9 | pip install frappe-bench 10 | 11 | git clone https://github.com/frappe/frappe --branch develop --depth 1 12 | bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench 13 | 14 | mkdir ~/frappe-bench/sites/test_site 15 | cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/ 16 | 17 | mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'" 18 | mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" 19 | 20 | mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'" 21 | mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe" 22 | mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'" 23 | 24 | mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'" 25 | mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES" 26 | 27 | cd ~/frappe-bench || exit 28 | 29 | sed -i 's/watch:/# watch:/g' Procfile 30 | sed -i 's/schedule:/# schedule:/g' Procfile 31 | sed -i 's/socketio:/# socketio:/g' Procfile 32 | sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile 33 | 34 | bench get-app https://github.com/frappe/erpnext.git --branch develop 35 | 36 | bench start & 37 | bench --site test_site reinstall --yes 38 | 39 | bench get-app https://github.com/frappe/erpnext_gst_compliance.git "${GITHUB_WORKSPACE}" 40 | bench --verbose --site test_site install-app erpnext_gst_compliance 41 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/adequare_integration/doctype/adequare_settings/adequare_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2021, Frappe and contributors 3 | # For license information, please see license.txt 4 | 5 | from __future__ import unicode_literals 6 | 7 | import frappe 8 | from frappe import _ 9 | from frappe.model.document import Document 10 | from frappe.utils.data import get_link_to_form 11 | from erpnext_gst_compliance.adequare_integration.adequare_connector import AdequareConnector 12 | 13 | class AdequareSettings(Document): 14 | def validate(self): 15 | if not self.enabled: 16 | return 17 | 18 | for row in self.credentials: 19 | gstin_company = self.get_company_linked_with_gstin(row.gstin) 20 | if not gstin_company: 21 | msg = _("The entered GSTIN {} doesn't matches with Company GSTIN.").format(row.gstin) + ' ' 22 | msg += _("Please ensure that company address has proper GSTIN set that matches with entered GSTIN.") 23 | frappe.throw(msg, title=_('Invalid GSTIN')) 24 | 25 | def on_update(self): 26 | current_service_provider = frappe.db.get_single_value('E Invoicing Settings', 'service_provider') 27 | if self.enabled and current_service_provider != self.name: 28 | link_to_settings = get_link_to_form('E Invoicing Settings', 'E Invoicing Settings') 29 | frappe.msgprint( 30 | _('You must set Adequare as E-Invoicing Service Provider in {} to use it for e-invoicing.') 31 | .format(link_to_settings), title=_('Set as Default Provider')) 32 | 33 | def get_company_linked_with_gstin(self, gstin): 34 | company_name = frappe.db.sql(""" 35 | select dl.link_name from `tabAddress` a, `tabDynamic Link` dl 36 | where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company' 37 | """, (gstin)) 38 | 39 | return company_name[0][0] if company_name and len(company_name) > 0 else None 40 | 41 | def get_connector(self): 42 | return AdequareConnector -------------------------------------------------------------------------------- /erpnext_gst_compliance/cleartax_integration/doctype/cleartax_settings/cleartax_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2021, Frappe and contributors 3 | # For license information, please see license.txt 4 | 5 | from __future__ import unicode_literals 6 | 7 | import frappe 8 | from frappe import _ 9 | from frappe.model.document import Document 10 | from frappe.utils.data import get_link_to_form 11 | from erpnext_gst_compliance.cleartax_integration.cleartax_connector import CleartaxConnector 12 | 13 | class CleartaxSettings(Document): 14 | def validate(self): 15 | if not self.enabled: 16 | return 17 | 18 | for row in self.credentials: 19 | gstin_company = self.get_company_linked_with_gstin(row.gstin) 20 | if not gstin_company: 21 | msg = _("The entered GSTIN {} doesn't matches with Company GSTIN.").format(row.gstin) + ' ' 22 | msg += _("Please ensure that company address has proper GSTIN set that matches with entered GSTIN.") 23 | frappe.throw(msg, title=_('Invalid GSTIN')) 24 | 25 | def on_update(self): 26 | current_service_provider = frappe.db.get_single_value('E Invoicing Settings', 'service_provider') 27 | if self.enabled and current_service_provider != self.name: 28 | link_to_settings = get_link_to_form('E Invoicing Settings', 'E Invoicing Settings') 29 | frappe.msgprint( 30 | _('You must set Cleartax as E-Invoicing Service Provider in {} to use it for e-invoicing.') 31 | .format(link_to_settings), title=_('Set as Default Provider')) 32 | 33 | def get_company_linked_with_gstin(self, gstin): 34 | company_name = frappe.db.sql(""" 35 | select dl.link_name from `tabAddress` a, `tabDynamic Link` dl 36 | where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company' 37 | """, (gstin)) 38 | 39 | return company_name[0][0] if company_name and len(company_name) > 0 else None 40 | 41 | def get_connector(self): 42 | return CleartaxConnector -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/report/e_invoice_summary/e_invoice_summary.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors 2 | // For license information, please see license.txt 3 | /* eslint-disable */ 4 | 5 | frappe.query_reports["E-Invoice Summary"] = { 6 | "filters": [ 7 | { 8 | "fieldtype": "Link", 9 | "options": "Company", 10 | "reqd": 1, 11 | "fieldname": "company", 12 | "label": __("Company"), 13 | "default": frappe.defaults.get_user_default("Company"), 14 | }, 15 | { 16 | "fieldtype": "Link", 17 | "options": "Customer", 18 | "fieldname": "customer", 19 | "label": __("Customer") 20 | }, 21 | { 22 | "fieldtype": "Date", 23 | "reqd": 1, 24 | "fieldname": "from_date", 25 | "label": __("From Date"), 26 | "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), 27 | }, 28 | { 29 | "fieldtype": "Date", 30 | "reqd": 1, 31 | "fieldname": "to_date", 32 | "label": __("To Date"), 33 | "default": frappe.datetime.get_today(), 34 | }, 35 | { 36 | "fieldtype": "Select", 37 | "fieldname": "status", 38 | "label": __("Status"), 39 | "options": "\nPending\nGenerated\nCancelled\nFailed" 40 | } 41 | ], 42 | 43 | "formatter": function (value, row, column, data, default_formatter) { 44 | value = default_formatter(value, row, column, data); 45 | 46 | if (column.fieldname == "einvoice_status" && value) { 47 | if (value == 'Pending') value = `${value}`; 48 | else if (value == 'Generated') value = `${value}`; 49 | else if (value == 'Cancelled') value = `${value}`; 50 | else if (value == 'Failed') value = `${value}`; 51 | } 52 | 53 | return value; 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/hooks.py: -------------------------------------------------------------------------------- 1 | from . import __version__ as app_version 2 | 3 | app_name = "erpnext_gst_compliance" 4 | app_title = "ERPNext GST Compliance" 5 | app_publisher = "Frappe Technologied Pvt. Ltd." 6 | app_description = "Manage GST Compliance of ERPNext" 7 | app_icon = "octicon octicon-file-directory" 8 | app_color = "grey" 9 | app_email = "developers@erpnext.com" 10 | app_license = "GNU GPL v3.0" 11 | 12 | before_tests = [ 13 | "erpnext.setup.utils.before_tests", 14 | "erpnext_gst_compliance.erpnext_gst_compliance.setup.before_test" 15 | ] 16 | after_install = "erpnext_gst_compliance.erpnext_gst_compliance.setup.setup" 17 | 18 | doctype_js = { 19 | "Sales Invoice": "public/js/sales_invoice.js" 20 | } 21 | 22 | doc_events = { 23 | "Sales Invoice": { 24 | "on_update": "erpnext_gst_compliance.erpnext_gst_compliance.doctype.e_invoice.e_invoice.validate_sales_invoice_change", 25 | "on_submit": "erpnext_gst_compliance.erpnext_gst_compliance.doctype.e_invoice.e_invoice.validate_sales_invoice_submission", 26 | "on_cancel": [ 27 | "erpnext_gst_compliance.erpnext_gst_compliance.doctype.e_invoice.e_invoice.validate_sales_invoice_cancellation", 28 | "erpnext_gst_compliance.erpnext_gst_compliance.doctype.e_invoice.e_invoice.cancel_e_invoice" 29 | ], 30 | "on_trash": [ 31 | "erpnext_gst_compliance.erpnext_gst_compliance.doctype.e_invoice.e_invoice.validate_sales_invoice_deletion", 32 | "erpnext_gst_compliance.erpnext_gst_compliance.doctype.e_invoice.e_invoice.delete_e_invoice" 33 | ] 34 | }, 35 | "Company": { 36 | "after_insert": "erpnext_gst_compliance.erpnext_gst_compliance.setup.on_company_update", 37 | "on_update": "erpnext_gst_compliance.erpnext_gst_compliance.setup.on_company_update" 38 | } 39 | } 40 | 41 | user_data_fields = [ 42 | { 43 | "doctype": "{doctype_1}", 44 | "filter_by": "{filter_by}", 45 | "redact_fields": ["{field_1}", "{field_2}"], 46 | "partial": 1, 47 | }, 48 | { 49 | "doctype": "{doctype_2}", 50 | "filter_by": "{filter_by}", 51 | "partial": 1, 52 | }, 53 | { 54 | "doctype": "{doctype_3}", 55 | "strict": False, 56 | }, 57 | { 58 | "doctype": "{doctype_4}" 59 | } 60 | ] 61 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | workflow_dispatch: 6 | push: 7 | 8 | jobs: 9 | 10 | tests: 11 | runs-on: ubuntu-18.04 12 | 13 | strategy: 14 | fail-fast: false 15 | 16 | name: Server 17 | 18 | services: 19 | mysql: 20 | image: mariadb:10.3 21 | env: 22 | MYSQL_ALLOW_EMPTY_PASSWORD: YES 23 | ports: 24 | - 3306:3306 25 | options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 26 | 27 | steps: 28 | - name: Clone 29 | uses: actions/checkout@v2 30 | 31 | - name: Setup Python 32 | uses: actions/setup-python@v2 33 | with: 34 | python-version: 3.7 35 | 36 | - name: Add to Hosts 37 | run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts 38 | 39 | - name: Cache pip 40 | uses: actions/cache@v2 41 | with: 42 | path: ~/.cache/pip 43 | key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt') }} 44 | restore-keys: | 45 | ${{ runner.os }}-pip- 46 | ${{ runner.os }}- 47 | - name: Cache node modules 48 | uses: actions/cache@v2 49 | env: 50 | cache-name: cache-node-modules 51 | with: 52 | path: ~/.npm 53 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 54 | restore-keys: | 55 | ${{ runner.os }}-build-${{ env.cache-name }}- 56 | ${{ runner.os }}-build- 57 | ${{ runner.os }}- 58 | 59 | - name: Get yarn cache directory path 60 | id: yarn-cache-dir-path 61 | run: echo "::set-output name=dir::$(yarn cache dir)" 62 | 63 | - uses: actions/cache@v2 64 | id: yarn-cache 65 | with: 66 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 67 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 68 | restore-keys: | 69 | ${{ runner.os }}-yarn- 70 | 71 | - name: Install 72 | run: | 73 | bash ${GITHUB_WORKSPACE}/.github/helper/install.sh 74 | 75 | 76 | - name: Run Tests 77 | run: cd ~/frappe-bench/ && bench --site test_site run-tests --app erpnext_gst_compliance 78 | env: 79 | TYPE: server 80 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice_request_log/e_invoice_request_log.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "autoname": "EINV-REQ-.#####", 4 | "creation": "2020-12-08 12:54:08.175992", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "user", 10 | "url", 11 | "headers", 12 | "response", 13 | "column_break_7", 14 | "timestamp", 15 | "reference_invoice", 16 | "data" 17 | ], 18 | "fields": [ 19 | { 20 | "fieldname": "user", 21 | "fieldtype": "Link", 22 | "label": "User", 23 | "options": "User" 24 | }, 25 | { 26 | "fieldname": "url", 27 | "fieldtype": "Data", 28 | "label": "URL" 29 | }, 30 | { 31 | "fieldname": "headers", 32 | "fieldtype": "Code", 33 | "label": "Headers", 34 | "options": "JSON" 35 | }, 36 | { 37 | "fieldname": "response", 38 | "fieldtype": "Code", 39 | "label": "Response", 40 | "options": "JSON" 41 | }, 42 | { 43 | "fieldname": "column_break_7", 44 | "fieldtype": "Column Break" 45 | }, 46 | { 47 | "default": "Now", 48 | "fieldname": "timestamp", 49 | "fieldtype": "Datetime", 50 | "label": "Timestamp" 51 | }, 52 | { 53 | "fieldname": "reference_invoice", 54 | "fieldtype": "Data", 55 | "label": "Reference Invoice" 56 | }, 57 | { 58 | "fieldname": "data", 59 | "fieldtype": "Code", 60 | "label": "Data", 61 | "options": "JSON" 62 | } 63 | ], 64 | "index_web_pages_for_search": 1, 65 | "links": [], 66 | "modified": "2021-08-13 13:04:26.673650", 67 | "modified_by": "Administrator", 68 | "module": "ERPNext GST Compliance", 69 | "name": "E Invoice Request Log", 70 | "owner": "Administrator", 71 | "permissions": [ 72 | { 73 | "email": 1, 74 | "export": 1, 75 | "print": 1, 76 | "read": 1, 77 | "report": 1, 78 | "role": "System Manager", 79 | "share": 1 80 | }, 81 | { 82 | "email": 1, 83 | "export": 1, 84 | "print": 1, 85 | "read": 1, 86 | "report": 1, 87 | "role": "Accounts User", 88 | "share": 1 89 | }, 90 | { 91 | "email": 1, 92 | "export": 1, 93 | "print": 1, 94 | "read": 1, 95 | "report": 1, 96 | "role": "Accounts Manager", 97 | "share": 1 98 | } 99 | ], 100 | "sort_field": "modified", 101 | "sort_order": "DESC" 102 | } -------------------------------------------------------------------------------- /erpnext_gst_compliance/adequare_integration/doctype/adequare_settings/adequare_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "creation": "2021-06-21 16:35:18.701406", 4 | "doctype": "DocType", 5 | "editable_grid": 1, 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "enabled", 9 | "sandbox_mode", 10 | "credentials", 11 | "auth_token", 12 | "token_expiry", 13 | "advanced_settings_section", 14 | "client_id", 15 | "column_break_8", 16 | "client_secret" 17 | ], 18 | "fields": [ 19 | { 20 | "default": "0", 21 | "fieldname": "enabled", 22 | "fieldtype": "Check", 23 | "label": "Enable" 24 | }, 25 | { 26 | "default": "0", 27 | "depends_on": "enabled", 28 | "fieldname": "sandbox_mode", 29 | "fieldtype": "Check", 30 | "label": "Sandbox Mode", 31 | "mandatory_depends_on": "enabled" 32 | }, 33 | { 34 | "fieldname": "auth_token", 35 | "fieldtype": "Data", 36 | "hidden": 1, 37 | "label": "Authorization Token", 38 | "read_only": 1 39 | }, 40 | { 41 | "depends_on": "enabled", 42 | "fieldname": "credentials", 43 | "fieldtype": "Table", 44 | "label": "Credentials", 45 | "mandatory_depends_on": "enabled", 46 | "options": "Adequare Credential" 47 | }, 48 | { 49 | "fieldname": "token_expiry", 50 | "fieldtype": "Datetime", 51 | "hidden": 1, 52 | "label": "Token Expiry", 53 | "read_only": 1 54 | }, 55 | { 56 | "collapsible": 1, 57 | "fieldname": "advanced_settings_section", 58 | "fieldtype": "Section Break", 59 | "label": "Advanced Settings" 60 | }, 61 | { 62 | "fieldname": "client_id", 63 | "fieldtype": "Data", 64 | "label": "Client ID" 65 | }, 66 | { 67 | "fieldname": "client_secret", 68 | "fieldtype": "Password", 69 | "label": "Client Secret" 70 | }, 71 | { 72 | "fieldname": "column_break_8", 73 | "fieldtype": "Column Break" 74 | } 75 | ], 76 | "index_web_pages_for_search": 1, 77 | "issingle": 1, 78 | "links": [], 79 | "modified": "2021-09-09 21:12:57.225905", 80 | "modified_by": "Administrator", 81 | "module": "Adequare Integration", 82 | "name": "Adequare Settings", 83 | "owner": "Administrator", 84 | "permissions": [ 85 | { 86 | "create": 1, 87 | "delete": 1, 88 | "email": 1, 89 | "print": 1, 90 | "read": 1, 91 | "role": "System Manager", 92 | "share": 1, 93 | "write": 1 94 | } 95 | ], 96 | "sort_field": "modified", 97 | "sort_order": "DESC", 98 | "track_changes": 1 99 | } -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/report/e_invoice_summary/e_invoice_summary.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors 2 | # For license information, please see license.txt 3 | 4 | from __future__ import unicode_literals 5 | import frappe 6 | from frappe import _ 7 | 8 | def execute(filters=None): 9 | validate_filters(filters) 10 | 11 | columns = get_columns() 12 | data = get_data(filters) 13 | 14 | return columns, data 15 | 16 | def validate_filters(filters={}): 17 | filters = frappe._dict(filters) 18 | 19 | if not filters.company: 20 | frappe.throw(_('{} is mandatory for generating E-Invoice Summary Report').format(_('Company')), title=_('Invalid Filter')) 21 | if filters.company: 22 | # validate if company has e-invoicing enabled 23 | pass 24 | if not filters.from_date or not filters.to_date: 25 | frappe.throw(_('From Date & To Date is mandatory for generating E-Invoice Summary Report'), title=_('Invalid Filter')) 26 | if filters.from_date > filters.to_date: 27 | frappe.throw(_('From Date must be before To Date'), title=_('Invalid Filter')) 28 | 29 | def get_data(filters={}): 30 | query_filters = { 31 | 'posting_date': ['between', [filters.from_date, filters.to_date]], 32 | 'einvoice_status': ['is', 'set'], 33 | 'company': filters.company 34 | } 35 | if filters.customer: 36 | query_filters['customer'] = filters.customer 37 | if filters.status: 38 | query_filters['einvoice_status'] = filters.status 39 | 40 | data = frappe.get_all( 41 | 'Sales Invoice', 42 | filters=query_filters, 43 | fields=[d.get('fieldname') for d in get_columns()] 44 | ) 45 | 46 | return data 47 | 48 | def get_columns(): 49 | return [ 50 | { 51 | "fieldtype": "Date", 52 | "fieldname": "posting_date", 53 | "label": _("Posting Date"), 54 | "width": 0 55 | }, 56 | { 57 | "fieldtype": "Link", 58 | "fieldname": "name", 59 | "label": _("Sales Invoice"), 60 | "options": "Sales Invoice", 61 | "width": 140 62 | }, 63 | { 64 | "fieldtype": "Data", 65 | "fieldname": "einvoice_status", 66 | "label": _("Status"), 67 | "width": 100 68 | }, 69 | { 70 | "fieldtype": "Link", 71 | "fieldname": "customer", 72 | "options": "Customer", 73 | "label": _("Customer") 74 | }, 75 | { 76 | "fieldtype": "Check", 77 | "fieldname": "is_return", 78 | "label": _("Is Return"), 79 | "width": 85 80 | }, 81 | { 82 | "fieldtype": "Data", 83 | "fieldname": "ack_no", 84 | "label": "Ack. No.", 85 | "width": 145 86 | }, 87 | { 88 | "fieldtype": "Data", 89 | "fieldname": "ack_date", 90 | "label": "Ack. Date", 91 | "width": 165 92 | }, 93 | { 94 | "fieldtype": "Data", 95 | "fieldname": "irn", 96 | "label": _("IRN No."), 97 | "width": 250 98 | }, 99 | { 100 | "fieldtype": "Currency", 101 | "options": "Company:company:default_currency", 102 | "fieldname": "base_grand_total", 103 | "label": _("Grand Total"), 104 | "width": 120 105 | } 106 | ] -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/e_invoicing_controller.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe and contributors 2 | # For license information, please see license.txt 3 | 4 | import six 5 | import frappe 6 | from frappe import _ 7 | from erpnext_gst_compliance.utils import safe_load_json 8 | from frappe.utils.data import now_datetime, get_link_to_form, time_diff_in_hours 9 | from erpnext_gst_compliance.erpnext_gst_compliance.doctype.e_invoice.e_invoice import create_einvoice, get_einvoice 10 | 11 | def parse_sales_invoice(sales_invoice): 12 | if isinstance(sales_invoice, six.string_types): 13 | sales_invoice = safe_load_json(sales_invoice) 14 | if not isinstance(sales_invoice, dict): 15 | frappe.throw(_('Invalid Argument: Sales Invoice')) # TODO: better message 16 | sales_invoice = frappe._dict(sales_invoice) 17 | 18 | return sales_invoice 19 | 20 | def get_service_provider_connector(): 21 | service_provider = frappe.db.get_single_value('E Invoicing Settings', 'service_provider') 22 | controller = frappe.get_doc(service_provider) 23 | connector = controller.get_connector() 24 | 25 | return connector 26 | 27 | @frappe.whitelist() 28 | def generate_irn(sales_invoice): 29 | sales_invoice = parse_sales_invoice(sales_invoice) 30 | validate_irn_generation(sales_invoice) 31 | connector = get_service_provider_connector() 32 | 33 | einvoice = create_einvoice(sales_invoice.name) 34 | success, errors = connector.generate_irn(einvoice) 35 | 36 | if not success: 37 | frappe.throw(errors, title=_('IRN Generation Failed'), as_list=1) 38 | else: 39 | frappe.msgprint(_("IRN Generated Successfully."), alert=1) 40 | 41 | return success 42 | 43 | def validate_irn_generation(sales_invoice): 44 | if sales_invoice.e_invoice: 45 | irn = frappe.db.get_value('E Invoice', sales_invoice.e_invoice, 'irn') 46 | if irn: 47 | msg = _('IRN is already generated for the Sales Invoice.') + ' ' 48 | msg += _('Check E-Invoice {} for more details.').format(get_link_to_form('E Invoice', sales_invoice.e_invoice)) 49 | frappe.throw(msg=msg, title=_('Invalid Request')) 50 | 51 | @frappe.whitelist() 52 | def cancel_irn(sales_invoice, reason, remark): 53 | sales_invoice = parse_sales_invoice(sales_invoice) 54 | connector = get_service_provider_connector() 55 | 56 | einvoice = get_einvoice(sales_invoice.name) 57 | validate_irn_cancellation(einvoice) 58 | success, errors = connector.cancel_irn(einvoice, reason, remark) 59 | 60 | if not success: 61 | frappe.throw(errors, title=_('IRN Cancellation Failed'), as_list=1) 62 | else: 63 | frappe.msgprint(_("IRN Cancelled Successfully."), alert=1) 64 | 65 | return success 66 | 67 | def validate_irn_cancellation(einvoice): 68 | if time_diff_in_hours(now_datetime(), einvoice.ack_date) > 24: 69 | frappe.throw(_('E-Invoice cannot be cancelled after 24 hours of IRN generation.'), 70 | title=_('Invalid Request')) 71 | 72 | if not einvoice.irn: 73 | frappe.throw(_('IRN not found. You must generate IRN before cancelling.'), 74 | title=_('Invalid Request')) 75 | 76 | if einvoice.irn_cancelled: 77 | frappe.throw(_('IRN is already cancelled. You cannot cancel e-invoice twice.'), 78 | title=_('Invalid Request')) 79 | 80 | @frappe.whitelist() 81 | def generate_eway_bill(sales_invoice_name, **kwargs): 82 | connector = get_service_provider_connector() 83 | 84 | eway_bill_details = frappe._dict(kwargs) 85 | einvoice = get_einvoice(sales_invoice_name) 86 | einvoice.set_eway_bill_details(eway_bill_details) 87 | success, errors = connector.generate_eway_bill(einvoice) 88 | 89 | if not success: 90 | frappe.throw(errors, title=_('E-Way Bill Generation Failed'), as_list=1) 91 | else: 92 | frappe.msgprint(_("E-Way Bill Generated Successfully."), alert=1) 93 | 94 | return success 95 | 96 | # cancel ewaybill api is currently not supported by E-Invoice Portal 97 | 98 | # @frappe.whitelist() 99 | # def cancel_ewaybill(sales_invoice_name, reason, remark): 100 | # connector = get_service_provider_connector() 101 | 102 | # einvoice = get_einvoice(sales_invoice_name) 103 | # success, errors = connector.cancel_ewaybill(einvoice, reason, remark) 104 | 105 | # if not success: 106 | # frappe.throw(errors, title=_('E-Way Bill Cancellation Failed'), as_list=1) 107 | 108 | # return success 109 | 110 | 111 | @frappe.whitelist() 112 | def cancel_ewaybill(sales_invoice_name): 113 | einvoice = get_einvoice(sales_invoice_name) 114 | 115 | einvoice.ewaybill = '' 116 | einvoice.ewaybill_cancelled = 1 117 | einvoice.status = 'E-Way Bill Cancelled' 118 | einvoice.flags.ignore_validate_update_after_submit = 1 119 | einvoice.flags.ignore_permissions = 1 120 | einvoice.save() -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice/test_e_invoice.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2021, Frappe and Contributors 3 | # See license.txt 4 | from __future__ import unicode_literals 5 | 6 | import frappe 7 | import unittest 8 | from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import get_sales_invoice_for_e_invoice 9 | 10 | class TestEInvoice(unittest.TestCase): 11 | def setUp(self): 12 | self.e_invoice, self.sales_invoice = make_e_invoice() 13 | self.assertEqual(self.e_invoice.status, 'IRN Pending') 14 | 15 | def test_mandatory_fields(self): 16 | mandatory_fields = [] 17 | party_fields = [ 18 | 'seller_gstin', 'seller_legal_name', 'seller_address_line_1', 'seller_location', 19 | 'seller_pincode', 'seller_state_code', 'buyer_gstin', 'buyer_legal_name', 'buyer_address_line_1', 20 | 'buyer_location', 'buyer_pincode', 'buyer_state_code', 'buyer_place_of_supply' 21 | ] 22 | if self.e_invoice.dispatch_legal_name: 23 | party_fields += [ 24 | 'dispatch_legal_name', 'dispatch_address_line_1', 'dispatch_location', 25 | 'dispatch_pincode', 'dispatch_state_code' 26 | ] 27 | if self.e_invoice.shipping_legal_name: 28 | party_fields += [ 29 | 'shipping_legal_name', 'shipping_address_line_1', 'shipping_location', 30 | 'shipping_pincode', 'shipping_state_code' 31 | ] 32 | 33 | mandatory_fields += party_fields 34 | mandatory_fields += ['ass_value', 'base_invoice_value'] 35 | if self.sales_invoice.transporter: 36 | mandatory_fields += ['distance'] 37 | 38 | for key in mandatory_fields: 39 | self.assertTrue(self.e_invoice.get(key)) 40 | 41 | def test_party_details(self): 42 | self.assertEqual(self.e_invoice.seller_legal_name, self.sales_invoice.company) 43 | self.assertEqual(self.e_invoice.seller_address_line_1, '_Test Address Line 1') 44 | self.assertEqual(self.e_invoice.seller_address_line_2, '_Test Address Line 2') 45 | self.assertEqual(self.e_invoice.seller_gstin, '27AAECE4835E1ZR') 46 | self.assertEqual(self.e_invoice.seller_pincode, '401108') 47 | self.assertEqual(self.e_invoice.seller_state_code, '27') 48 | 49 | self.assertEqual(self.e_invoice.buyer_legal_name, self.sales_invoice.customer) 50 | self.assertEqual(self.e_invoice.buyer_address_line_1, '_Test Address Line 1') 51 | self.assertEqual(self.e_invoice.buyer_address_line_2, '_Test Address Line 2') 52 | self.assertEqual(self.e_invoice.buyer_gstin, '27AACCM7806M1Z3') 53 | self.assertEqual(self.e_invoice.buyer_pincode, '410038') 54 | self.assertEqual(self.e_invoice.buyer_place_of_supply, '27') 55 | 56 | self.assertEqual(self.e_invoice.shipping_address_line_1, '_Test Address Line 1') 57 | self.assertEqual(self.e_invoice.shipping_address_line_2, '_Test Address Line 2') 58 | self.assertEqual(self.e_invoice.shipping_pincode, '410098') 59 | self.assertEqual(self.e_invoice.shipping_state_code, '27') 60 | self.assertFalse(self.e_invoice.shipping_gstin) 61 | 62 | self.assertEqual(self.e_invoice.dispatch_address_line_1, '_Test Dispatch Address Line 1') 63 | self.assertEqual(self.e_invoice.dispatch_address_line_2, '_Test Dispatch Address Line 2') 64 | self.assertEqual(self.e_invoice.dispatch_pincode, '1100101') 65 | self.assertEqual(self.e_invoice.dispatch_state_code, '07') 66 | 67 | def test_item_validations(self): 68 | item = self.e_invoice.items[0] 69 | self.assertEqual(item.hsn_code, '990002') 70 | # if hsn_code starts with 99 then its a service item 71 | self.assertTrue(item.is_service_item) 72 | self.assertEqual(item.quantity, 2000) 73 | 74 | item = self.e_invoice.items[1] 75 | self.assertEqual(item.hsn_code, '890002') 76 | # if hsn_code doesn't starts with 99 then not a service item 77 | self.assertFalse(item.is_service_item) 78 | self.assertEqual(item.quantity, 420) 79 | 80 | self.e_invoice.validate_items() 81 | 82 | gst_rate_copy = self.e_invoice.items[0].gst_rate 83 | # invalid gst rate should throw an error 84 | self.e_invoice.items[0].gst_rate = 12.5 85 | self.assertRaises(frappe.ValidationError, self.e_invoice.validate_items) 86 | self.e_invoice.items[0].gst_rate = gst_rate_copy 87 | 88 | def test_invalid_uom(self): 89 | item = self.e_invoice.items[0] 90 | item.unit = 'BAGS' 91 | self.assertRaises(frappe.ValidationError, self.e_invoice.save) 92 | 93 | self.e_invoice.reload() 94 | item = self.e_invoice.items[0] 95 | item.unit = 'BAG' 96 | self.e_invoice.save() 97 | 98 | def make_e_invoice(): 99 | sales_invoice = get_sales_invoice_for_e_invoice() 100 | sales_invoice.items[0].gst_hsn_code = '990002' 101 | sales_invoice.items[1].gst_hsn_code = '890002' 102 | sales_invoice.save() 103 | 104 | e_invoice = frappe.new_doc('E Invoice') 105 | e_invoice.invoice = sales_invoice.name 106 | e_invoice.sync_with_sales_invoice() 107 | e_invoice.save() 108 | 109 | return e_invoice, sales_invoice -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice_item/e_invoice_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "creation": "2021-05-08 15:21:31.926611", 4 | "doctype": "DocType", 5 | "editable_grid": 1, 6 | "engine": "InnoDB", 7 | "field_order": [ 8 | "product_details_section", 9 | "si_item_ref", 10 | "item_name", 11 | "is_service_item", 12 | "column_break_4", 13 | "hsn_code", 14 | "section_break_6", 15 | "quantity", 16 | "unit", 17 | "rate", 18 | "amount", 19 | "column_break_11", 20 | "discount", 21 | "taxable_value", 22 | "tax_details_section", 23 | "gst_rate", 24 | "cgst_amount", 25 | "sgst_amount", 26 | "igst_amount", 27 | "column_break_19", 28 | "cess_rate", 29 | "cess_amount", 30 | "cess_nadv_amount", 31 | "other_charges", 32 | "section_break_24", 33 | "total_item_value" 34 | ], 35 | "fields": [ 36 | { 37 | "fieldname": "product_details_section", 38 | "fieldtype": "Section Break", 39 | "label": "Product Details" 40 | }, 41 | { 42 | "fieldname": "item_name", 43 | "fieldtype": "Data", 44 | "in_list_view": 1, 45 | "label": "Item Name" 46 | }, 47 | { 48 | "default": "0", 49 | "fieldname": "is_service_item", 50 | "fieldtype": "Check", 51 | "label": "Is Service Item" 52 | }, 53 | { 54 | "fieldname": "column_break_4", 55 | "fieldtype": "Column Break" 56 | }, 57 | { 58 | "fieldname": "hsn_code", 59 | "fieldtype": "Data", 60 | "in_list_view": 1, 61 | "label": "HSN Code" 62 | }, 63 | { 64 | "fieldname": "section_break_6", 65 | "fieldtype": "Section Break" 66 | }, 67 | { 68 | "fieldname": "quantity", 69 | "fieldtype": "Float", 70 | "in_list_view": 1, 71 | "label": "Quantity", 72 | "non_negative": 1, 73 | "precision": "3" 74 | }, 75 | { 76 | "fieldname": "unit", 77 | "fieldtype": "Data", 78 | "label": "Unit" 79 | }, 80 | { 81 | "fieldname": "rate", 82 | "fieldtype": "Currency", 83 | "in_list_view": 1, 84 | "label": "Rate", 85 | "non_negative": 1, 86 | "precision": "3" 87 | }, 88 | { 89 | "fieldname": "amount", 90 | "fieldtype": "Currency", 91 | "in_list_view": 1, 92 | "label": "Amount", 93 | "non_negative": 1, 94 | "precision": "2" 95 | }, 96 | { 97 | "fieldname": "column_break_11", 98 | "fieldtype": "Column Break" 99 | }, 100 | { 101 | "fieldname": "discount", 102 | "fieldtype": "Currency", 103 | "label": "Discount", 104 | "non_negative": 1, 105 | "precision": "2" 106 | }, 107 | { 108 | "fieldname": "taxable_value", 109 | "fieldtype": "Currency", 110 | "label": "Taxable Value", 111 | "non_negative": 1, 112 | "precision": "2" 113 | }, 114 | { 115 | "fieldname": "tax_details_section", 116 | "fieldtype": "Section Break", 117 | "label": "Tax Details" 118 | }, 119 | { 120 | "fieldname": "gst_rate", 121 | "fieldtype": "Float", 122 | "label": "GST Rate", 123 | "non_negative": 1, 124 | "precision": "3" 125 | }, 126 | { 127 | "fieldname": "cgst_amount", 128 | "fieldtype": "Currency", 129 | "label": "CGST Amount", 130 | "non_negative": 1, 131 | "precision": "2" 132 | }, 133 | { 134 | "fieldname": "sgst_amount", 135 | "fieldtype": "Currency", 136 | "label": "SGST Amount", 137 | "non_negative": 1, 138 | "precision": "2" 139 | }, 140 | { 141 | "fieldname": "igst_amount", 142 | "fieldtype": "Currency", 143 | "label": "IGST Amount", 144 | "non_negative": 1, 145 | "precision": "2" 146 | }, 147 | { 148 | "fieldname": "column_break_19", 149 | "fieldtype": "Column Break" 150 | }, 151 | { 152 | "fieldname": "cess_rate", 153 | "fieldtype": "Float", 154 | "label": "Cess Rate", 155 | "non_negative": 1, 156 | "precision": "3" 157 | }, 158 | { 159 | "fieldname": "cess_amount", 160 | "fieldtype": "Currency", 161 | "label": "Cess Amount", 162 | "non_negative": 1, 163 | "precision": "2" 164 | }, 165 | { 166 | "fieldname": "cess_nadv_amount", 167 | "fieldtype": "Currency", 168 | "label": "Cess Non-Advol Amount", 169 | "non_negative": 1, 170 | "precision": "2" 171 | }, 172 | { 173 | "fieldname": "other_charges", 174 | "fieldtype": "Currency", 175 | "label": "Other Charges", 176 | "non_negative": 1, 177 | "precision": "2" 178 | }, 179 | { 180 | "fieldname": "section_break_24", 181 | "fieldtype": "Section Break" 182 | }, 183 | { 184 | "fieldname": "total_item_value", 185 | "fieldtype": "Currency", 186 | "label": "Total Item Value", 187 | "non_negative": 1, 188 | "precision": "2" 189 | }, 190 | { 191 | "fieldname": "si_item_ref", 192 | "fieldtype": "Data", 193 | "hidden": 1, 194 | "label": "Item Ref", 195 | "read_only": 1 196 | } 197 | ], 198 | "index_web_pages_for_search": 1, 199 | "istable": 1, 200 | "links": [], 201 | "modified": "2021-08-03 18:58:32.922947", 202 | "modified_by": "Administrator", 203 | "module": "ERPNext GST Compliance", 204 | "name": "E Invoice Item", 205 | "owner": "Administrator", 206 | "permissions": [], 207 | "sort_field": "modified", 208 | "sort_order": "DESC", 209 | "track_changes": 1 210 | } -------------------------------------------------------------------------------- /erpnext_gst_compliance/cleartax_integration/doctype/cleartax_settings/test_cleartax_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2021, Frappe and Contributors 3 | # See license.txt 4 | from __future__ import unicode_literals 5 | 6 | import frappe 7 | import unittest 8 | from erpnext_gst_compliance.cleartax_integration.cleartax_connector import CleartaxConnector 9 | from erpnext_gst_compliance.erpnext_gst_compliance.doctype.e_invoice.test_e_invoice import make_e_invoice 10 | 11 | class TestCleartaxSettings(unittest.TestCase): 12 | 13 | def test_incorrect_company_gstin(self): 14 | cleartax_settings = frappe.get_doc('Cleartax Settings') 15 | cleartax_settings.enabled = 1 16 | 17 | cleartax_settings.append('credentials', { 18 | 'company': '_Test Company', 19 | 'gstin': '27AAFCD5862R013', 20 | 'owner_id': 'test_owner_id' 21 | }) 22 | 23 | self.assertRaises(frappe.ValidationError, cleartax_settings.save) 24 | 25 | class TestCleartaxConnector(unittest.TestCase): 26 | def setUp(self): 27 | cleartax_settings = frappe.get_single('Cleartax Settings') 28 | cleartax_settings.enabled = 1 29 | cleartax_settings.credentials = [] 30 | cleartax_settings.append('credentials', { 31 | 'company': '_Test Company', 32 | 'gstin': '27AAECE4835E1ZR', 33 | 'owner_id': 'test_owner_id' 34 | }) 35 | cleartax_settings.flags.ignore_validate = True 36 | cleartax_settings.save() 37 | 38 | einvoice, _ = make_e_invoice() 39 | self.connector = CleartaxConnector('27AAECE4835E1ZR') 40 | self.connector.einvoice = einvoice 41 | 42 | self.valid_irn_response = [{ 43 | "document_status": "IRN_GENERATED", 44 | "govt_response": { 45 | "Success": "Y", 46 | "AckNo": 112110085331107, 47 | "AckDt": "2021-06-19 23:17:00", 48 | "Irn": "47d1353009acd3db7c999b174b1c68f52e382f88a497921be49be31f9769b017", 49 | "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoxMTIxMTAwODUzMzExMDcsXCJBY2tEdFwiOlwiMjAyMS0wNi0xOSAyMzoxNzowMFwiLFwiSXJuXCI6XCI0N2QxMzUzMDA5YWNkM2RiN2M5OTliMTc0YjFjNjhmNTJlMzgyZjg4YTQ5NzkyMWJlNDliZTMxZjk3NjliMDE3XCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIixcIklnc3RPbkludHJhXCI6XCJOXCJ9LFwiRG9jRHRsc1wiOntcIlR5cFwiOlwiSU5WXCIsXCJOb1wiOlwiU0lOVi0yMS0wMDAwN1wiLFwiRHRcIjpcIjE5LzA2LzIwMjFcIn0sXCJTZWxsZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjI5QUFGQ0Q1ODYyUjAwMFwiLFwiTGdsTm1cIjpcIlVuaWNvIFBsYXN0aWNzIFB2dC4gTHRkLlwiLFwiQWRkcjFcIjpcIkFrc2h5YSBOYWdhciAxc3QgQmxvY2sgMXN0IENyb3NzLCBSYW1tdXJ0aHkgbmFnYXJcIixcIkxvY1wiOlwiQmFuZ2Fsb3JlXCIsXCJQaW5cIjo1NjAwMTYsXCJTdGNkXCI6XCIyOVwifSxcIkJ1eWVyRHRsc1wiOntcIkdzdGluXCI6XCIyN0FBQkNVOTYwM1IxWk5cIixcIkxnbE5tXCI6XCJBdmFyeWEgUmV0YWlsIFB2dC4gTHRkLlwiLFwiUG9zXCI6XCIyN1wiLFwiQWRkcjFcIjpcIkJhamFqIEJoYXZhbiwgR3JkIEZsb29yLCBOYXJpbWFuIFBvaW50IFJvYWQsIE5hcmltYW4gUG9pbnRcIixcIkxvY1wiOlwiTXVtYmFpXCIsXCJQaW5cIjo0MDAwMjEsXCJTdGNkXCI6XCIyN1wifSxcIkl0ZW1MaXN0XCI6W3tcIkl0ZW1Ob1wiOjAsXCJTbE5vXCI6XCIxXCIsXCJJc1NlcnZjXCI6XCJOXCIsXCJQcmREZXNjXCI6XCJBcHBsZSBFYXJwb2RzXCIsXCJIc25DZFwiOlwiODUxODMwMDBcIixcIlF0eVwiOjEuMCxcIlVuaXRcIjpcIk5PU1wiLFwiVW5pdFByaWNlXCI6NjAwMC4wLFwiVG90QW10XCI6NjAwMC4wMCxcIkRpc2NvdW50XCI6MC4wMCxcIkFzc0FtdFwiOjYwMDAuMDAsXCJHc3RSdFwiOjE4LjAwLFwiSWdzdEFtdFwiOjEwODAuMDAsXCJDZ3N0QW10XCI6MC4wMCxcIlNnc3RBbXRcIjowLjAwLFwiQ2VzUnRcIjowLjAwLFwiQ2VzQW10XCI6MC4wMCxcIkNlc05vbkFkdmxBbXRcIjowLjAwLFwiT3RoQ2hyZ1wiOjAuMDAsXCJUb3RJdGVtVmFsXCI6NzA4MC4wMH1dLFwiVmFsRHRsc1wiOntcIkFzc1ZhbFwiOjYwMDAuMDAsXCJDZ3N0VmFsXCI6MC4wMCxcIlNnc3RWYWxcIjowLjAwLFwiSWdzdFZhbFwiOjEwODAuMDAsXCJDZXNWYWxcIjowLjAwLFwiRGlzY291bnRcIjowLjAwLFwiT3RoQ2hyZ1wiOjAuMDAsXCJSbmRPZmZBbXRcIjowLjAwLFwiVG90SW52VmFsXCI6NzA4MC4wMCxcIlRvdEludlZhbEZjXCI6NzA4MC4wMH19IiwiaXNzIjoiTklDIn0.NZrxxau7RQyky2kq9HsfWGcQ35OWkL4XYMKk_FuHbsUa-q-PL-FeY6YuSfcLy6EqTRDvPexvfTsN_tQeNzzocvR3G0rVsEHHnB-yoZuWn66UDWXokkiPFR3KQf0tBYWCnummmfpjIiSNWlf0Ib-DJ3SprxmpF0cQ_RL2jJKcEUfVDy2G7y7MQZ2TAPGaIy3pdaZDVZe3zKeG6fWb6oJtFtJk9lV7mzQrpCnHO6BA8ChoLwKR_6RwJwwCAdF8FYKV9obYDTcjzyxnBBc2Z2sgc8Q29MBgWpoHo6MjF3axR025_nAvxK540mi45JzNJsNS6oSsaKunQtDKFfh90TDqOg", 50 | "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjI5QUFGQ0Q1ODYyUjAwMFwiLFwiQnV5ZXJHc3RpblwiOlwiMjdBQUJDVTk2MDNSMVpOXCIsXCJEb2NOb1wiOlwiU0lOVi0yMS0wMDAwN1wiLFwiRG9jVHlwXCI6XCJJTlZcIixcIkRvY0R0XCI6XCIxOS8wNi8yMDIxXCIsXCJUb3RJbnZWYWxcIjo3MDgwLjAwLFwiSXRlbUNudFwiOjEsXCJNYWluSHNuQ29kZVwiOlwiODUxODMwMDBcIixcIklyblwiOlwiNDdkMTM1MzAwOWFjZDNkYjdjOTk5YjE3NGIxYzY4ZjUyZTM4MmY4OGE0OTc5MjFiZTQ5YmUzMWY5NzY5YjAxN1wiLFwiSXJuRHRcIjpcIjIwMjEtMDYtMTkgMjM6MTc6MDBcIn0iLCJpc3MiOiJOSUMifQ.BixNn3wWO7SGYdKhqNWL_djEAVSDeH3haHseJ_vEF2y-XOcBmoC6tbTk83xdiLlwORwlnw6qxS5ClarT5xTAPLWNqrW2E-buydqXdNTQKkZgYe0DCSj_1ItEYMfZ_9zvglHYKPBz_kmG_CiDODcpcbIEcWE35TrCLJCoezb4WhSrT9VbaWRMo1zyHWu9vbKUXGcX27zlWnaCQbG_5W3_lsgz9g2fx6RME7u4otMvHyEBpPM6nAdeSCh_hx7X24U0fejwNMAGW2Y_qoqk0UcyT3THVaeWxcB4R5USczSD3KjFBCN-T6mFV5iprXrMewnWQOeu1QT6cxMUfaUkk1LHsw", 51 | "Status": "ACT" 52 | }, 53 | "gstin": "27AAECE4835E1ZR", 54 | "owner_id": None, 55 | }] 56 | 57 | self.invalid_irn_response = [{ 58 | "document_status": "IRN_GENERATION_FAILED", 59 | "govt_response": { 60 | "Success": "N", 61 | "ErrorDetails": [ 62 | { 63 | "error_code": "107", 64 | "error_message": "lineItems[0].gstRate : Invalid GST Tax rate. Please correct the Tax Rate Values and try again", 65 | "error_source": "CLEARTAX" 66 | }, 67 | { 68 | "error_code": "107", 69 | "error_message": "lineItems[0].igstAmount : Invalid value of Tax Amount, provide either IGST or both SGST and CGST", 70 | "error_source": "CLEARTAX" 71 | } 72 | ] 73 | }, 74 | "gstin": "27AAECE4835E1ZR", 75 | "owner_id": None, 76 | }] 77 | 78 | self.valid_irn_cancel_response = [{ 79 | "document_status": "IRN_CANCELLED", 80 | "govt_response": { 81 | "Success": "Y", 82 | "Irn": "736499fdad251b128f6ff55f7518600f7361335f32d972915e3e0589225697d5", 83 | "CancelDate": "2021-06-19 22:10:00" 84 | }, 85 | "gstin": "29AAFCD5862R000", 86 | "owner_id": None 87 | }] 88 | 89 | def test_irn_generation(self): 90 | # with successful response 91 | response = self.valid_irn_response 92 | response = self.connector.sanitize_response(response) 93 | self.connector.handle_successful_irn_generation(response) 94 | self.assertTrue(response.get('Success')) 95 | self.assertEqual(self.connector.einvoice.irn, response.get('Irn')) 96 | self.assertEqual(self.connector.einvoice.ack_no, response.get('AckNo')) 97 | self.assertEqual(self.connector.einvoice.ack_date, response.get('AckDt')) 98 | self.assertEqual(self.connector.einvoice.status, 'IRN Generated') 99 | 100 | # with error response 101 | response = self.invalid_irn_response 102 | response = self.connector.sanitize_response(response) 103 | errors = response.get('Errors') 104 | self.assertFalse(response.get('Success')) 105 | self.assertEqual(len(errors), 2) 106 | 107 | def test_irn_cancellation(self): 108 | # with successful response 109 | response = self.valid_irn_cancel_response 110 | response = self.connector.sanitize_response(response) 111 | self.connector.handle_successful_irn_cancellation(response) 112 | self.assertTrue(response.get('Success')) 113 | self.assertEqual(self.connector.einvoice.irn_cancelled, 1) 114 | self.assertEqual(self.connector.einvoice.irn_cancel_date, response.get('CancelDate')) 115 | self.assertEqual(self.connector.einvoice.status, 'IRN Cancelled') 116 | 117 | def tearDown(self): 118 | cleartax_settings = frappe.get_single('Cleartax Settings') 119 | cleartax_settings.enabled = 0 120 | cleartax_settings.auth_token = None 121 | cleartax_settings.token_expiry = None 122 | cleartax_settings.flags.ignore_validate = True 123 | cleartax_settings.save() -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/print_format/gst_e_invoice/gst_e_invoice.html: -------------------------------------------------------------------------------- 1 | {%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%} 2 | {%- set einvoice = json.loads(doc.signed_einvoice) -%} 3 | 4 |
5 |
6 | {% if letter_head and not no_letterhead %} 7 |
{{ letter_head }}
8 | {% endif %} 9 | 12 |
13 | {% if print_settings.repeat_header_footer %} 14 | 24 | {% endif %} 25 |
1. Transaction Details
26 |
27 |
28 |
29 |
30 |
{{ einvoice.Irn }}
31 |
32 |
33 |
34 |
{{ einvoice.AckNo }}
35 |
36 |
37 |
38 |
{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}
39 |
40 |
41 |
42 |
{{ einvoice.TranDtls.SupTyp }}
43 |
44 |
45 |
46 |
{{ einvoice.DocDtls.Typ }}
47 |
48 |
49 |
50 |
{{ einvoice.DocDtls.No }}
51 |
52 |
53 |
54 | 55 |
56 |
57 |
2. Party Details
58 |
59 | {%- set seller = einvoice.SellerDtls -%} 60 |
61 |
Seller
62 |

{{ seller.Gstin }}

63 |

{{ seller.LglNm }}

64 |

{{ seller.Addr1 }}

65 | {%- if seller.Addr2 -%}

{{ seller.Addr2 }}

{% endif %} 66 |

{{ seller.Loc }}

67 |

{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}

68 | 69 | {%- if einvoice.ShipDtls -%} 70 | {%- set shipping = einvoice.ShipDtls -%} 71 |
Shipping
72 |

{{ shipping.Gstin }}

73 |

{{ shipping.LglNm }}

74 |

{{ shipping.Addr1 }}

75 | {%- if shipping.Addr2 -%}

{{ shipping.Addr2 }}

{% endif %} 76 |

{{ shipping.Loc }}

77 |

{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}

78 | {% endif %} 79 |
80 | {%- set buyer = einvoice.BuyerDtls -%} 81 |
82 |
Buyer
83 |

{{ buyer.Gstin }}

84 |

{{ buyer.LglNm }}

85 |

{{ buyer.Addr1 }}

86 | {%- if buyer.Addr2 -%}

{{ buyer.Addr2 }}

{% endif %} 87 |

{{ buyer.Loc }}

88 |

{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}

89 |
90 |
91 |
92 |
3. Item Details
93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | {% for item in einvoice.ItemList %} 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | {% endfor %} 125 | 126 |
Sr. No.ItemHSN CodeQtyUOMRateDiscountTaxable AmountTax RateOther ChargesTotal
{{ item.SlNo }}{{ item.PrdDesc }}{{ item.HsnCd }}{{ item.Qty }}{{ item.Unit }}{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}{{ item.GstRt + item.CesRt }} %{{ frappe.utils.fmt_money(0, None, "INR") }}{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}
127 |
128 |
129 |
4. Value Details
130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | {%- set value_details = einvoice.ValDtls -%} 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 |
Taxable AmountCGSTSGSTIGSTCESSState CESSDiscountOther ChargesRound OffTotal Value
{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}{{ frappe.utils.fmt_money(0, None, "INR") }}{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}{{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }}{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}
161 |
162 |
-------------------------------------------------------------------------------- /erpnext_gst_compliance/adequare_integration/doctype/adequare_settings/test_adequare_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, Frappe Technologied Pvt. Ltd. and Contributors 2 | # See license.txt 3 | 4 | import frappe 5 | import unittest 6 | from erpnext_gst_compliance.adequare_integration.adequare_connector import AdequareConnector 7 | from erpnext_gst_compliance.erpnext_gst_compliance.doctype.e_invoice.test_e_invoice import make_e_invoice 8 | 9 | class TestAdequareSettings(unittest.TestCase): 10 | pass 11 | 12 | class TestAdequareConnector(unittest.TestCase): 13 | def setUp(self): 14 | adequare_settings = frappe.get_single('Adequare Settings') 15 | adequare_settings.enabled = 1 16 | adequare_settings.credentials = [] 17 | adequare_settings.append('credentials', { 18 | 'company': '_Test Company', 19 | 'gstin': '27AAECE4835E1ZR', 20 | 'username': 'test', 21 | 'password': 'test' 22 | }) 23 | adequare_settings.flags.ignore_validate = True 24 | adequare_settings.save() 25 | 26 | einvoice, _ = make_e_invoice() 27 | self.connector = AdequareConnector('27AAECE4835E1ZR') 28 | self.connector.einvoice = einvoice 29 | 30 | self.valid_token_response = { 31 | "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJnc3AiXSwiZXhwIjoxNjI4MTYwNTk4LCJhdXRob3JpdGllcyI6WyJST0xFX1NCX0VfQVBJX0VJIl0sImp0aSI6ImNkY2IzNDAxLTkyNWQtNGE2MS1hZDNjLWM3OGJhZGU3NzQwNCIsImNsaWVudF9pZCI6Ijg1OTcxQTYyNTkwNzQ2MjU5MEE5NDY4NjQyNjY0RUY3In0.yeeoX4x5sBq2pGcy_-VN-UugBwgyo446rT2tM62rnlk", 32 | "token_type": "bearer", 33 | "expires_in": 2591999, 34 | "scope": "gsp", 35 | "jti": "cdcb3401-925d-4a61-ad3c-c78bade77404" 36 | } 37 | 38 | self.valid_irn_response = { 39 | "success": True, 40 | "message": "IRN generated successfully", 41 | "result": { 42 | "AckNo": 132110029183779, 43 | "AckDt": "2021-06-02 19:04:34", 44 | "Irn": "2dcf96dedb78955d5c2cc44a495b9810d2fbd9509fd970b7a7d3a9dd32799547", 45 | "SignedInvoice": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiQWNrTm9cIjoxMzIxMTAwMjkxODM3NzksXCJBY2tEdFwiOlwiMjAyMS0wNi0wMiAxOTowNDozNFwiLFwiSXJuXCI6XCIyZGNmOTZkZWRiNzg5NTVkNWMyY2M0NGE0OTViOTgxMGQyZmJkOTUwOWZkOTcwYjdhN2QzYTlkZDMyNzk5NTQ3XCIsXCJWZXJzaW9uXCI6XCIxLjFcIixcIlRyYW5EdGxzXCI6e1wiVGF4U2NoXCI6XCJHU1RcIixcIlN1cFR5cFwiOlwiQjJCXCIsXCJSZWdSZXZcIjpcIk5cIn0sXCJEb2NEdGxzXCI6e1wiVHlwXCI6XCJJTlZcIixcIk5vXCI6XCJTSU5WLTIxLUIyQi0wMDIxXCIsXCJEdFwiOlwiMDIvMDYvMjAyMVwifSxcIlNlbGxlckR0bHNcIjp7XCJHc3RpblwiOlwiMDFBTUJQRzc3NzNNMDAyXCIsXCJMZ2xObVwiOlwiR2xhY2UgVGVjaG5vbG9naWVzXCIsXCJBZGRyMVwiOlwiQWtzaHlhIE5hZ2FyIDFzdCBCbG9jayAxc3QgQ3Jvc3MsIFJhbW11cnRoeSBuYWdhclwiLFwiTG9jXCI6XCJCYW5nYWxvcmVcIixcIlBpblwiOjE5MzUwMixcIlN0Y2RcIjpcIjAxXCJ9LFwiQnV5ZXJEdGxzXCI6e1wiR3N0aW5cIjpcIjM2QUFFQ0YxMTUxQTFaQ1wiLFwiTGdsTm1cIjpcIkVsZWdhbnQgR2xvYmFsXCIsXCJQb3NcIjpcIjM2XCIsXCJBZGRyMVwiOlwiIzUtOS0yODYvMi8yNSwgUmFqaXYgR2FuZGhpIE5hZ2FyLFwiLFwiQWRkcjJcIjpcIlByYXNoYW50aCBOYWdhciwgTW9vc2FwZXQgVmlsbGFnZSxcIixcIkxvY1wiOlwiS3VrYXRwYWxseSwgSHlkZXJhYmFkLFwiLFwiUGluXCI6NTAwMDcyLFwiU3RjZFwiOlwiMzZcIn0sXCJJdGVtTGlzdFwiOlt7XCJJdGVtTm9cIjowLFwiU2xOb1wiOlwiMVwiLFwiSXNTZXJ2Y1wiOlwiTlwiLFwiUHJkRGVzY1wiOlwiQmFza2V0IEJhbGxcIixcIkhzbkNkXCI6XCI5NTA2NjIzMFwiLFwiUXR5XCI6MS4wLFwiVW5pdFwiOlwiTm9zXCIsXCJVbml0UHJpY2VcIjo2MDAuMCxcIlRvdEFtdFwiOjYwMC4wLFwiRGlzY291bnRcIjowLjAsXCJBc3NBbXRcIjo2MDAuMCxcIkdzdFJ0XCI6MTguMCxcIklnc3RBbXRcIjoxMDguMCxcIkNnc3RBbXRcIjowLjAsXCJTZ3N0QW10XCI6MC4wLFwiQ2VzUnRcIjowLjAsXCJDZXNBbXRcIjowLjAsXCJDZXNOb25BZHZsQW10XCI6MC4wLFwiT3RoQ2hyZ1wiOjAuMCxcIlRvdEl0ZW1WYWxcIjo3MDguMH1dLFwiVmFsRHRsc1wiOntcIkFzc1ZhbFwiOjYwMC4wLFwiQ2dzdFZhbFwiOjAuMCxcIlNnc3RWYWxcIjowLjAsXCJJZ3N0VmFsXCI6MTA4LjAsXCJDZXNWYWxcIjowLjAsXCJEaXNjb3VudFwiOjAuMCxcIk90aENocmdcIjowLjAsXCJSbmRPZmZBbXRcIjowLjAsXCJUb3RJbnZWYWxcIjo3MDguMCxcIlRvdEludlZhbEZjXCI6NzA4LjB9fSIsImlzcyI6Ik5JQyJ9.dLpI1jKuCLGrTM9iola09tsAIREQHMkIzgvn4krQqas63pcKygTWemK1zP48IRIlBIxyGDG-s4V8Jy9IDwLAa31ZaR4g69PGdiW8b6Do71nhqnxwVSYPEc62YJ1Hn9w3fgH2mVxHnENYebNbNdqVgTuowCi733VbRc5v55moaBshhB9L6ofWgtZYeP4vjTG-AOttts0paXR4r30wBbneQBOgqLgMHusQY7-XFh3b4qN-0LSXe8lzE_s7-Wl3Ak4idbTwsFWbNEHVNclRwUZ36NvWlEOuUOoLdOCyzyYlwbuCtsco5eck9t0rdu1EnPSZ6CDvBn6lDmVjaBp_A4IMHA", 46 | "SignedQRCode": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkVEQzU3REUxMzU4QjMwMEJBOUY3OTM0MEE2Njk2ODMxRjNDODUwNDciLCJ0eXAiOiJKV1QiLCJ4NXQiOiI3Y1Y5NFRXTE1BdXA5NU5BcG1sb01mUElVRWMifQ.eyJkYXRhIjoie1wiU2VsbGVyR3N0aW5cIjpcIjAxQU1CUEc3NzczTTAwMlwiLFwiQnV5ZXJHc3RpblwiOlwiMzZBQUVDRjExNTFBMVpDXCIsXCJEb2NOb1wiOlwiU0lOVi0yMS1CMkItMDAyMVwiLFwiRG9jVHlwXCI6XCJJTlZcIixcIkRvY0R0XCI6XCIwMi8wNi8yMDIxXCIsXCJUb3RJbnZWYWxcIjo3MDguMCxcIkl0ZW1DbnRcIjoxLFwiTWFpbkhzbkNvZGVcIjpcIjk1MDY2MjMwXCIsXCJJcm5cIjpcIjJkY2Y5NmRlZGI3ODk1NWQ1YzJjYzQ0YTQ5NWI5ODEwZDJmYmQ5NTA5ZmQ5NzBiN2E3ZDNhOWRkMzI3OTk1NDdcIixcIklybkR0XCI6XCIyMDIxLTA2LTAyIDE5OjA0OjM0XCJ9IiwiaXNzIjoiTklDIn0.nkRNS9Wfb-RQcbSxuOW8XKxdQhr_uYs0AvbYViGmG2VafRv9SrWU1GPcP9RF8UVlrL0OYcUn_BEWqTL3zAPfzTFdeCTi1FKBwlQUjbOIRtkQWuBAEHmL6yDoldKkr3oszNc5YKR7R4Cu8d4cC2PnzJE0TdwOFVm4_g4O2wXkxC1bIUfU88AYeop_uCQ-1nV0sjfBzNB1baEG9mL1DGg7v8US8V_GeM6nMZGsAOPKlz5oZKser0K0L9Pc9V62k41XbveZIBtA6XQBQNpN0tipYMsbbRQnpqnKCv8Kkg_GY58hynalzmn5G7d3Nsthzjsqpan1hXMNZY6c2ums2ktx8A", 47 | "Status": "ACT", 48 | "EwbNo": None, 49 | "EwbDt": None, 50 | "EwbValidTill": None, 51 | "Remarks": None 52 | } 53 | } 54 | 55 | self.invalid_irn_response = { 56 | "success": False, 57 | "message": "2258 : Supplier GSTIN state codedoes not match with the state code passed in supplier details" 58 | } 59 | 60 | self.valid_irn_cancel_response = { 61 | "success": True, 62 | "message": "E-Invoice is cancelled successfully", 63 | "result": { 64 | "Irn": "2dcf96dedb78955d5c2cc44a495b9810d2fbd9509fd970b7a7d3a9dd32799547", 65 | "CancelDate": "2021-03-04 11:20:00" 66 | } 67 | } 68 | 69 | self.invalid_irn_cancel_response = { 70 | "success": False, 71 | "message": "2270 : The allowed cancellation time limit is crossed, you cannot cancel the IRN" 72 | } 73 | 74 | def test_token_generation(self): 75 | self.assertFalse(self.connector.settings.token_expiry) 76 | response = frappe._dict(self.valid_token_response) 77 | self.connector.handle_successful_token_generation(response) 78 | self.assertTrue(self.connector.settings.auth_token) 79 | self.assertTrue(self.connector.settings.token_expiry) 80 | 81 | def test_irn_generation(self): 82 | # with successful response 83 | response = frappe._dict(self.valid_irn_response) 84 | success, errors = self.connector.handle_irn_generation_response(response) 85 | self.assertTrue(success) 86 | self.assertEqual(self.connector.einvoice.irn, response.result.get('Irn')) 87 | self.assertEqual(self.connector.einvoice.ack_no, response.result.get('AckNo')) 88 | self.assertEqual(self.connector.einvoice.ack_date, response.result.get('AckDt')) 89 | self.assertEqual(self.connector.einvoice.status, 'IRN Generated') 90 | 91 | # with error response 92 | response = frappe._dict(self.invalid_irn_response) 93 | success, errors = self.connector.handle_irn_generation_response(response) 94 | self.assertFalse(success) 95 | self.assertEqual(len(errors), 1) 96 | 97 | def test_irn_cancellation(self): 98 | # with successful response 99 | response = frappe._dict(self.valid_irn_cancel_response) 100 | success, errors = self.connector.handle_irn_cancellation_response(response) 101 | self.assertTrue(success) 102 | self.assertEqual(self.connector.einvoice.irn_cancelled, 1) 103 | self.assertEqual(self.connector.einvoice.irn_cancel_date, response.result.get('CancelDate')) 104 | self.assertEqual(self.connector.einvoice.status, 'IRN Cancelled') 105 | 106 | # with error response 107 | response = frappe._dict(self.invalid_irn_cancel_response) 108 | success, errors = self.connector.handle_irn_cancellation_response(response) 109 | self.assertFalse(success) 110 | self.assertEqual(len(errors), 1) 111 | 112 | def tearDown(self): 113 | adequare_settings = frappe.get_single('Adequare Settings') 114 | adequare_settings.enabled = 0 115 | adequare_settings.auth_token = None 116 | adequare_settings.token_expiry = None 117 | adequare_settings.flags.ignore_validate = True 118 | adequare_settings.save() -------------------------------------------------------------------------------- /erpnext_gst_compliance/public/js/sales_invoice.js: -------------------------------------------------------------------------------- 1 | frappe.ui.form.on('Sales Invoice', { 2 | async refresh(frm) { 3 | if (frm.is_dirty()) return; 4 | 5 | const invoice_eligible = await get_einvoice_eligibility(frm.doc); 6 | 7 | if (!invoice_eligible) return; 8 | 9 | const { einvoice_status } = frm.doc; 10 | 11 | const add_einvoice_button = (label, action) => { 12 | if (!frm.custom_buttons[label]) { 13 | frm.add_custom_button(label, action, __('E-Invoicing')); 14 | } 15 | }; 16 | 17 | const e_invoicing_controller = 'erpnext_gst_compliance.erpnext_gst_compliance.e_invoicing_controller'; 18 | 19 | if (!einvoice_status || einvoice_status == 'IRN Pending') { 20 | // Generate IRN 21 | add_einvoice_button(__('Generate IRN'), async () => { 22 | if (frm.is_dirty()) return raise_form_is_dirty_error(); 23 | 24 | await frm.reload_doc(); 25 | frappe.call({ 26 | method: e_invoicing_controller + '.generate_irn', 27 | args: { sales_invoice: frm.doc }, 28 | callback: () => frm.reload_doc(), 29 | error: () => frm.reload_doc(), 30 | freeze: true 31 | }); 32 | }); 33 | } 34 | 35 | 36 | if (['IRN Generated', 'E-Way Bill Cancelled'].includes(einvoice_status)) { 37 | // Cancel IRN 38 | const fields = get_irn_cancellation_fields(); 39 | const action = () => { 40 | if (frm.is_dirty()) return raise_form_is_dirty_error(); 41 | 42 | const d = new frappe.ui.Dialog({ 43 | title: __("Cancel IRN"), 44 | fields: fields, 45 | primary_action: function() { 46 | const data = d.get_values(); 47 | frappe.call({ 48 | method: e_invoicing_controller + '.cancel_irn', 49 | args: { 50 | sales_invoice: frm.doc, 51 | reason: data.reason.split('-')[0], 52 | remark: data.remark 53 | }, 54 | freeze: true, 55 | callback: () => { 56 | frm.reload_doc(); 57 | d.hide(); 58 | }, 59 | error: () => d.hide() 60 | }); 61 | }, 62 | primary_action_label: __('Submit') 63 | }); 64 | d.show(); 65 | }; 66 | add_einvoice_button(__('Cancel IRN'), action); 67 | } 68 | 69 | if (['IRN Generated', 'E-Way Bill Cancelled'].includes(einvoice_status)) { 70 | // Generate E-Way Bill 71 | const action = () => { 72 | const d = new frappe.ui.Dialog({ 73 | title: __('Generate E-Way Bill'), 74 | size: "large", 75 | fields: get_eway_bill_fields(frm), 76 | primary_action: function() { 77 | const data = d.get_values(); 78 | frappe.call({ 79 | method: e_invoicing_controller + '.generate_eway_bill', 80 | args: { 81 | sales_invoice_name: frm.doc.name, 82 | ...data 83 | }, 84 | freeze: true, 85 | callback: () => { 86 | frm.reload_doc(); 87 | d.hide(); 88 | }, 89 | error: () => d.hide() 90 | }); 91 | }, 92 | primary_action_label: __('Submit') 93 | }); 94 | d.show(); 95 | }; 96 | 97 | add_einvoice_button(__("Generate E-Way Bill"), action); 98 | } 99 | 100 | // cancel ewaybill api is currently not supported by E-Invoice Portal 101 | 102 | // if (einvoice_status == 'E-Way Bill Generated') { 103 | // // Cancel E-Way Bill 104 | // const fields = get_irn_cancellation_fields(); 105 | // const action = () => { 106 | // if (frm.is_dirty()) return raise_form_is_dirty_error(); 107 | 108 | // const d = new frappe.ui.Dialog({ 109 | // title: __("Cancel E-Way Bill"), 110 | // fields: fields, 111 | // primary_action: function() { 112 | // const data = d.get_values(); 113 | // frappe.call({ 114 | // method: e_invoicing_controller + '.cancel_ewaybill', 115 | // args: { 116 | // sales_invoice_name: frm.doc.name, 117 | // reason: data.reason.split('-')[0], 118 | // remark: data.remark 119 | // }, 120 | // freeze: true, 121 | // callback: () => { 122 | // frm.reload_doc(); 123 | // d.hide(); 124 | // }, 125 | // error: () => d.hide() 126 | // }); 127 | // }, 128 | // primary_action_label: __('Submit') 129 | // }); 130 | // d.show(); 131 | // }; 132 | // add_einvoice_button(__('Cancel E-Way Bill'), action); 133 | // } 134 | 135 | if (einvoice_status == 'E-Way Bill Generated') { 136 | const action = () => { 137 | let message = __('Cancellation of e-way bill using API is currently not supported. '); 138 | message += '

'; 139 | message += __('You must perform this action only if you have already cancelled the e-way bill on the portal.') + ' '; 140 | 141 | const d = frappe.msgprint({ 142 | title: __('Update E-Way Bill Cancelled Status?'), 143 | message: message, 144 | indicator: 'orange', 145 | primary_action: { 146 | action: function() { 147 | frappe.call({ 148 | method: e_invoicing_controller + '.cancel_ewaybill', 149 | args: { 150 | sales_invoice_name: frm.doc.name 151 | }, 152 | freeze: true, 153 | callback: () => { 154 | frm.reload_doc(); 155 | d.hide(); 156 | }, 157 | error: () => d.hide() 158 | }); 159 | }, 160 | label: __('Update Status') 161 | } 162 | }); 163 | }; 164 | add_einvoice_button(__("Cancel E-Way Bill"), action); 165 | } 166 | } 167 | }); 168 | 169 | const get_einvoice_eligibility = async (doc) => { 170 | frappe.dom.freeze(); 171 | const { message: invoice_eligible } = await frappe.call({ 172 | method: 'erpnext_gst_compliance.erpnext_gst_compliance.doctype.e_invoice.e_invoice.validate_einvoice_eligibility', 173 | args: { doc: doc }, 174 | debounce: 2000 175 | }); 176 | frappe.dom.unfreeze(); 177 | 178 | return invoice_eligible; 179 | } 180 | 181 | const get_irn_cancellation_fields = () => { 182 | return [ 183 | { 184 | "label": "Reason", 185 | "fieldname": "reason", 186 | "fieldtype": "Select", 187 | "reqd": 1, 188 | "default": "1-Duplicate", 189 | "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] 190 | }, 191 | { 192 | "label": "Remark", 193 | "fieldname": "remark", 194 | "fieldtype": "Data", 195 | "reqd": 1 196 | } 197 | ]; 198 | } 199 | 200 | const raise_form_is_dirty_error = () => { 201 | frappe.throw({ 202 | message: __('You must save the document before making e-invoicing request.'), 203 | title: __('Unsaved Document') 204 | }); 205 | } 206 | 207 | const get_eway_bill_fields = () => { 208 | return [ 209 | { 210 | 'fieldname': 'transporter', 211 | 'label': 'Transporter', 212 | 'fieldtype': 'Link', 213 | 'options': 'Supplier' 214 | }, 215 | { 216 | 'fieldname': 'transporter_gstin', 217 | 'label': 'GST Transporter ID', 218 | 'fieldtype': 'Data', 219 | 'fetch_from': 'transporter.gst_transporter_id' 220 | }, 221 | { 222 | 'fieldname': 'transport_document_no', 223 | 'label': 'Transport Receipt No', 224 | 'fieldtype': 'Data' 225 | }, 226 | { 227 | 'fieldname': 'vehicle_no', 228 | 'label': 'Vehicle No', 229 | 'fieldtype': 'Data' 230 | }, 231 | { 232 | 'fieldname': 'distance', 233 | 'label': 'Distance (in km)', 234 | 'fieldtype': 'Float' 235 | }, 236 | { 237 | 'fieldname': 'transporter_col_break', 238 | 'fieldtype': 'Column Break', 239 | }, 240 | { 241 | 'fieldname': 'transporter_name', 242 | 'label': 'Transporter Name', 243 | 'fieldtype': 'Data', 244 | 'fetch_from': 'transporter.name' 245 | }, 246 | { 247 | 'fieldname': 'mode_of_transport', 248 | 'label': 'Mode of Transport', 249 | 'fieldtype': 'Select', 250 | 'options': `\nRoad\nAir\nRail\nShip` 251 | }, 252 | { 253 | 'fieldname': 'transport_document_date', 254 | 'label': 'Transport Receipt Date', 255 | 'fieldtype': 'Date', 256 | 'mandatory_depends_on': 'eval: doc.mode_of_transport == "Road"' 257 | }, 258 | { 259 | 'fieldname': 'vehicle_type', 260 | 'label': 'GST Vehicle Type', 261 | 'fieldtype': 'Select', 262 | 'options': `\nRegular\nOver Dimensional Cargo (ODC)`, 263 | 'depends_on': 'eval:(doc.mode_of_transport === "Road")', 264 | 'default': '' 265 | } 266 | ]; 267 | }; -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/setup.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe.utils import cint, add_to_date 3 | from frappe.custom.doctype.custom_field.custom_field import create_custom_fields 4 | 5 | def setup(): 6 | copy_adequare_credentials() 7 | enable_report_and_print_format() 8 | setup_custom_fields() 9 | handle_existing_e_invoices() 10 | 11 | def on_company_update(doc, method=""): 12 | if frappe.db.count('Company', {'country': 'India'}) <=1: 13 | setup_custom_fields() 14 | 15 | def setup_custom_fields(): 16 | custom_fields = { 17 | 'Sales Invoice': [ 18 | dict( 19 | fieldname='einvoice_section', label='E-Invoice Details', 20 | fieldtype='Section Break', insert_after='amended_from', 21 | print_hide=1, depends_on='eval: doc.e_invoice || doc.irn', 22 | collapsible_depends_on='eval: doc.e_invoice || doc.irn', 23 | collapsible=1, hidden=0 24 | ), 25 | 26 | dict( 27 | fieldname='irn', label='IRN', fieldtype='Data', read_only=1, 28 | depends_on='irn', insert_after='einvoice_section', no_copy=1, 29 | print_hide=1, fetch_from='e_invoice.irn', hidden=0, translatable=0 30 | ), 31 | 32 | dict( 33 | fieldname='irn_cancel_date', label='IRN Cancelled On', 34 | fieldtype='Data', read_only=1, insert_after='irn', hidden=0, 35 | depends_on='eval: doc.einvoice_status == "IRN Cancelled"', 36 | fetch_from='e_invoice.irn_cancel_date', no_copy=1, print_hide=1, translatable=0 37 | ), 38 | 39 | dict( 40 | fieldname='ack_no', label='Ack. No.', fieldtype='Data', 41 | read_only=1, insert_after='irn', no_copy=1, hidden=0, 42 | depends_on='eval: doc.einvoice_status != "IRN Cancelled"', 43 | fetch_from='e_invoice.ack_no', print_hide=1, translatable=0 44 | ), 45 | 46 | dict( 47 | fieldname='ack_date', label='Ack. Date', fieldtype='Data', 48 | read_only=1, insert_after='ack_no', hidden=0, 49 | depends_on='eval: doc.einvoice_status != "IRN Cancelled"', 50 | fetch_from='e_invoice.ack_date', no_copy=1, print_hide=1, translatable=0 51 | ), 52 | 53 | dict( 54 | fieldname='col_break_1', label='', fieldtype='Column Break', 55 | insert_after='ack_date', print_hide=1, read_only=1 56 | ), 57 | 58 | dict( 59 | fieldname='e_invoice', label='E-Invoice', fieldtype='Link', 60 | read_only=1, insert_after='col_break_1', 61 | options='E Invoice', no_copy=1, print_hide=1, 62 | depends_on='eval: doc.e_invoice || doc.irn', hidden=0 63 | ), 64 | 65 | dict( 66 | fieldname='einvoice_status', label='E-Invoice Status', 67 | fieldtype='Data', read_only=1, no_copy=1, hidden=0, 68 | insert_after='e_invoice', print_hide=1, options='', 69 | depends_on='eval: doc.e_invoice || doc.irn', 70 | fetch_from='e_invoice.status', translatable=0 71 | ), 72 | 73 | dict( 74 | fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', 75 | hidden=1, insert_after='ack_date', fetch_from='e_invoice.qrcode_path', 76 | no_copy=1, print_hide=1, read_only=1 77 | ), 78 | 79 | dict( 80 | fieldname='ewaybill', label='E-Way Bill No.', fieldtype='Data', 81 | allow_on_submit=1, insert_after='einvoice_status', fetch_from='e_invoice.ewaybill', translatable=0, 82 | depends_on='eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)' 83 | ), 84 | 85 | dict( 86 | fieldname='eway_bill_validity', label='E-Way Bill Validity', 87 | fieldtype='Data', no_copy=1, print_hide=1, depends_on='ewaybill', 88 | read_only=1, allow_on_submit=1, insert_after='ewaybill' 89 | ) 90 | ] 91 | } 92 | 93 | print('Creating Custom Fields for E-Invoicing...') 94 | create_custom_fields(custom_fields, update=True) 95 | 96 | def copy_adequare_credentials(): 97 | if frappe.db.exists('E Invoice Settings'): 98 | credentials = frappe.db.sql('select * from `tabE Invoice User`', as_dict=1) 99 | if not credentials: 100 | return 101 | 102 | print('Copying Credentials for E-Invoicing...') 103 | from frappe.utils.password import get_decrypted_password 104 | try: 105 | adequare_settings = frappe.get_single('Adequare Settings') 106 | adequare_settings.credentials = [] 107 | for credential in credentials: 108 | adequare_settings.append('credentials', { 109 | 'company': credential.company, 110 | 'gstin': credential.gstin, 111 | 'username': credential.username, 112 | 'password': get_decrypted_password('E Invoice User', credential.name) 113 | }) 114 | adequare_settings.enabled = 1 115 | adequare_settings.flags.ignore_validate = True 116 | adequare_settings.save() 117 | frappe.db.commit() 118 | 119 | e_invoicing_settings = frappe.get_single('E Invoicing Settings') 120 | e_invoicing_settings.service_provider = 'Adequare Settings' 121 | e_invoicing_settings.save() 122 | except: 123 | frappe.log_error(title="Failed to copy Adeqaure Credentials") 124 | 125 | def enable_report_and_print_format(): 126 | frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0) 127 | if not frappe.db.get_value('Custom Role', dict(report='E-Invoice Summary')): 128 | frappe.get_doc(dict( 129 | doctype='Custom Role', 130 | report='E-Invoice Summary', 131 | roles= [ 132 | dict(role='Accounts User'), 133 | dict(role='Accounts Manager') 134 | ] 135 | )).insert() 136 | 137 | def handle_existing_e_invoices(): 138 | if frappe.get_all('Sales Invoice', {'irn': ['is', 'set']}): 139 | try: 140 | update_sales_invoices() 141 | create_einvoices() 142 | except Exception: 143 | frappe.log_error(title="Backporting Sales Invoices Failed") 144 | 145 | def update_sales_invoices(): 146 | einvoices = frappe.db.sql(""" 147 | select 148 | name, irn, irn_cancelled, ewaybill, eway_bill_cancelled, einvoice_status 149 | from 150 | `tabSales Invoice` 151 | where 152 | ifnull(irn, '') != '' 153 | """, as_dict=1) 154 | 155 | if not einvoices: 156 | return 157 | 158 | print('Updating Sales Invoices...') 159 | for invoice in einvoices: 160 | einvoice_status = 'IRN Generated' 161 | if cint(invoice.irn_cancelled): 162 | einvoice_status = 'IRN Cancelled' 163 | if invoice.ewaybill: 164 | einvoice_status = 'E-Way Bill Generated' 165 | if cint(invoice.eway_bill_cancelled): 166 | einvoice_status = 'E-Way Bill Cancelled' 167 | 168 | frappe.db.set_value('Sales Invoice', invoice.name, 'einvoice_status', einvoice_status, update_modified=False) 169 | frappe.db.commit() 170 | 171 | def create_einvoices(): 172 | draft_einvoices = frappe.db.sql(""" 173 | select 174 | name, irn, ack_no, ack_date, irn_cancelled, irn_cancel_date, 175 | ewaybill, eway_bill_validity, einvoice_status, qrcode_image, docstatus 176 | from 177 | `tabSales Invoice` 178 | where 179 | ifnull(irn, '') != '' AND docstatus = 0 180 | """, as_dict=1) 181 | 182 | draft_einvoices_names = [d.name for d in draft_einvoices] 183 | 184 | # sales invoices with irns created within 24 hours 185 | recent_einvoices = frappe.db.sql(""" 186 | select 187 | name, irn, ack_no, ack_date, irn_cancelled, irn_cancel_date, 188 | ewaybill, eway_bill_validity, einvoice_status, qrcode_image, docstatus 189 | from 190 | `tabSales Invoice` 191 | where 192 | ifnull(irn, '') != '' AND docstatus != 2 AND 193 | timestamp(ack_date) >= %s and name not in %s 194 | """, (add_to_date(None, hours=-24), draft_einvoices_names), as_dict=1) 195 | 196 | if not draft_einvoices + recent_einvoices: 197 | return 198 | 199 | print('Creating E-Invoices...') 200 | for invoice in draft_einvoices + recent_einvoices: 201 | try: 202 | einvoice = frappe.new_doc('E Invoice') 203 | 204 | einvoice.invoice = invoice.name 205 | einvoice.irn = invoice.irn 206 | einvoice.ack_no = invoice.ack_no 207 | einvoice.ack_date = invoice.ack_date 208 | einvoice.ewaybill = invoice.ewaybill 209 | einvoice.status = invoice.einvoice_status 210 | einvoice.qrcode_path = invoice.qrcode_image 211 | einvoice.irn_cancelled = invoice.irn_cancelled 212 | einvoice.irn_cancelled_on = invoice.irn_cancel_date 213 | einvoice.eway_bill_validity = invoice.eway_bill_validity 214 | 215 | einvoice.sync_with_sales_invoice() 216 | 217 | einvoice.flags.ignore_permissions = 1 218 | einvoice.flags.ignore_validate = 1 219 | einvoice.save() 220 | if invoice.docstatus != 0: 221 | einvoice.submit() 222 | 223 | except Exception: 224 | frappe.log_error(title="E-Invoice Creation Failed") 225 | 226 | def before_test(): 227 | from frappe.test_runner import make_test_records_for_doctype 228 | for doctype in ['Company', 'Customer']: 229 | frappe.local.test_objects[doctype] = [] 230 | make_test_records_for_doctype(doctype, force=1) -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/print_format/gst_e_invoice/gst_e_invoice.json: -------------------------------------------------------------------------------- 1 | { 2 | "absolute_value": 0, 3 | "align_labels_right": 1, 4 | "creation": "2020-10-10 18:01:21.032914", 5 | "custom_format": 0, 6 | "default_print_language": "en-US", 7 | "disabled": 1, 8 | "doc_type": "Sales Invoice", 9 | "docstatus": 0, 10 | "doctype": "Print Format", 11 | "font": "Default", 12 | "html": "{%- from \"templates/print_formats/standard_macros.html\" import add_header, render_field, print_value -%}\n{%- set e_invoice = frappe.get_doc('E Invoice', doc.e_invoice) -%}\n\n
\n\t
\n\t\t{% if letter_head and not no_letterhead %}\n\t\t\t
{{ letter_head }}
\n\t\t{% endif %}\n\t\t
\n\t\t\t

E Invoice
{{ doc.name }}

\n\t\t
\n\t
\n\t{% if print_settings.repeat_header_footer %}\n\t
\n\t\t{% if not no_letterhead and footer %}\n\t\t
\n\t\t\t{{ footer }}\n\t\t
\n\t\t{% endif %}\n\t\t

\n\t\t\t{{ _(\"Page {0} of {1}\").format('', '') }}\n\t\t

\n\t
\n\t{% endif %}\n\t
1. Transaction Details
\n\t
\n\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t
{{ e_invoice.irn }}
\n\t\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t
{{ e_invoice.ack_no }}
\n\t\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t
{{ frappe.utils.format_datetime(e_invoice.ack_date, \"dd/MM/yyyy hh:mm:ss\") }}
\n\t\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t
{{ e_invoice.supply_type }}
\n\t\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t
{{ e_invoice.invoice_type }}
\n\t\t\t
\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t
{{ e_invoice.invoice }}
\n\t\t\t
\n\t\t
\n\t\t
\n\t\t\t\n\t\t
\n\t
\n\t
2. Party Details
\n\t
\n\t\t
\n\t\t\t
Seller
\n\t\t\t

{{ e_invoice.seller_gstin }}

\n\t\t\t

{{ e_invoice.seller_legal_name }}

\n\t\t\t

{{ e_invoice.seller_address_line_1 }}

\n\t\t\t{%- if e_invoice.seller_address_line_2 -%}

{{ e_invoice.seller_address_line_2 }}

{% endif %}\n\t\t\t

{{ e_invoice.seller_location }}

\n\t\t\t

{{ frappe.db.get_value(\"Address\", doc.company_address, \"gst_state\") }} - {{ e_invoice.seller_pincode }}

\n\n\t\t\t{%- if e_invoice.shipping_legal_name -%}\n\t\t\t\t
Shipping
\n\t\t\t\t

{{ e_invoice.shipping_gstin }}

\n\t\t\t\t

{{ e_invoice.shipping_legal_name }}

\n\t\t\t\t

{{ e_invoice.shipping_address_line_1 }}

\n\t\t\t\t{%- if e_invoice.shipping_address_line_2 -%}

{{ e_invoice.shipping_address_line_2 }}

{% endif %}\n\t\t\t\t

{{ e_invoice.shipping_location }}

\n\t\t\t\t

{{ frappe.db.get_value(\"Address\", doc.shipping_address_name, \"gst_state\") }} - {{ e_invoice.shipping_pincode }}

\n\t\t\t{% endif %}\n\t\t
\n\t\t
\n\t\t\t
Buyer
\n\t\t\t

{{ e_invoice.buyer_gstin }}

\n\t\t\t

{{ e_invoice.buyer_legal_name }}

\n\t\t\t

{{ e_invoice.buyer_address_line_1 }}

\n\t\t\t{%- if e_invoice.buyer_address_line_2 -%}

{{ e_invoice.buyer_address_line_2 }}

{% endif %}\n\t\t\t

{{ e_invoice.buyer_location }}

\n\t\t\t

{{ frappe.db.get_value(\"Address\", doc.customer_address, \"gst_state\") }} - {{ e_invoice.buyer_pincode }}

\n\t\t
\n\t
\n\t
\n\t\t
3. Item Details
\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t{% for item in e_invoice.items %}\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t{% endfor %}\n\t\t\t\n\t\t
Sr. No.ItemHSN CodeQtyUOMRateDiscountTaxable AmountTax RateOther ChargesTotal
{{ item.idx }}{{ item.item_name }}{{ item.hsn_code }}{{ item.quantity }}{{ item.unit }}{{ frappe.utils.fmt_money(item.rate, None, \"INR\") }}{{ frappe.utils.fmt_money(item.discount, None, \"INR\") }}{{ frappe.utils.fmt_money(item.taxable_value, None, \"INR\") }}{{ item.gst_rate + item.cess_rate }} %{{ frappe.utils.fmt_money(0, None, \"INR\") }}{{ frappe.utils.fmt_money(item.total_item_value, None, \"INR\") }}
\n\t
\n\t
\n\t\t
4. Value Details
\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t
Taxable AmountCGSTSGSTIGSTCESSState CESSDiscountOther ChargesRound OffTotal Value
{{ frappe.utils.fmt_money(e_invoice.ass_value, None, \"INR\") }}{{ frappe.utils.fmt_money(e_invoice.cgst_value, None, \"INR\") }}{{ frappe.utils.fmt_money(e_invoice.sgst_value, None, \"INR\") }}{{ frappe.utils.fmt_money(e_invoice.igst_value, None, \"INR\") }}{{ frappe.utils.fmt_money(e_invoice.cess_value, None, \"INR\") }}{{ frappe.utils.fmt_money(0, None, \"INR\") }}{{ frappe.utils.fmt_money(e_invoice.invoice_discount, None, \"INR\") }}{{ frappe.utils.fmt_money(e_invoice.other_charges, None, \"INR\") }}{{ frappe.utils.fmt_money(e_invoice.round_off_amount, None, \"INR\") }}{{ frappe.utils.fmt_money(e_invoice.base_invoice_value, None, \"INR\") }}
\n\t
\n
", 13 | "idx": 0, 14 | "line_breaks": 1, 15 | "modified": "2021-08-18 18:49:00.550424", 16 | "modified_by": "Administrator", 17 | "module": "ERPNext GST Compliance", 18 | "name": "GST E-Invoice", 19 | "owner": "Administrator", 20 | "print_format_builder": 0, 21 | "print_format_type": "Jinja", 22 | "raw_printing": 0, 23 | "show_section_headings": 1, 24 | "standard": "Yes" 25 | } -------------------------------------------------------------------------------- /erpnext_gst_compliance/cleartax_integration/cleartax_connector.py: -------------------------------------------------------------------------------- 1 | import os 2 | import io 3 | import frappe 4 | 5 | from frappe import _ 6 | from json import dumps 7 | from pyqrcode import create as qrcreate 8 | from erpnext_gst_compliance.utils import log_exception 9 | from frappe.integrations.utils import make_post_request, make_get_request, make_put_request 10 | 11 | class CleartaxConnector: 12 | def __init__(self, gstin): 13 | 14 | self.gstin = gstin 15 | self.settings = frappe.get_cached_doc("Cleartax Settings") 16 | self.business = self.get_business_settings() 17 | self.auth_token = self.settings.auth_token 18 | self.host = self.get_host_url() 19 | self.endpoints = self.get_endpoints() 20 | 21 | self.validate() 22 | 23 | def get_business_settings(self): 24 | return next( 25 | filter(lambda row: row.gstin == self.gstin, self.settings.credentials), 26 | frappe._dict({}), 27 | ) 28 | 29 | def get_host_url(self): 30 | if self.settings.sandbox_mode: 31 | return "https://einvoicing.internal.cleartax.co" 32 | else: 33 | return "https://api-einv.cleartax.in" 34 | 35 | def get_endpoints(self): 36 | base_url = self.host + "/v2/eInvoice" 37 | return frappe._dict({ 38 | "generate_irn": base_url + "/generate", 39 | "cancel_irn": base_url + "/cancel", 40 | "generate_ewaybill": base_url + "/ewaybill", 41 | "cancel_ewaybill": base_url + "/ewaybill/cancel" 42 | }) 43 | 44 | def validate(self): 45 | if not self.settings.enabled: 46 | frappe.throw(_("Cleartax Settings is not enabled. Please configure Cleartax Settings and try again.")) 47 | 48 | if not self.business.owner_id: 49 | frappe.throw( 50 | _("Cannot find Owner ID for GSTIN {}. Please add cleartax credentials for mentioned GSTIN in Cleartax Settings. ") 51 | .format(self.gstin)) 52 | 53 | @log_exception 54 | def get_headers(self): 55 | return frappe._dict({ 56 | "x-cleartax-auth-token": self.settings.get_password('auth_token'), 57 | "x-cleartax-product": "EInvoice", 58 | "Content-Type": "application/json", 59 | "owner_id": self.business.get_password('owner_id'), 60 | "gstin": self.gstin 61 | }) 62 | 63 | def log_einvoice_request(self, url, headers, payload, response): 64 | headers.update({ 65 | "x-cleartax-auth-token": self.auth_token, 66 | "owner_id": self.business.owner_id 67 | }) 68 | request_log = frappe.get_doc({ 69 | "doctype": "E Invoice Request Log", 70 | "user": frappe.session.user, 71 | "reference_invoice": self.einvoice.name, 72 | "url": url, 73 | "headers": dumps(headers, indent=4) if headers else None, 74 | "data": dumps(payload, indent=4) if isinstance(payload, dict) else payload, 75 | "response": dumps(response, indent=4) if response else None 76 | }) 77 | request_log.save(ignore_permissions=True) 78 | frappe.db.commit() 79 | 80 | @log_exception 81 | def make_request(self, req_type, url, headers, payload): 82 | if req_type == 'post': 83 | response = make_post_request(url, headers=headers, data=payload) 84 | elif req_type == 'put': 85 | response = make_put_request(url, headers=headers, data=payload) 86 | else: 87 | response = make_get_request(url, headers=headers, data=payload) 88 | 89 | self.log_einvoice_request(url, headers, payload, response) 90 | 91 | return response 92 | 93 | @log_exception 94 | def make_irn_request(self): 95 | headers = self.get_headers() 96 | url = self.endpoints.generate_irn 97 | 98 | einvoice_json = self.einvoice.get_einvoice_json() 99 | 100 | payload = [{"transaction": einvoice_json}] 101 | payload = dumps(payload, indent=4) 102 | 103 | response = self.make_request('put', url, headers, payload) 104 | # Sample Response -> https://docs.cleartax.in/cleartax-for-developers/e-invoicing-api/e-invoicing-api-reference/cleartax-e-invoicing-apis-xml-schema#sample-response 105 | 106 | response = self.sanitize_response(response) 107 | if response.get('Success'): 108 | self.handle_successful_irn_generation(response) 109 | 110 | return response 111 | 112 | @staticmethod 113 | def generate_irn(einvoice): 114 | business_gstin = einvoice.seller_gstin 115 | connector = CleartaxConnector(business_gstin) 116 | connector.einvoice = einvoice 117 | response = connector.make_irn_request() 118 | success, errors = response.get('Success'), response.get('Errors') 119 | 120 | return success, errors 121 | 122 | def sanitize_response(self, response): 123 | sanitized_response = [] 124 | for entry in response: 125 | govt_response = frappe._dict(entry.get('govt_response', {})) 126 | success = govt_response.get('Success', "N") 127 | 128 | if success == "Y": 129 | # return irn & other info 130 | govt_response.update({'Success': True}) 131 | sanitized_response.append(govt_response) 132 | else: 133 | # return error message list 134 | error_details = govt_response.get('ErrorDetails', []) 135 | error_list = [] 136 | 137 | for d in error_details: 138 | if d.get('error_source') == 'CLEARTAX': 139 | # cleartax gives back the exact key that causes the error 140 | # error_message = "sellerDetails.pinCode : " 141 | d['error_message'] = d['error_message'].split(' : ')[-1] 142 | error_list.append(d.get('error_message')) 143 | 144 | sanitized_response.append({ 145 | 'Success': False, 146 | 'Errors': error_list 147 | }) 148 | 149 | return sanitized_response[0] if len(sanitized_response) == 1 else sanitized_response 150 | 151 | def handle_successful_irn_generation(self, response): 152 | status = 'IRN Generated' 153 | irn = response.get('Irn') 154 | ack_no = response.get('AckNo') 155 | ack_date = response.get('AckDt') 156 | ewaybill = response.get('EwbNo') 157 | ewaybill_validity = response.get('EwbValidTill') 158 | qrcode = self.generate_qrcode(response.get('SignedQRCode')) 159 | 160 | self.einvoice.update({ 161 | 'irn': irn, 162 | 'status': status, 163 | 'ack_no': ack_no, 164 | 'ack_date': ack_date, 165 | 'ewaybill': ewaybill, 166 | 'qrcode_path': qrcode, 167 | 'ewaybill_validity': ewaybill_validity 168 | }) 169 | self.einvoice.flags.ignore_permissions = 1 170 | self.einvoice.submit() 171 | 172 | def generate_qrcode(self, signed_qrcode): 173 | doctype = self.einvoice.doctype 174 | docname = self.einvoice.name 175 | filename = '{} - QRCode.png'.format(docname).replace(os.path.sep, "__") 176 | 177 | qr_image = io.BytesIO() 178 | url = qrcreate(signed_qrcode, error='L') 179 | url.png(qr_image, scale=2, quiet_zone=1) 180 | _file = frappe.get_doc({ 181 | 'doctype': 'File', 182 | 'file_name': filename, 183 | 'attached_to_doctype': doctype, 184 | 'attached_to_name': docname, 185 | 'attached_to_field': 'qrcode_path', 186 | 'is_private': 1, 187 | 'content': qr_image.getvalue() 188 | }) 189 | _file.save() 190 | return _file.file_url 191 | 192 | @log_exception 193 | def make_cancel_irn_request(self, reason, remark): 194 | headers = self.get_headers() 195 | url = self.endpoints.cancel_irn 196 | 197 | irn = self.einvoice.irn 198 | 199 | payload = [{'irn': irn, 'CnlRsn': reason, 'CnlRem': remark}] 200 | payload = dumps(payload, indent=4) 201 | 202 | response = self.make_request('put', url, headers, payload) 203 | # Sample Response -> https://docs.cleartax.in/cleartax-for-developers/e-invoicing-api/e-invoicing-api-reference/cleartax-e-invoicing-apis-xml-schema#sample-response-1 204 | 205 | response = self.sanitize_response(response) 206 | if response.get('Success'): 207 | self.handle_successful_irn_cancellation(response) 208 | 209 | return response 210 | 211 | def handle_successful_irn_cancellation(self, response): 212 | self.einvoice.irn_cancelled = 1 213 | self.einvoice.irn_cancel_date = response.get('CancelDate') 214 | self.einvoice.status = 'IRN Cancelled' 215 | self.einvoice.flags.ignore_validate_update_after_submit = 1 216 | self.einvoice.flags.ignore_permissions = 1 217 | self.einvoice.save() 218 | 219 | @staticmethod 220 | def cancel_irn(einvoice, reason, remark): 221 | business_gstin = einvoice.seller_gstin 222 | connector = CleartaxConnector(business_gstin) 223 | connector.einvoice = einvoice 224 | response = connector.make_cancel_irn_request(reason, remark) 225 | success, errors = response.get('Success'), response.get('Errors') 226 | 227 | return success, errors 228 | 229 | @log_exception 230 | def make_eway_bill_request(self): 231 | headers = self.get_headers() 232 | url = self.endpoints.generate_ewaybill 233 | 234 | eway_bill_json = self.einvoice.get_eway_bill_json() 235 | 236 | payload = [eway_bill_json] 237 | payload = dumps(payload, indent=4) 238 | 239 | response = self.make_request('post', url, headers, payload) 240 | # Sample Response -> https://docs.cleartax.in/cleartax-for-developers/e-invoicing-api/e-invoicing-api-reference/cleartax-e-invoicing-apis-xml-schema#sample-response-3 241 | 242 | response = self.sanitize_response(response) 243 | if response.get('Success'): 244 | self.handle_successful_ewaybill_generation(response) 245 | 246 | return response 247 | 248 | def handle_successful_ewaybill_generation(self, response): 249 | self.einvoice.ewaybill = response.get('EwbNo') 250 | self.einvoice.ewaybill_validity = response.get('EwbValidTill') 251 | self.einvoice.status = 'E-Way Bill Generated' 252 | self.einvoice.flags.ignore_validate_update_after_submit = 1 253 | self.einvoice.flags.ignore_permissions = 1 254 | self.einvoice.save() 255 | 256 | @staticmethod 257 | def generate_eway_bill(einvoice): 258 | business_gstin = einvoice.seller_gstin 259 | connector = CleartaxConnector(business_gstin) 260 | connector.einvoice = einvoice 261 | response = connector.make_eway_bill_request() 262 | success, errors = response.get('Success'), response.get('Errors') 263 | 264 | return success, errors 265 | 266 | @log_exception 267 | def make_cancel_ewaybill_request(self, reason, remark): 268 | headers = self.get_headers() 269 | url = self.endpoints.cancel_ewaybill 270 | 271 | ewaybill = self.einvoice.ewaybill 272 | 273 | payload = {'ewbNo': ewaybill, 'cancelRsnCode': reason, 'cancelRmrk': remark} 274 | payload = dumps(payload, indent=4) 275 | 276 | response = self.make_request('post', url, headers, payload) 277 | # Sample Response -> https://docs.cleartax.in/cleartax-for-developers/e-invoicing-api/e-invoicing-api-reference/cleartax-e-invoicing-apis-xml-schema#sample-response-4 278 | 279 | response = self.sanitize_response(response) 280 | if response.get('Success'): 281 | self.handle_successful_ewaybill_cancellation() 282 | 283 | return response 284 | 285 | def handle_successful_ewaybill_cancellation(self): 286 | self.einvoice.ewaybill_cancelled = 1 287 | self.einvoice.status = 'E-Way Bill Cancelled' 288 | self.einvoice.flags.ignore_validate_update_after_submit = 1 289 | self.einvoice.flags.ignore_permissions = 1 290 | self.einvoice.save() 291 | 292 | @staticmethod 293 | def cancel_ewaybill(einvoice, reason, remark): 294 | business_gstin = einvoice.seller_gstin 295 | connector = CleartaxConnector(business_gstin) 296 | connector.einvoice = einvoice 297 | response = connector.make_cancel_ewaybill_request(reason, remark) 298 | success, errors = response.get('Success'), response.get('Errors') 299 | 300 | return success, errors -------------------------------------------------------------------------------- /erpnext_gst_compliance/adequare_integration/adequare_connector.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import io 4 | import base64 5 | import frappe 6 | 7 | from frappe import _ 8 | from json import dumps 9 | from pyqrcode import create as qrcreate 10 | from frappe.utils.data import get_link_to_form 11 | from erpnext_gst_compliance.utils import log_exception 12 | from frappe.integrations.utils import make_post_request, make_get_request 13 | from frappe.utils.data import add_to_date, time_diff_in_seconds, now_datetime 14 | 15 | class AdequareConnector: 16 | def __init__(self, gstin): 17 | 18 | self.gstin = gstin 19 | self.settings = frappe.get_cached_doc("Adequare Settings") 20 | self.credentials = self.get_user_credentials() 21 | self.host = self.get_host_url() 22 | self.endpoints = self.get_endpoints() 23 | 24 | self.validate() 25 | 26 | def get_user_credentials(self): 27 | return next(filter(lambda row: row.gstin == self.gstin, self.settings.credentials), frappe._dict()) 28 | 29 | def get_host_url(self): 30 | if self.settings.sandbox_mode: 31 | return "https://gsp.adaequare.com/test" 32 | else: 33 | return "https://gsp.adaequare.com" 34 | 35 | def get_endpoints(self): 36 | return frappe._dict({ 37 | "authenticate": 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token', 38 | "generate_irn": self.host + '/enriched/ei/api/invoice', 39 | "cancel_irn": self.host + '/enriched/ei/api/invoice/cancel', 40 | "irn_details": self.host + '/enriched/ei/api/invoice/irn', 41 | "gstin_details": self.host + '/enriched/ei/api/master/gstin', 42 | "cancel_ewaybill": self.host + '/enriched/ei/api/ewayapi', 43 | "generate_ewaybill": self.host + '/enriched/ei/api/ewaybill', 44 | }) 45 | 46 | def validate(self): 47 | if not self.settings.enabled: 48 | frappe.throw(_("Adequare Settings is not enabled. Please configure Adequare Settings and try again.")) 49 | 50 | if not self.credentials: 51 | settings_form = get_link_to_form('Adequare Settings', 'Adequare Settings') 52 | frappe.throw(_("Cannot find Adequare Credentials for selected Company GSTIN {}. Please check {}.") 53 | .format(self.gstin, settings_form)) 54 | 55 | @log_exception 56 | def make_request(self, req_type, url, headers, payload): 57 | if req_type == 'post': 58 | response = make_post_request(url, headers=headers, data=payload) 59 | else: 60 | response = make_get_request(url, headers=headers, data=payload) 61 | 62 | self.log_einvoice_request(url, headers, payload, response) 63 | 64 | return response 65 | 66 | def log_einvoice_request(self, url, headers, payload, response): 67 | headers.update({ 'password': self.credentials.password }) 68 | request_log = frappe.get_doc({ 69 | "doctype": "E Invoice Request Log", 70 | "user": frappe.session.user, 71 | "reference_invoice": self.einvoice.name, 72 | "url": url, 73 | "headers": dumps(headers, indent=4) if headers else None, 74 | "data": dumps(payload, indent=4) if isinstance(payload, dict) else payload, 75 | "response": dumps(response, indent=4) if response else None 76 | }) 77 | request_log.save(ignore_permissions=True) 78 | frappe.db.commit() 79 | 80 | @log_exception 81 | def fetch_auth_token(self): 82 | client_id = self.settings.client_id or frappe.conf.einvoice_client_id 83 | client_secret = self.settings.get_password('client_secret') or frappe.conf.einvoice_client_secret 84 | headers = { 85 | 'gspappid': client_id, 86 | 'gspappsecret': client_secret 87 | } 88 | url = self.endpoints.authenticate 89 | res = self.make_request('post', url, headers, None) 90 | self.handle_successful_token_generation(res) 91 | 92 | @log_exception 93 | def handle_successful_token_generation(self, res): 94 | self.settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token')) 95 | self.settings.token_expiry = add_to_date(None, seconds=res.get('expires_in')) 96 | self.settings.save(ignore_permissions=True) 97 | self.settings.reload() 98 | frappe.db.commit() 99 | 100 | @log_exception 101 | def get_auth_token(self): 102 | if time_diff_in_seconds(self.settings.token_expiry, now_datetime()) < 150.0: 103 | self.fetch_auth_token() 104 | 105 | return self.settings.auth_token 106 | 107 | @log_exception 108 | def get_headers(self): 109 | return { 110 | 'content-type': 'application/json', 111 | 'user_name': self.credentials.username, 112 | 'password': self.credentials.get_password(), 113 | 'gstin': self.credentials.gstin, 114 | 'authorization': self.get_auth_token(), 115 | 'requestid': str(base64.b64encode(os.urandom(18))), 116 | } 117 | 118 | @log_exception 119 | def make_irn_request(self): 120 | headers = self.get_headers() 121 | url = self.endpoints.generate_irn 122 | 123 | einvoice_json = self.einvoice.get_einvoice_json() 124 | payload = dumps(einvoice_json, indent=4) 125 | 126 | response = self.make_request('post', url, headers, payload) 127 | 128 | sucess, errors = self.handle_irn_generation_response(response) 129 | return sucess, errors 130 | 131 | @staticmethod 132 | @log_exception 133 | def generate_irn(einvoice): 134 | gstin = einvoice.seller_gstin 135 | connector = AdequareConnector(gstin) 136 | connector.einvoice = einvoice 137 | success, errors = connector.make_irn_request() 138 | 139 | return success, errors 140 | 141 | @log_exception 142 | def handle_irn_generation_response(self, response): 143 | if response.get('success'): 144 | govt_response = response.get('result') 145 | self.handle_successful_irn_generation(govt_response) 146 | elif '2150' in response.get('message'): 147 | govt_response = response.get('result') 148 | self.handle_irn_already_generated(govt_response) 149 | else: 150 | errors = response.get('message') 151 | errors = self.sanitize_error_message(errors) 152 | return False, errors 153 | 154 | return True, [] 155 | 156 | def handle_successful_irn_generation(self, response): 157 | status = 'IRN Generated' 158 | irn = response.get('Irn') 159 | ack_no = response.get('AckNo') 160 | ack_date = response.get('AckDt') 161 | ewaybill = response.get('EwbNo') 162 | ewaybill_validity = response.get('EwbValidTill') 163 | qrcode = self.generate_qrcode(response.get('SignedQRCode')) 164 | 165 | self.einvoice.update({ 166 | 'irn': irn, 167 | 'status': status, 168 | 'ack_no': ack_no, 169 | 'ack_date': ack_date, 170 | 'ewaybill': ewaybill, 171 | 'qrcode_path': qrcode, 172 | 'ewaybill_validity': ewaybill_validity 173 | }) 174 | self.einvoice.flags.ignore_permissions = 1 175 | self.einvoice.submit() 176 | 177 | def generate_qrcode(self, signed_qrcode): 178 | doctype = self.einvoice.doctype 179 | docname = self.einvoice.name 180 | filename = '{} - QRCode.png'.format(docname).replace(os.path.sep, "__") 181 | 182 | qr_image = io.BytesIO() 183 | url = qrcreate(signed_qrcode, error='L') 184 | url.png(qr_image, scale=2, quiet_zone=1) 185 | _file = frappe.get_doc({ 186 | 'doctype': 'File', 187 | 'file_name': filename, 188 | 'attached_to_doctype': doctype, 189 | 'attached_to_name': docname, 190 | 'attached_to_field': 'qrcode_path', 191 | 'is_private': 0, 192 | 'content': qr_image.getvalue() 193 | }) 194 | _file.save() 195 | return _file.file_url 196 | 197 | def handle_irn_already_generated(self, response): 198 | # IRN already generated but not updated in invoice 199 | # Extract the IRN from the response description and fetch irn details 200 | irn = response[0].get('Desc').get('Irn') 201 | success, irn_details = self.make_get_irn_details_request(irn) 202 | if success: 203 | self.handle_successful_irn_generation(irn_details) 204 | 205 | def sanitize_error_message(self, message): 206 | ''' 207 | On validation errors, response message looks something like this: 208 | message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 209 | 3095 : Supplier GSTIN is inactive' 210 | we search for string between ':' to extract the error messages 211 | errors = [ 212 | ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ', 213 | ': Test' 214 | ] 215 | then we trim down the message by looping over errors 216 | ''' 217 | if not message: 218 | return [] 219 | 220 | if not ' : ' in message: 221 | return [message] 222 | 223 | errors = re.findall(' : [^:]+', message) 224 | for idx, e in enumerate(errors): 225 | # remove colons 226 | errors[idx] = errors[idx].replace(':', '').strip() 227 | # if not last 228 | if idx != len(errors) - 1: 229 | # remove last 7 chars eg: ', 3095 ' 230 | errors[idx] = errors[idx][:-6] 231 | 232 | return errors 233 | 234 | @log_exception 235 | def make_get_irn_details_request(self, irn): 236 | headers = self.get_headers() 237 | url = self.endpoints.irn_details 238 | 239 | params = '?irn={irn}'.format(irn=irn) 240 | response = self.make_request('get', url + params, headers, None) 241 | 242 | if response.get('success'): 243 | return True, response.get('result') 244 | else: 245 | errors = response.get('message') 246 | errors = self.sanitize_error_message(errors) 247 | return False, errors 248 | 249 | @log_exception 250 | def make_cancel_irn_request(self, reason, remark): 251 | headers = self.get_headers() 252 | irn = self.einvoice.irn 253 | 254 | payload = {'Irn': irn, 'Cnlrsn': reason, 'Cnlrem': remark} 255 | payload = dumps(payload, indent=4) 256 | 257 | url = self.endpoints.cancel_irn 258 | response = self.make_request('post', url, headers, payload) 259 | 260 | sucess, errors = self.handle_irn_cancellation_response(response) 261 | return sucess, errors 262 | 263 | @log_exception 264 | def handle_irn_cancellation_response(self, response): 265 | irn_already_cancelled = '9999' in response.get('message') 266 | if response.get('success') or irn_already_cancelled: 267 | self.handle_successful_irn_cancellation(response) 268 | else: 269 | errors = response.get('message') 270 | errors = self.sanitize_error_message(errors) 271 | return False, errors 272 | 273 | return True, [] 274 | 275 | def handle_successful_irn_cancellation(self, response): 276 | self.einvoice.irn_cancelled = 1 277 | if response.get('result'): 278 | self.einvoice.irn_cancel_date = response.get('result').get('CancelDate') 279 | self.einvoice.status = 'IRN Cancelled' 280 | self.einvoice.flags.ignore_validate_update_after_submit = 1 281 | self.einvoice.flags.ignore_permissions = 1 282 | self.einvoice.save() 283 | 284 | @staticmethod 285 | @log_exception 286 | def cancel_irn(einvoice, reason, remark): 287 | gstin = einvoice.seller_gstin 288 | connector = AdequareConnector(gstin) 289 | connector.einvoice = einvoice 290 | success, errors = connector.make_cancel_irn_request(reason, remark) 291 | 292 | return success, errors 293 | 294 | @log_exception 295 | def make_eway_bill_request(self): 296 | headers = self.get_headers() 297 | url = self.endpoints.generate_ewaybill 298 | 299 | eway_bill_json = self.einvoice.get_eway_bill_json() 300 | payload = dumps(eway_bill_json, indent=4) 301 | 302 | response = self.make_request('post', url, headers, payload) 303 | 304 | if response.get('success'): 305 | govt_response = response.get('result') 306 | self.handle_successful_ewaybill_generation(govt_response) 307 | else: 308 | errors = response.get('message') 309 | errors = self.sanitize_error_message(errors) 310 | return False, errors 311 | 312 | return True, [] 313 | 314 | def handle_successful_ewaybill_generation(self, response): 315 | self.einvoice.ewaybill = response.get('EwbNo') 316 | self.einvoice.ewaybill_validity = response.get('EwbValidTill') 317 | self.einvoice.status = 'E-Way Bill Generated' 318 | self.einvoice.flags.ignore_validate_update_after_submit = 1 319 | self.einvoice.flags.ignore_permissions = 1 320 | self.einvoice.save() 321 | 322 | @staticmethod 323 | @log_exception 324 | def generate_eway_bill(einvoice): 325 | gstin = einvoice.seller_gstin 326 | connector = AdequareConnector(gstin) 327 | connector.einvoice = einvoice 328 | success, errors = connector.make_eway_bill_request() 329 | 330 | return success, errors 331 | 332 | @log_exception 333 | def make_cancel_ewaybill_request(self, reason, remark): 334 | headers = self.get_headers() 335 | url = self.endpoints.cancel_ewaybill 336 | 337 | ewaybill = self.einvoice.ewaybill 338 | 339 | payload = {'ewbNo': ewaybill, 'cancelRsnCode': reason, 'cancelRmrk': remark} 340 | payload = dumps(payload, indent=4) 341 | 342 | response = self.make_request('post', url, headers, payload) 343 | 344 | if response.get('success'): 345 | self.handle_successful_ewaybill_cancellation() 346 | else: 347 | errors = response.get('message') 348 | errors = self.sanitize_error_message(errors) 349 | return False, errors 350 | 351 | return True, [] 352 | 353 | def handle_successful_ewaybill_cancellation(self): 354 | self.einvoice.ewaybill = '' 355 | self.einvoice.ewaybill_cancelled = 1 356 | self.einvoice.status = 'E-Way Bill Cancelled' 357 | self.einvoice.flags.ignore_validate_update_after_submit = 1 358 | self.einvoice.flags.ignore_permissions = 1 359 | self.einvoice.save() 360 | 361 | @staticmethod 362 | @log_exception 363 | def cancel_ewaybill(einvoice, reason, remark): 364 | gstin = einvoice.seller_gstin 365 | connector = AdequareConnector(gstin) 366 | connector.einvoice = einvoice 367 | success, errors = connector.make_cancel_ewaybill_request(reason, remark) 368 | 369 | return success, errors 370 | -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice/e_invoice.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "autoname": "field:invoice", 4 | "creation": "2021-05-08 14:30:49.929314", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "section_break_1", 10 | "status", 11 | "irn", 12 | "ack_no", 13 | "ack_date", 14 | "ewaybill", 15 | "irn_cancel_date", 16 | "irn_cancelled", 17 | "column_break_5", 18 | "qrcode_path", 19 | "qrcode", 20 | "ewaybill_validity", 21 | "version", 22 | "transaction_details_section", 23 | "tax_scheme", 24 | "reverse_charge", 25 | "igst_on_intra", 26 | "column_break_6", 27 | "supply_type", 28 | "ecommerce_gstin", 29 | "document_details_section", 30 | "invoice", 31 | "invoice_date", 32 | "column_break_12", 33 | "invoice_type", 34 | "seller_details_section", 35 | "seller_gstin", 36 | "seller_legal_name", 37 | "seller_trade_name", 38 | "seller_address_line_1", 39 | "seller_address_line_2", 40 | "column_break_20", 41 | "seller_location", 42 | "seller_pincode", 43 | "seller_state_code", 44 | "seller_phone", 45 | "seller_email", 46 | "buyer_details_section", 47 | "buyer_gstin", 48 | "buyer_legal_name", 49 | "buyer_trade_name", 50 | "buyer_address_line_1", 51 | "buyer_address_line_2", 52 | "column_break_33", 53 | "buyer_location", 54 | "buyer_pincode", 55 | "buyer_state_code", 56 | "buyer_place_of_supply", 57 | "buyer_phone", 58 | "buyer_email", 59 | "dispatch_details_section", 60 | "dispatch_legal_name", 61 | "dispatch_address_line_1", 62 | "dispatch_address_line_2", 63 | "column_break_43", 64 | "dispatch_location", 65 | "dispatch_pincode", 66 | "dispatch_state_code", 67 | "shipping_details_section", 68 | "shipping_gstin", 69 | "shipping_legal_name", 70 | "shipping_trade_name", 71 | "shipping_location", 72 | "column_break_51", 73 | "shipping_address_line_1", 74 | "shipping_address_line_2", 75 | "shipping_pincode", 76 | "shipping_state_code", 77 | "item_details_section", 78 | "items", 79 | "item_totals_section", 80 | "items_ass_value", 81 | "items_cgst", 82 | "items_sgst", 83 | "items_igst", 84 | "column_break_76", 85 | "items_cess", 86 | "items_cess_nadv", 87 | "item_other_charges", 88 | "items_total_value", 89 | "value_details_section", 90 | "ass_value", 91 | "cgst_value", 92 | "sgst_value", 93 | "igst_value", 94 | "cess_value", 95 | "base_invoice_value", 96 | "column_break_64", 97 | "invoice_discount", 98 | "other_charges", 99 | "round_off_amount", 100 | "state_cess_value", 101 | "invoice_value", 102 | "payment_details_section", 103 | "payee_name", 104 | "account_detail", 105 | "mode", 106 | "branch_or_ifsc", 107 | "column_break_74", 108 | "payment_term", 109 | "credit_days", 110 | "paid_amount", 111 | "payment_due", 112 | "reference_details_section", 113 | "previous_document_no", 114 | "column_break_81", 115 | "previous_document_date", 116 | "export_details_section", 117 | "export_bill_no", 118 | "export_bill_date", 119 | "export_duty", 120 | "claiming_refund", 121 | "column_break_87", 122 | "port_code", 123 | "currency_code", 124 | "country_code", 125 | "eway_bill_details_section", 126 | "transporter_gstin", 127 | "transporter_name", 128 | "mode_of_transport", 129 | "distance", 130 | "column_break_97", 131 | "transport_document_no", 132 | "transport_document_date", 133 | "vehicle_no", 134 | "vehicle_type", 135 | "other_details_section", 136 | "company", 137 | "column_break_106", 138 | "amended_from" 139 | ], 140 | "fields": [ 141 | { 142 | "default": "1.1", 143 | "fieldname": "version", 144 | "fieldtype": "Float", 145 | "hidden": 1, 146 | "label": "Version", 147 | "precision": "1", 148 | "print_hide": 1, 149 | "read_only": 1 150 | }, 151 | { 152 | "fieldname": "transaction_details_section", 153 | "fieldtype": "Section Break", 154 | "label": "Transaction Details" 155 | }, 156 | { 157 | "default": "GST", 158 | "fieldname": "tax_scheme", 159 | "fieldtype": "Data", 160 | "label": "Tax Scheme", 161 | "read_only": 1, 162 | "reqd": 1 163 | }, 164 | { 165 | "default": "0", 166 | "fieldname": "reverse_charge", 167 | "fieldtype": "Check", 168 | "label": "Reverse Charge" 169 | }, 170 | { 171 | "default": "0", 172 | "depends_on": "igst_on_intra", 173 | "fieldname": "igst_on_intra", 174 | "fieldtype": "Check", 175 | "label": "IGST on Intra" 176 | }, 177 | { 178 | "fieldname": "column_break_6", 179 | "fieldtype": "Column Break" 180 | }, 181 | { 182 | "fieldname": "supply_type", 183 | "fieldtype": "Select", 184 | "label": "Supply Type", 185 | "options": "B2B\nSEZWP\nSEZWOP\nEXPWP\nEXPWOP\nDEXP" 186 | }, 187 | { 188 | "depends_on": "ecommerce_gstin", 189 | "fieldname": "ecommerce_gstin", 190 | "fieldtype": "Data", 191 | "label": "Ecommerce GSTIN" 192 | }, 193 | { 194 | "fieldname": "amended_from", 195 | "fieldtype": "Link", 196 | "label": "Amended From", 197 | "no_copy": 1, 198 | "options": "E Invoice", 199 | "print_hide": 1, 200 | "read_only": 1 201 | }, 202 | { 203 | "fieldname": "document_details_section", 204 | "fieldtype": "Section Break", 205 | "label": "Document Details" 206 | }, 207 | { 208 | "fetch_from": "invoice.posting_date", 209 | "fieldname": "invoice_date", 210 | "fieldtype": "Date", 211 | "in_list_view": 1, 212 | "label": "Invoice Date", 213 | "reqd": 1 214 | }, 215 | { 216 | "fieldname": "column_break_12", 217 | "fieldtype": "Column Break" 218 | }, 219 | { 220 | "fieldname": "invoice_type", 221 | "fieldtype": "Select", 222 | "label": "Invoice Type", 223 | "options": "INV\nCRN\nDBN" 224 | }, 225 | { 226 | "collapsible": 1, 227 | "fieldname": "seller_details_section", 228 | "fieldtype": "Section Break", 229 | "label": "Seller Details" 230 | }, 231 | { 232 | "fieldname": "column_break_20", 233 | "fieldtype": "Column Break" 234 | }, 235 | { 236 | "fieldname": "seller_gstin", 237 | "fieldtype": "Data", 238 | "label": "GSTIN", 239 | "reqd": 1 240 | }, 241 | { 242 | "fieldname": "seller_legal_name", 243 | "fieldtype": "Data", 244 | "label": "Legal Name", 245 | "reqd": 1 246 | }, 247 | { 248 | "depends_on": "seller_trade_name", 249 | "fieldname": "seller_trade_name", 250 | "fieldtype": "Data", 251 | "label": "Trade Name" 252 | }, 253 | { 254 | "fieldname": "seller_address_line_1", 255 | "fieldtype": "Data", 256 | "label": "Address Line 1", 257 | "reqd": 1 258 | }, 259 | { 260 | "depends_on": "seller_address_line_2", 261 | "fieldname": "seller_address_line_2", 262 | "fieldtype": "Data", 263 | "label": "Address Line 2" 264 | }, 265 | { 266 | "fieldname": "seller_location", 267 | "fieldtype": "Data", 268 | "label": "Location", 269 | "reqd": 1 270 | }, 271 | { 272 | "fieldname": "seller_pincode", 273 | "fieldtype": "Data", 274 | "label": "Pincode", 275 | "reqd": 1 276 | }, 277 | { 278 | "fieldname": "seller_state_code", 279 | "fieldtype": "Data", 280 | "label": "State Code", 281 | "reqd": 1 282 | }, 283 | { 284 | "depends_on": "seller_phone", 285 | "fieldname": "seller_phone", 286 | "fieldtype": "Data", 287 | "label": "Phone" 288 | }, 289 | { 290 | "depends_on": "seller_email", 291 | "fieldname": "seller_email", 292 | "fieldtype": "Data", 293 | "label": "Email" 294 | }, 295 | { 296 | "collapsible": 1, 297 | "fieldname": "buyer_details_section", 298 | "fieldtype": "Section Break", 299 | "label": "Buyer Details" 300 | }, 301 | { 302 | "fieldname": "column_break_33", 303 | "fieldtype": "Column Break" 304 | }, 305 | { 306 | "fieldname": "buyer_gstin", 307 | "fieldtype": "Data", 308 | "label": "GSTIN", 309 | "reqd": 1 310 | }, 311 | { 312 | "fieldname": "buyer_legal_name", 313 | "fieldtype": "Data", 314 | "label": "Legal Name", 315 | "reqd": 1 316 | }, 317 | { 318 | "depends_on": "buyer_trade_name", 319 | "fieldname": "buyer_trade_name", 320 | "fieldtype": "Data", 321 | "label": "Trade Name" 322 | }, 323 | { 324 | "fieldname": "buyer_address_line_1", 325 | "fieldtype": "Data", 326 | "label": "Address Line 1", 327 | "reqd": 1 328 | }, 329 | { 330 | "depends_on": "buyer_address_line_2", 331 | "fieldname": "buyer_address_line_2", 332 | "fieldtype": "Data", 333 | "label": "Address Line 2" 334 | }, 335 | { 336 | "fieldname": "buyer_location", 337 | "fieldtype": "Data", 338 | "label": "Location", 339 | "reqd": 1 340 | }, 341 | { 342 | "fieldname": "buyer_pincode", 343 | "fieldtype": "Data", 344 | "label": "Pincode", 345 | "reqd": 1 346 | }, 347 | { 348 | "fieldname": "buyer_state_code", 349 | "fieldtype": "Data", 350 | "label": "State Code", 351 | "reqd": 1 352 | }, 353 | { 354 | "depends_on": "buyer_phone", 355 | "fieldname": "buyer_phone", 356 | "fieldtype": "Data", 357 | "label": "Phone" 358 | }, 359 | { 360 | "depends_on": "buyer_email", 361 | "fieldname": "buyer_email", 362 | "fieldtype": "Data", 363 | "label": "Email" 364 | }, 365 | { 366 | "depends_on": "dispatch_legal_name", 367 | "fieldname": "dispatch_details_section", 368 | "fieldtype": "Section Break", 369 | "label": "Dispatch Details" 370 | }, 371 | { 372 | "fieldname": "column_break_43", 373 | "fieldtype": "Column Break" 374 | }, 375 | { 376 | "fieldname": "dispatch_legal_name", 377 | "fieldtype": "Data", 378 | "label": "Legal Name" 379 | }, 380 | { 381 | "fieldname": "dispatch_address_line_1", 382 | "fieldtype": "Data", 383 | "label": "Address Line 1", 384 | "mandatory_depends_on": "dispatch_legal_name" 385 | }, 386 | { 387 | "depends_on": "dispatch_address_line_2", 388 | "fieldname": "dispatch_address_line_2", 389 | "fieldtype": "Data", 390 | "label": "Address Line 2" 391 | }, 392 | { 393 | "fieldname": "dispatch_location", 394 | "fieldtype": "Data", 395 | "label": "Location", 396 | "mandatory_depends_on": "dispatch_legal_name" 397 | }, 398 | { 399 | "fieldname": "dispatch_pincode", 400 | "fieldtype": "Data", 401 | "label": "Pincode", 402 | "mandatory_depends_on": "dispatch_legal_name" 403 | }, 404 | { 405 | "fieldname": "dispatch_state_code", 406 | "fieldtype": "Data", 407 | "label": "State Code", 408 | "mandatory_depends_on": "dispatch_legal_name" 409 | }, 410 | { 411 | "depends_on": "shippping_legal_name", 412 | "fieldname": "shipping_details_section", 413 | "fieldtype": "Section Break", 414 | "label": "Shipping Details" 415 | }, 416 | { 417 | "fieldname": "column_break_51", 418 | "fieldtype": "Column Break" 419 | }, 420 | { 421 | "fieldname": "value_details_section", 422 | "fieldtype": "Section Break", 423 | "label": "Value Details" 424 | }, 425 | { 426 | "fieldname": "ass_value", 427 | "fieldtype": "Currency", 428 | "label": "Assessable Value", 429 | "non_negative": 1, 430 | "precision": "2", 431 | "reqd": 1 432 | }, 433 | { 434 | "fieldname": "cgst_value", 435 | "fieldtype": "Currency", 436 | "label": "CGST Value", 437 | "non_negative": 1, 438 | "precision": "2" 439 | }, 440 | { 441 | "fieldname": "sgst_value", 442 | "fieldtype": "Currency", 443 | "label": "SGST Value", 444 | "non_negative": 1, 445 | "precision": "2" 446 | }, 447 | { 448 | "fieldname": "igst_value", 449 | "fieldtype": "Currency", 450 | "label": "IGST Value", 451 | "non_negative": 1, 452 | "precision": "2" 453 | }, 454 | { 455 | "depends_on": "cess_value", 456 | "fieldname": "cess_value", 457 | "fieldtype": "Currency", 458 | "label": "Cess Value", 459 | "non_negative": 1, 460 | "precision": "2" 461 | }, 462 | { 463 | "depends_on": "eval: doc.base_invoice_value != doc.invoice_value", 464 | "fieldname": "base_invoice_value", 465 | "fieldtype": "Currency", 466 | "label": "Invoice Value (Company Currency)", 467 | "non_negative": 1, 468 | "precision": "2", 469 | "reqd": 1 470 | }, 471 | { 472 | "fieldname": "column_break_64", 473 | "fieldtype": "Column Break" 474 | }, 475 | { 476 | "fieldname": "invoice_discount", 477 | "fieldtype": "Currency", 478 | "label": "Invoice Discount", 479 | "non_negative": 1, 480 | "precision": "2" 481 | }, 482 | { 483 | "fieldname": "other_charges", 484 | "fieldtype": "Currency", 485 | "label": "Other Charges", 486 | "non_negative": 1, 487 | "precision": "2" 488 | }, 489 | { 490 | "fieldname": "round_off_amount", 491 | "fieldtype": "Currency", 492 | "label": "Round Off Amount", 493 | "non_negative": 1, 494 | "precision": "2" 495 | }, 496 | { 497 | "fieldname": "invoice_value", 498 | "fieldtype": "Currency", 499 | "label": "Invoice Value", 500 | "non_negative": 1, 501 | "precision": "2", 502 | "reqd": 1 503 | }, 504 | { 505 | "depends_on": "state_cess_value", 506 | "fieldname": "state_cess_value", 507 | "fieldtype": "Currency", 508 | "label": "State Cess Value", 509 | "non_negative": 1, 510 | "precision": "2" 511 | }, 512 | { 513 | "depends_on": "payee_name", 514 | "fieldname": "payment_details_section", 515 | "fieldtype": "Section Break", 516 | "label": "Payment Details" 517 | }, 518 | { 519 | "fieldname": "payee_name", 520 | "fieldtype": "Data", 521 | "label": "Payee Name" 522 | }, 523 | { 524 | "fieldname": "account_detail", 525 | "fieldtype": "Data", 526 | "label": "Account Detail" 527 | }, 528 | { 529 | "fieldname": "mode", 530 | "fieldtype": "Data", 531 | "label": "Mode of Payment" 532 | }, 533 | { 534 | "fieldname": "branch_or_ifsc", 535 | "fieldtype": "Data", 536 | "label": "Branch or IFSC Code" 537 | }, 538 | { 539 | "fieldname": "payment_term", 540 | "fieldtype": "Data", 541 | "label": "Payment Term" 542 | }, 543 | { 544 | "fieldname": "credit_days", 545 | "fieldtype": "Int", 546 | "label": "Credit Days" 547 | }, 548 | { 549 | "fieldname": "paid_amount", 550 | "fieldtype": "Currency", 551 | "label": "Paid Amount", 552 | "non_negative": 1, 553 | "precision": "2" 554 | }, 555 | { 556 | "fieldname": "payment_due", 557 | "fieldtype": "Currency", 558 | "label": "Payment Due", 559 | "non_negative": 1, 560 | "precision": "2" 561 | }, 562 | { 563 | "depends_on": "previous_document_no", 564 | "fieldname": "reference_details_section", 565 | "fieldtype": "Section Break", 566 | "label": "Reference Details" 567 | }, 568 | { 569 | "fieldname": "previous_document_no", 570 | "fieldtype": "Data", 571 | "label": "Previous Document No." 572 | }, 573 | { 574 | "fieldname": "previous_document_date", 575 | "fieldtype": "Date", 576 | "label": "Previous Document Date" 577 | }, 578 | { 579 | "depends_on": "export_bill_no", 580 | "fieldname": "export_details_section", 581 | "fieldtype": "Section Break", 582 | "label": "Export Details" 583 | }, 584 | { 585 | "fieldname": "export_bill_no", 586 | "fieldtype": "Data", 587 | "label": "Shipping Bill No." 588 | }, 589 | { 590 | "fieldname": "export_bill_date", 591 | "fieldtype": "Date", 592 | "label": "Shipping Bill Date" 593 | }, 594 | { 595 | "fieldname": "port_code", 596 | "fieldtype": "Data", 597 | "label": "Port Code" 598 | }, 599 | { 600 | "default": "0", 601 | "fieldname": "claiming_refund", 602 | "fieldtype": "Check", 603 | "label": "Claiming Refund" 604 | }, 605 | { 606 | "fieldname": "currency_code", 607 | "fieldtype": "Data", 608 | "label": "Currency Code" 609 | }, 610 | { 611 | "fieldname": "country_code", 612 | "fieldtype": "Data", 613 | "label": "Country Code" 614 | }, 615 | { 616 | "fieldname": "export_duty", 617 | "fieldtype": "Currency", 618 | "label": "Export Duty", 619 | "non_negative": 1, 620 | "precision": "2" 621 | }, 622 | { 623 | "depends_on": "transporter_name", 624 | "fieldname": "eway_bill_details_section", 625 | "fieldtype": "Section Break", 626 | "label": "E-Way Bill Details" 627 | }, 628 | { 629 | "fieldname": "column_break_87", 630 | "fieldtype": "Column Break" 631 | }, 632 | { 633 | "fetch_from": "invoice.distance", 634 | "fieldname": "distance", 635 | "fieldtype": "Int", 636 | "label": "Distance" 637 | }, 638 | { 639 | "fetch_from": "invoice.gst_vehicle_type", 640 | "fieldname": "vehicle_type", 641 | "fieldtype": "Select", 642 | "label": "Vehicle Type", 643 | "options": "\nRegular\nOver Dimensional Cargo (ODC)" 644 | }, 645 | { 646 | "fieldname": "column_break_74", 647 | "fieldtype": "Column Break" 648 | }, 649 | { 650 | "fieldname": "column_break_81", 651 | "fieldtype": "Column Break" 652 | }, 653 | { 654 | "fieldname": "column_break_97", 655 | "fieldtype": "Column Break" 656 | }, 657 | { 658 | "fieldname": "item_details_section", 659 | "fieldtype": "Section Break", 660 | "label": "Item Details" 661 | }, 662 | { 663 | "fieldname": "items", 664 | "fieldtype": "Table", 665 | "label": "Items", 666 | "options": "E Invoice Item", 667 | "reqd": 1 668 | }, 669 | { 670 | "fieldname": "other_details_section", 671 | "fieldtype": "Section Break", 672 | "label": "Other Details", 673 | "print_hide": 1 674 | }, 675 | { 676 | "fetch_from": "invoice.company", 677 | "fieldname": "company", 678 | "fieldtype": "Data", 679 | "label": "Company", 680 | "read_only": 1 681 | }, 682 | { 683 | "fieldname": "column_break_106", 684 | "fieldtype": "Column Break" 685 | }, 686 | { 687 | "fieldname": "invoice", 688 | "fieldtype": "Link", 689 | "in_list_view": 1, 690 | "label": "Invoice Number", 691 | "options": "Sales Invoice", 692 | "reqd": 1, 693 | "unique": 1 694 | }, 695 | { 696 | "depends_on": "buyer_place_of_supply", 697 | "fieldname": "buyer_place_of_supply", 698 | "fieldtype": "Data", 699 | "label": "Place of Supply" 700 | }, 701 | { 702 | "fetch_from": "invoice.gst_transporter_id", 703 | "fieldname": "transporter_gstin", 704 | "fieldtype": "Data", 705 | "label": "Transporter GSTIN" 706 | }, 707 | { 708 | "fetch_from": "invoice.transporter_name", 709 | "fieldname": "transporter_name", 710 | "fieldtype": "Data", 711 | "label": "Transporter Name" 712 | }, 713 | { 714 | "fetch_from": "invoice.mode_of_transport", 715 | "fieldname": "mode_of_transport", 716 | "fieldtype": "Select", 717 | "label": "Mode of Transport", 718 | "options": "Road\nAir\nRail\nShip" 719 | }, 720 | { 721 | "fetch_from": "invoice.lr_no", 722 | "fieldname": "transport_document_no", 723 | "fieldtype": "Data", 724 | "label": "Transport Document No." 725 | }, 726 | { 727 | "fetch_from": "invoice.lr_date", 728 | "fieldname": "transport_document_date", 729 | "fieldtype": "Date", 730 | "label": "Transport Document Date" 731 | }, 732 | { 733 | "fetch_from": "invoice.vehicle_no", 734 | "fieldname": "vehicle_no", 735 | "fieldtype": "Data", 736 | "label": "Vehicle No." 737 | }, 738 | { 739 | "depends_on": "shipping_gstin", 740 | "fieldname": "shipping_gstin", 741 | "fieldtype": "Data", 742 | "label": "GSTIN" 743 | }, 744 | { 745 | "fieldname": "shipping_legal_name", 746 | "fieldtype": "Data", 747 | "label": "Legal Name" 748 | }, 749 | { 750 | "depends_on": "shipping_trade_name", 751 | "fieldname": "shipping_trade_name", 752 | "fieldtype": "Data", 753 | "label": "Trade Name" 754 | }, 755 | { 756 | "fieldname": "shipping_location", 757 | "fieldtype": "Data", 758 | "label": "Location", 759 | "mandatory_depends_on": "shippping_legal_name" 760 | }, 761 | { 762 | "fieldname": "shipping_address_line_1", 763 | "fieldtype": "Data", 764 | "label": "Address Line 1", 765 | "mandatory_depends_on": "shippping_legal_name" 766 | }, 767 | { 768 | "depends_on": "shippping_address_line_2", 769 | "fieldname": "shipping_address_line_2", 770 | "fieldtype": "Data", 771 | "label": "Address Line 2" 772 | }, 773 | { 774 | "fieldname": "shipping_pincode", 775 | "fieldtype": "Data", 776 | "label": "Pincode", 777 | "mandatory_depends_on": "shippping_legal_name" 778 | }, 779 | { 780 | "fieldname": "shipping_state_code", 781 | "fieldtype": "Data", 782 | "label": "State Code", 783 | "mandatory_depends_on": "shippping_legal_name" 784 | }, 785 | { 786 | "fieldname": "section_break_1", 787 | "fieldtype": "Section Break" 788 | }, 789 | { 790 | "fieldname": "irn", 791 | "fieldtype": "Data", 792 | "label": "IRN", 793 | "read_only": 1 794 | }, 795 | { 796 | "depends_on": "eval: !doc.irn_cancelled", 797 | "fieldname": "ack_no", 798 | "fieldtype": "Data", 799 | "label": "Acknowlegment No.", 800 | "read_only": 1 801 | }, 802 | { 803 | "depends_on": "eval: !doc.irn_cancelled", 804 | "fieldname": "ack_date", 805 | "fieldtype": "Data", 806 | "label": "Acknowledgment Date", 807 | "read_only": 1 808 | }, 809 | { 810 | "fieldname": "column_break_5", 811 | "fieldtype": "Column Break" 812 | }, 813 | { 814 | "depends_on": "eval: !doc.irn_cancelled", 815 | "fieldname": "ewaybill", 816 | "fieldtype": "Data", 817 | "label": "E-Way Bill No.", 818 | "read_only": 1 819 | }, 820 | { 821 | "depends_on": "eval: !doc.irn_cancelled", 822 | "fieldname": "ewaybill_validity", 823 | "fieldtype": "Data", 824 | "label": "E-Way Bill Validity", 825 | "read_only": 1 826 | }, 827 | { 828 | "fieldname": "qrcode_path", 829 | "fieldtype": "Attach Image", 830 | "hidden": 1, 831 | "label": "QRCode File Path", 832 | "read_only": 1 833 | }, 834 | { 835 | "depends_on": "eval: !doc.irn_cancelled && doc.irn", 836 | "fieldname": "qrcode", 837 | "fieldtype": "Image", 838 | "label": "QRCode", 839 | "options": "qrcode_path" 840 | }, 841 | { 842 | "default": "0", 843 | "depends_on": "irn_cancelled", 844 | "fieldname": "irn_cancelled", 845 | "fieldtype": "Check", 846 | "label": "IRN Cancelled" 847 | }, 848 | { 849 | "depends_on": "irn_cancelled", 850 | "fieldname": "irn_cancel_date", 851 | "fieldtype": "Data", 852 | "label": "IRN Cancellation Date" 853 | }, 854 | { 855 | "default": "IRN Pending", 856 | "fieldname": "status", 857 | "fieldtype": "Select", 858 | "hidden": 1, 859 | "in_list_view": 1, 860 | "in_standard_filter": 1, 861 | "label": "Status", 862 | "options": "IRN Pending\nIRN Generated\nIRN Cancelled\nE-Way Bill Generated\nE-Way Bill Cancelled", 863 | "read_only": 1 864 | }, 865 | { 866 | "collapsible": 1, 867 | "fieldname": "item_totals_section", 868 | "fieldtype": "Section Break", 869 | "label": "Item Totals", 870 | "print_hide": 1 871 | }, 872 | { 873 | "fieldname": "items_igst", 874 | "fieldtype": "Currency", 875 | "label": "IGST", 876 | "non_negative": 1, 877 | "precision": "2" 878 | }, 879 | { 880 | "fieldname": "items_ass_value", 881 | "fieldtype": "Currency", 882 | "label": "Taxable Value", 883 | "non_negative": 1, 884 | "precision": "2" 885 | }, 886 | { 887 | "fieldname": "items_cgst", 888 | "fieldtype": "Currency", 889 | "label": "CGST", 890 | "non_negative": 1, 891 | "precision": "2" 892 | }, 893 | { 894 | "fieldname": "items_sgst", 895 | "fieldtype": "Currency", 896 | "label": "SGST", 897 | "non_negative": 1, 898 | "precision": "2" 899 | }, 900 | { 901 | "fieldname": "column_break_76", 902 | "fieldtype": "Column Break" 903 | }, 904 | { 905 | "fieldname": "items_cess", 906 | "fieldtype": "Currency", 907 | "label": "Cess", 908 | "non_negative": 1, 909 | "precision": "2" 910 | }, 911 | { 912 | "fieldname": "items_cess_nadv", 913 | "fieldtype": "Currency", 914 | "label": "Cess Non-Advolem", 915 | "non_negative": 1, 916 | "precision": "2" 917 | }, 918 | { 919 | "fieldname": "item_other_charges", 920 | "fieldtype": "Currency", 921 | "label": "Other Charges", 922 | "non_negative": 1, 923 | "precision": "2" 924 | }, 925 | { 926 | "fieldname": "items_total_value", 927 | "fieldtype": "Currency", 928 | "label": "Total Value", 929 | "non_negative": 1, 930 | "precision": "2" 931 | } 932 | ], 933 | "index_web_pages_for_search": 1, 934 | "is_submittable": 1, 935 | "links": [], 936 | "modified": "2021-08-11 14:38:47.743093", 937 | "modified_by": "Administrator", 938 | "module": "ERPNext GST Compliance", 939 | "name": "E Invoice", 940 | "owner": "Administrator", 941 | "permissions": [ 942 | { 943 | "email": 1, 944 | "export": 1, 945 | "print": 1, 946 | "read": 1, 947 | "report": 1, 948 | "role": "System Manager", 949 | "share": 1 950 | } 951 | ], 952 | "sort_field": "modified", 953 | "sort_order": "DESC", 954 | "track_changes": 1 955 | } -------------------------------------------------------------------------------- /erpnext_gst_compliance/erpnext_gst_compliance/doctype/e_invoice/e_invoice.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2021, Frappe and contributors 3 | # For license information, please see license.txt 4 | 5 | from __future__ import unicode_literals 6 | 7 | import six 8 | import frappe 9 | from frappe import _ 10 | from json import loads, dumps 11 | from frappe.model import default_fields 12 | from frappe.model.document import Document 13 | from frappe.utils.data import cint, format_date, getdate, flt 14 | from frappe.core.doctype.version.version import get_diff 15 | 16 | from erpnext.regional.india.utils import get_gst_accounts 17 | 18 | class EInvoice(Document): 19 | def validate(self): 20 | self.validate_uom() 21 | self.validate_items() 22 | 23 | def before_submit(self): 24 | if not self.irn: 25 | msg = _("Cannot submit e-invoice without IRN.") + ' ' 26 | msg += _("You must generate IRN for the sales invoice to submit this e-invoice.") 27 | frappe.throw(msg, title=_("Missing IRN")) 28 | 29 | def on_update(self): 30 | self.update_sales_invoice() 31 | 32 | def on_update_after_submit(self): 33 | self.update_sales_invoice() 34 | 35 | def update_sales_invoice(self): 36 | frappe.db.set_value('Sales Invoice', self.invoice, { 37 | 'irn': self.irn, 38 | 'ack_no': self.ack_no, 39 | 'e_invoice': self.name, 40 | 'ack_date': self.ack_date, 41 | 'ewaybill': self.ewaybill, 42 | 'einvoice_status': self.status, 43 | 'qrcode_image': self.qrcode_path, 44 | 'irn_cancel_date': self.irn_cancel_date, 45 | 'eway_bill_validity': self.ewaybill_validity 46 | }, update_modified=False) 47 | 48 | def on_cancel(self): 49 | frappe.db.set_value('Sales Invoice', self.invoice, 'e_invoice', self.name, update_modified=False) 50 | 51 | @frappe.whitelist() 52 | def fetch_invoice_details(self): 53 | self.set_sales_invoice() 54 | self.set_invoice_type() 55 | self.set_supply_type() 56 | self.set_seller_details() 57 | self.set_buyer_details() 58 | self.set_shipping_details() 59 | self.set_dispatch_details() 60 | self.set_item_details() 61 | self.set_value_details() 62 | self.set_payment_details() 63 | self.set_return_doc_reference() 64 | 65 | def set_sales_invoice(self): 66 | self.sales_invoice = frappe.get_doc('Sales Invoice', self.invoice) 67 | 68 | def set_invoice_type(self): 69 | self.invoice_type = 'CRN' if self.sales_invoice.is_return else 'INV' 70 | 71 | def set_supply_type(self): 72 | gst_category = self.sales_invoice.gst_category 73 | 74 | if gst_category == 'Registered Regular': self.supply_type = 'B2B' 75 | elif gst_category == 'SEZ': self.supply_type = 'SEZWOP' 76 | elif gst_category == 'Overseas': self.supply_type = 'EXPWOP' 77 | elif gst_category == 'Deemed Export': self.supply_type = 'DEXP' 78 | 79 | def set_seller_details(self): 80 | company_address = self.sales_invoice.company_address 81 | if not company_address: 82 | frappe.throw(_('Company address must be set to be able to generate e-invoice.')) 83 | 84 | seller_address = frappe.get_all('Address', {'name': company_address}, ['*'])[0] 85 | mandatory_field_label_map = { 86 | 'gstin': 'GSTIN', 87 | 'address_line1': 'Address Lines', 88 | 'city': 'City', 89 | 'pincode': 'Pincode', 90 | 'gst_state_number': 'State Code' 91 | } 92 | for field, field_label in mandatory_field_label_map.items(): 93 | if not seller_address[field]: 94 | frappe.throw(_('Company address {} must have {} set to be able to generate e-invoice.') 95 | .format(company_address, field_label)) 96 | 97 | self.seller_legal_name = self.company 98 | self.seller_gstin = seller_address.gstin 99 | self.seller_location = seller_address.city 100 | self.seller_pincode = seller_address.pincode 101 | self.seller_address_line_1 = seller_address.address_line1 102 | self.seller_address_line_2 = seller_address.address_line2 103 | self.seller_state_code = seller_address.gst_state_number 104 | 105 | def set_buyer_details(self): 106 | customer_address = self.sales_invoice.customer_address 107 | if not customer_address: 108 | frappe.throw(_('Customer address must be set to be able to generate e-invoice.')) 109 | 110 | is_export = self.supply_type == 'EXPWOP' 111 | buyer_address = frappe.get_all('Address', {'name': customer_address}, ['*'])[0] 112 | mandatory_field_label_map = { 113 | 'gstin': 'GSTIN', 114 | 'address_line1': 'Address Lines', 115 | 'city': 'City', 116 | 'pincode': 'Pincode', 117 | 'gst_state_number': 'State Code' 118 | } 119 | for field, field_label in mandatory_field_label_map.items(): 120 | if field == 'gstin': 121 | if not buyer_address.gstin and not is_export: 122 | frappe.throw(_('Customer address {} must have {} set to be able to generate e-invoice.') 123 | .format(customer_address, field_label)) 124 | continue 125 | 126 | if not buyer_address[field]: 127 | frappe.throw(_('Customer address {} must have {} set to be able to generate e-invoice.') 128 | .format(customer_address, field_label)) 129 | 130 | self.buyer_legal_name = self.sales_invoice.customer 131 | self.buyer_gstin = buyer_address.gstin 132 | self.buyer_location = buyer_address.city 133 | self.buyer_pincode = buyer_address.pincode 134 | self.buyer_address_line_1 = buyer_address.address_line1 135 | self.buyer_address_line_2 = buyer_address.address_line2 136 | self.buyer_state_code = buyer_address.gst_state_number 137 | self.buyer_place_of_supply = buyer_address.gst_state_number 138 | 139 | if is_export: 140 | self.buyer_gstin = 'URP' 141 | self.buyer_state_code = 96 142 | self.buyer_pincode = 999999 143 | self.buyer_place_of_supply = 96 144 | 145 | def set_shipping_details(self): 146 | shipping_address_name = self.sales_invoice.shipping_address_name 147 | if shipping_address_name: 148 | is_export = self.supply_type == 'EXPWOP' 149 | shipping_address = frappe.get_all('Address', {'name': shipping_address_name}, ['*'])[0] 150 | 151 | self.shipping_legal_name = shipping_address.address_title 152 | self.shipping_gstin = shipping_address.gstin 153 | self.shipping_location = shipping_address.city 154 | self.shipping_pincode = shipping_address.pincode 155 | self.shipping_address_line_1 = shipping_address.address_line1 156 | self.shipping_address_line_2 = shipping_address.address_line2 157 | self.shipping_state_code = shipping_address.gst_state_number 158 | 159 | if is_export: 160 | self.shipping_gstin = 'URP' 161 | self.shipping_state_code = 96 162 | self.shipping_pincode = 999999 163 | self.shipping_place_of_supply = 96 164 | 165 | def set_dispatch_details(self): 166 | dispatch_address_name = self.sales_invoice.dispatch_address_name 167 | if dispatch_address_name: 168 | dispatch_address = frappe.get_all('Address', {'name': dispatch_address_name}, ['*'])[0] 169 | 170 | self.dispatch_legal_name = dispatch_address.address_title 171 | self.dispatch_location = dispatch_address.city 172 | self.dispatch_pincode = dispatch_address.pincode 173 | self.dispatch_address_line_1 = dispatch_address.address_line1 174 | self.dispatch_address_line_2 = dispatch_address.address_line2 175 | self.dispatch_state_code = dispatch_address.gst_state_number 176 | 177 | def set_item_details(self): 178 | sales_invoice_item_names = [d.name for d in self.sales_invoice.items] 179 | e_invoice_item_names = [d.si_item_ref for d in self.items] 180 | item_added_or_removed = sales_invoice_item_names != e_invoice_item_names 181 | 182 | if self.items and not item_added_or_removed: 183 | self.update_items_from_invoice() 184 | else: 185 | self.fetch_items_from_invoice() 186 | 187 | def fetch_items_from_invoice(self): 188 | for item in self.sales_invoice.items: 189 | if not item.gst_hsn_code: 190 | frappe.throw(_('Row #{}: Item {} must have HSN code set to be able to generate e-invoice.') 191 | .format(item.idx, item.item_code)) 192 | 193 | is_service_item = item.gst_hsn_code[:2] == "99" 194 | 195 | if flt(item.qty) == 0.0: 196 | rate = abs(item.taxable_value) 197 | else: 198 | rate = abs((abs(item.taxable_value)) / item.qty) 199 | 200 | einvoice_item = frappe._dict({ 201 | 'si_item_ref': item.name, 202 | 'item_code': item.item_code, 203 | 'item_name': item.item_name, 204 | 'is_service_item': is_service_item, 205 | 'hsn_code': item.gst_hsn_code, 206 | 'quantity': abs(item.qty), 207 | 'discount': 0, 208 | 'unit': item.uom, 209 | 'rate': rate, 210 | 'amount': abs(item.taxable_value), 211 | 'taxable_value': abs(item.taxable_value) 212 | }) 213 | 214 | self.set_item_tax_details(einvoice_item) 215 | 216 | einvoice_item.total_item_value = abs( 217 | einvoice_item.taxable_value + einvoice_item.igst_amount + 218 | einvoice_item.sgst_amount + einvoice_item.cgst_amount + 219 | einvoice_item.cess_amount + einvoice_item.cess_nadv_amount + 220 | einvoice_item.other_charges 221 | ) 222 | self.append('items', einvoice_item) 223 | 224 | self.set_calculated_item_totals() 225 | 226 | def update_items_from_invoice(self): 227 | for i, einvoice_item in enumerate(self.items): 228 | item = self.sales_invoice.items[i] 229 | 230 | if not item.gst_hsn_code: 231 | frappe.throw(_('Row #{}: Item {} must have HSN code set to be able to generate e-invoice.') 232 | .format(item.idx, item.item_code)) 233 | 234 | is_service_item = item.gst_hsn_code[:2] == "99" 235 | 236 | einvoice_item.update({ 237 | 'item_code': item.item_code, 238 | 'item_name': item.item_name, 239 | 'is_service_item': is_service_item, 240 | 'hsn_code': item.gst_hsn_code, 241 | 'quantity': abs(item.qty), 242 | 'discount': 0, 243 | 'unit': item.uom, 244 | 'rate': abs((abs(item.taxable_value)) / item.qty), 245 | 'amount': abs(item.taxable_value), 246 | 'taxable_value': abs(item.taxable_value) 247 | }) 248 | 249 | self.set_item_tax_details(einvoice_item) 250 | 251 | einvoice_item.total_item_value = abs( 252 | einvoice_item.taxable_value + einvoice_item.igst_amount + 253 | einvoice_item.sgst_amount + einvoice_item.cgst_amount + 254 | einvoice_item.cess_amount + einvoice_item.cess_nadv_amount + 255 | einvoice_item.other_charges 256 | ) 257 | 258 | self.set_calculated_item_totals() 259 | 260 | def set_calculated_item_totals(self): 261 | item_total_fields = ['items_ass_value', 'items_igst', 'items_sgst', 'items_cgst', 262 | 'items_cess', 'items_cess_nadv', 'items_other_charges', 'items_total_value'] 263 | 264 | for field in item_total_fields: 265 | self.set(field, 0) 266 | 267 | for item in self.items: 268 | self.items_ass_value += item.taxable_value 269 | self.items_igst += item.igst_amount 270 | self.items_sgst += item.sgst_amount 271 | self.items_cess += item.cess_amount 272 | self.items_cess_nadv += item.cess_nadv_amount 273 | self.items_other_charges += item.other_charges 274 | self.items_total_value += item.total_item_value 275 | 276 | def set_item_tax_details(self, item): 277 | gst_accounts = get_gst_accounts(self.company) 278 | gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] 279 | 280 | for attr in ['gst_rate', 'cgst_amount', 'sgst_amount', 'igst_amount', 281 | 'cess_rate', 'cess_amount', 'cess_nadv_amount', 'other_charges']: 282 | item.update({ attr: 0 }) 283 | 284 | for t in self.sales_invoice.taxes: 285 | is_applicable = t.tax_amount and t.account_head in gst_accounts_list 286 | if is_applicable: 287 | # this contains item wise tax rate & tax amount (incl. discount) 288 | item_tax_detail = loads(t.item_wise_tax_detail).get(item.item_code or item.item_name) 289 | 290 | item_tax_rate = item_tax_detail[0] 291 | # item tax amount excluding discount amount 292 | item_tax_amount = (item_tax_rate / 100) * item.taxable_value 293 | 294 | if t.account_head in gst_accounts.cess_account: 295 | item_tax_amount_after_discount = item_tax_detail[1] 296 | if t.charge_type == 'On Item Quantity': 297 | item.cess_nadv_amount += abs(item_tax_amount_after_discount) 298 | else: 299 | item.cess_rate += item_tax_rate 300 | item.cess_amount += abs(item_tax_amount_after_discount) 301 | 302 | for tax_type in ['igst', 'cgst', 'sgst']: 303 | if t.account_head in gst_accounts[f'{tax_type}_account']: 304 | item.gst_rate += item_tax_rate 305 | amt_fieldname = f'{tax_type}_amount' 306 | item.update({ 307 | amt_fieldname: item.get(amt_fieldname, 0) + abs(item_tax_amount) 308 | }) 309 | else: 310 | # TODO: other charges per item 311 | pass 312 | 313 | def set_value_details(self): 314 | self.ass_value = abs(sum([i.taxable_value for i in self.get('items')])) 315 | self.invoice_discount = 0 316 | self.round_off_amount = self.sales_invoice.base_rounding_adjustment 317 | self.base_invoice_value = abs(self.sales_invoice.base_rounded_total) or abs(self.sales_invoice.base_grand_total) 318 | self.invoice_value = abs(self.sales_invoice.rounded_total) or abs(self.sales_invoice.grand_total) 319 | 320 | self.set_invoice_tax_details() 321 | 322 | def set_invoice_tax_details(self): 323 | gst_accounts = get_gst_accounts(self.company) 324 | gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] 325 | 326 | self.cgst_value = 0 327 | self.sgst_value = 0 328 | self.igst_value = 0 329 | self.cess_value = 0 330 | self.other_charges = 0 331 | considered_rows = [] 332 | 333 | for t in self.sales_invoice.taxes: 334 | tax_amount = t.base_tax_amount_after_discount_amount 335 | 336 | if t.account_head in gst_accounts_list: 337 | if t.account_head in gst_accounts.cess_account: 338 | # using after discount amt since item also uses after discount amt for cess calc 339 | self.cess_value += abs(t.base_tax_amount_after_discount_amount) 340 | 341 | for tax in ['igst', 'cgst', 'sgst']: 342 | if t.account_head in gst_accounts[f'{tax}_account']: 343 | new_value = self.get(f'{tax}_value') + abs(tax_amount) 344 | self.set(f'{tax}_value', new_value) 345 | 346 | self.update_other_charges(t, gst_accounts_list, considered_rows) 347 | else: 348 | self.other_charges += abs(tax_amount) 349 | 350 | def update_other_charges(self, tax_row, gst_accounts_list, considered_rows): 351 | taxes = self.sales_invoice.get('taxes') 352 | prev_row_id = cint(tax_row.row_id) - 1 353 | 354 | if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows: 355 | if tax_row.charge_type == 'On Previous Row Amount': 356 | amount = taxes[prev_row_id].tax_amount_after_discount_amount 357 | self.other_charges -= abs(amount) 358 | considered_rows.append(prev_row_id) 359 | if tax_row.charge_type == 'On Previous Row Total': 360 | amount = taxes[prev_row_id].base_total - self.sales_invoice.base_net_total 361 | self.other_charges -= abs(amount) 362 | considered_rows.append(prev_row_id) 363 | 364 | def set_payment_details(self): 365 | if self.sales_invoice.is_pos and self.sales_invoice.base_paid_amount: 366 | self.payee_name = self.company 367 | self.mode = ', '.join([d.mode_of_payment for d in self.sales_invoice.payments if d.amount > 0]) 368 | self.paid_amount = self.sales_invoice.base_paid_amount 369 | self.outstanding_amount = self.sales_invoice.outstanding_amount 370 | 371 | def set_return_doc_reference(self): 372 | if self.sales_invoice.is_return: 373 | if not self.sales_invoice.return_against: 374 | frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.') 375 | .format(frappe.bold('Return Against')), title=_('Missing Field')) 376 | 377 | self.previous_document_no = self.sales_invoice.return_against 378 | original_invoice_date = frappe.db.get_value('Sales Invoice', self.sales_invoice.return_against, 'posting_date') 379 | self.previous_document_date = format_date(original_invoice_date, 'dd/mm/yyyy') 380 | 381 | def get_einvoice_json(self): 382 | einvoice_json = { 383 | "Version": str(self.version), 384 | "TranDtls": { 385 | "TaxSch": self.tax_scheme, 386 | "SupTyp": self.supply_type, 387 | "RegRev": "Y" if self.reverse_charge else "N", 388 | "EcmGstin": self.ecommerce_gstin, 389 | "IgstOnIntra": "Y" if self.igst_on_intra else "N" 390 | }, 391 | "DocDtls": { 392 | "Typ": self.invoice_type, 393 | "No": self.invoice, 394 | "Dt": format_date(self.invoice_date, 'dd/mm/yyyy') 395 | } 396 | } 397 | 398 | einvoice_json.update(self.get_address_json()) 399 | einvoice_json.update(self.get_item_list_json()) 400 | einvoice_json.update(self.get_invoice_value_json()) 401 | einvoice_json.update(self.get_payment_details_json()) 402 | einvoice_json.update(self.get_return_details_json()) 403 | einvoice_json.update(self.get_export_details_json()) 404 | einvoice_json.update(self.get_ewaybill_details_json()) 405 | 406 | return einvoice_json 407 | 408 | def get_address_json(self): 409 | addresses = {} 410 | seller_address = { 411 | "Gstin": self.seller_gstin, 412 | "LglNm": self.seller_legal_name, 413 | "TrdNm": self.seller_trade_name, 414 | "Addr1": self.seller_address_line_1, 415 | "Loc": self.seller_location, 416 | "Pin": cint(self.seller_pincode), 417 | "Stcd": self.seller_state_code, 418 | "Ph": self.seller_phone, 419 | "Em": self.seller_email 420 | } 421 | if self.seller_address_line_2: 422 | seller_address.update({"Addr2": self.seller_address_line_2}) 423 | addresses.update({ "SellerDtls": seller_address }) 424 | 425 | buyer_address = { 426 | "Gstin": self.buyer_gstin, 427 | "LglNm": self.buyer_legal_name, 428 | "TrdNm": self.buyer_trade_name, 429 | "Pos": self.buyer_place_of_supply, 430 | "Addr1": self.buyer_address_line_1, 431 | "Loc": self.buyer_location, 432 | "Pin": cint(self.buyer_pincode), 433 | "Stcd": self.buyer_state_code, 434 | "Ph": self.buyer_phone, 435 | "Em": self.buyer_email 436 | } 437 | if self.buyer_address_line_2: 438 | buyer_address.update({"Addr2": self.buyer_address_line_2}) 439 | addresses.update({ "BuyerDtls": buyer_address }) 440 | 441 | if self.dispatch_legal_name: 442 | dispatch_address = { 443 | "Nm": self.dispatch_legal_name, 444 | "Addr1": self.dispatch_address_line_1, 445 | "Loc": self.dispatch_location, 446 | "Pin": cint(self.dispatch_pincode), 447 | "Stcd": self.dispatch_state_code 448 | } 449 | if self.dispatch_address_line_2: 450 | dispatch_address.update({"Addr2": self.dispatch_address_line_2}) 451 | addresses.update({ "DispDtls": dispatch_address }) 452 | 453 | if self.shipping_legal_name: 454 | shipping_address = { 455 | "Gstin": self.shipping_gstin, 456 | "LglNm": self.shipping_legal_name, 457 | "TrdNm": self.shipping_trade_name, 458 | "Pos": self.shipping_place_of_supply, 459 | "Addr1": self.shipping_address_line_1, 460 | "Loc": self.shipping_location, 461 | "Pin": cint(self.shipping_pincode), 462 | "Stcd": self.shipping_state_code 463 | } 464 | if self.shipping_address_line_2: 465 | shipping_address.update({"Addr2": self.shipping_address_line_2}) 466 | addresses.update({ "ShipDtls": shipping_address }) 467 | 468 | return addresses 469 | 470 | def get_item_list_json(self): 471 | item_list = [] 472 | for row in self.items: 473 | item = { 474 | "SlNo": str(row.idx), 475 | "PrdDesc": row.item_name, 476 | "IsServc": "Y" if row.is_service_item else "N", 477 | "HsnCd": row.hsn_code, 478 | "Qty": row.quantity, 479 | "Unit": row.unit, 480 | "UnitPrice": row.rate, 481 | "TotAmt": row.amount, 482 | "Discount": row.discount, 483 | "AssAmt": row.taxable_value, 484 | "GstRt": row.gst_rate, 485 | "IgstAmt": row.igst_amount, 486 | "CgstAmt": row.cgst_amount, 487 | "SgstAmt": row.sgst_amount, 488 | "CesRt": row.cess_rate, 489 | "CesAmt": row.cess_amount, 490 | "CesNonAdvlAmt": row.cess_nadv_amount, 491 | "OthChrg": row.other_charges, 492 | "TotItemVal": row.total_item_value 493 | } 494 | item_list.append(item) 495 | return { 496 | "ItemList": item_list 497 | } 498 | 499 | def get_invoice_value_json(self): 500 | return { 501 | "ValDtls": { 502 | "AssVal": self.ass_value, 503 | "CgstVal": self.cgst_value, 504 | "SgstVal": self.sgst_value, 505 | "IgstVal": self.igst_value, 506 | "CesVal": self.cess_value, 507 | "StCesVal": self.state_cess_value, 508 | "Discount": self.invoice_discount, 509 | "OthChrg": self.other_charges, 510 | "RndOffAmt": self.round_off_amount, 511 | "TotInvVal": self.base_invoice_value, 512 | "TotInvValFc": self.invoice_value 513 | } 514 | } 515 | 516 | def get_payment_details_json(self): 517 | if not self.payee_name: 518 | return {} 519 | 520 | return { 521 | "PayDtls": { 522 | "Nm": self.payee_name, 523 | "AccDet": self.account_detail, 524 | "Mode": self.mode, 525 | "FinInsBr": self.branch_or_ifsc, 526 | "PayTerm": self.payment_term, 527 | "CrDay": self.credit_days, 528 | "PaidAmt": self.paid_amount, 529 | "PaymtDue": self.payment_due 530 | }, 531 | } 532 | 533 | def get_return_details_json(self): 534 | if not self.previous_document_no: 535 | return {} 536 | 537 | return { 538 | "RefDtls": { 539 | "PrecDocDtls": [ 540 | { 541 | "InvNo": self.previous_document_no, 542 | "InvDt": format_date(self.previous_document_date, 'dd/mm/yyyy') 543 | } 544 | ] 545 | } 546 | } 547 | 548 | def get_export_details_json(self): 549 | if not self.export_bill_no: 550 | return {} 551 | 552 | return { 553 | "ExpDtls": { 554 | "ShipBNo": self.export_bill_no, 555 | "ShipBDt": format_date(self.export_bill_date, 'dd/mm/yyyy'), 556 | "Port": self.port_code, 557 | "RefClm": "Y" if self.claiming_refund else "N", 558 | "ForCur": self.currency_code, 559 | "CntCode": self.country_code 560 | } 561 | } 562 | 563 | def get_ewaybill_details_json(self): 564 | if not self.sales_invoice.transporter: 565 | return {} 566 | 567 | mode_of_transport = {'': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4'} 568 | vehicle_type = {'': None, 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O'} 569 | 570 | mode_of_transport = mode_of_transport[self.mode_of_transport] 571 | vehicle_type = vehicle_type[self.vehicle_type] 572 | 573 | return { 574 | "EwbDtls": { 575 | "TransId": self.transporter_gstin, 576 | "TransName": self.transporter_name, 577 | "Distance": cint(self.distance) or 0, 578 | "TransDocNo": self.transport_document_no, 579 | "TransDocDt": format_date(self.transport_document_date, 'dd/mm/yyyy'), 580 | "VehNo": self.vehicle_no, 581 | "VehType": vehicle_type, 582 | "TransMode": mode_of_transport 583 | } 584 | } 585 | 586 | def sync_with_sales_invoice(self): 587 | # to fetch details from 'fetch_from' fields 588 | self._action = 'save' 589 | self._validate_links() 590 | self.fetch_invoice_details() 591 | 592 | def validate_items(self): 593 | error_list = [] 594 | for item in self.items: 595 | if (item.cgst_amount or item.sgst_amount) and item.igst_amount: 596 | error_list.append(_('Row #{}: Invalid value of Tax Amount, provide either IGST or both SGST and CGST.') 597 | .format(item.idx)) 598 | 599 | if item.gst_rate not in [0.000, 0.100, 0.250, 0.500, 1.000, 1.500, 3.000, 5.000, 7.500, 12.000, 18.000, 28.000]: 600 | error_list.append(_('Row #{}: Invalid GST Tax rate. Please correct the Tax Rate Values and try again.') 601 | .format(item.idx)) 602 | 603 | total_gst_amount = item.cgst_amount + item.sgst_amount + item.igst_amount 604 | if abs((item.taxable_value * item.gst_rate / 100) - total_gst_amount) > 1: 605 | error_list.append(_('Row #{}: Invalid GST Tax rate. Please correct the Tax Rate Values and try again.') 606 | .format(item.idx)) 607 | 608 | if not item.hsn_code: 609 | error_list.append(_('Row #{}: HSN Code is mandatory for e-invoice generation.') 610 | .format(item.idx)) 611 | 612 | if abs(self.base_invoice_value - (self.items_total_value - self.invoice_discount + self.other_charges + self.round_off_amount)) > 1: 613 | msg = _('Invalid Total Invoice Value.') + ' ' 614 | msg += _('The Total Invoice Value should be equal to the Sum of Total Value of All Items - Invoice Discount + Invoice Other charges + Round-off amount.') + ' ' 615 | msg += _('Please correct the Invoice Value and try again.') 616 | error_list.append(msg) 617 | 618 | if error_list: 619 | frappe.throw(error_list, title=_('E Invoice Validation Failed'), as_list=1) 620 | 621 | def validate_uom(self): 622 | valid_uoms = ['BAG', 'BAL', 'BDL', 'BKL', 'BOU', 'BOX', 'BTL', 'BUN', 'CAN', 'CCM', 'CMS', 'CBM', 'CTN', 'DOZ', 'DRM', 'GGK', 'GMS', 'GRS', 'GYD', 'KGS', 'KLR', 'KME', 'LTR', 'MLS', 'MLT', 'MTR', 'MTS', 'NOS', 'OTH', 'PAC', 'PCS', 'PRS', 'QTL', 'ROL', 'SET', 'SQF', 'SQM', 'SQY', 'TBS', 'TGM', 'THD', 'TON', 'TUB', 'UGS', 'UNT', 'YD'] 623 | for item in self.items: 624 | if item.unit and item.unit.upper() not in valid_uoms: 625 | msg = _('Row #{}: {} has invalid UOM set.').format(item.idx, item.item_name) + ' ' 626 | msg += _('Please set proper UOM as defined by e-invoice portal.') 627 | msg += '

' 628 | uom_list_link = 'this' 629 | msg += _('You can refer {} link to check valid UOMs defined by e-invoice portal.').format(uom_list_link) 630 | frappe.throw(msg, title=_('Invalid Item UOM')) 631 | 632 | def set_eway_bill_details(self, details): 633 | self.sales_invoice = frappe._dict() 634 | self.sales_invoice.transporter = details.transporter 635 | self.transporter_gstin = details.transporter_gstin 636 | self.transporter_name = details.transporter_name 637 | self.distance = details.distance 638 | self.transport_document_no = details.transport_document_no 639 | self.transport_document_date = details.transport_document_date 640 | self.vehicle_no = details.vehicle_no 641 | self.vehicle_type = details.vehicle_type or '' 642 | self.mode_of_transport = details.mode_of_transport 643 | 644 | def get_eway_bill_json(self): 645 | eway_bill_details = self.get_ewaybill_details_json().get('EwbDtls') 646 | eway_bill_details.update({ 'Irn': self.irn }) 647 | 648 | return eway_bill_details 649 | 650 | def create_einvoice(sales_invoice): 651 | if frappe.db.exists('E Invoice', sales_invoice): 652 | einvoice = frappe.get_doc('E Invoice', sales_invoice) 653 | else: 654 | einvoice = frappe.new_doc('E Invoice') 655 | einvoice.invoice = sales_invoice 656 | 657 | einvoice.sync_with_sales_invoice() 658 | einvoice.flags.ignore_permissions = 1 659 | einvoice.save() 660 | frappe.db.commit() 661 | 662 | return einvoice 663 | 664 | def get_einvoice(sales_invoice): 665 | return frappe.get_doc('E Invoice', sales_invoice) 666 | 667 | def validate_sales_invoice_change(doc, method=""): 668 | invoice_eligible = validate_einvoice_eligibility(doc) 669 | 670 | if not invoice_eligible: 671 | return 672 | 673 | if doc.einvoice_status in ['IRN Cancelled', 'IRN Pending']: 674 | return 675 | 676 | if doc.docstatus == 0 and doc._action == 'save': 677 | einvoice = get_einvoice(doc.e_invoice) 678 | einvoice_copy = get_einvoice(doc.e_invoice) 679 | einvoice_copy.sync_with_sales_invoice() 680 | 681 | # to ignore changes in default fields 682 | einvoice = remove_default_fields(einvoice) 683 | einvoice_copy = remove_default_fields(einvoice_copy) 684 | diff = get_diff(einvoice, einvoice_copy) 685 | 686 | if diff: 687 | frappe.log_error( 688 | message=dumps(diff, indent=2), 689 | title=_('E-Invoice: Edit Not Allowed') 690 | ) 691 | frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) 692 | 693 | def remove_default_fields(doc): 694 | clone = frappe.copy_doc(doc) 695 | for fieldname in clone.as_dict(): 696 | value = doc.get(fieldname) 697 | if isinstance(value, list): 698 | trimmed_child_docs = [] 699 | for d in value: 700 | trimmed_child_docs.append(remove_default_fields(d)) 701 | doc.set(fieldname, trimmed_child_docs) 702 | 703 | if fieldname == 'name': 704 | # do not reset name, since it is used to check child table row changes 705 | continue 706 | 707 | if fieldname in default_fields or fieldname == '__islocal': 708 | doc.set(fieldname, None) 709 | 710 | return doc 711 | 712 | @frappe.whitelist() 713 | def validate_einvoice_eligibility(doc): 714 | if isinstance(doc, six.string_types): 715 | doc = loads(doc) 716 | 717 | service_provider = frappe.db.get_single_value('E Invoicing Settings', 'service_provider') 718 | if not service_provider: 719 | return False 720 | 721 | einvoicing_enabled = cint(frappe.db.get_single_value(service_provider, 'enabled')) 722 | if not einvoicing_enabled: 723 | return False 724 | 725 | einvoicing_eligible_from = '2021-04-01' 726 | if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from): 727 | return False 728 | 729 | eligible_companies = frappe.db.get_single_value('E Invoicing Settings', 'companies') 730 | invalid_company = doc.get('company') not in eligible_companies 731 | invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] 732 | inter_company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') 733 | has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst')) 734 | 735 | # if export invoice, then taxes can be empty 736 | # invoice can only be ineligible if no taxes applied and is not an export invoice 737 | no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas' 738 | 739 | if invalid_company or invalid_supply_type or inter_company_transaction or no_taxes_applied or has_non_gst_item: 740 | return False 741 | 742 | return True 743 | 744 | def validate_sales_invoice_submission(doc, method=""): 745 | invoice_eligible = validate_einvoice_eligibility(doc) 746 | 747 | if not invoice_eligible: 748 | return 749 | 750 | if not doc.get('einvoice_status') or doc.get('einvoice_status') == 'IRN Pending': 751 | frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) 752 | 753 | def validate_sales_invoice_cancellation(doc, method=""): 754 | invoice_eligible = validate_einvoice_eligibility(doc) 755 | 756 | if not invoice_eligible: 757 | return 758 | 759 | if doc.get('einvoice_status') != 'IRN Cancelled': 760 | frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancellation Not Allowed')) 761 | 762 | def validate_sales_invoice_deletion(doc, method=""): 763 | invoice_eligible = validate_einvoice_eligibility(doc) 764 | 765 | if not invoice_eligible: 766 | return 767 | 768 | if doc.get('einvoice_status') != 'IRN Cancelled': 769 | frappe.throw(_('You must cancel IRN before deleting the document.'), title=_('Deletion Not Allowed')) 770 | 771 | def cancel_e_invoice(doc, method=""): 772 | if doc.get('e_invoice'): 773 | e_invoice = frappe.get_doc('E Invoice', doc.get('e_invoice')) 774 | e_invoice.flags.ignore_permissions = True 775 | e_invoice.cancel() 776 | 777 | def delete_e_invoice(doc, method=""): 778 | if doc.get('e_invoice'): 779 | frappe.db.set_value('Sales Invoice', doc.get('name'), 'e_invoice', '') 780 | frappe.delete_doc( 781 | doctype='E Invoice', 782 | name=doc.get('e_invoice'), 783 | ignore_missing=True 784 | ) --------------------------------------------------------------------------------