├── 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 |
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 | | Sr. No. |
97 | Item |
98 | HSN Code |
99 | Qty |
100 | UOM |
101 | Rate |
102 | Discount |
103 | Taxable Amount |
104 | Tax Rate |
105 | Other Charges |
106 | Total |
107 |
108 |
109 |
110 | {% for item in einvoice.ItemList %}
111 |
112 | | {{ item.SlNo }} |
113 | {{ item.PrdDesc }} |
114 | {{ item.HsnCd }} |
115 | {{ item.Qty }} |
116 | {{ item.Unit }} |
117 | {{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }} |
118 | {{ frappe.utils.fmt_money(item.Discount, None, "INR") }} |
119 | {{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }} |
120 | {{ item.GstRt + item.CesRt }} % |
121 | {{ frappe.utils.fmt_money(0, None, "INR") }} |
122 | {{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }} |
123 |
124 | {% endfor %}
125 |
126 |
127 |
128 |
129 |
4. Value Details
130 |
131 |
132 |
133 | | Taxable Amount |
134 | CGST |
135 | SGST |
136 | IGST |
137 | CESS |
138 | State CESS |
139 | Discount |
140 | Other Charges |
141 | Round Off |
142 | Total Value |
143 |
144 |
145 |
146 | {%- set value_details = einvoice.ValDtls -%}
147 |
148 | | {{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }} |
149 | {{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }} |
150 | {{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }} |
151 | {{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }} |
152 | {{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }} |
153 | {{ frappe.utils.fmt_money(0, None, "INR") }} |
154 | {{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }} |
155 | {{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }} |
156 | {{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }} |
157 | {{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }} |
158 |
159 |
160 |
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{% if print_settings.repeat_header_footer %}\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| Sr. No. | \n\t\t\t\t\tItem | \n\t\t\t\t\tHSN Code | \n\t\t\t\t\tQty | \n\t\t\t\t\tUOM | \n\t\t\t\t\tRate | \n\t\t\t\t\tDiscount | \n\t\t\t\t\tTaxable Amount | \n\t\t\t\t\tTax Rate | \n\t\t\t\t\tOther Charges | \n\t\t\t\t\tTotal | \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| {{ item.idx }} | \n\t\t\t\t\t\t{{ item.item_name }} | \n\t\t\t\t\t\t{{ item.hsn_code }} | \n\t\t\t\t\t\t{{ item.quantity }} | \n\t\t\t\t\t\t{{ item.unit }} | \n\t\t\t\t\t\t{{ frappe.utils.fmt_money(item.rate, None, \"INR\") }} | \n\t\t\t\t\t\t{{ frappe.utils.fmt_money(item.discount, None, \"INR\") }} | \n\t\t\t\t\t\t{{ frappe.utils.fmt_money(item.taxable_value, None, \"INR\") }} | \n\t\t\t\t\t\t{{ item.gst_rate + item.cess_rate }} % | \n\t\t\t\t\t\t{{ frappe.utils.fmt_money(0, None, \"INR\") }} | \n\t\t\t\t\t\t{{ frappe.utils.fmt_money(item.total_item_value, None, \"INR\") }} | \n\t\t\t\t\t
\n\t\t\t\t{% endfor %}\n\t\t\t\n\t\t
\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| Taxable Amount | \n\t\t\t\t\tCGST | \n\t\t\t\t\tSGST | \n\t\t\t\t\tIGST | \n\t\t\t\t\tCESS | \n\t\t\t\t\tState CESS | \n\t\t\t\t\tDiscount | \n\t\t\t\t\tOther Charges | \n\t\t\t\t\tRound Off | \n\t\t\t\t\tTotal Value | \n\t\t\t\t
\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t| {{ frappe.utils.fmt_money(e_invoice.ass_value, None, \"INR\") }} | \n\t\t\t\t\t{{ frappe.utils.fmt_money(e_invoice.cgst_value, None, \"INR\") }} | \n\t\t\t\t\t{{ frappe.utils.fmt_money(e_invoice.sgst_value, None, \"INR\") }} | \n\t\t\t\t\t{{ frappe.utils.fmt_money(e_invoice.igst_value, None, \"INR\") }} | \n\t\t\t\t\t{{ frappe.utils.fmt_money(e_invoice.cess_value, None, \"INR\") }} | \n\t\t\t\t\t{{ frappe.utils.fmt_money(0, None, \"INR\") }} | \n\t\t\t\t\t{{ frappe.utils.fmt_money(e_invoice.invoice_discount, None, \"INR\") }} | \n\t\t\t\t\t{{ frappe.utils.fmt_money(e_invoice.other_charges, None, \"INR\") }} | \n\t\t\t\t\t{{ frappe.utils.fmt_money(e_invoice.round_off_amount, None, \"INR\") }} | \n\t\t\t\t\t{{ frappe.utils.fmt_money(e_invoice.base_invoice_value, None, \"INR\") }} | \n\t\t\t\t
\n\t\t\t\n\t\t
\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 | )
--------------------------------------------------------------------------------