From f396b35622b4bbcc8c979c839a1a364ba979f95e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Sep 2020 21:19:59 +0530 Subject: [PATCH 001/115] feat: init e-invoice settings --- .../doctype/e_invoice_settings/__init__.py | 0 .../e_invoice_settings/e_invoice_settings.js | 8 ++ .../e_invoice_settings.json | 82 +++++++++++++++++++ .../e_invoice_settings/e_invoice_settings.py | 10 +++ .../test_e_invoice_settings.py | 10 +++ 5 files changed, 110 insertions(+) create mode 100644 erpnext/erpnext_integrations/doctype/e_invoice_settings/__init__.py create mode 100644 erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js create mode 100644 erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json create mode 100644 erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py create mode 100644 erpnext/erpnext_integrations/doctype/e_invoice_settings/test_e_invoice_settings.py diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/__init__.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js new file mode 100644 index 00000000000..9c67e619504 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('E Invoice Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json new file mode 100644 index 00000000000..44d1bd63b55 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json @@ -0,0 +1,82 @@ +{ + "actions": [], + "creation": "2020-09-24 16:23:16.235722", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "client_id", + "client_secret", + "public_key_file", + "column_break_3", + "gstin", + "username", + "password" + ], + "fields": [ + { + "fieldname": "client_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Client ID", + "reqd": 1 + }, + { + "fieldname": "client_secret", + "fieldtype": "Data", + "label": "Client Secret", + "reqd": 1 + }, + { + "fieldname": "public_key_file", + "fieldtype": "Attach", + "label": "Public Key", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "gstin", + "fieldtype": "Data", + "label": "GSTIN", + "reqd": 1 + }, + { + "fieldname": "username", + "fieldtype": "Data", + "label": "Username", + "reqd": 1 + }, + { + "fieldname": "password", + "fieldtype": "Password", + "label": "Password", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2020-09-25 21:19:17.159664", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "E Invoice Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py new file mode 100644 index 00000000000..24ab76a5d84 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class EInvoiceSettings(Document): + pass diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/test_e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/test_e_invoice_settings.py new file mode 100644 index 00000000000..a11ce63ee6c --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/test_e_invoice_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestEInvoiceSettings(unittest.TestCase): + pass From 18afbc0617c4683cefd2048bdbea2932998f5de0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Sep 2020 21:27:50 +0530 Subject: [PATCH 002/115] feat: read public key file --- .../e_invoice_settings/e_invoice_settings.json | 9 ++++++++- .../e_invoice_settings/e_invoice_settings.py | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json index 44d1bd63b55..b91ef817520 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json @@ -8,6 +8,7 @@ "client_id", "client_secret", "public_key_file", + "public_key", "column_break_3", "gstin", "username", @@ -54,12 +55,18 @@ "fieldtype": "Password", "label": "Password", "reqd": 1 + }, + { + "fieldname": "public_key", + "fieldtype": "Long Text", + "hidden": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-09-25 21:19:17.159664", + "modified": "2020-09-25 21:24:00.404460", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "E Invoice Settings", diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py index 24ab76a5d84..62e945af87a 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py @@ -3,8 +3,19 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe +from frappe.utils import cstr from frappe.model.document import Document class EInvoiceSettings(Document): - pass + def validate(self): + pass + + def before_save(self): + if not self.public_key or self.has_value_changed('public_key_file'): + self.public_key = self.read_key_file() + + def read_key_file(self): + key_file = frappe.get_doc('File', dict(attached_to_name=self.doctype, attached_to_field='public_key_file')) + with open(key_file.get_full_path(), 'rb') as f: + return cstr(f.read()) From 5669a5f6188b5d30f41a6384ef67a1fed570dcde Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Sep 2020 21:52:00 +0530 Subject: [PATCH 003/115] feat: rsa encryption with public key --- .../e_invoice_settings/e_invoice_settings.js | 16 +++++++-- .../e_invoice_settings.json | 12 +++++-- .../e_invoice_settings/e_invoice_settings.py | 35 +++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js index 9c67e619504..1f9054461f4 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js @@ -2,7 +2,19 @@ // For license information, please see license.txt frappe.ui.form.on('E Invoice Settings', { - // refresh: function(frm) { + refresh: function(frm) { + frm.trigger("show_fetch_token_btn"); + }, - // } + show_fetch_token_btn(frm) { + frm.add_custom_button(__("Fetch Token"), + () => { + frm.call({ + doc: frm.doc, + method: 'make_authentication_request', + freeze: true + }); + } + ); + } }); diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json index b91ef817520..5278c6eebd2 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json @@ -12,7 +12,8 @@ "column_break_3", "gstin", "username", - "password" + "password", + "auto_refresh_token" ], "fields": [ { @@ -61,12 +62,19 @@ "fieldtype": "Long Text", "hidden": 1, "read_only": 1 + }, + { + "default": "0", + "description": "Token will be automatically refreshed 10 mins before expiry", + "fieldname": "auto_refresh_token", + "fieldtype": "Check", + "label": "Auto Refresh Token" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-09-25 21:24:00.404460", + "modified": "2020-09-25 21:32:36.553678", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "E Invoice Settings", diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py index 62e945af87a..129dcd35b6e 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py @@ -3,9 +3,15 @@ # For license information, please see license.txt from __future__ import unicode_literals +import os +import json +import base64 import frappe from frappe.utils import cstr +from Crypto.PublicKey import RSA +from Crypto.Cipher import PKCS1_v1_5 from frappe.model.document import Document +from frappe.integrations.utils import make_post_request class EInvoiceSettings(Document): def validate(self): @@ -19,3 +25,32 @@ class EInvoiceSettings(Document): key_file = frappe.get_doc('File', dict(attached_to_name=self.doctype, attached_to_field='public_key_file')) with open(key_file.get_full_path(), 'rb') as f: return cstr(f.read()) + + def rsa_encrypt(self, msg, key): + if not (isinstance(msg, bytes) or isinstance(msg, bytearray)): + msg = str.encode(msg) + + rsa_pub_key = RSA.import_key(key) + cipher = PKCS1_v1_5.new(rsa_pub_key) + enc_msg = cipher.encrypt(msg) + b64_enc_msg = base64.b64encode(enc_msg) + return b64_enc_msg.decode() + + def make_authentication_request(self): + endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' + headers = { 'content-type': 'application/json' } + headers.update(dict(client_id=self.client_id, client_secret=self.client_secret)) + payload = dict(UserName=self.username, ForceRefreshAccessToken=bool(self.auto_refresh_token)) + + appkey = bytearray(os.urandom(32)) + enc_appkey = self.rsa_encrypt(appkey, self.public_key) + + password = self.get_password(fieldname='password') + enc_password = self.rsa_encrypt(password, self.public_key) + + payload.update(dict(Password=enc_password, AppKey=enc_appkey)) + + res = make_post_request(endpoint, headers=headers, data=json.dumps({ 'data': payload })) + + print(res) + From e73a9deb42bf40af95367093f58a477c74edbd6d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Sep 2020 21:59:38 +0530 Subject: [PATCH 004/115] feat: save token and sek from auth request --- .../e_invoice_settings/e_invoice_settings.js | 23 ++++++++++------- .../e_invoice_settings.json | 25 +++++++++++++++++-- .../e_invoice_settings/e_invoice_settings.py | 18 ++++++++++++- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js index 1f9054461f4..9fc67ce1b98 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js @@ -7,14 +7,19 @@ frappe.ui.form.on('E Invoice Settings', { }, show_fetch_token_btn(frm) { - frm.add_custom_button(__("Fetch Token"), - () => { - frm.call({ - doc: frm.doc, - method: 'make_authentication_request', - freeze: true - }); - } - ); + const { token_expiry } = frm.doc; + const now = frappe.datetime.now_datetime(); + const expiry_in_mins = moment(token_expiry).diff(now, "minute"); + if (expiry_in_mins <= 1) { + frm.add_custom_button(__("Fetch Token"), + () => { + frm.call({ + doc: frm.doc, + method: 'make_authentication_request', + freeze: true + }); + } + ); + } } }); diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json index 5278c6eebd2..888406b534c 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json @@ -13,7 +13,10 @@ "gstin", "username", "password", - "auto_refresh_token" + "auto_refresh_token", + "auth_token", + "token_expiry", + "sek" ], "fields": [ { @@ -69,12 +72,30 @@ "fieldname": "auto_refresh_token", "fieldtype": "Check", "label": "Auto Refresh Token" + }, + { + "fieldname": "auth_token", + "fieldtype": "Data", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "token_expiry", + "fieldtype": "Datetime", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "sek", + "fieldtype": "Data", + "hidden": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-09-25 21:32:36.553678", + "modified": "2020-09-25 21:57:58.187647", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "E Invoice Settings", diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py index 129dcd35b6e..f157e4ca0fa 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py @@ -10,6 +10,7 @@ import frappe from frappe.utils import cstr from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5 +from frappe.utils.data import get_datetime from frappe.model.document import Document from frappe.integrations.utils import make_post_request @@ -35,6 +36,9 @@ class EInvoiceSettings(Document): enc_msg = cipher.encrypt(msg) b64_enc_msg = base64.b64encode(enc_msg) return b64_enc_msg.decode() + + def decrypt_sek(self, enc_sek, key): + return enc_sek def make_authentication_request(self): endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' @@ -52,5 +56,17 @@ class EInvoiceSettings(Document): res = make_post_request(endpoint, headers=headers, data=json.dumps({ 'data': payload })) - print(res) + self.extract_token_and_sek(res, appkey) + + def extract_token_and_sek(self, response, appkey): + data = response.get('Data') + auth_token = data.get('AuthToken') + token_expiry = data.get('TokenExpiry') + enc_sek = data.get('Sek') + sek = self.decrypt_sek(enc_sek, appkey) + + self.auth_token = auth_token + self.token_expiry = get_datetime(token_expiry) + self.sek = sek + self.save() From f8346ce62f9bc5fc21231bcad8c807b82d141592 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Sep 2020 22:02:45 +0530 Subject: [PATCH 005/115] chore: handle error response --- .../doctype/e_invoice_settings/e_invoice_settings.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py index f157e4ca0fa..92ec80ef246 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py @@ -37,8 +37,8 @@ class EInvoiceSettings(Document): b64_enc_msg = base64.b64encode(enc_msg) return b64_enc_msg.decode() - def decrypt_sek(self, enc_sek, key): - return enc_sek + def aes_decrypt(self, msg, key): + return msg def make_authentication_request(self): endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' @@ -55,6 +55,7 @@ class EInvoiceSettings(Document): payload.update(dict(Password=enc_password, AppKey=enc_appkey)) res = make_post_request(endpoint, headers=headers, data=json.dumps({ 'data': payload })) + self.handle_err_response(res) self.extract_token_and_sek(res, appkey) @@ -63,10 +64,14 @@ class EInvoiceSettings(Document): auth_token = data.get('AuthToken') token_expiry = data.get('TokenExpiry') enc_sek = data.get('Sek') - sek = self.decrypt_sek(enc_sek, appkey) + sek = self.aes_decrypt(enc_sek, appkey) self.auth_token = auth_token self.token_expiry = get_datetime(token_expiry) self.sek = sek self.save() + def handle_err_response(self, response): + if response.get('Status') == 0: + err_msg = response.get('ErrorDetails')[0].get('ErrorMessage') + frappe.throw(_(err_msg), title=_("API Request Failed")) \ No newline at end of file From b6509ead64c5b70ce59be8e20d54c271f5c46b2c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Sep 2020 22:17:47 +0530 Subject: [PATCH 006/115] feat: AES decryption of SEK with appkey --- .../e_invoice_settings/e_invoice_settings.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py index 92ec80ef246..866d75ac8c3 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py @@ -7,9 +7,11 @@ import os import json import base64 import frappe +from frappe import _ from frappe.utils import cstr from Crypto.PublicKey import RSA -from Crypto.Cipher import PKCS1_v1_5 +from Crypto.Cipher import PKCS1_v1_5, AES +from Crypto.Util.Padding import pad, unpad from frappe.utils.data import get_datetime from frappe.model.document import Document from frappe.integrations.utils import make_post_request @@ -37,8 +39,16 @@ class EInvoiceSettings(Document): b64_enc_msg = base64.b64encode(enc_msg) return b64_enc_msg.decode() - def aes_decrypt(self, msg, key): - return msg + def aes_decrypt(self, enc_msg, key): + if not (isinstance(key, bytes) or isinstance(key, bytearray)): + key = base64.b64decode(key) + + cipher = AES.new(key, AES.MODE_ECB) + b64_enc_msg = base64.b64decode(enc_msg) + msg_bytes = cipher.decrypt(b64_enc_msg) + msg_bytes = unpad(msg_bytes, AES.block_size) # due to ECB/PKCS5Padding + b64_msg_bytes = base64.b64encode(msg_bytes) + return b64_msg_bytes.decode() def make_authentication_request(self): endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' From 7c427aa890d97090be63639f5a2b9dd0690ccee3 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 25 Sep 2020 23:29:45 +0530 Subject: [PATCH 007/115] feat: decrypt json data with SEK --- .../e_invoice_settings/e_invoice_settings.js | 15 +++++++++++- .../e_invoice_settings/e_invoice_settings.py | 24 ++++++++++++++++--- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js index 9fc67ce1b98..9c3d0807355 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js @@ -4,6 +4,18 @@ frappe.ui.form.on('E Invoice Settings', { refresh: function(frm) { frm.trigger("show_fetch_token_btn"); + frm.add_custom_button(__("Get GSTIN Details"), + () => { + frm.call({ + doc: frm.doc, + method: 'get_gstin_details', + args: { + 'gstin': '27AAACW8099E1ZX' + }, + freeze: true, + callback: (res) => console.log(res) + }); + }); }, show_fetch_token_btn(frm) { @@ -16,7 +28,8 @@ frappe.ui.form.on('E Invoice Settings', { frm.call({ doc: frm.doc, method: 'make_authentication_request', - freeze: true + freeze: true, + callback: () => frm.refresh() }); } ); diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py index 866d75ac8c3..9a9a1b22ff6 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py @@ -14,7 +14,7 @@ from Crypto.Cipher import PKCS1_v1_5, AES from Crypto.Util.Padding import pad, unpad from frappe.utils.data import get_datetime from frappe.model.document import Document -from frappe.integrations.utils import make_post_request +from frappe.integrations.utils import make_post_request, make_get_request class EInvoiceSettings(Document): def validate(self): @@ -40,15 +40,18 @@ class EInvoiceSettings(Document): return b64_enc_msg.decode() def aes_decrypt(self, enc_msg, key): + encode_as_b64 = True if not (isinstance(key, bytes) or isinstance(key, bytearray)): key = base64.b64decode(key) + encode_as_b64 = False cipher = AES.new(key, AES.MODE_ECB) b64_enc_msg = base64.b64decode(enc_msg) msg_bytes = cipher.decrypt(b64_enc_msg) msg_bytes = unpad(msg_bytes, AES.block_size) # due to ECB/PKCS5Padding - b64_msg_bytes = base64.b64encode(msg_bytes) - return b64_msg_bytes.decode() + if encode_as_b64: + msg_bytes = base64.b64encode(msg_bytes) + return msg_bytes.decode() def make_authentication_request(self): endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' @@ -80,6 +83,21 @@ class EInvoiceSettings(Document): self.token_expiry = get_datetime(token_expiry) self.sek = sek self.save() + + def get_gstin_details(self, gstin): + endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/Master/gstin/{gstin}'.format(gstin=gstin) + headers = { 'content-type': 'application/json' } + headers.update(dict(client_id=self.client_id, client_secret=self.client_secret, user_name=self.username)) + headers.update(dict(Gstin=self.gstin, AuthToken=self.auth_token)) + + res = make_get_request(endpoint, headers=headers) + self.handle_err_response(res) + + enc_json = res.get('Data') + json_str = self.aes_decrypt(enc_json, self.sek) + data = json.loads(json_str) + + return data def handle_err_response(self, response): if response.get('Status') == 0: From 93b3c2ce06de2ff5c803f3ecbfacefc5a94fd13e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 28 Sep 2020 12:11:35 +0530 Subject: [PATCH 008/115] feat: make e invoice from erpnext sales invoice --- .../e_invoice_settings/e_inv_item_schema.json | 25 ++++ .../e_invoice_settings/e_inv_schema.json | 52 +++++++ .../e_invoice_settings/e_invoice_settings.js | 2 +- .../e_invoice_settings/e_invoice_settings.py | 141 +++++++++++++++++- 4 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_item_schema.json create mode 100644 erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_schema.json diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_item_schema.json b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_item_schema.json new file mode 100644 index 00000000000..f98e4e5c94e --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_item_schema.json @@ -0,0 +1,25 @@ +{{ + "SlNo": "{item.sr_no}", + "PrdDesc": "{item.description}", + "IsServc": "{item.is_service_item}", + "HsnCd": "{item.gst_hsn_code}", + "Barcde": "{item.barcode}", + "Unit": "{item.uom}", + "Qty": "{item.qty}", + "UnitPrice": "{item.base_rate}", + "TotAmt": "{item.base_amount}", + "Discount": "{item.discount_amount}", + "AssAmt": "{item.base_amount}", + "PrdSlNo": "{item.serial_no}", + "GstRt": "{item.tax_rate}", + "IgstAmt": "{item.igst_amount}", + "CgstAmt": "{item.cgst_amount}", + "SgstAmt": "{item.sgst_amount}", + "CesRt": "{item.cess_rate}", + "CesAmt": "{item.cess_amount}", + "TotItemVal": "{item.total_value}", + "BchDtls": {{ + "Nm": "{item.batch_no}", + "ExpDt": "{item.batch_expiry_date}" + }} +}} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_schema.json b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_schema.json new file mode 100644 index 00000000000..d194b45bf9e --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_schema.json @@ -0,0 +1,52 @@ +{{ + "Version": "1.01", + "TranDtls": {{ + "TaxSch": "{trans_details.tax_scheme}", + "SupTyp": "{trans_details.supply_type}", + "RegRev": "{trans_details.reverse_charge}" + }}, + "DocDtls": {{ + "Typ": "{doc_details.invoice_type}", + "No": "{doc_details.invoice_name}", + "Dt": "{doc_details.invoice_date}" + }}, + "SellerDtls": {{ + "Gstin": "{seller_details.gstin}", + "LglNm": "{seller_details.legal_name}", + "TrdNm": "{seller_details.trade_name}", + "Loc": "{seller_details.location}", + "Pin": "{seller_details.pincode}", + "Stcd": "{seller_details.state_code}", + "Addr1": "{seller_details.address_line1}", + "Addr2": "{seller_details.address_line2}", + "Ph": "{seller_details.phone}", + "Em": "{seller_details.email}" + }}, + "BuyerDtls": {{ + "Gstin": "{buyer_details.gstin}", + "LglNm": "{buyer_details.legal_name}", + "TrdNm": "{buyer_details.trade_name}", + "Addr1": "{buyer_details.address_line1}", + "Addr2": "{buyer_details.address_line2}", + "Loc": "{buyer_details.location}", + "Pin": "{buyer_details.pincode}", + "Stcd": "{buyer_details.state_code}", + "Ph": "{buyer_details.phone}", + "Em": "{buyer_details.email}", + "Pos": "{buyer_details.place_of_supply}" + }}, + "ItemList": [ + {item_list} + ], + "ValDtls": {{ + "AssVal": "{value_details.base_net_total}", + "CgstVal": "{value_details.total_cgst_amt}", + "SgstVal": "{value_details.total_sgst_amt}", + "IgstVal": "{value_details.total_igst_amt}", + "CesVal": "{value_details.total_cess_amt}", + "Discount": "{value_details.invoice_discount_amt}", + "RndOffAmt": "{value_details.round_off}", + "TotInvVal": "{value_details.base_grand_total}", + "TotInvValFc": "{value_details.grand_total}" + }} +}} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js index 9c3d0807355..6d7eccceece 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js @@ -10,7 +10,7 @@ frappe.ui.form.on('E Invoice Settings', { doc: frm.doc, method: 'get_gstin_details', args: { - 'gstin': '27AAACW8099E1ZX' + 'gstin': '36AAECF1151A1ZC' }, freeze: true, callback: (res) => console.log(res) diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py index 9a9a1b22ff6..50725091bf0 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py @@ -7,13 +7,14 @@ import os import json import base64 import frappe -from frappe import _ from frappe.utils import cstr from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5, AES from Crypto.Util.Padding import pad, unpad -from frappe.utils.data import get_datetime from frappe.model.document import Document +from frappe import _, get_module_path, scrub +from frappe.utils.data import get_datetime, cstr +from erpnext.regional.india.utils import get_gst_accounts from frappe.integrations.utils import make_post_request, make_get_request class EInvoiceSettings(Document): @@ -102,4 +103,138 @@ class EInvoiceSettings(Document): def handle_err_response(self, response): if response.get('Status') == 0: err_msg = response.get('ErrorDetails')[0].get('ErrorMessage') - frappe.throw(_(err_msg), title=_("API Request Failed")) \ No newline at end of file + frappe.throw(_(err_msg), title=_('API Request Failed')) + + def get_schema(self, name): + schema_path = os.path.join(os.path.dirname(__file__), "{name}_schema.json".format(name=name)) + with open(schema_path, 'r') as f: + return cstr(f.read()) + + def get_trans_details(self, invoice): + supply_type = '' + if invoice.gst_category == 'Registered Regular': supply_type = 'B2B' + elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP' + elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP' + elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP' + + if not supply_type: + return _('Invalid invoice transaction category.') + + return frappe._dict(dict( + tax_scheme='GST', supply_type=supply_type, reverse_charge=invoice.reverse_charge + )) + + def get_doc_details(self, invoice): + invoice_type = 'CRN' if invoice.is_return else 'INV' + invoice_name = invoice.name + invoice_date = invoice.posting_date + + return frappe._dict(dict( + invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date + )) + + def get_party_gstin_details(self, party_address): + gstin, address_line1, address_line2, phone, email_id = frappe.db.get_value( + "Address", party_address, ["gstin", "address_line1", "address_line2", "phone", "email_id"] + ) + gstin_details = self.get_gstin_details(gstin) + legal_name = gstin_details.get('LegalName') + trade_name = gstin_details.get('TradeName') + location = gstin_details.get('AddrLoc') + state_code = gstin_details.get('StateCode') + pincode = gstin_details.get('AddrPncd') + + return frappe._dict(dict( + gstin=gstin, legal_name=legal_name, trade_name=trade_name, location=location, + pincode=pincode, state_code=state_code, address_line1=address_line1, + address_line2=address_line2, email=email_id, phone=phone + )) + + def get_item_list(self, invoice): + item_list = [] + gst_accounts = get_gst_accounts(invoice.company) + gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] + + for d in invoice.items: + item_schema = self.get_schema("e_inv_item") + item = frappe._dict(dict()) + item.update(d.as_dict()) + item.sr_no = d.idx + item.description = d.item_name + item.is_service_item = "N" if frappe.db.get_value("Item", d.item_code, "is_stock_item") else "Y" + item.batch_expiry_date = frappe.db.get_value("Batch", d.batch_no, "expiry_date") if d.batch_no else None + item.tax_rate = 0 + item.igst_amount = 0 + item.cgst_amount = 0 + item.sgst_amount = 0 + item.cess_rate = 0 + item.cess_amount = 0 + for t in invoice.taxes: + if t.account_head in gst_accounts_list: + item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) + if t.account_head in gst_accounts.cess_account: + item.cess_rate += item_tax_detail[0] + item.cess_amount += item_tax_detail[1] + elif t.account_head in gst_accounts.igst_account: + item.tax_rate += item_tax_detail[0] + item.igst_amount += item_tax_detail[1] + elif t.account_head in gst_accounts.sgst_account: + item.tax_rate += item_tax_detail[0] + item.sgst_amount += item_tax_detail[1] + elif t.account_head in gst_accounts.cgst_account: + item.tax_rate += item_tax_detail[0] + item.cgst_amount += item_tax_detail[1] + + item.total_value = item.base_amount + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount + e_inv_item = item_schema.format(item=item) + item_list.append(e_inv_item) + + return ", ".join(item_list) + + def get_value_details(self, invoice): + gst_accounts = get_gst_accounts(invoice.company) + gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] + + value_details = frappe._dict(dict()) + value_details.base_net_total = invoice.base_net_total + value_details.invoice_discount_amt = invoice.discount_amount + value_details.round_off = invoice.base_rounding_adjustment + value_details.base_grand_total = invoice.base_rounded_total + value_details.grand_total = invoice.rounded_total + value_details.total_cgst_amt = 0 + value_details.total_sgst_amt = 0 + value_details.total_igst_amt = 0 + value_details.total_cess_amt = 0 + for t in invoice.taxes: + if t.account_head in gst_accounts_list: + if t.account_head in gst_accounts.cess_account: + value_details.total_cess_amt += t.base_tax_amount + elif t.account_head in gst_accounts.igst_account: + value_details.total_igst_amt += t.base_tax_amount + elif t.account_head in gst_accounts.sgst_account: + value_details.total_sgst_amt += t.base_tax_amount + elif t.account_head in gst_accounts.cgst_account: + value_details.total_cgst_amt += t.base_tax_amount + + return value_details + + def make_e_invoice(self, invoice): + schema = self.get_schema("e_inv") + + trans_details = self.get_trans_details(invoice) + doc_details = self.get_doc_details(invoice) + seller_details = self.get_party_gstin_details(invoice.company_address) + buyer_details = self.get_party_gstin_details(invoice.customer_address) + place_of_supply = invoice.place_of_supply.split('-')[0] + buyer_details.update(dict(place_of_supply=place_of_supply)) + + item_list = self.get_item_list(invoice) + value_details = self.get_value_details(invoice) + + e_invoice = schema.format( + trans_details=trans_details, doc_details=doc_details, + seller_details=seller_details, buyer_details=buyer_details, + item_list=item_list, value_details=value_details + ) + + return json.loads(e_invoice) \ No newline at end of file From cc3e0bf8064d70e9df16181d6dd317a621837b2c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Sep 2020 11:20:18 +0530 Subject: [PATCH 009/115] feat: generate IRN --- .../e_invoice_settings/e_inv_validation.json | 838 ++++++++++++++++++ .../e_invoice_settings/e_invoice_settings.js | 13 + .../e_invoice_settings/e_invoice_settings.py | 120 ++- 3 files changed, 960 insertions(+), 11 deletions(-) create mode 100644 erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_validation.json diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_validation.json b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_validation.json new file mode 100644 index 00000000000..51da5943637 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_validation.json @@ -0,0 +1,838 @@ +{ + "Version": { + "type": "string", + "minLength": 1, + "maxLength": 6 + }, + "Irn": { + "type": "string", + "minLength": 64, + "maxLength": 64 + }, + "TranDtls": { + "type": "object", + "properties": { + "TaxSch": { + "type": "string", + "minLength": 3, + "maxLength": 10, + "enum": ["GST"] + }, + "SupTyp": { + "type": "string", + "minLength": 3, + "maxLength": 10, + "enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"] + }, + "RegRev": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"] + }, + "EcmGstin": { + "type": "string", + "minLength": 15, + "maxLength": 15, + "pattern": "([0-9]{2}[0-9A-Z]{13})" + }, + "IgstOnIntra": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"] + } + }, + "required": ["TaxSch", "SupTyp"] + }, + "DocDtls": { + "type": "object", + "properties": { + "Typ": { + "type": "string", + "minLength": 3, + "maxLength": 3, + "enum": ["INV", "CRN", "DBN"] + }, + "No": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$" + }, + "Dt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + } + }, + "required": ["Typ", "No", "Dt"] + }, + "SellerDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "minLength": 15, + "maxLength": 15, + "pattern": "([0-9]{2}[0-9A-Z]{13})" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr1": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 50 + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999 + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2 + }, + "Ph": { + "type": "string", + "minLength": 6, + "maxLength": 12 + }, + "Em": { + "type": "string", + "minLength": 6, + "maxLength": 100 + } + }, + "required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "BuyerDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "minLength": 3, + "maxLength": 15, + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Pos": { + "type": "string", + "minLength": 1, + "maxLength": 2 + }, + "Addr1": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999 + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2 + }, + "Ph": { + "type": "string", + "minLength": 6, + "maxLength": 12 + }, + "Em": { + "type": "string", + "minLength": 6, + "maxLength": 100 + } + }, + "required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"] + }, + "DispDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr1": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999 + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2 + } + }, + "required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "ShipDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "maxLength": 15, + "minLength": 3, + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr1": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999 + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2 + } + }, + "required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "ItemList": [ + { + "type": "object", + "properties": { + "SlNo": { + "type": "string", + "minLength": 1, + "maxLength": 6 + }, + "PrdDesc": { + "type": "string", + "minLength": 3, + "maxLength": 300 + }, + "IsServc": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"] + }, + "HsnCd": { + "type": "string", + "minLength": 4, + "maxLength": 8 + }, + "Barcde": { + "type": "string", + "minLength": 3, + "maxLength": 30 + }, + "Qty": { + "type": "number", + "minimum": 0, + "maximum": 9999999999.999 + }, + "FreeQty": { + "type": "number", + "minimum": 0, + "maximum": 9999999999.999 + }, + "Unit": { + "type": "string", + "minLength": 3, + "maxLength": 8 + }, + "UnitPrice": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.999 + }, + "TotAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "Discount": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "PreTaxVal": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "AssAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "GstRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999 + }, + "IgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "CgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "SgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "CesRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999 + }, + "CesAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "CesNonAdvlAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "StateCesRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999 + }, + "StateCesAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "StateCesNonAdvlAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "OthChrg": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "TotItemVal": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "OrdLineRef": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "OrgCntry": { + "type": "string", + "minLength": 2, + "maxLength": 2 + }, + "PrdSlNo": { + "type": "string", + "minLength": 1, + "maxLength": 20 + }, + "BchDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 3, + "maxLength": 20 + }, + "ExpDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + }, + "WrDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + } + }, + "required": ["Nm"] + }, + "AttribDtls": { + "type": "Array", + "Attribute": [ + { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "Val": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + } + } + ] + } + }, + "required": [ + "SlNo", + "IsServc", + "HsnCd", + "UnitPrice", + "TotAmt", + "AssAmt", + "GstRt", + "TotItemVal" + ] + } + ], + "ValDtls": { + "type": "object", + "properties": { + "AssVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "CgstVal": { + "type": "number", + "maximum": 99999999999999.99, + "minimum": 0 + }, + "SgstVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "IgstVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "CesVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "StCesVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "Discount": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "OthChrg": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "RndOffAmt": { + "type": "number", + "minimum": 0, + "maximum": 99.99 + }, + "TotInvVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "TotInvValFc": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + } + }, + "required": ["AssVal", "TotInvVal"] + }, + "PayDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "AccDet": { + "type": "string", + "minLength": 1, + "maxLength": 18 + }, + "Mode": { + "type": "string", + "minLength": 1, + "maxLength": 18 + }, + "FinInsBr": { + "type": "string", + "minLength": 1, + "maxLength": 11 + }, + "PayTerm": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "PayInstr": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "CrTrn": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "DirDr": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "CrDay": { + "type": "number", + "minimum": 0, + "maximum": 9999 + }, + "PaidAmt": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "PaymtDue": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + } + } + }, + "RefDtls": { + "type": "object", + "properties": { + "InvRm": { + "type": "string", + "maxLength": 100, + "minLength": 3, + "pattern": "^[0-9A-Za-z/-]{3,100}$" + }, + "DocPerdDtls": { + "type": "object", + "properties": { + "InvStDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + }, + "InvEndDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + } + }, + "required": ["InvStDt", "InvEndDt"] + }, + "PrecDocDtls": { + "type": "Array", + "PrecDocument": [ + { + "type": "object", + "properties": { + "InvNo": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$" + }, + "InvDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + }, + "OthRefNo": { + "type": "string", + "minLength": 1, + "maxLength": 20 + } + } + } + ], + "required": ["InvNo", "InvDt"] + }, + "ContrDtls": { + "type": "Array", + "Contract": [ + { + "type": "object", + "properties": { + "RecAdvRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "RecAdvDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + }, + "TendRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "ContrRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "ExtRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "ProjRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "PORefr": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^([0-9A-Za-z/-]){1,16}$" + }, + "PORefDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + } + } + } + ] + } + }, + "required": ["InvStDt", "InvEndDt"] + }, + "AddlDocDtls": { + "type": "Array", + "AddlDocument": [ + { + "type": "object", + "properties": { + "Url": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Docs": { + "type": "string", + "minLength": 3, + "maxLength": 1000 + }, + "Info": { + "type": "string", + "minLength": 3, + "maxLength": 1000 + } + } + } + ] + }, + "ExpDtls": { + "type": "object", + "properties": { + "ShipBNo": { + "type": "string", + "minLength": 1, + "maxLength": 20 + }, + "ShipBDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + }, + "Port": { + "type": "string", + "minLength": 2, + "maxLength": 10, + "pattern": "^[0-9A-Za-z]{2,10}$" + }, + "RefClm": { + "type": "string", + "minLength": 1, + "maxLength": 1 + }, + "ForCur": { + "type": "string", + "minLength": 3, + "maxLength": 16 + }, + "CntCode": { + "type": "string", + "minLength": 2, + "maxLength": 2 + }, + "ExpDuty": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + } + } + }, + "EwbDtls": { + "type": "object", + "properties": { + "TransId": { + "type": "string", + "minLength": 15, + "maxLength": 15 + }, + "TransName": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "TransMode": { + "type": "string", + "maxLength": 1, + "minLength": 1, + "enum": ["1", 2, 3, 4] + }, + "Distance": { + "type": "number", + "minimum": 1, + "maximum": 9999 + }, + "TransDocNo": { + "type": "string", + "minLength": 1, + "maxLength": 15, + "pattern": "^([0-9A-Z/-]){1,15}$" + }, + "TransDocDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + }, + "VehNo": { + "type": "string", + "minLength": 4, + "maxLength": 20 + }, + "VehType": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["O", "R"] + } + }, + "required": ["Distance"] + }, + "required": [ + "Version", + "TranDtls", + "DocDtls", + "SellerDtls", + "BuyerDtls", + "ItemList", + "ValDtls" + ] +} diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js index 6d7eccceece..4dc7988ec5e 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js @@ -16,6 +16,19 @@ frappe.ui.form.on('E Invoice Settings', { callback: (res) => console.log(res) }); }); + + frm.add_custom_button(__("Generate IRN"), + () => { + frm.call({ + doc: frm.doc, + method: 'generate_irn', + args: { + 'invoice': 'SINV-20-21-0044' + }, + freeze: true, + callback: (res) => console.log(res) + }); + }); }, show_fetch_token_btn(frm) { diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py index 50725091bf0..87d06f45bce 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import os +import re import json import base64 import frappe @@ -13,8 +14,8 @@ from Crypto.Cipher import PKCS1_v1_5, AES from Crypto.Util.Padding import pad, unpad from frappe.model.document import Document from frappe import _, get_module_path, scrub -from frappe.utils.data import get_datetime, cstr from erpnext.regional.india.utils import get_gst_accounts +from frappe.utils.data import get_datetime, cstr, cint, format_date from frappe.integrations.utils import make_post_request, make_get_request class EInvoiceSettings(Document): @@ -53,6 +54,17 @@ class EInvoiceSettings(Document): if encode_as_b64: msg_bytes = base64.b64encode(msg_bytes) return msg_bytes.decode() + + def aes_encrypt(self, msg, key): + if not (isinstance(key, bytes) or isinstance(key, bytearray)): + key = base64.b64decode(key) + + cipher = AES.new(key, AES.MODE_ECB) + bytes_msg = str.encode(msg) + padded_bytes_msg = pad(bytes_msg, AES.block_size) + enc_msg = cipher.encrypt(padded_bytes_msg) + b64_enc_msg = base64.b64encode(enc_msg) + return b64_enc_msg.decode() def make_authentication_request(self): endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' @@ -85,12 +97,44 @@ class EInvoiceSettings(Document): self.sek = sek self.save() - def get_gstin_details(self, gstin): - endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/Master/gstin/{gstin}'.format(gstin=gstin) + def get_header(self): headers = { 'content-type': 'application/json' } headers.update(dict(client_id=self.client_id, client_secret=self.client_secret, user_name=self.username)) headers.update(dict(Gstin=self.gstin, AuthToken=self.auth_token)) + return headers + + def get_gstin_details(self, gstin): + endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/Master/gstin/{gstin}'.format(gstin=gstin) + headers = self.get_header() + + res = make_get_request(endpoint, headers=headers) + self.handle_err_response(res) + + enc_json = res.get('Data') + json_str = self.aes_decrypt(enc_json, self.sek) + data = json.loads(json_str) + + return data + + def generate_irn(self, invoice): + endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice' + headers = self.get_header() + + invoice = frappe.get_doc("Sales Invoice", invoice) + e_invoice = self.make_e_invoice(invoice) + enc_e_invoice_json = self.aes_encrypt(e_invoice, self.sek) + payload = dict(Data=enc_e_invoice_json) + + res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) + self.handle_err_response(res) + + return res + + def get_irn_detials(self, irn): + endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/irn/{irn}'.format(irn=irn) + headers = self.get_header() + res = make_get_request(endpoint, headers=headers) self.handle_err_response(res) @@ -102,12 +146,13 @@ class EInvoiceSettings(Document): def handle_err_response(self, response): if response.get('Status') == 0: + print(response) err_msg = response.get('ErrorDetails')[0].get('ErrorMessage') frappe.throw(_(err_msg), title=_('API Request Failed')) - def get_schema(self, name): - schema_path = os.path.join(os.path.dirname(__file__), "{name}_schema.json".format(name=name)) - with open(schema_path, 'r') as f: + def read_json(self, name): + file_path = os.path.join(os.path.dirname(__file__), "{name}.json".format(name=name)) + with open(file_path, 'r') as f: return cstr(f.read()) def get_trans_details(self, invoice): @@ -127,7 +172,7 @@ class EInvoiceSettings(Document): def get_doc_details(self, invoice): invoice_type = 'CRN' if invoice.is_return else 'INV' invoice_name = invoice.name - invoice_date = invoice.posting_date + invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy') return frappe._dict(dict( invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date @@ -142,7 +187,7 @@ class EInvoiceSettings(Document): trade_name = gstin_details.get('TradeName') location = gstin_details.get('AddrLoc') state_code = gstin_details.get('StateCode') - pincode = gstin_details.get('AddrPncd') + pincode = cint(gstin_details.get('AddrPncd')) return frappe._dict(dict( gstin=gstin, legal_name=legal_name, trade_name=trade_name, location=location, @@ -156,13 +201,14 @@ class EInvoiceSettings(Document): gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] for d in invoice.items: - item_schema = self.get_schema("e_inv_item") + item_schema = self.read_json("e_inv_item_schema") item = frappe._dict(dict()) item.update(d.as_dict()) item.sr_no = d.idx item.description = d.item_name item.is_service_item = "N" if frappe.db.get_value("Item", d.item_code, "is_stock_item") else "Y" item.batch_expiry_date = frappe.db.get_value("Batch", d.batch_no, "expiry_date") if d.batch_no else None + item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None item.tax_rate = 0 item.igst_amount = 0 item.cgst_amount = 0 @@ -219,7 +265,9 @@ class EInvoiceSettings(Document): return value_details def make_e_invoice(self, invoice): - schema = self.get_schema("e_inv") + schema = self.read_json("e_inv_schema") + validations = self.read_json("e_inv_validation") + validations = json.loads(validations) trans_details = self.get_trans_details(invoice) doc_details = self.get_doc_details(invoice) @@ -236,5 +284,55 @@ class EInvoiceSettings(Document): seller_details=seller_details, buyer_details=buyer_details, item_list=item_list, value_details=value_details ) + e_invoice = json.loads(e_invoice) - return json.loads(e_invoice) \ No newline at end of file + self.run_e_invoice_validations(validations, e_invoice) + + return json.dumps(e_invoice) + + def run_e_invoice_validations(self, validations, e_invoice): + type_map = { + "string": cstr, + "number": cint, + "object": dict, + "array": list + } + # validate root mandatory keys + mandatory_fields = validations.get('required') + if mandatory_fields and not set(mandatory_fields).issubset(set(e_invoice.keys())): + print("Mandatory condition failed") + + for field, value in validations.items(): + if isinstance(value, list): value = value[0] + + invoice_value = e_invoice.get(field) + if not invoice_value: + print(field, "Value undefined") + continue + + should_be_of_type = type_map[value.get('type').lower()] + if should_be_of_type == dict: + properties = value.get('properties') + + if isinstance(invoice_value, list): + for d in invoice_value: + self.run_e_invoice_validations(properties, d) + else: + self.run_e_invoice_validations(properties, invoice_value) + continue + + e_invoice[field] = None if invoice_value == "None" else invoice_value + e_invoice[field] = should_be_of_type(invoice_value) if e_invoice[field] else e_invoice[field] + + should_be_of_len = value.get('maxLength') + should_be_greater_than = value.get('minimum') + should_be_less_than = value.get('maximum') + pattern_str = value.get('pattern') + pattern = re.compile(pattern_str or "") + + if should_be_of_type == 'string' and not len(invoice_value) <= should_be_of_len: + print("Max Length Exceeded", field, invoice_value) + if should_be_of_type == 'number' and not (should_be_greater_than <= invoice_value <= should_be_of_len): + print("Value too large", field, invoice_value) + if pattern_str and not pattern.match(invoice_value): + print("Pattern Mismatch", field, invoice_value) From 448ab6e3dfe3cc4fac6a5ff497f7bcad9b703d11 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Sep 2020 12:47:37 +0530 Subject: [PATCH 010/115] feat: decode signed json and QR code --- .../e_invoice_settings/e_invoice_settings.js | 13 ++++++++++++ .../e_invoice_settings/e_invoice_settings.py | 21 +++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js index 4dc7988ec5e..23415bb60a6 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js @@ -29,6 +29,19 @@ frappe.ui.form.on('E Invoice Settings', { callback: (res) => console.log(res) }); }); + + frm.add_custom_button(__("Fetch IRN Details"), + () => { + frm.call({ + doc: frm.doc, + method: 'get_irn_details', + args: { + 'irn': 'c63d9e180dfdaa9242e29e2e1e0a8d76f20e116ed3de179a2e9120f384e1b432' + }, + freeze: true, + callback: (res) => console.log(res) + }); + }); }, show_fetch_token_btn(frm) { diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py index 87d06f45bce..b141971f76f 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import os import re +import jwt import json import base64 import frappe @@ -65,6 +66,9 @@ class EInvoiceSettings(Document): enc_msg = cipher.encrypt(padded_bytes_msg) b64_enc_msg = base64.b64encode(enc_msg) return b64_enc_msg.decode() + + def jwt_decrypt(self, token): + return jwt.decode(token, verify=False) def make_authentication_request(self): endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' @@ -129,9 +133,12 @@ class EInvoiceSettings(Document): res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) self.handle_err_response(res) - return res + data = json.loads(res) + self.handle_irn_response(data) + + return data - def get_irn_detials(self, irn): + def get_irn_details(self, irn): endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/irn/{irn}'.format(irn=irn) headers = self.get_header() @@ -140,9 +147,19 @@ class EInvoiceSettings(Document): enc_json = res.get('Data') json_str = self.aes_decrypt(enc_json, self.sek) + data = json.loads(json_str) + self.handle_irn_response(data) return data + + def handle_irn_response(self, data): + enc_signed_invoice = data['SignedInvoice'] + enc_signed_qr_code = data['SignedQRCode'] + signed_invoice = self.jwt_decrypt(enc_signed_invoice)['data'] + signed_qr_code = self.jwt_decrypt(enc_signed_qr_code)['data'] + data['DecryptedSignedInvoice'] = json.loads(signed_invoice) + data['DecryptedSignedQRCode'] = json.loads(signed_qr_code) def handle_err_response(self, response): if response.get('Status') == 0: From dda44bb1f53cd70b51b05d3f942c36a27c657e9d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Sep 2020 14:17:50 +0530 Subject: [PATCH 011/115] chore: validations --- .../e_invoice_settings/e_invoice_settings.js | 2 ++ .../e_invoice_settings/e_invoice_settings.json | 15 ++++++++++++++- .../e_invoice_settings/e_invoice_settings.py | 6 ++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js index 23415bb60a6..01b835fd5c9 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js @@ -3,6 +3,8 @@ frappe.ui.form.on('E Invoice Settings', { refresh: function(frm) { + if (!frm.doc.enable) return; + frm.trigger("show_fetch_token_btn"); frm.add_custom_button(__("Get GSTIN Details"), () => { diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json index 888406b534c..c94336cbbad 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json @@ -5,6 +5,8 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "enable", + "section_break_2", "client_id", "client_secret", "public_key_file", @@ -90,12 +92,23 @@ "fieldtype": "Data", "hidden": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "enable", + "fieldtype": "Check", + "label": "Enable" + }, + { + "depends_on": "enable", + "fieldname": "section_break_2", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-09-25 21:57:58.187647", + "modified": "2020-09-29 14:06:36.385386", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "E Invoice Settings", diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py index b141971f76f..02a6e98957f 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py @@ -9,7 +9,6 @@ import jwt import json import base64 import frappe -from frappe.utils import cstr from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5, AES from Crypto.Util.Padding import pad, unpad @@ -21,7 +20,10 @@ from frappe.integrations.utils import make_post_request, make_get_request class EInvoiceSettings(Document): def validate(self): - pass + mandatory_fields = ['client_id', 'client_secret', 'gstin', 'username', 'password', 'public_key'] + for d in mandatory_fields: + if not self.get(d): + frappe.throw(_("{} is required").format(frappe.unscrub(d)), title=_("Missing Values")) def before_save(self): if not self.public_key or self.has_value_changed('public_key_file'): From d971917ecf613d7016c4f4b95a4151cea4a5236b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Sep 2020 14:23:48 +0530 Subject: [PATCH 012/115] feat: cancel IRN --- .../e_invoice_settings/e_invoice_settings.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py index 02a6e98957f..270e62e0022 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py @@ -154,6 +154,19 @@ class EInvoiceSettings(Document): self.handle_irn_response(data) return data + + def cancel_irn(self, irn, reason, remark=''): + endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/Cancel' + headers = self.get_header() + + cancel_e_inv = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark)) + enc_json = self.aes_encrypt(cancel_e_inv, self.sek) + payload = dict(Data=enc_json) + + res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) + self.handle_err_response(res) + + return res def handle_irn_response(self, data): enc_signed_invoice = data['SignedInvoice'] From 418818f0eca813517d2d4408392f797cdfdd63f7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Sep 2020 16:25:08 +0530 Subject: [PATCH 013/115] feat: complete e-invoice schema --- .../e_invoice_settings/e_inv_item_schema.json | 1 + .../e_invoice_settings/e_inv_schema.json | 59 ++++++- .../e_invoice_settings/e_inv_validation.json | 148 ++++++++---------- .../e_invoice_settings/e_invoice_settings.js | 4 +- .../e_invoice_settings/e_invoice_settings.py | 85 ++++++++-- 5 files changed, 205 insertions(+), 92 deletions(-) diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_item_schema.json b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_item_schema.json index f98e4e5c94e..b4f54448326 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_item_schema.json +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_item_schema.json @@ -6,6 +6,7 @@ "Barcde": "{item.barcode}", "Unit": "{item.uom}", "Qty": "{item.qty}", + "FreeQty": "{item.free_qty}", "UnitPrice": "{item.base_rate}", "TotAmt": "{item.base_amount}", "Discount": "{item.discount_amount}", diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_schema.json b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_schema.json index d194b45bf9e..15230fe22f9 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_schema.json +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_schema.json @@ -3,7 +3,9 @@ "TranDtls": {{ "TaxSch": "{trans_details.tax_scheme}", "SupTyp": "{trans_details.supply_type}", - "RegRev": "{trans_details.reverse_charge}" + "RegRev": "{trans_details.reverse_charge}", + "EcmGstin": "{trans_details.ecom_gstin}", + "IgstOnIntra": "{trans_details.igst_on_intra}" }}, "DocDtls": {{ "Typ": "{doc_details.invoice_type}", @@ -35,6 +37,24 @@ "Em": "{buyer_details.email}", "Pos": "{buyer_details.place_of_supply}" }}, + "DispDtls": {{ + "Nm": "{dispatch_details.company_name}", + "Addr1": "{dispatch_details.address_line1}", + "Addr2": "{dispatch_details.address_line2}", + "Loc": "{dispatch_details.location}", + "Pin": "{dispatch_details.pincode}", + "Stcd": "{dispatch_details.state_code}" + }}, + "ShipDtls": {{ + "Gstin": "{shipping_details.gstin}", + "LglNm": "{shipping_details.legal_name}", + "TrdNm": "{shipping_details.trader_name}", + "Addr1": "{shipping_details.address_line1}", + "Addr2": "{shipping_details.address_line2}", + "Loc": "{shipping_details.location}", + "Pin": "{shipping_details.pincode}", + "Stcd": "{shipping_details.state_code}" + }}, "ItemList": [ {item_list} ], @@ -48,5 +68,42 @@ "RndOffAmt": "{value_details.round_off}", "TotInvVal": "{value_details.base_grand_total}", "TotInvValFc": "{value_details.grand_total}" + }}, + "PayDtls": {{ + "Nm": "{payment_details.payee_name}", + "AccDet": "{payment_details.account_no}", + "Mode": "{payment_details.mode_of_payment}", + "FinInsBr": "{payment_details.ifsc_code}", + "PayTerm": "{payment_details.terms}", + "PaidAmt": "{payment_details.paid_amount}", + "PaymtDue": "{payment_details.outstanding_amount}" + }}, + "RefDtls": {{ + "DocPerdDtls": {{ + "InvStDt": "{period_details.start_date}", + "InvEndDt": "{period_details.end_date}" + }}, + "PrecDocDtls": {{ + "InvNo": "{prev_doc_details.invoice_name}", + "InvDt": "{prev_doc_details.invoice_date}" + }} + }}, + "ExpDtls": {{ + "ShipBNo": "{export_details.bill_no}", + "ShipBDt": "{export_details.bill_date}", + "Port": "{export_details.port}", + "ForCur": "{export_details.foreign_curr_code}", + "CntCode": "{export_details.country_code}", + "ExpDuty": "{export_details.export_duty}" + }}, + "EwbDtls": {{ + "TransId": "{eway_bill_details.gstin}", + "TransName": "{eway_bill_details.name}", + "TransMode": "{eway_bill_details.mode_of_transport}", + "Distance": "{eway_bill_details.distance}", + "TransDocNo": "{eway_bill_details.document_name}", + "TransDocDt": "{eway_bill_details.document_date}", + "VehNo": "{eway_bill_details.vehicle_no}", + "VehType": "{eway_bill_details.vehicle_type}" }} }} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_validation.json b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_validation.json index 51da5943637..da224d9633f 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_validation.json +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_validation.json @@ -621,90 +621,80 @@ "required": ["InvStDt", "InvEndDt"] }, "PrecDocDtls": { - "type": "Array", - "PrecDocument": [ - { - "type": "object", - "properties": { - "InvNo": { - "type": "string", - "minLength": 1, - "maxLength": 16, - "pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$" - }, - "InvDt": { - "type": "string", - "maxLength": 10, - "minLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" - }, - "OthRefNo": { - "type": "string", - "minLength": 1, - "maxLength": 20 - } - } + "type": "object", + "properties": { + "InvNo": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$" + }, + "InvDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + }, + "OthRefNo": { + "type": "string", + "minLength": 1, + "maxLength": 20 } - ], + }, "required": ["InvNo", "InvDt"] }, "ContrDtls": { - "type": "Array", - "Contract": [ - { - "type": "object", - "properties": { - "RecAdvRefr": { - "type": "string", - "minLength": 1, - "maxLength": 20, - "pattern": "^([0-9A-Za-z/-]){1,20}$" - }, - "RecAdvDt": { - "type": "string", - "minLength": 10, - "maxLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" - }, - "TendRefr": { - "type": "string", - "minLength": 1, - "maxLength": 20, - "pattern": "^([0-9A-Za-z/-]){1,20}$" - }, - "ContrRefr": { - "type": "string", - "minLength": 1, - "maxLength": 20, - "pattern": "^([0-9A-Za-z/-]){1,20}$" - }, - "ExtRefr": { - "type": "string", - "minLength": 1, - "maxLength": 20, - "pattern": "^([0-9A-Za-z/-]){1,20}$" - }, - "ProjRefr": { - "type": "string", - "minLength": 1, - "maxLength": 20, - "pattern": "^([0-9A-Za-z/-]){1,20}$" - }, - "PORefr": { - "type": "string", - "minLength": 1, - "maxLength": 16, - "pattern": "^([0-9A-Za-z/-]){1,16}$" - }, - "PORefDt": { - "type": "string", - "minLength": 10, - "maxLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" - } - } + "type": "object", + "properties": { + "RecAdvRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "RecAdvDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + }, + "TendRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "ContrRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "ExtRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "ProjRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "PORefr": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^([0-9A-Za-z/-]){1,16}$" + }, + "PORefDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" } - ] + } } }, "required": ["InvStDt", "InvEndDt"] diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js index 01b835fd5c9..c97ebed013f 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js @@ -4,7 +4,7 @@ frappe.ui.form.on('E Invoice Settings', { refresh: function(frm) { if (!frm.doc.enable) return; - + frm.trigger("show_fetch_token_btn"); frm.add_custom_button(__("Get GSTIN Details"), () => { @@ -25,7 +25,7 @@ frappe.ui.form.on('E Invoice Settings', { doc: frm.doc, method: 'generate_irn', args: { - 'invoice': 'SINV-20-21-0044' + 'invoice': 'SINV-20-21-0051' }, freeze: true, callback: (res) => console.log(res) diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py index 270e62e0022..b795296a6a6 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py @@ -129,13 +129,17 @@ class EInvoiceSettings(Document): invoice = frappe.get_doc("Sales Invoice", invoice) e_invoice = self.make_e_invoice(invoice) + enc_e_invoice_json = self.aes_encrypt(e_invoice, self.sek) payload = dict(Data=enc_e_invoice_json) res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) self.handle_err_response(res) - data = json.loads(res) + enc_json = res.get('Data') + json_str = self.aes_decrypt(enc_json, self.sek) + + data = json.loads(json_str) self.handle_irn_response(data) return data @@ -178,8 +182,12 @@ class EInvoiceSettings(Document): def handle_err_response(self, response): if response.get('Status') == 0: + err_details = response.get('ErrorDetails') print(response) - err_msg = response.get('ErrorDetails')[0].get('ErrorMessage') + err_msg = "" + for d in err_details: + err_msg += d.get('ErrorMessage') + err_msg += "
" frappe.throw(_(err_msg), title=_('API Request Failed')) def read_json(self, name): @@ -220,6 +228,8 @@ class EInvoiceSettings(Document): location = gstin_details.get('AddrLoc') state_code = gstin_details.get('StateCode') pincode = cint(gstin_details.get('AddrPncd')) + if state_code == 97: + pincode = 999999 return frappe._dict(dict( gstin=gstin, legal_name=legal_name, trade_name=trade_name, location=location, @@ -227,6 +237,17 @@ class EInvoiceSettings(Document): address_line2=address_line2, email=email_id, phone=phone )) + def get_overseas_address_details(self, party_address): + address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value( + "Address", party_address, ["address_title", "address_line1", "address_line2", "city", "phone", "email_id"] + ) + + return frappe._dict(dict( + gstin='URP', legal_name=address_title, address_line1=address_line1, + address_line2=address_line2, email=email_id, phone=phone, + pincode=999999, state_code=96, place_of_supply=96, location=city + )) + def get_item_list(self, invoice): item_list = [] gst_accounts = get_gst_accounts(invoice.company) @@ -295,6 +316,23 @@ class EInvoiceSettings(Document): value_details.total_cgst_amt += t.base_tax_amount return value_details + + def get_payment_details(self, invoice): + payee_name = invoice.company + mode_of_payment = ", ".join([d.mode_of_payment for d in invoice.payments]) + paid_amount = invoice.base_paid_amount + outstanding_amount = invoice.outstanding_amount + + return frappe._dict(dict( + payee_name=payee_name, mode_of_payment=mode_of_payment, + paid_amount=paid_amount, outstanding_amount=outstanding_amount + )) + + def get_return_doc_reference(self, invoice): + invoice_date = frappe.db.get_value("Sales Invoice", invoice.return_against, "posting_date") + return frappe._dict(dict( + invoice_name=invoice.return_against, invoice_date=invoice_date + )) def make_e_invoice(self, invoice): schema = self.read_json("e_inv_schema") @@ -304,17 +342,39 @@ class EInvoiceSettings(Document): trans_details = self.get_trans_details(invoice) doc_details = self.get_doc_details(invoice) seller_details = self.get_party_gstin_details(invoice.company_address) - buyer_details = self.get_party_gstin_details(invoice.customer_address) - place_of_supply = invoice.place_of_supply.split('-')[0] - buyer_details.update(dict(place_of_supply=place_of_supply)) + + if invoice.gst_category == 'Overseas': + buyer_details = self.get_overseas_address_details(invoice.customer_address) + else: + buyer_details = self.get_party_gstin_details(invoice.customer_address) + place_of_supply = invoice.place_of_supply.split('-')[0] + buyer_details.update(dict(place_of_supply=place_of_supply)) item_list = self.get_item_list(invoice) value_details = self.get_value_details(invoice) + dispatch_details = frappe._dict({}) + period_details = frappe._dict({}) + shipping_details = frappe._dict({}) + export_details = frappe._dict({}) + eway_bill_details = frappe._dict({}) + if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: + shipping_details = self.get_party_gstin_details(invoice.shipping_address_name) + + payment_details = frappe._dict({}) + if invoice.is_pos and invoice.base_paid_amount: + payment_details = self.get_payment_details(invoice) + + prev_doc_details = frappe._dict({}) + if invoice.is_return and invoice.return_against: + prev_doc_details = self.get_return_doc_reference(invoice) + e_invoice = schema.format( - trans_details=trans_details, doc_details=doc_details, - seller_details=seller_details, buyer_details=buyer_details, - item_list=item_list, value_details=value_details + trans_details=trans_details, doc_details=doc_details, dispatch_details=dispatch_details, + seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details, + item_list=item_list, value_details=value_details, payment_details=payment_details, + period_details=period_details, prev_doc_details=prev_doc_details, + export_details=export_details, eway_bill_details=eway_bill_details ) e_invoice = json.loads(e_invoice) @@ -339,7 +399,7 @@ class EInvoiceSettings(Document): invoice_value = e_invoice.get(field) if not invoice_value: - print(field, "Value undefined") + print(field, "value undefined") continue should_be_of_type = type_map[value.get('type').lower()] @@ -351,9 +411,14 @@ class EInvoiceSettings(Document): self.run_e_invoice_validations(properties, d) else: self.run_e_invoice_validations(properties, invoice_value) + if not invoice_value: + e_invoice.pop(field, None) continue - e_invoice[field] = None if invoice_value == "None" else invoice_value + if invoice_value == "None": + e_invoice.pop(field, None) + continue + e_invoice[field] = should_be_of_type(invoice_value) if e_invoice[field] else e_invoice[field] should_be_of_len = value.get('maxLength') From bdfd30760a0ad15f12c33bb3f2547669d7750cd0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Sep 2020 17:10:22 +0530 Subject: [PATCH 014/115] chore: move e-invoice settings to regional --- .../doctype/e_invoice_settings/__init__.py | 0 .../e_invoice_settings/e_inv_item_schema.json | 0 .../e_invoice_settings/e_inv_schema.json | 0 .../e_invoice_settings/e_inv_validation.json | 0 .../e_invoice_settings/e_invoice_settings.js | 0 .../e_invoice_settings.json | 38 +++++++++---------- .../e_invoice_settings/e_invoice_settings.py | 0 .../test_e_invoice_settings.py | 0 8 files changed, 19 insertions(+), 19 deletions(-) rename erpnext/{erpnext_integrations => regional}/doctype/e_invoice_settings/__init__.py (100%) rename erpnext/{erpnext_integrations => regional}/doctype/e_invoice_settings/e_inv_item_schema.json (100%) rename erpnext/{erpnext_integrations => regional}/doctype/e_invoice_settings/e_inv_schema.json (100%) rename erpnext/{erpnext_integrations => regional}/doctype/e_invoice_settings/e_inv_validation.json (100%) rename erpnext/{erpnext_integrations => regional}/doctype/e_invoice_settings/e_invoice_settings.js (100%) rename erpnext/{erpnext_integrations => regional}/doctype/e_invoice_settings/e_invoice_settings.json (96%) rename erpnext/{erpnext_integrations => regional}/doctype/e_invoice_settings/e_invoice_settings.py (100%) rename erpnext/{erpnext_integrations => regional}/doctype/e_invoice_settings/test_e_invoice_settings.py (100%) diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/__init__.py b/erpnext/regional/doctype/e_invoice_settings/__init__.py similarity index 100% rename from erpnext/erpnext_integrations/doctype/e_invoice_settings/__init__.py rename to erpnext/regional/doctype/e_invoice_settings/__init__.py diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_item_schema.json b/erpnext/regional/doctype/e_invoice_settings/e_inv_item_schema.json similarity index 100% rename from erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_item_schema.json rename to erpnext/regional/doctype/e_invoice_settings/e_inv_item_schema.json diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_schema.json b/erpnext/regional/doctype/e_invoice_settings/e_inv_schema.json similarity index 100% rename from erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_schema.json rename to erpnext/regional/doctype/e_invoice_settings/e_inv_schema.json diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_validation.json b/erpnext/regional/doctype/e_invoice_settings/e_inv_validation.json similarity index 100% rename from erpnext/erpnext_integrations/doctype/e_invoice_settings/e_inv_validation.json rename to erpnext/regional/doctype/e_invoice_settings/e_inv_validation.json diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js similarity index 100% rename from erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.js rename to erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json similarity index 96% rename from erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json rename to erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json index c94336cbbad..3df9243925c 100644 --- a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.json +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json @@ -21,6 +21,17 @@ "sek" ], "fields": [ + { + "default": "0", + "fieldname": "enable", + "fieldtype": "Check", + "label": "Enable" + }, + { + "depends_on": "enable", + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, { "fieldname": "client_id", "fieldtype": "Data", @@ -40,6 +51,12 @@ "label": "Public Key", "reqd": 1 }, + { + "fieldname": "public_key", + "fieldtype": "Long Text", + "hidden": 1, + "read_only": 1 + }, { "fieldname": "column_break_3", "fieldtype": "Column Break" @@ -62,12 +79,6 @@ "label": "Password", "reqd": 1 }, - { - "fieldname": "public_key", - "fieldtype": "Long Text", - "hidden": 1, - "read_only": 1 - }, { "default": "0", "description": "Token will be automatically refreshed 10 mins before expiry", @@ -92,25 +103,14 @@ "fieldtype": "Data", "hidden": 1, "read_only": 1 - }, - { - "default": "0", - "fieldname": "enable", - "fieldtype": "Check", - "label": "Enable" - }, - { - "depends_on": "enable", - "fieldname": "section_break_2", - "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-09-29 14:06:36.385386", + "modified": "2020-09-29 17:05:33.512227", "modified_by": "Administrator", - "module": "ERPNext Integrations", + "module": "Regional", "name": "E Invoice Settings", "owner": "Administrator", "permissions": [ diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py similarity index 100% rename from erpnext/erpnext_integrations/doctype/e_invoice_settings/e_invoice_settings.py rename to erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py diff --git a/erpnext/erpnext_integrations/doctype/e_invoice_settings/test_e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py similarity index 100% rename from erpnext/erpnext_integrations/doctype/e_invoice_settings/test_e_invoice_settings.py rename to erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py From 8eda5aca09b671b75ecb66832ac260b0bbc9e9c8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Sep 2020 17:36:55 +0530 Subject: [PATCH 015/115] chore: split einvoice settings and operations --- .../e_invoice_settings/e_invoice_settings.py | 416 +---------------- erpnext/regional/india/e_invoice_utils.py | 431 ++++++++++++++++++ .../einv_item_schema.json} | 0 .../einv_schema.json} | 0 .../einv_validation.json} | 0 5 files changed, 434 insertions(+), 413 deletions(-) create mode 100644 erpnext/regional/india/e_invoice_utils.py rename erpnext/regional/{doctype/e_invoice_settings/e_inv_item_schema.json => india/einv_item_schema.json} (100%) rename erpnext/regional/{doctype/e_invoice_settings/e_inv_schema.json => india/einv_schema.json} (100%) rename erpnext/regional/{doctype/e_invoice_settings/e_inv_validation.json => india/einv_validation.json} (100%) diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py index b795296a6a6..1d86208347d 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -3,20 +3,10 @@ # For license information, please see license.txt from __future__ import unicode_literals -import os -import re -import jwt -import json -import base64 import frappe -from Crypto.PublicKey import RSA -from Crypto.Cipher import PKCS1_v1_5, AES -from Crypto.Util.Padding import pad, unpad +from frappe import _ +from frappe.utils.data import cstr from frappe.model.document import Document -from frappe import _, get_module_path, scrub -from erpnext.regional.india.utils import get_gst_accounts -from frappe.utils.data import get_datetime, cstr, cint, format_date -from frappe.integrations.utils import make_post_request, make_get_request class EInvoiceSettings(Document): def validate(self): @@ -32,404 +22,4 @@ class EInvoiceSettings(Document): def read_key_file(self): key_file = frappe.get_doc('File', dict(attached_to_name=self.doctype, attached_to_field='public_key_file')) with open(key_file.get_full_path(), 'rb') as f: - return cstr(f.read()) - - def rsa_encrypt(self, msg, key): - if not (isinstance(msg, bytes) or isinstance(msg, bytearray)): - msg = str.encode(msg) - - rsa_pub_key = RSA.import_key(key) - cipher = PKCS1_v1_5.new(rsa_pub_key) - enc_msg = cipher.encrypt(msg) - b64_enc_msg = base64.b64encode(enc_msg) - return b64_enc_msg.decode() - - def aes_decrypt(self, enc_msg, key): - encode_as_b64 = True - if not (isinstance(key, bytes) or isinstance(key, bytearray)): - key = base64.b64decode(key) - encode_as_b64 = False - - cipher = AES.new(key, AES.MODE_ECB) - b64_enc_msg = base64.b64decode(enc_msg) - msg_bytes = cipher.decrypt(b64_enc_msg) - msg_bytes = unpad(msg_bytes, AES.block_size) # due to ECB/PKCS5Padding - if encode_as_b64: - msg_bytes = base64.b64encode(msg_bytes) - return msg_bytes.decode() - - def aes_encrypt(self, msg, key): - if not (isinstance(key, bytes) or isinstance(key, bytearray)): - key = base64.b64decode(key) - - cipher = AES.new(key, AES.MODE_ECB) - bytes_msg = str.encode(msg) - padded_bytes_msg = pad(bytes_msg, AES.block_size) - enc_msg = cipher.encrypt(padded_bytes_msg) - b64_enc_msg = base64.b64encode(enc_msg) - return b64_enc_msg.decode() - - def jwt_decrypt(self, token): - return jwt.decode(token, verify=False) - - def make_authentication_request(self): - endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' - headers = { 'content-type': 'application/json' } - headers.update(dict(client_id=self.client_id, client_secret=self.client_secret)) - payload = dict(UserName=self.username, ForceRefreshAccessToken=bool(self.auto_refresh_token)) - - appkey = bytearray(os.urandom(32)) - enc_appkey = self.rsa_encrypt(appkey, self.public_key) - - password = self.get_password(fieldname='password') - enc_password = self.rsa_encrypt(password, self.public_key) - - payload.update(dict(Password=enc_password, AppKey=enc_appkey)) - - res = make_post_request(endpoint, headers=headers, data=json.dumps({ 'data': payload })) - self.handle_err_response(res) - - self.extract_token_and_sek(res, appkey) - - def extract_token_and_sek(self, response, appkey): - data = response.get('Data') - auth_token = data.get('AuthToken') - token_expiry = data.get('TokenExpiry') - enc_sek = data.get('Sek') - sek = self.aes_decrypt(enc_sek, appkey) - - self.auth_token = auth_token - self.token_expiry = get_datetime(token_expiry) - self.sek = sek - self.save() - - def get_header(self): - headers = { 'content-type': 'application/json' } - headers.update(dict(client_id=self.client_id, client_secret=self.client_secret, user_name=self.username)) - headers.update(dict(Gstin=self.gstin, AuthToken=self.auth_token)) - - return headers - - def get_gstin_details(self, gstin): - endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/Master/gstin/{gstin}'.format(gstin=gstin) - headers = self.get_header() - - res = make_get_request(endpoint, headers=headers) - self.handle_err_response(res) - - enc_json = res.get('Data') - json_str = self.aes_decrypt(enc_json, self.sek) - data = json.loads(json_str) - - return data - - def generate_irn(self, invoice): - endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice' - headers = self.get_header() - - invoice = frappe.get_doc("Sales Invoice", invoice) - e_invoice = self.make_e_invoice(invoice) - - enc_e_invoice_json = self.aes_encrypt(e_invoice, self.sek) - payload = dict(Data=enc_e_invoice_json) - - res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) - self.handle_err_response(res) - - enc_json = res.get('Data') - json_str = self.aes_decrypt(enc_json, self.sek) - - data = json.loads(json_str) - self.handle_irn_response(data) - - return data - - def get_irn_details(self, irn): - endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/irn/{irn}'.format(irn=irn) - headers = self.get_header() - - res = make_get_request(endpoint, headers=headers) - self.handle_err_response(res) - - enc_json = res.get('Data') - json_str = self.aes_decrypt(enc_json, self.sek) - - data = json.loads(json_str) - self.handle_irn_response(data) - - return data - - def cancel_irn(self, irn, reason, remark=''): - endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/Cancel' - headers = self.get_header() - - cancel_e_inv = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark)) - enc_json = self.aes_encrypt(cancel_e_inv, self.sek) - payload = dict(Data=enc_json) - - res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) - self.handle_err_response(res) - - return res - - def handle_irn_response(self, data): - enc_signed_invoice = data['SignedInvoice'] - enc_signed_qr_code = data['SignedQRCode'] - signed_invoice = self.jwt_decrypt(enc_signed_invoice)['data'] - signed_qr_code = self.jwt_decrypt(enc_signed_qr_code)['data'] - data['DecryptedSignedInvoice'] = json.loads(signed_invoice) - data['DecryptedSignedQRCode'] = json.loads(signed_qr_code) - - def handle_err_response(self, response): - if response.get('Status') == 0: - err_details = response.get('ErrorDetails') - print(response) - err_msg = "" - for d in err_details: - err_msg += d.get('ErrorMessage') - err_msg += "
" - frappe.throw(_(err_msg), title=_('API Request Failed')) - - def read_json(self, name): - file_path = os.path.join(os.path.dirname(__file__), "{name}.json".format(name=name)) - with open(file_path, 'r') as f: - return cstr(f.read()) - - def get_trans_details(self, invoice): - supply_type = '' - if invoice.gst_category == 'Registered Regular': supply_type = 'B2B' - elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP' - elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP' - elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP' - - if not supply_type: - return _('Invalid invoice transaction category.') - - return frappe._dict(dict( - tax_scheme='GST', supply_type=supply_type, reverse_charge=invoice.reverse_charge - )) - - def get_doc_details(self, invoice): - invoice_type = 'CRN' if invoice.is_return else 'INV' - invoice_name = invoice.name - invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy') - - return frappe._dict(dict( - invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date - )) - - def get_party_gstin_details(self, party_address): - gstin, address_line1, address_line2, phone, email_id = frappe.db.get_value( - "Address", party_address, ["gstin", "address_line1", "address_line2", "phone", "email_id"] - ) - gstin_details = self.get_gstin_details(gstin) - legal_name = gstin_details.get('LegalName') - trade_name = gstin_details.get('TradeName') - location = gstin_details.get('AddrLoc') - state_code = gstin_details.get('StateCode') - pincode = cint(gstin_details.get('AddrPncd')) - if state_code == 97: - pincode = 999999 - - return frappe._dict(dict( - gstin=gstin, legal_name=legal_name, trade_name=trade_name, location=location, - pincode=pincode, state_code=state_code, address_line1=address_line1, - address_line2=address_line2, email=email_id, phone=phone - )) - - def get_overseas_address_details(self, party_address): - address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value( - "Address", party_address, ["address_title", "address_line1", "address_line2", "city", "phone", "email_id"] - ) - - return frappe._dict(dict( - gstin='URP', legal_name=address_title, address_line1=address_line1, - address_line2=address_line2, email=email_id, phone=phone, - pincode=999999, state_code=96, place_of_supply=96, location=city - )) - - def get_item_list(self, invoice): - item_list = [] - gst_accounts = get_gst_accounts(invoice.company) - gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] - - for d in invoice.items: - item_schema = self.read_json("e_inv_item_schema") - item = frappe._dict(dict()) - item.update(d.as_dict()) - item.sr_no = d.idx - item.description = d.item_name - item.is_service_item = "N" if frappe.db.get_value("Item", d.item_code, "is_stock_item") else "Y" - item.batch_expiry_date = frappe.db.get_value("Batch", d.batch_no, "expiry_date") if d.batch_no else None - item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None - item.tax_rate = 0 - item.igst_amount = 0 - item.cgst_amount = 0 - item.sgst_amount = 0 - item.cess_rate = 0 - item.cess_amount = 0 - for t in invoice.taxes: - if t.account_head in gst_accounts_list: - item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) - if t.account_head in gst_accounts.cess_account: - item.cess_rate += item_tax_detail[0] - item.cess_amount += item_tax_detail[1] - elif t.account_head in gst_accounts.igst_account: - item.tax_rate += item_tax_detail[0] - item.igst_amount += item_tax_detail[1] - elif t.account_head in gst_accounts.sgst_account: - item.tax_rate += item_tax_detail[0] - item.sgst_amount += item_tax_detail[1] - elif t.account_head in gst_accounts.cgst_account: - item.tax_rate += item_tax_detail[0] - item.cgst_amount += item_tax_detail[1] - - item.total_value = item.base_amount + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount - e_inv_item = item_schema.format(item=item) - item_list.append(e_inv_item) - - return ", ".join(item_list) - - def get_value_details(self, invoice): - gst_accounts = get_gst_accounts(invoice.company) - gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] - - value_details = frappe._dict(dict()) - value_details.base_net_total = invoice.base_net_total - value_details.invoice_discount_amt = invoice.discount_amount - value_details.round_off = invoice.base_rounding_adjustment - value_details.base_grand_total = invoice.base_rounded_total - value_details.grand_total = invoice.rounded_total - value_details.total_cgst_amt = 0 - value_details.total_sgst_amt = 0 - value_details.total_igst_amt = 0 - value_details.total_cess_amt = 0 - for t in invoice.taxes: - if t.account_head in gst_accounts_list: - if t.account_head in gst_accounts.cess_account: - value_details.total_cess_amt += t.base_tax_amount - elif t.account_head in gst_accounts.igst_account: - value_details.total_igst_amt += t.base_tax_amount - elif t.account_head in gst_accounts.sgst_account: - value_details.total_sgst_amt += t.base_tax_amount - elif t.account_head in gst_accounts.cgst_account: - value_details.total_cgst_amt += t.base_tax_amount - - return value_details - - def get_payment_details(self, invoice): - payee_name = invoice.company - mode_of_payment = ", ".join([d.mode_of_payment for d in invoice.payments]) - paid_amount = invoice.base_paid_amount - outstanding_amount = invoice.outstanding_amount - - return frappe._dict(dict( - payee_name=payee_name, mode_of_payment=mode_of_payment, - paid_amount=paid_amount, outstanding_amount=outstanding_amount - )) - - def get_return_doc_reference(self, invoice): - invoice_date = frappe.db.get_value("Sales Invoice", invoice.return_against, "posting_date") - return frappe._dict(dict( - invoice_name=invoice.return_against, invoice_date=invoice_date - )) - - def make_e_invoice(self, invoice): - schema = self.read_json("e_inv_schema") - validations = self.read_json("e_inv_validation") - validations = json.loads(validations) - - trans_details = self.get_trans_details(invoice) - doc_details = self.get_doc_details(invoice) - seller_details = self.get_party_gstin_details(invoice.company_address) - - if invoice.gst_category == 'Overseas': - buyer_details = self.get_overseas_address_details(invoice.customer_address) - else: - buyer_details = self.get_party_gstin_details(invoice.customer_address) - place_of_supply = invoice.place_of_supply.split('-')[0] - buyer_details.update(dict(place_of_supply=place_of_supply)) - - item_list = self.get_item_list(invoice) - value_details = self.get_value_details(invoice) - - dispatch_details = frappe._dict({}) - period_details = frappe._dict({}) - shipping_details = frappe._dict({}) - export_details = frappe._dict({}) - eway_bill_details = frappe._dict({}) - if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: - shipping_details = self.get_party_gstin_details(invoice.shipping_address_name) - - payment_details = frappe._dict({}) - if invoice.is_pos and invoice.base_paid_amount: - payment_details = self.get_payment_details(invoice) - - prev_doc_details = frappe._dict({}) - if invoice.is_return and invoice.return_against: - prev_doc_details = self.get_return_doc_reference(invoice) - - e_invoice = schema.format( - trans_details=trans_details, doc_details=doc_details, dispatch_details=dispatch_details, - seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details, - item_list=item_list, value_details=value_details, payment_details=payment_details, - period_details=period_details, prev_doc_details=prev_doc_details, - export_details=export_details, eway_bill_details=eway_bill_details - ) - e_invoice = json.loads(e_invoice) - - self.run_e_invoice_validations(validations, e_invoice) - - return json.dumps(e_invoice) - - def run_e_invoice_validations(self, validations, e_invoice): - type_map = { - "string": cstr, - "number": cint, - "object": dict, - "array": list - } - # validate root mandatory keys - mandatory_fields = validations.get('required') - if mandatory_fields and not set(mandatory_fields).issubset(set(e_invoice.keys())): - print("Mandatory condition failed") - - for field, value in validations.items(): - if isinstance(value, list): value = value[0] - - invoice_value = e_invoice.get(field) - if not invoice_value: - print(field, "value undefined") - continue - - should_be_of_type = type_map[value.get('type').lower()] - if should_be_of_type == dict: - properties = value.get('properties') - - if isinstance(invoice_value, list): - for d in invoice_value: - self.run_e_invoice_validations(properties, d) - else: - self.run_e_invoice_validations(properties, invoice_value) - if not invoice_value: - e_invoice.pop(field, None) - continue - - if invoice_value == "None": - e_invoice.pop(field, None) - continue - - e_invoice[field] = should_be_of_type(invoice_value) if e_invoice[field] else e_invoice[field] - - should_be_of_len = value.get('maxLength') - should_be_greater_than = value.get('minimum') - should_be_less_than = value.get('maximum') - pattern_str = value.get('pattern') - pattern = re.compile(pattern_str or "") - - if should_be_of_type == 'string' and not len(invoice_value) <= should_be_of_len: - print("Max Length Exceeded", field, invoice_value) - if should_be_of_type == 'number' and not (should_be_greater_than <= invoice_value <= should_be_of_len): - print("Value too large", field, invoice_value) - if pattern_str and not pattern.match(invoice_value): - print("Pattern Mismatch", field, invoice_value) + return cstr(f.read()) \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py new file mode 100644 index 00000000000..3abcf16a72a --- /dev/null +++ b/erpnext/regional/india/e_invoice_utils.py @@ -0,0 +1,431 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import os +import re +import jwt +import json +import base64 +import frappe +from Crypto.PublicKey import RSA +from Crypto.Cipher import PKCS1_v1_5, AES +from Crypto.Util.Padding import pad, unpad +from frappe.model.document import Document +from frappe import _, get_module_path, scrub +from erpnext.regional.india.utils import get_gst_accounts +from frappe.utils.data import get_datetime, cstr, cint, format_date +from frappe.integrations.utils import make_post_request, make_get_request + +def get_einv_credentials(): + return frappe.get_doc("E Invoice Settings") + +def rsa_encrypt(msg, key): + if not (isinstance(msg, bytes) or isinstance(msg, bytearray)): + msg = str.encode(msg) + + rsa_pub_key = RSA.import_key(key) + cipher = PKCS1_v1_5.new(rsa_pub_key) + enc_msg = cipher.encrypt(msg) + b64_enc_msg = base64.b64encode(enc_msg) + return b64_enc_msg.decode() + +def aes_decrypt(enc_msg, key): + encode_as_b64 = True + if not (isinstance(key, bytes) or isinstance(key, bytearray)): + key = base64.b64decode(key) + encode_as_b64 = False + + cipher = AES.new(key, AES.MODE_ECB) + b64_enc_msg = base64.b64decode(enc_msg) + msg_bytes = cipher.decrypt(b64_enc_msg) + msg_bytes = unpad(msg_bytes, AES.block_size) # due to ECB/PKCS5Padding + if encode_as_b64: + msg_bytes = base64.b64encode(msg_bytes) + return msg_bytes.decode() + +def aes_encrypt(msg, key): + if not (isinstance(key, bytes) or isinstance(key, bytearray)): + key = base64.b64decode(key) + + cipher = AES.new(key, AES.MODE_ECB) + bytes_msg = str.encode(msg) + padded_bytes_msg = pad(bytes_msg, AES.block_size) + enc_msg = cipher.encrypt(padded_bytes_msg) + b64_enc_msg = base64.b64encode(enc_msg) + return b64_enc_msg.decode() + +def jwt_decrypt(token): + return jwt.decode(token, verify=False) + +def get_header(creds): + headers = { 'content-type': 'application/json' } + headers.update(dict(client_id=creds.client_id, client_secret=creds.client_secret, user_name=creds.username)) + headers.update(dict(Gstin=creds.gstin, AuthToken=creds.auth_token)) + return headers + +def fetch_token(self): + einv_creds = get_einv_credentials() + + endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' + headers = { 'content-type': 'application/json' } + headers.update(dict(client_id=einv_creds.client_id, client_secret=einv_creds.client_secret)) + payload = dict(UserName=einv_creds.username, ForceRefreshAccessToken=bool(einv_creds.auto_refresh_token)) + + appkey = bytearray(os.urandom(32)) + enc_appkey = rsa_encrypt(appkey, einv_creds.public_key) + + password = einv_creds.get_password(fieldname='password') + enc_password = rsa_encrypt(password, einv_creds.public_key) + + payload.update(dict(Password=enc_password, AppKey=enc_appkey)) + + res = make_post_request(endpoint, headers=headers, data=json.dumps({ 'data': payload })) + handle_err_response(res) + + auth_token, token_expiry, sek = extract_token_and_sek(res, appkey) + + einv_creds.auth_token = auth_token + einv_creds.token_expiry = get_datetime(token_expiry) + einv_creds.sek = sek + einv_creds.save() + +def extract_token_and_sek(response, appkey): + data = response.get('Data') + auth_token = data.get('AuthToken') + token_expiry = data.get('TokenExpiry') + enc_sek = data.get('Sek') + sek = aes_decrypt(enc_sek, appkey) + return auth_token, token_expiry, sek + +def get_gstin_details(gstin): + einv_creds = get_einv_credentials() + + endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/Master/gstin/{gstin}'.format(gstin=gstin) + headers = get_header(einv_creds) + + res = make_get_request(endpoint, headers=headers) + handle_err_response(res) + + enc_json = res.get('Data') + json_str = aes_decrypt(enc_json, einv_creds.sek) + data = json.loads(json_str) + + return data + +def generate_irn(invoice): + einv_creds = get_einv_credentials() + + endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice' + headers = get_header(einv_creds) + + e_invoice = make_e_invoice(invoice) + + enc_e_invoice_json = aes_encrypt(e_invoice, einv_creds.sek) + payload = dict(Data=enc_e_invoice_json) + + res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) + handle_err_response(res) + + enc_json = res.get('Data') + json_str = aes_decrypt(enc_json, einv_creds.sek) + + data = json.loads(json_str) + handle_irn_response(data) + + return data + +def get_irn_details(irn): + einv_creds = get_einv_credentials() + + endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/irn/{irn}'.format(irn=irn) + headers = get_header(einv_creds) + + res = make_get_request(endpoint, headers=headers) + handle_err_response(res) + + enc_json = res.get('Data') + json_str = aes_decrypt(enc_json, einv_creds.sek) + + data = json.loads(json_str) + handle_irn_response(data) + + return data + +def cancel_irn(irn, reason, remark=''): + einv_creds = get_einv_credentials() + + endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/Cancel' + headers = get_header() + + cancel_e_inv = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark)) + enc_json = aes_encrypt(cancel_e_inv, einv_creds.sek) + payload = dict(Data=enc_json) + + res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) + handle_err_response(res) + + return res + +def handle_irn_response(data): + enc_signed_invoice = data['SignedInvoice'] + enc_signed_qr_code = data['SignedQRCode'] + signed_invoice = jwt_decrypt(enc_signed_invoice)['data'] + signed_qr_code = jwt_decrypt(enc_signed_qr_code)['data'] + data['DecryptedSignedInvoice'] = json.loads(signed_invoice) + data['DecryptedSignedQRCode'] = json.loads(signed_qr_code) + +def handle_err_response(response): + if response.get('Status') == 0: + err_details = response.get('ErrorDetails') + print(response) + err_msg = "" + for d in err_details: + err_msg += d.get('ErrorMessage') + err_msg += "
" + frappe.throw(_(err_msg), title=_('API Request Failed')) + +def read_json(name): + file_path = os.path.join(os.path.dirname(__file__), "{name}.json".format(name=name)) + with open(file_path, 'r') as f: + return cstr(f.read()) + +def get_trans_details(invoice): + supply_type = '' + if invoice.gst_category == 'Registered Regular': supply_type = 'B2B' + elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP' + elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP' + elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP' + + if not supply_type: + return _('Invalid invoice transaction category.') + + return frappe._dict(dict( + tax_scheme='GST', supply_type=supply_type, reverse_charge=invoice.reverse_charge + )) + +def get_doc_details(invoice): + invoice_type = 'CRN' if invoice.is_return else 'INV' + invoice_name = invoice.name + invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy') + + return frappe._dict(dict( + invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date + )) + +def get_party_gstin_details(party_address): + gstin, address_line1, address_line2, phone, email_id = frappe.db.get_value( + "Address", party_address, ["gstin", "address_line1", "address_line2", "phone", "email_id"] + ) + gstin_details = self.get_gstin_details(gstin) + legal_name = gstin_details.get('LegalName') + trade_name = gstin_details.get('TradeName') + location = gstin_details.get('AddrLoc') + state_code = gstin_details.get('StateCode') + pincode = cint(gstin_details.get('AddrPncd')) + if state_code == 97: + pincode = 999999 + + return frappe._dict(dict( + gstin=gstin, legal_name=legal_name, trade_name=trade_name, location=location, + pincode=pincode, state_code=state_code, address_line1=address_line1, + address_line2=address_line2, email=email_id, phone=phone + )) + +def get_overseas_address_details(party_address): + address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value( + "Address", party_address, ["address_title", "address_line1", "address_line2", "city", "phone", "email_id"] + ) + + return frappe._dict(dict( + gstin='URP', legal_name=address_title, address_line1=address_line1, + address_line2=address_line2, email=email_id, phone=phone, + pincode=999999, state_code=96, place_of_supply=96, location=city + )) + +def get_item_list(invoice): + item_list = [] + gst_accounts = get_gst_accounts(invoice.company) + gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] + + for d in invoice.items: + item_schema = read_json("einv_item_schema") + item = frappe._dict(dict()) + item.update(d.as_dict()) + item.sr_no = d.idx + item.description = d.item_name + item.is_service_item = "N" if frappe.db.get_value("Item", d.item_code, "is_stock_item") else "Y" + item.batch_expiry_date = frappe.db.get_value("Batch", d.batch_no, "expiry_date") if d.batch_no else None + item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None + item.tax_rate = 0 + item.igst_amount = 0 + item.cgst_amount = 0 + item.sgst_amount = 0 + item.cess_rate = 0 + item.cess_amount = 0 + for t in invoice.taxes: + if t.account_head in gst_accounts_list: + item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) + if t.account_head in gst_accounts.cess_account: + item.cess_rate += item_tax_detail[0] + item.cess_amount += item_tax_detail[1] + elif t.account_head in gst_accounts.igst_account: + item.tax_rate += item_tax_detail[0] + item.igst_amount += item_tax_detail[1] + elif t.account_head in gst_accounts.sgst_account: + item.tax_rate += item_tax_detail[0] + item.sgst_amount += item_tax_detail[1] + elif t.account_head in gst_accounts.cgst_account: + item.tax_rate += item_tax_detail[0] + item.cgst_amount += item_tax_detail[1] + + item.total_value = item.base_amount + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount + e_inv_item = item_schema.format(item=item) + item_list.append(e_inv_item) + + return ", ".join(item_list) + +def get_value_details(invoice): + gst_accounts = get_gst_accounts(invoice.company) + gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] + + value_details = frappe._dict(dict()) + value_details.base_net_total = invoice.base_net_total + value_details.invoice_discount_amt = invoice.discount_amount + value_details.round_off = invoice.base_rounding_adjustment + value_details.base_grand_total = invoice.base_rounded_total + value_details.grand_total = invoice.rounded_total + value_details.total_cgst_amt = 0 + value_details.total_sgst_amt = 0 + value_details.total_igst_amt = 0 + value_details.total_cess_amt = 0 + for t in invoice.taxes: + if t.account_head in gst_accounts_list: + if t.account_head in gst_accounts.cess_account: + value_details.total_cess_amt += t.base_tax_amount + elif t.account_head in gst_accounts.igst_account: + value_details.total_igst_amt += t.base_tax_amount + elif t.account_head in gst_accounts.sgst_account: + value_details.total_sgst_amt += t.base_tax_amount + elif t.account_head in gst_accounts.cgst_account: + value_details.total_cgst_amt += t.base_tax_amount + + return value_details + +def get_payment_details(invoice): + payee_name = invoice.company + mode_of_payment = ", ".join([d.mode_of_payment for d in invoice.payments]) + paid_amount = invoice.base_paid_amount + outstanding_amount = invoice.outstanding_amount + + return frappe._dict(dict( + payee_name=payee_name, mode_of_payment=mode_of_payment, + paid_amount=paid_amount, outstanding_amount=outstanding_amount + )) + +def get_return_doc_reference(invoice): + invoice_date = frappe.db.get_value("Sales Invoice", invoice.return_against, "posting_date") + return frappe._dict(dict( + invoice_name=invoice.return_against, invoice_date=invoice_date + )) + +def make_e_invoice(invoice): + schema = read_json("einv_schema") + validations = read_json("einv_validation") + validations = json.loads(validations) + + trans_details = get_trans_details(invoice) + doc_details = get_doc_details(invoice) + seller_details = get_party_gstin_details(invoice.company_address) + + if invoice.gst_category == 'Overseas': + buyer_details = get_overseas_address_details(invoice.customer_address) + else: + buyer_details = get_party_gstin_details(invoice.customer_address) + place_of_supply = invoice.place_of_supply.split('-')[0] + buyer_details.update(dict(place_of_supply=place_of_supply)) + + item_list = get_item_list(invoice) + value_details = get_value_details(invoice) + + dispatch_details = frappe._dict({}) + period_details = frappe._dict({}) + shipping_details = frappe._dict({}) + export_details = frappe._dict({}) + eway_bill_details = frappe._dict({}) + if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: + shipping_details = get_party_gstin_details(invoice.shipping_address_name) + + payment_details = frappe._dict({}) + if invoice.is_pos and invoice.base_paid_amount: + payment_details = get_payment_details(invoice) + + prev_doc_details = frappe._dict({}) + if invoice.is_return and invoice.return_against: + prev_doc_details = get_return_doc_reference(invoice) + + e_invoice = schema.format( + trans_details=trans_details, doc_details=doc_details, dispatch_details=dispatch_details, + seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details, + item_list=item_list, value_details=value_details, payment_details=payment_details, + period_details=period_details, prev_doc_details=prev_doc_details, + export_details=export_details, eway_bill_details=eway_bill_details + ) + e_invoice = json.loads(e_invoice) + + run_e_invoice_validations(validations, e_invoice) + + return json.dumps(e_invoice) + +def run_e_invoice_validations(validations, e_invoice): + type_map = { + "string": cstr, + "number": cint, + "object": dict, + "array": list + } + # validate root mandatory keys + mandatory_fields = validations.get('required') + if mandatory_fields and not set(mandatory_fields).issubset(set(e_invoice.keys())): + print("Mandatory condition failed") + + for field, value in validations.items(): + if isinstance(value, list): value = value[0] + + invoice_value = e_invoice.get(field) + if not invoice_value: + print(field, "value undefined") + continue + + should_be_of_type = type_map[value.get('type').lower()] + if should_be_of_type == dict: + properties = value.get('properties') + + if isinstance(invoice_value, list): + for d in invoice_value: + self.run_e_invoice_validations(properties, d) + else: + self.run_e_invoice_validations(properties, invoice_value) + if not invoice_value: + e_invoice.pop(field, None) + continue + + if invoice_value == "None": + e_invoice.pop(field, None) + continue + + e_invoice[field] = should_be_of_type(invoice_value) if e_invoice[field] else e_invoice[field] + + should_be_of_len = value.get('maxLength') + should_be_greater_than = value.get('minimum') + should_be_less_than = value.get('maximum') + pattern_str = value.get('pattern') + pattern = re.compile(pattern_str or "") + + if should_be_of_type == 'string' and not len(invoice_value) <= should_be_of_len: + print("Max Length Exceeded", field, invoice_value) + if should_be_of_type == 'number' and not (should_be_greater_than <= invoice_value <= should_be_of_len): + print("Value too large", field, invoice_value) + if pattern_str and not pattern.match(invoice_value): + print("Pattern Mismatch", field, invoice_value) \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_settings/e_inv_item_schema.json b/erpnext/regional/india/einv_item_schema.json similarity index 100% rename from erpnext/regional/doctype/e_invoice_settings/e_inv_item_schema.json rename to erpnext/regional/india/einv_item_schema.json diff --git a/erpnext/regional/doctype/e_invoice_settings/e_inv_schema.json b/erpnext/regional/india/einv_schema.json similarity index 100% rename from erpnext/regional/doctype/e_invoice_settings/e_inv_schema.json rename to erpnext/regional/india/einv_schema.json diff --git a/erpnext/regional/doctype/e_invoice_settings/e_inv_validation.json b/erpnext/regional/india/einv_validation.json similarity index 100% rename from erpnext/regional/doctype/e_invoice_settings/e_inv_validation.json rename to erpnext/regional/india/einv_validation.json From 221e5993a8ab87788b089baf6da75b94a1278e57 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Sep 2020 18:39:13 +0530 Subject: [PATCH 016/115] chore: rename schema to template & js cleanup --- .../e_invoice_settings/e_invoice_settings.js | 44 +------------------ erpnext/regional/india/e_invoice_utils.py | 5 ++- ...em_schema.json => einv_item_template.json} | 0 .../{einv_schema.json => einv_template.json} | 0 4 files changed, 5 insertions(+), 44 deletions(-) rename erpnext/regional/india/{einv_item_schema.json => einv_item_template.json} (100%) rename erpnext/regional/india/{einv_schema.json => einv_template.json} (100%) diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js index c97ebed013f..1b7efeeeddf 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js @@ -4,58 +4,18 @@ frappe.ui.form.on('E Invoice Settings', { refresh: function(frm) { if (!frm.doc.enable) return; - frm.trigger("show_fetch_token_btn"); - frm.add_custom_button(__("Get GSTIN Details"), - () => { - frm.call({ - doc: frm.doc, - method: 'get_gstin_details', - args: { - 'gstin': '36AAECF1151A1ZC' - }, - freeze: true, - callback: (res) => console.log(res) - }); - }); - - frm.add_custom_button(__("Generate IRN"), - () => { - frm.call({ - doc: frm.doc, - method: 'generate_irn', - args: { - 'invoice': 'SINV-20-21-0051' - }, - freeze: true, - callback: (res) => console.log(res) - }); - }); - - frm.add_custom_button(__("Fetch IRN Details"), - () => { - frm.call({ - doc: frm.doc, - method: 'get_irn_details', - args: { - 'irn': 'c63d9e180dfdaa9242e29e2e1e0a8d76f20e116ed3de179a2e9120f384e1b432' - }, - freeze: true, - callback: (res) => console.log(res) - }); - }); }, show_fetch_token_btn(frm) { const { token_expiry } = frm.doc; const now = frappe.datetime.now_datetime(); const expiry_in_mins = moment(token_expiry).diff(now, "minute"); - if (expiry_in_mins <= 1) { + if (expiry_in_mins <= 1 || !token_expiry) { frm.add_custom_button(__("Fetch Token"), () => { frm.call({ - doc: frm.doc, - method: 'make_authentication_request', + method: 'erpnext.regional.india.e_invoice_utils.fetch_token', freeze: true, callback: () => frm.refresh() }); diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index 3abcf16a72a..228430b580a 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -65,6 +65,7 @@ def get_header(creds): headers.update(dict(Gstin=creds.gstin, AuthToken=creds.auth_token)) return headers +@frappe.whitelist() def fetch_token(self): einv_creds = get_einv_credentials() @@ -250,7 +251,7 @@ def get_item_list(invoice): gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] for d in invoice.items: - item_schema = read_json("einv_item_schema") + item_schema = read_json("einv_item_template") item = frappe._dict(dict()) item.update(d.as_dict()) item.sr_no = d.idx @@ -331,7 +332,7 @@ def get_return_doc_reference(invoice): )) def make_e_invoice(invoice): - schema = read_json("einv_schema") + schema = read_json("einv_template") validations = read_json("einv_validation") validations = json.loads(validations) diff --git a/erpnext/regional/india/einv_item_schema.json b/erpnext/regional/india/einv_item_template.json similarity index 100% rename from erpnext/regional/india/einv_item_schema.json rename to erpnext/regional/india/einv_item_template.json diff --git a/erpnext/regional/india/einv_schema.json b/erpnext/regional/india/einv_template.json similarity index 100% rename from erpnext/regional/india/einv_schema.json rename to erpnext/regional/india/einv_template.json From 395ecf239222be5b1883bea68d60bfb613b6a1e1 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Sep 2020 18:39:35 +0530 Subject: [PATCH 017/115] feat: make IRN field on regional setup --- .../doctype/e_invoice_settings/e_invoice_settings.py | 3 +++ erpnext/regional/india/setup.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py index 1d86208347d..cdfe89be307 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -7,6 +7,7 @@ import frappe from frappe import _ from frappe.utils.data import cstr from frappe.model.document import Document +from frappe.custom.doctype.property_setter.property_setter import make_property_setter class EInvoiceSettings(Document): def validate(self): @@ -19,6 +20,8 @@ class EInvoiceSettings(Document): if not self.public_key or self.has_value_changed('public_key_file'): self.public_key = self.read_key_file() + make_property_setter("Sales Invoice", "irn", "reqd", self.enable, "Data") + def read_key_file(self): key_file = frappe.get_doc('File', dict(attached_to_name=self.doctype, attached_to_field='public_key_file')) with open(key_file.get_full_path(), 'rb') as f: diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 77a466fdff7..a66298d270f 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -376,6 +376,10 @@ def make_custom_fields(update=True): } ] + si_einvoice_fields = [ + dict(fieldname='irn', label='IRN', fieldtyp='Data', insert_after='customer') + ] + custom_fields = { 'Address': [ dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data', @@ -388,7 +392,7 @@ def make_custom_fields(update=True): 'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields, 'Purchase Order': purchase_invoice_gst_fields, 'Purchase Receipt': purchase_invoice_gst_fields, - 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields, + 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields, 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields, 'Sales Order': sales_invoice_gst_fields, 'Tax Category': inter_state_gst_field, From 18666b6a7484ca80153488c3bb915af28793179b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Sep 2020 22:30:02 +0530 Subject: [PATCH 018/115] feat: Generate & Cancel IRN from Sales Invoice --- .../doctype/sales_invoice/regional/india.js | 2 + .../doctype/sales_invoice/sales_invoice.py | 2 +- erpnext/controllers/accounts_controller.py | 10 ++++ erpnext/hooks.py | 3 +- .../e_invoice_settings/e_invoice_settings.py | 2 - erpnext/regional/india/e_invoice_utils.py | 58 +++++++++++++----- erpnext/regional/india/einvoice.js | 59 +++++++++++++++++++ erpnext/regional/india/setup.py | 5 +- 8 files changed, 122 insertions(+), 19 deletions(-) create mode 100644 erpnext/regional/india/einvoice.js diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js index 1ed4b92e7a4..e97671ad6e0 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js @@ -1,6 +1,8 @@ {% include "erpnext/regional/india/taxes.js" %} +{% include "erpnext/regional/india/einvoice.js" %} erpnext.setup_auto_gst_taxation('Sales Invoice'); +erpnext.setup_einvoice_actions('Sales Invoice') frappe.ui.form.on("Sales Invoice", { setup: function(frm) { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 7f5603b58b0..bd09253c559 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -225,9 +225,9 @@ class SalesInvoice(SellingController): frappe.throw(_("At least one mode of payment is required for POS invoice.")) def before_cancel(self): + super(SalesInvoice, self).before_cancel() self.update_time_sheet(None) - def on_cancel(self): super(SalesInvoice, self).on_cancel() diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 58c7e847910..fba4d4c8483 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -106,8 +106,14 @@ class AccountsController(TransactionBase): self.validate_deferred_start_and_end_date() validate_regional(self) + + validate_einvoice_fields(self) + if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) + + def before_cancel(self): + validate_einvoice_fields(self) def validate_deferred_start_and_end_date(self): for d in self.items: @@ -1406,3 +1412,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil @erpnext.allow_regional def validate_regional(doc): pass + +@erpnext.allow_regional +def validate_einvoice_fields(doc): + pass diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 5270e7beea2..d5b49a2ee22 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -357,7 +357,8 @@ regional_overrides = { 'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details', 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', - 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries' + 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries', + 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice_utils.validate_einvoice_fields', }, 'United Arab Emirates': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data' diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py index cdfe89be307..9f64c081c23 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -20,8 +20,6 @@ class EInvoiceSettings(Document): if not self.public_key or self.has_value_changed('public_key_file'): self.public_key = self.read_key_file() - make_property_setter("Sales Invoice", "irn", "reqd", self.enable, "Data") - def read_key_file(self): key_file = frappe.get_doc('File', dict(attached_to_name=self.doctype, attached_to_field='public_key_file')) with open(key_file.get_full_path(), 'rb') as f: diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index 228430b580a..fdba06183de 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -18,6 +18,14 @@ from erpnext.regional.india.utils import get_gst_accounts from frappe.utils.data import get_datetime, cstr, cint, format_date from frappe.integrations.utils import make_post_request, make_get_request +def validate_einvoice_fields(doc): + if not doc.doctype in ['Sales Invoice', 'Purchase Invoice']: return + + if doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: + frappe.throw(_("You must generate IRN before submitting the document."), title=_("Missing IRN")) + elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: + frappe.throw(_("You must cancel IRN before cancelling the document."), title=_("Not Allowed")) + def get_einv_credentials(): return frappe.get_doc("E Invoice Settings") @@ -66,7 +74,7 @@ def get_header(creds): return headers @frappe.whitelist() -def fetch_token(self): +def fetch_token(): einv_creds = get_einv_credentials() endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' @@ -100,6 +108,16 @@ def extract_token_and_sek(response, appkey): sek = aes_decrypt(enc_sek, appkey) return auth_token, token_expiry, sek +def attach_signed_json(invoice, data): + f = frappe.get_doc({ + "doctype": "File", + "file_name": invoice.name + "e_invoice.json", + "attached_to_doctype": invoice.doctype, + "attached_to_name": invoice.name, + "content": json.dumps(data), + "is_private": True + }).insert() + def get_gstin_details(gstin): einv_creds = get_einv_credentials() @@ -115,19 +133,20 @@ def get_gstin_details(gstin): return data -def generate_irn(invoice): - einv_creds = get_einv_credentials() - +@frappe.whitelist() +def generate_irn(doctype, name): endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice' + einv_creds = get_einv_credentials() headers = get_header(einv_creds) + invoice = frappe.get_doc(doctype, name) e_invoice = make_e_invoice(invoice) enc_e_invoice_json = aes_encrypt(e_invoice, einv_creds.sek) payload = dict(Data=enc_e_invoice_json) res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) - handle_err_response(res) + res = handle_err_response(res) enc_json = res.get('Data') json_str = aes_decrypt(enc_json, einv_creds.sek) @@ -135,6 +154,8 @@ def generate_irn(invoice): data = json.loads(json_str) handle_irn_response(data) + attach_signed_json(invoice, data['DecryptedSignedInvoice']) + return data def get_irn_details(irn): @@ -146,19 +167,20 @@ def get_irn_details(irn): res = make_get_request(endpoint, headers=headers) handle_err_response(res) - enc_json = res.get('Data') - json_str = aes_decrypt(enc_json, einv_creds.sek) + # enc_json = res.get('Data') + # json_str = aes_decrypt(enc_json, einv_creds.sek) - data = json.loads(json_str) - handle_irn_response(data) + # data = json.loads(json_str) + # handle_irn_response(data) - return data + return res +@frappe.whitelist() def cancel_irn(irn, reason, remark=''): einv_creds = get_einv_credentials() endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/Cancel' - headers = get_header() + headers = get_header(einv_creds) cancel_e_inv = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark)) enc_json = aes_encrypt(cancel_e_inv, einv_creds.sek) @@ -183,10 +205,18 @@ def handle_err_response(response): print(response) err_msg = "" for d in err_details: + err_code = d.get('ErrorCode') + if err_code == '2150': + irn = [d['Desc']['Irn'] for d in response.get('InfoDtls') if d['InfCd'] == 'DUPIRN'] + response = get_irn_details(irn[0]) + return response + err_msg += d.get('ErrorMessage') err_msg += "
" frappe.throw(_(err_msg), title=_('API Request Failed')) + return response + def read_json(name): file_path = os.path.join(os.path.dirname(__file__), "{name}.json".format(name=name)) with open(file_path, 'r') as f: @@ -219,7 +249,7 @@ def get_party_gstin_details(party_address): gstin, address_line1, address_line2, phone, email_id = frappe.db.get_value( "Address", party_address, ["gstin", "address_line1", "address_line2", "phone", "email_id"] ) - gstin_details = self.get_gstin_details(gstin) + gstin_details = get_gstin_details(gstin) legal_name = gstin_details.get('LegalName') trade_name = gstin_details.get('TradeName') location = gstin_details.get('AddrLoc') @@ -405,9 +435,9 @@ def run_e_invoice_validations(validations, e_invoice): if isinstance(invoice_value, list): for d in invoice_value: - self.run_e_invoice_validations(properties, d) + run_e_invoice_validations(properties, d) else: - self.run_e_invoice_validations(properties, invoice_value) + run_e_invoice_validations(properties, invoice_value) if not invoice_value: e_invoice.pop(field, None) continue diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js new file mode 100644 index 00000000000..76a2bff6e56 --- /dev/null +++ b/erpnext/regional/india/einvoice.js @@ -0,0 +1,59 @@ +erpnext.setup_einvoice_actions = (doctype) => { + frappe.ui.form.on(doctype, { + refresh(frm) { + const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable"); + if (!einvoicing_enabled) return; + + if (frm.doc.docstatus == 0 && !frm.doc.irn) { + frm.add_custom_button( + "Generate IRN", + () => { + frappe.call({ + method: 'erpnext.regional.india.e_invoice_utils.generate_irn', + args: { doctype: frm.doc.doctype, name: frm.doc.name }, + freeze: true, + callback: (res) => { + console.log(res.message); + frm.set_value('irn', res.message['Irn']); + frm.set_value('signed_einvoice', JSON.stringify(res.message['DecryptedSignedInvoice'])); + frm.set_value('signed_qr_code', JSON.stringify(res.message['DecryptedSignedQRCode'])); + frm.save(); + } + }) + } + ) + } else if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) { + frm.add_custom_button( + "Cancel IRN", + () => { + const d = new frappe.ui.Dialog({ + title: __('Cancel IRN'), + fields: [ + { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data entry mistake", "3-Order Cancelled", "4-Other"] }, + { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 } + ], + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice_utils.cancel_irn', + args: { irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark }, + freeze: true, + callback: (res) => { + if (res.message['Status'] == 1) { + frm.set_value('irn_cancelled', 1); + frm.save_or_update(); + } + d.hide(); + } + }) + }, + primary_action_label: __('Submit') + }); + d.show(); + } + ) + } + } + }) +} \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index a66298d270f..5343f16456b 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -377,7 +377,10 @@ def make_custom_fields(update=True): ] si_einvoice_fields = [ - dict(fieldname='irn', label='IRN', fieldtyp='Data', insert_after='customer') + dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer'), + dict(fieldname='irn_cancelled', fieldtype='Check', hidden=1, read_only=1, default=0, allow_on_submit=1), + dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, read_only=1), + dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, read_only=1) ] custom_fields = { From 9003718481178f6add80fa51c62053bb141ff396 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 12:28:03 +0530 Subject: [PATCH 019/115] chore: minor fixes --- erpnext/regional/india/einvoice.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index 76a2bff6e56..d03a4c87e03 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -4,7 +4,7 @@ erpnext.setup_einvoice_actions = (doctype) => { const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable"); if (!einvoicing_enabled) return; - if (frm.doc.docstatus == 0 && !frm.doc.irn) { + if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) { frm.add_custom_button( "Generate IRN", () => { @@ -30,7 +30,7 @@ erpnext.setup_einvoice_actions = (doctype) => { title: __('Cancel IRN'), fields: [ { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", - "options": ["1-Duplicate", "2-Data entry mistake", "3-Order Cancelled", "4-Other"] }, + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 } ], primary_action: function() { From cf2ebc0d6f7951e58e698bcd8b5bc0a7f3e0b211 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 12:59:27 +0530 Subject: [PATCH 020/115] fix: item discount --- erpnext/regional/india/e_invoice_utils.py | 47 +++++++++++-------- .../regional/india/einv_item_template.json | 4 +- erpnext/regional/india/einv_validation.json | 3 +- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index fdba06183de..e45ae37bf3b 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -15,7 +15,7 @@ from Crypto.Util.Padding import pad, unpad from frappe.model.document import Document from frappe import _, get_module_path, scrub from erpnext.regional.india.utils import get_gst_accounts -from frappe.utils.data import get_datetime, cstr, cint, format_date +from frappe.utils.data import get_datetime, cstr, cint, format_date, flt from frappe.integrations.utils import make_post_request, make_get_request def validate_einvoice_fields(doc): @@ -289,6 +289,9 @@ def get_item_list(invoice): item.is_service_item = "N" if frappe.db.get_value("Item", d.item_code, "is_stock_item") else "Y" item.batch_expiry_date = frappe.db.get_value("Batch", d.batch_no, "expiry_date") if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None + item.unit_rate = item.base_price_list_rate if item.discount_amount else item.base_rate + item.total_amount = item.unit_rate * item.qty + item.discount_amount = item.discount_amount * item.qty item.tax_rate = 0 item.igst_amount = 0 item.cgst_amount = 0 @@ -315,6 +318,8 @@ def get_item_list(invoice): e_inv_item = item_schema.format(item=item) item_list.append(e_inv_item) + print(e_inv_item) + return ", ".join(item_list) def get_value_details(invoice): @@ -405,28 +410,25 @@ def make_e_invoice(invoice): ) e_invoice = json.loads(e_invoice) - run_e_invoice_validations(validations, e_invoice) + error_msgs = run_e_invoice_validations(validations, e_invoice, []) + if error_msgs: + frappe.throw(_("{}").format("
".join(error_msgs)), title=_("E Invoice Validation")) return json.dumps(e_invoice) -def run_e_invoice_validations(validations, e_invoice): +def run_e_invoice_validations(validations, e_invoice, error_msgs=[]): type_map = { "string": cstr, "number": cint, "object": dict, "array": list } - # validate root mandatory keys - mandatory_fields = validations.get('required') - if mandatory_fields and not set(mandatory_fields).issubset(set(e_invoice.keys())): - print("Mandatory condition failed") for field, value in validations.items(): if isinstance(value, list): value = value[0] invoice_value = e_invoice.get(field) if not invoice_value: - print(field, "value undefined") continue should_be_of_type = type_map[value.get('type').lower()] @@ -435,28 +437,35 @@ def run_e_invoice_validations(validations, e_invoice): if isinstance(invoice_value, list): for d in invoice_value: - run_e_invoice_validations(properties, d) + run_e_invoice_validations(properties, d, error_msgs) else: - run_e_invoice_validations(properties, invoice_value) + run_e_invoice_validations(properties, invoice_value, error_msgs) + # remove keys with empty dicts if not invoice_value: e_invoice.pop(field, None) continue if invoice_value == "None": + # remove keys with empty values e_invoice.pop(field, None) continue - - e_invoice[field] = should_be_of_type(invoice_value) if e_invoice[field] else e_invoice[field] + + # convert to int or str + e_invoice[field] = should_be_of_type(invoice_value) should_be_of_len = value.get('maxLength') - should_be_greater_than = value.get('minimum') - should_be_less_than = value.get('maximum') + should_be_greater_than = flt(value.get('minimum')) + should_be_less_than = flt(value.get('maximum')) pattern_str = value.get('pattern') pattern = re.compile(pattern_str or "") - if should_be_of_type == 'string' and not len(invoice_value) <= should_be_of_len: - print("Max Length Exceeded", field, invoice_value) - if should_be_of_type == 'number' and not (should_be_greater_than <= invoice_value <= should_be_of_len): - print("Value too large", field, invoice_value) + field_label = value.get("label") or field + + if value.get('type').lower() == 'string' and len(invoice_value) > should_be_of_len: + error_msgs.append("{} should not exceed {} characters".format(field_label, should_be_of_len)) + if value.get('type').lower() == 'number' and not (flt(invoice_value) <= should_be_less_than): + error_msgs.append("{} should be less than {}".format(field_label, should_be_less_than)) if pattern_str and not pattern.match(invoice_value): - print("Pattern Mismatch", field, invoice_value) \ No newline at end of file + error_msgs.append("{} should match {}".format(field_label, pattern_str)) + + return error_msgs \ No newline at end of file diff --git a/erpnext/regional/india/einv_item_template.json b/erpnext/regional/india/einv_item_template.json index b4f54448326..f87b0f15f3c 100644 --- a/erpnext/regional/india/einv_item_template.json +++ b/erpnext/regional/india/einv_item_template.json @@ -7,8 +7,8 @@ "Unit": "{item.uom}", "Qty": "{item.qty}", "FreeQty": "{item.free_qty}", - "UnitPrice": "{item.base_rate}", - "TotAmt": "{item.base_amount}", + "UnitPrice": "{item.unit_rate}", + "TotAmt": "{item.total_amount}", "Discount": "{item.discount_amount}", "AssAmt": "{item.base_amount}", "PrdSlNo": "{item.serial_no}", diff --git a/erpnext/regional/india/einv_validation.json b/erpnext/regional/india/einv_validation.json index da224d9633f..28bfc396e30 100644 --- a/erpnext/regional/india/einv_validation.json +++ b/erpnext/regional/india/einv_validation.json @@ -58,7 +58,8 @@ "type": "string", "minLength": 1, "maxLength": 16, - "pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$" + "pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$", + "label": "Document Name" }, "Dt": { "type": "string", From db13eb56c4ab94e1d073053f14096a32b8a17aa8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 13:07:38 +0530 Subject: [PATCH 021/115] chore: show irn cancelled check after cancellation --- erpnext/regional/india/setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 5343f16456b..c3bfb842a13 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -377,8 +377,10 @@ def make_custom_fields(update=True): ] si_einvoice_fields = [ - dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer'), - dict(fieldname='irn_cancelled', fieldtype='Check', hidden=1, read_only=1, default=0, allow_on_submit=1), + dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, + insert_after='customer', depends_on='eval:(doc.irn_cancelled === 0)'), + dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', + depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, read_only=1), dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, read_only=1) ] From 7a40ad340f55e9d9dac084783d8744dcd7b9b916 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 13:14:07 +0530 Subject: [PATCH 022/115] fix: hide cancel irn dialog on error --- erpnext/regional/india/einvoice.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index d03a4c87e03..3aed88fb0eb 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -45,7 +45,8 @@ erpnext.setup_einvoice_actions = (doctype) => { frm.save_or_update(); } d.hide(); - } + }, + error: () => d.hide() }) }, primary_action_label: __('Submit') From cea1a15dcd86cf9c038b96ad02bad4ad19a17a75 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 13:56:52 +0530 Subject: [PATCH 023/115] fix: public key is required on validate --- .../regional/doctype/e_invoice_settings/e_invoice_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py index 9f64c081c23..14ca3726506 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -11,7 +11,7 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_ class EInvoiceSettings(Document): def validate(self): - mandatory_fields = ['client_id', 'client_secret', 'gstin', 'username', 'password', 'public_key'] + mandatory_fields = ['client_id', 'client_secret', 'gstin', 'username', 'password', 'public_key_file'] for d in mandatory_fields: if not self.get(d): frappe.throw(_("{} is required").format(frappe.unscrub(d)), title=_("Missing Values")) From 175b26e6e8ddbbdf44bf8753b7e9e4d58d1105f1 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 14:32:12 +0530 Subject: [PATCH 024/115] fix: cannot find attached key file --- .../regional/doctype/e_invoice_settings/e_invoice_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py index 14ca3726506..e90d07edbd6 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -21,6 +21,6 @@ class EInvoiceSettings(Document): self.public_key = self.read_key_file() def read_key_file(self): - key_file = frappe.get_doc('File', dict(attached_to_name=self.doctype, attached_to_field='public_key_file')) + key_file = frappe.get_doc('File', dict(attached_to_name=self.doctype)) with open(key_file.get_full_path(), 'rb') as f: return cstr(f.read()) \ No newline at end of file From 389610fb450aae3561c680fc1ddb27ec1c22db35 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 15:02:08 +0530 Subject: [PATCH 025/115] fix: validation if e invoicing is disabled --- erpnext/regional/india/e_invoice_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index e45ae37bf3b..001e9ac358e 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -19,7 +19,8 @@ from frappe.utils.data import get_datetime, cstr, cint, format_date, flt from frappe.integrations.utils import make_post_request, make_get_request def validate_einvoice_fields(doc): - if not doc.doctype in ['Sales Invoice', 'Purchase Invoice']: return + e_invoice_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable") + if not doc.doctype in ['Sales Invoice', 'Purchase Invoice'] or not e_invoice_enabled: return if doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: frappe.throw(_("You must generate IRN before submitting the document."), title=_("Missing IRN")) From 59a757b3e3fcde094aa1b095798dc2f33676c095 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 15:17:30 +0530 Subject: [PATCH 026/115] fix: do not show generate irn for invalid supply type --- erpnext/regional/india/einvoice.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index 3aed88fb0eb..6ba306a0563 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -2,7 +2,11 @@ erpnext.setup_einvoice_actions = (doctype) => { frappe.ui.form.on(doctype, { refresh(frm) { const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable"); - if (!einvoicing_enabled) return; + const supply_type = frm.doc.gst_category; + if (!einvoicing_enabled + || !['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type)) { + return; + } if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) { frm.add_custom_button( From dfbdd0b2eed1b1a5f2137c16f32af08802313b46 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 17:10:18 +0530 Subject: [PATCH 027/115] fix: update irn_cancelled after cancelling irn --- erpnext/regional/india/einvoice.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index 6ba306a0563..95ccaa5907b 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -43,12 +43,10 @@ erpnext.setup_einvoice_actions = (doctype) => { method: 'erpnext.regional.india.e_invoice_utils.cancel_irn', args: { irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark }, freeze: true, - callback: (res) => { - if (res.message['Status'] == 1) { - frm.set_value('irn_cancelled', 1); - frm.save_or_update(); - } - d.hide(); + callback: () => { + frm.set_value('irn_cancelled', 1); + frm.save("Update"); + d.hide() }, error: () => d.hide() }) From 816235e99c971e12ce47e5ba351b2c0b035f35fe Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 17:14:58 +0530 Subject: [PATCH 028/115] chore: show irn field for proper gst_category --- erpnext/regional/india/setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index c3bfb842a13..9adaf8048eb 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -377,8 +377,8 @@ def make_custom_fields(update=True): ] si_einvoice_fields = [ - dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, - insert_after='customer', depends_on='eval:(doc.irn_cancelled === 0)'), + dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', + depends_on='eval:in_list(["Register Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, read_only=1), From 6a9af076e5c32f3a684fc3a5b0a8aa28604c41a6 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 18:24:24 +0530 Subject: [PATCH 029/115] feat: e-way bill details in e-invoice --- erpnext/regional/india/e_invoice_utils.py | 37 +++++++++++++++++++---- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index 001e9ac358e..f597e329621 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -238,7 +238,11 @@ def get_trans_details(invoice): )) def get_doc_details(invoice): - invoice_type = 'CRN' if invoice.is_return else 'INV' + if invoice.doctype == 'Purchase Invoice' and invoice.is_return: + invoice_type = 'DBN' + else: + invoice_type = 'CRN' if invoice.is_return else 'INV' + invoice_name = invoice.name invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy') @@ -367,6 +371,24 @@ def get_return_doc_reference(invoice): invoice_name=invoice.return_against, invoice_date=invoice_date )) +def get_eway_bill_details(invoice): + if not invoice.distance: + frappe.throw(_("Distance is mandatory for E-Way Bill generation"), title=_("Missing Values")) + + mode_of_transport = { 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } + vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } + + return frappe._dict(dict( + gstin=invoice.gst_transporter_id, + name=invoice.transporter_name, + mode_of_transport=mode_of_transport[invoice.mode_of_transport], + distance=invoice.distance, + document_name=invoice.lr_no, + document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'), + vehicle_no=invoice.vehicle_no, + vehicle_type=vehicle_type[invoice.gst_vehicle_type] + )) + def make_e_invoice(invoice): schema = read_json("einv_template") validations = read_json("einv_validation") @@ -385,12 +407,8 @@ def make_e_invoice(invoice): item_list = get_item_list(invoice) value_details = get_value_details(invoice) - - dispatch_details = frappe._dict({}) - period_details = frappe._dict({}) + shipping_details = frappe._dict({}) - export_details = frappe._dict({}) - eway_bill_details = frappe._dict({}) if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: shipping_details = get_party_gstin_details(invoice.shipping_address_name) @@ -401,6 +419,13 @@ def make_e_invoice(invoice): prev_doc_details = frappe._dict({}) if invoice.is_return and invoice.return_against: prev_doc_details = get_return_doc_reference(invoice) + + dispatch_details = frappe._dict({}) + period_details = frappe._dict({}) + export_details = frappe._dict({}) + eway_bill_details = frappe._dict({}) + if invoice.transporter: + eway_bill_details = get_eway_bill_details(invoice) e_invoice = schema.format( trans_details=trans_details, doc_details=doc_details, dispatch_details=dispatch_details, From 22754a9c583654242e883899437a5a3f2d761df5 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 18:51:39 +0530 Subject: [PATCH 030/115] fix: save e-way bill no on irn generation --- erpnext/regional/india/einvoice.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index 95ccaa5907b..05cf56e8f51 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -21,6 +21,8 @@ erpnext.setup_einvoice_actions = (doctype) => { frm.set_value('irn', res.message['Irn']); frm.set_value('signed_einvoice', JSON.stringify(res.message['DecryptedSignedInvoice'])); frm.set_value('signed_qr_code', JSON.stringify(res.message['DecryptedSignedQRCode'])); + + if (res.message['EwbNo']) frm.set_value('ewaybill', res.message['EwbNo']); frm.save(); } }) From ec282629887c2b824cd4e5f5b135c6a5bf0334e4 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 18:52:05 +0530 Subject: [PATCH 031/115] chore: no copy on e invoice custom fields --- erpnext/regional/india/setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 9adaf8048eb..0b032019145 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -377,12 +377,12 @@ def make_custom_fields(update=True): ] si_einvoice_fields = [ - dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', + dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, depends_on='eval:in_list(["Register Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), - dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', + dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1 depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), - dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, read_only=1), - dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, read_only=1) + dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, read_only=1), + dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, read_only=1) ] custom_fields = { From cc43805f25c1e3b69c18d78cf5bc6a409d95eac7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 30 Sep 2020 19:34:09 +0530 Subject: [PATCH 032/115] feat: cancel e-way bill before cancelling IRN --- erpnext/regional/india/e_invoice_utils.py | 15 ++++++++++ erpnext/regional/india/einvoice.js | 35 ++++++++++++++++++++++- erpnext/regional/india/setup.py | 8 ++++-- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index f597e329621..dc38b25bed7 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -192,6 +192,21 @@ def cancel_irn(irn, reason, remark=''): return res +@frappe.whitelist() +def cancel_eway_bill(eway_bill, reason, remark=''): + einv_creds = get_einv_credentials() + endpoint = 'https://einv-apisandbox.nic.in/ewaybillapi/v1.03/ewayapi' + headers = get_header(einv_creds) + + cancel_eway_bill_json = json.dumps(dict(ewbNo=eway_bill, cancelRsnCode=reason, cancelRmrk=remark)) + enc_json = aes_encrypt(cancel_eway_bill_json, einv_creds.sek) + payload = dict(action="CANEWB", Data=enc_json) + + res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) + handle_err_response(res) + + return res + def handle_irn_response(data): enc_signed_invoice = data['SignedInvoice'] enc_signed_qr_code = data['SignedQRCode'] diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index 05cf56e8f51..20461ea317b 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -28,7 +28,8 @@ erpnext.setup_einvoice_actions = (doctype) => { }) } ) - } else if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) { + } + if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) { frm.add_custom_button( "Cancel IRN", () => { @@ -59,6 +60,38 @@ erpnext.setup_einvoice_actions = (doctype) => { } ) } + if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled && !frm.doc.eway_bill_cancelled) { + frm.add_custom_button( + "Cancel E-Way Bill", + () => { + const d = new frappe.ui.Dialog({ + title: __('Cancel E-Way Bill'), + fields: [ + { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, + { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 } + ], + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice_utils.cancel_eway_bill', + args: { eway_bill: frm.doc.ewaybill, reason: data.reason.split('-')[0], remark: data.remark }, + freeze: true, + callback: () => { + frm.set_value('eway_bill_cancelled', 1); + frm.save("Update"); + d.hide() + }, + error: () => d.hide() + }) + }, + primary_action_label: __('Submit') + }); + d.show(); + } + ) + } } + }) } \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 0b032019145..d5aba17a88f 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -369,7 +369,7 @@ def make_custom_fields(update=True): 'fieldname': 'ewaybill', 'label': 'e-Way Bill No.', 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.docstatus === 1)', + 'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)', 'allow_on_submit': 1, 'insert_after': 'tax_id', 'translatable': 0 @@ -379,10 +379,12 @@ def make_custom_fields(update=True): si_einvoice_fields = [ dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, depends_on='eval:in_list(["Register Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), - dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1 + dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, read_only=1), - dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, read_only=1) + dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, read_only=1), + dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, + depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), ] custom_fields = { From 08e89b88f4c45fd2dca1f096601440a787f0b27d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 6 Oct 2020 21:27:00 +0530 Subject: [PATCH 033/115] feat: manual download / upload json --- erpnext/regional/india/e_invoice_utils.py | 95 +++++++--- erpnext/regional/india/einv_validation.json | 21 ++- erpnext/regional/india/einvoice.js | 188 ++++++++++++++------ 3 files changed, 214 insertions(+), 90 deletions(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index dc38b25bed7..6109f800f7d 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -22,10 +22,12 @@ def validate_einvoice_fields(doc): e_invoice_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable") if not doc.doctype in ['Sales Invoice', 'Purchase Invoice'] or not e_invoice_enabled: return - if doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: + if doc.docstatus == 0 and doc._action == 'save' and doc.irn: + frappe.throw(_("You cannot edit the invoice after generating IRN"), title=_("Edit Not Allowed")) + elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: frappe.throw(_("You must generate IRN before submitting the document."), title=_("Missing IRN")) elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: - frappe.throw(_("You must cancel IRN before cancelling the document."), title=_("Not Allowed")) + frappe.throw(_("You must cancel IRN before cancelling the document."), title=_("Cancel Not Allowed")) def get_einv_credentials(): return frappe.get_doc("E Invoice Settings") @@ -266,20 +268,24 @@ def get_doc_details(invoice): )) def get_party_gstin_details(party_address): - gstin, address_line1, address_line2, phone, email_id = frappe.db.get_value( - "Address", party_address, ["gstin", "address_line1", "address_line2", "phone", "email_id"] - ) - gstin_details = get_gstin_details(gstin) - legal_name = gstin_details.get('LegalName') - trade_name = gstin_details.get('TradeName') - location = gstin_details.get('AddrLoc') - state_code = gstin_details.get('StateCode') - pincode = cint(gstin_details.get('AddrPncd')) + address = frappe.get_all("Address", filters={"name": party_address}, fields=["*"])[0] + + # gstin_details = get_gstin_details(gstin) + gstin = address.get('gstin') + legal_name = address.get('address_title') + # trade_name = gstin_details.get('TradeName') + location = address.get('city') + state_code = address.get('gst_state_number') + pincode = cint(address.get('pincode')) + address_line1 = address.get('address_line1') + address_line2 = address.get('address_line2') + email_id = address.get('email_id') + phone = address.get('phone') if state_code == 97: pincode = 999999 return frappe._dict(dict( - gstin=gstin, legal_name=legal_name, trade_name=trade_name, location=location, + gstin=gstin, legal_name=legal_name, location=location, pincode=pincode, state_code=state_code, address_line1=address_line1, address_line2=address_line2, email=email_id, phone=phone )) @@ -338,8 +344,6 @@ def get_item_list(invoice): e_inv_item = item_schema.format(item=item) item_list.append(e_inv_item) - print(e_inv_item) - return ", ".join(item_list) def get_value_details(invoice): @@ -404,10 +408,11 @@ def get_eway_bill_details(invoice): vehicle_type=vehicle_type[invoice.gst_vehicle_type] )) -def make_e_invoice(invoice): +@frappe.whitelist() +def make_e_invoice(doctype, name): + invoice = frappe.get_doc(doctype, name) schema = read_json("einv_template") - validations = read_json("einv_validation") - validations = json.loads(validations) + validations = json.loads(read_json("einv_validation")) trans_details = get_trans_details(invoice) doc_details = get_doc_details(invoice) @@ -451,13 +456,17 @@ def make_e_invoice(invoice): ) e_invoice = json.loads(e_invoice) - error_msgs = run_e_invoice_validations(validations, e_invoice, []) + error_msgs = validate_einvoice(validations, e_invoice, []) if error_msgs: - frappe.throw(_("{}").format("
".join(error_msgs)), title=_("E Invoice Validation")) + if len(error_msgs) > 1: + li = ["
  • "+ d +"
  • " for d in error_msgs] + frappe.throw(_("""
      {}
    """).format("".join(li)), title=_("E Invoice Validation Failed")) + else: + frappe.throw(_("{}").format(error_msgs[0]), title=_("E Invoice Validation Failed")) - return json.dumps(e_invoice) + return {'einvoice': json.dumps(e_invoice)} -def run_e_invoice_validations(validations, e_invoice, error_msgs=[]): +def validate_einvoice(validations, e_invoice, error_msgs=[]): type_map = { "string": cstr, "number": cint, @@ -478,9 +487,9 @@ def run_e_invoice_validations(validations, e_invoice, error_msgs=[]): if isinstance(invoice_value, list): for d in invoice_value: - run_e_invoice_validations(properties, d, error_msgs) + validate_einvoice(properties, d, error_msgs) else: - run_e_invoice_validations(properties, invoice_value, error_msgs) + validate_einvoice(properties, invoice_value, error_msgs) # remove keys with empty dicts if not invoice_value: e_invoice.pop(field, None) @@ -507,6 +516,42 @@ def run_e_invoice_validations(validations, e_invoice, error_msgs=[]): if value.get('type').lower() == 'number' and not (flt(invoice_value) <= should_be_less_than): error_msgs.append("{} should be less than {}".format(field_label, should_be_less_than)) if pattern_str and not pattern.match(invoice_value): - error_msgs.append("{} should match {}".format(field_label, pattern_str)) + error_msgs.append(value.get('validationMsg')) - return error_msgs \ No newline at end of file + return error_msgs + +@frappe.whitelist() +def download_einvoice(): + data = frappe._dict(frappe.local.form_dict) + einvoice = data['einvoice'] + name = data['name'] + + frappe.response['filename'] = "E-Invoice-" + name + ".json" + frappe.response['filecontent'] = einvoice + frappe.response['content_type'] = 'application/json' + frappe.response['type'] = 'download' + +@frappe.whitelist() +def upload_einvoice(): + signed_einvoice = json.loads(frappe.local.uploaded_file) + data = frappe._dict(frappe.local.form_dict) + doctype = data['doctype'] + name = data['docname'] + + frappe.db.set_value(doctype, name, "irn", signed_einvoice.get("Irn")) + frappe.db.set_value(doctype, name, "ewaybill", signed_einvoice.get("EwbNo")) + +@frappe.whitelist() +def download_cancel_einvoice(): + data = frappe._dict(frappe.local.form_dict) + name = data['name'] + irn = data['irn'] + reason = data['reason'] + remark = data['remark'] + + cancel_einvoice = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark)) + + frappe.response['filename'] = "Cancel E-Invoice " + name + ".json" + frappe.response['filecontent'] = cancel_einvoice + frappe.response['content_type'] = 'application/json' + frappe.response['type'] = 'download' \ No newline at end of file diff --git a/erpnext/regional/india/einv_validation.json b/erpnext/regional/india/einv_validation.json index 28bfc396e30..9f0ef5006c4 100644 --- a/erpnext/regional/india/einv_validation.json +++ b/erpnext/regional/india/einv_validation.json @@ -59,13 +59,15 @@ "minLength": 1, "maxLength": 16, "pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$", - "label": "Document Name" + "label": "Document Name", + "validationMsg": "Document name should not be starting with 0, / and -" }, "Dt": { "type": "string", "minLength": 10, "maxLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "validationMsg": "Document Date is invalid" } }, "required": ["Typ", "No", "Dt"] @@ -77,7 +79,8 @@ "type": "string", "minLength": 15, "maxLength": 15, - "pattern": "([0-9]{2}[0-9A-Z]{13})" + "pattern": "([0-9]{2}[0-9A-Z]{13})", + "validationMsg": "Seller GSTIN is invalid" }, "LglNm": { "type": "string", @@ -134,7 +137,8 @@ "type": "string", "minLength": 3, "maxLength": 15, - "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$" + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", + "validationMsg": "Buyer GSTIN is invalid" }, "LglNm": { "type": "string", @@ -232,7 +236,8 @@ "type": "string", "maxLength": 15, "minLength": 3, - "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$" + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", + "validationMsg": "Shipping Address GSTIN is invalid" }, "LglNm": { "type": "string", @@ -429,13 +434,15 @@ "type": "string", "maxLength": 10, "minLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "validationMsg": "Expiry Date is invalid" }, "WrDt": { "type": "string", "maxLength": 10, "minLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "validationMsg": "Warranty Date is invalid" } }, "required": ["Nm"] diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index 20461ea317b..4f98ffc88ab 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -7,27 +7,131 @@ erpnext.setup_einvoice_actions = (doctype) => { || !['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type)) { return; } + // if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) { + // frm.add_custom_button( + // "Generate IRN", + // () => { + // frappe.call({ + // method: 'erpnext.regional.india.e_invoice_utils.generate_irn', + // args: { doctype: frm.doc.doctype, name: frm.doc.name }, + // freeze: true, + // callback: (res) => { + // console.log(res.message); + // frm.set_value('irn', res.message['Irn']); + // frm.set_value('signed_einvoice', JSON.stringify(res.message['DecryptedSignedInvoice'])); + // frm.set_value('signed_qr_code', JSON.stringify(res.message['DecryptedSignedQRCode'])); + + // if (res.message['EwbNo']) frm.set_value('ewaybill', res.message['EwbNo']); + // frm.save(); + // } + // }) + // } + // ) + // } + + // if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) { + // frm.add_custom_button( + // "Cancel IRN", + // () => { + // const d = new frappe.ui.Dialog({ + // title: __('Cancel IRN'), + // fields: [ + // { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", + // "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, + // { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 } + // ], + // primary_action: function() { + // const data = d.get_values(); + // frappe.call({ + // method: 'erpnext.regional.india.e_invoice_utils.cancel_irn', + // args: { irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark }, + // freeze: true, + // callback: () => { + // frm.set_value('irn_cancelled', 1); + // frm.save("Update"); + // d.hide() + // }, + // error: () => d.hide() + // }) + // }, + // primary_action_label: __('Submit') + // }); + // d.show(); + // } + // ) + // } + + // if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled && !frm.doc.eway_bill_cancelled) { + // frm.add_custom_button( + // "Cancel E-Way Bill", + // () => { + // const d = new frappe.ui.Dialog({ + // title: __('Cancel E-Way Bill'), + // fields: [ + // { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", + // "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, + // { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 } + // ], + // primary_action: function() { + // const data = d.get_values(); + // frappe.call({ + // method: 'erpnext.regional.india.e_invoice_utils.cancel_eway_bill', + // args: { eway_bill: frm.doc.ewaybill, reason: data.reason.split('-')[0], remark: data.remark }, + // freeze: true, + // callback: () => { + // frm.set_value('eway_bill_cancelled', 1); + // frm.save("Update"); + // d.hide() + // }, + // error: () => d.hide() + // }) + // }, + // primary_action_label: __('Submit') + // }); + // d.show(); + // } + // ) + // } if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) { frm.add_custom_button( - "Generate IRN", + "Download E-Invoice", () => { frappe.call({ - method: 'erpnext.regional.india.e_invoice_utils.generate_irn', + method: 'erpnext.regional.india.e_invoice_utils.make_e_invoice', args: { doctype: frm.doc.doctype, name: frm.doc.name }, freeze: true, callback: (res) => { - console.log(res.message); - frm.set_value('irn', res.message['Irn']); - frm.set_value('signed_einvoice', JSON.stringify(res.message['DecryptedSignedInvoice'])); - frm.set_value('signed_qr_code', JSON.stringify(res.message['DecryptedSignedQRCode'])); - - if (res.message['EwbNo']) frm.set_value('ewaybill', res.message['EwbNo']); - frm.save(); + if (!res.exc) { + const args = { + cmd: 'erpnext.regional.india.e_invoice_utils.download_einvoice', + einvoice: res.message.einvoice, + name: frm.doc.name + }; + open_url_post(frappe.request.url, args); + } } }) } - ) + ); + } + if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) { + frm.add_custom_button( + "Upload Signed E-Invoice", + () => { + new frappe.ui.FileUploader({ + method: 'erpnext.regional.india.e_invoice_utils.upload_einvoice', + allow_multiple: 0, + doctype: frm.doc.doctype, + docname: frm.doc.name, + on_success: (attachment, r) => { + if (!r.exc) { + frm.reload_doc(); + } + } + }); + } + ); } if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) { frm.add_custom_button( @@ -36,62 +140,30 @@ erpnext.setup_einvoice_actions = (doctype) => { const d = new frappe.ui.Dialog({ title: __('Cancel IRN'), fields: [ - { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", - "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, - { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 } + { + "label" : "Reason", "fieldname": "reason", + "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 + } ], primary_action: function() { const data = d.get_values(); - frappe.call({ - method: 'erpnext.regional.india.e_invoice_utils.cancel_irn', - args: { irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark }, - freeze: true, - callback: () => { - frm.set_value('irn_cancelled', 1); - frm.save("Update"); - d.hide() - }, - error: () => d.hide() - }) + const args = { + cmd: 'erpnext.regional.india.e_invoice_utils.download_cancel_einvoice', + irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark, name: frm.doc.name + }; + open_url_post(frappe.request.url, args); + d.hide(); }, - primary_action_label: __('Submit') + primary_action_label: __('Download JSON') }); d.show(); } - ) - } - if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled && !frm.doc.eway_bill_cancelled) { - frm.add_custom_button( - "Cancel E-Way Bill", - () => { - const d = new frappe.ui.Dialog({ - title: __('Cancel E-Way Bill'), - fields: [ - { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", - "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, - { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 } - ], - primary_action: function() { - const data = d.get_values(); - frappe.call({ - method: 'erpnext.regional.india.e_invoice_utils.cancel_eway_bill', - args: { eway_bill: frm.doc.ewaybill, reason: data.reason.split('-')[0], remark: data.remark }, - freeze: true, - callback: () => { - frm.set_value('eway_bill_cancelled', 1); - frm.save("Update"); - d.hide() - }, - error: () => d.hide() - }) - }, - primary_action_label: __('Submit') - }); - d.show(); - } - ) + ); } } - }) } \ No newline at end of file From ada41295bb990ef302916f51f02ab11f6d606c94 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 7 Oct 2020 11:07:11 +0530 Subject: [PATCH 034/115] chore: group e-invoicing actions --- erpnext/regional/india/e_invoice_utils.py | 15 ++++++++++--- erpnext/regional/india/einvoice.js | 26 ++++++++++++++++------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index 6109f800f7d..b33c15507c0 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -464,7 +464,7 @@ def make_e_invoice(doctype, name): else: frappe.throw(_("{}").format(error_msgs[0]), title=_("E Invoice Validation Failed")) - return {'einvoice': json.dumps(e_invoice)} + return {'einvoice': json.dumps([e_invoice])} def validate_einvoice(validations, e_invoice, error_msgs=[]): type_map = { @@ -549,9 +549,18 @@ def download_cancel_einvoice(): reason = data['reason'] remark = data['remark'] - cancel_einvoice = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark)) + cancel_einvoice = json.dumps([dict(Irn=irn, CnlRsn=reason, CnlRem=remark)]) frappe.response['filename'] = "Cancel E-Invoice " + name + ".json" frappe.response['filecontent'] = cancel_einvoice frappe.response['content_type'] = 'application/json' - frappe.response['type'] = 'download' \ No newline at end of file + frappe.response['type'] = 'download' + +@frappe.whitelist() +def download_cancel_ack(): + cancel_ack = json.loads(frappe.local.uploaded_file) + data = frappe._dict(frappe.local.form_dict) + doctype = data['doctype'] + name = data['docname'] + + frappe.db.set_value(doctype, name, "irn_cancelled", 1) \ No newline at end of file diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index 4f98ffc88ab..2255358b8da 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -112,10 +112,7 @@ erpnext.setup_einvoice_actions = (doctype) => { } } }) - } - ); - } - if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) { + }, "E-Invoicing"); frm.add_custom_button( "Upload Signed E-Invoice", () => { @@ -130,8 +127,7 @@ erpnext.setup_einvoice_actions = (doctype) => { } } }); - } - ); + }, "E-Invoicing"); } if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) { frm.add_custom_button( @@ -161,8 +157,22 @@ erpnext.setup_einvoice_actions = (doctype) => { primary_action_label: __('Download JSON') }); d.show(); - } - ); + }, "E-Invoicing"); + frm.add_custom_button( + "Upload Cancel JSON", + () => { + new frappe.ui.FileUploader({ + method: 'erpnext.regional.india.e_invoice_utils.download_cancel_ack', + allow_multiple: 0, + doctype: frm.doc.doctype, + docname: frm.doc.name, + on_success: (attachment, r) => { + if (!r.exc) { + frm.reload_doc(); + } + } + }); + }, "E-Invoicing"); } } }) From 61824d3735fab7f59fc499988d94841759c7034b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 7 Oct 2020 11:08:11 +0530 Subject: [PATCH 035/115] fix: fn name --- erpnext/regional/india/e_invoice_utils.py | 2 +- erpnext/regional/india/einvoice.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index b33c15507c0..f7ae758a601 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -557,7 +557,7 @@ def download_cancel_einvoice(): frappe.response['type'] = 'download' @frappe.whitelist() -def download_cancel_ack(): +def upload_cancel_ack(): cancel_ack = json.loads(frappe.local.uploaded_file) data = frappe._dict(frappe.local.form_dict) doctype = data['doctype'] diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index 2255358b8da..f1494b81716 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -162,7 +162,7 @@ erpnext.setup_einvoice_actions = (doctype) => { "Upload Cancel JSON", () => { new frappe.ui.FileUploader({ - method: 'erpnext.regional.india.e_invoice_utils.download_cancel_ack', + method: 'erpnext.regional.india.e_invoice_utils.upload_cancel_ack', allow_multiple: 0, doctype: frm.doc.doctype, docname: frm.doc.name, From b3e0f482ac02c4b6c3ea22f16d4dd1d78d5ce1bd Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 7 Oct 2020 11:21:47 +0530 Subject: [PATCH 036/115] chore: save signed invoice and qrcode after uplaoding irn --- erpnext/regional/india/e_invoice_utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index f7ae758a601..8d54f7e90ad 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -538,8 +538,13 @@ def upload_einvoice(): doctype = data['doctype'] name = data['docname'] - frappe.db.set_value(doctype, name, "irn", signed_einvoice.get("Irn")) - frappe.db.set_value(doctype, name, "ewaybill", signed_einvoice.get("EwbNo")) + enc_signed_invoice = signed_einvoice.get('SignedInvoice') + decrypted_signed_invoice = jwt_decrypt(enc_signed_invoice)['data'] + + frappe.db.set_value(doctype, name, 'irn', signed_einvoice.get('Irn')) + frappe.db.set_value(doctype, name, 'ewaybill', signed_einvoice.get('EwbNo')) + frappe.db.set_value(doctype, name, 'signed_qr_code', signed_einvoice.get('SignedQRCode')) + frappe.db.set_value(doctype, name, 'signed_einvoice', decrypted_signed_invoice) @frappe.whitelist() def download_cancel_einvoice(): From 9c00b227036064d8ee28fd8db492e7f856f8cc9e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 7 Oct 2020 12:26:45 +0530 Subject: [PATCH 037/115] fix: fetch token if not valid --- erpnext/regional/india/e_invoice_utils.py | 23 +++++++++++++++-------- erpnext/regional/india/einvoice.js | 3 ++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index 8d54f7e90ad..86f65285244 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -15,8 +15,8 @@ from Crypto.Util.Padding import pad, unpad from frappe.model.document import Document from frappe import _, get_module_path, scrub from erpnext.regional.india.utils import get_gst_accounts -from frappe.utils.data import get_datetime, cstr, cint, format_date, flt from frappe.integrations.utils import make_post_request, make_get_request +from frappe.utils.data import get_datetime, cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime def validate_einvoice_fields(doc): e_invoice_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable") @@ -29,8 +29,13 @@ def validate_einvoice_fields(doc): elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: frappe.throw(_("You must cancel IRN before cancelling the document."), title=_("Cancel Not Allowed")) -def get_einv_credentials(): - return frappe.get_doc("E Invoice Settings") +def get_einv_credentials(for_token=False): + creds = frappe.get_doc("E Invoice Settings") + if not for_token and (not creds.token_expiry or time_diff_in_seconds(now_datetime(), creds.token_expiry) > 5.0): + fetch_token() + creds.load_from_db() + + return creds def rsa_encrypt(msg, key): if not (isinstance(msg, bytes) or isinstance(msg, bytearray)): @@ -78,7 +83,7 @@ def get_header(creds): @frappe.whitelist() def fetch_token(): - einv_creds = get_einv_credentials() + einv_creds = get_einv_credentials(for_token=True) endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' headers = { 'content-type': 'application/json' } @@ -270,11 +275,13 @@ def get_doc_details(invoice): def get_party_gstin_details(party_address): address = frappe.get_all("Address", filters={"name": party_address}, fields=["*"])[0] - # gstin_details = get_gstin_details(gstin) gstin = address.get('gstin') - legal_name = address.get('address_title') - # trade_name = gstin_details.get('TradeName') - location = address.get('city') + gstin_details = get_gstin_details(gstin) + # legal_name = address.get('address_title') + legal_name = gstin_details.get('LegalName') + trade_name = gstin_details.get('TradeName') + # location = address.get('city') + location = gstin_details.get('Loc') state_code = address.get('gst_state_number') pincode = cint(address.get('pincode')) address_line1 = address.get('address_line1') diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index f1494b81716..f30e81a500c 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -157,7 +157,8 @@ erpnext.setup_einvoice_actions = (doctype) => { primary_action_label: __('Download JSON') }); d.show(); - }, "E-Invoicing"); + }, "E-Invoicing"); + frm.add_custom_button( "Upload Cancel JSON", () => { From d3a6b9ed81f20e91e93c103909001ee3aa2f726f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 10 Oct 2020 12:35:50 +0530 Subject: [PATCH 038/115] chore: move einvoicing stuff to seperate folder --- .../doctype/sales_invoice/regional/india.js | 2 +- erpnext/hooks.py | 2 +- .../e_invoice_settings/e_invoice_settings.js | 2 +- .../india/{ => e_invoice}/e_invoice_utils.py | 0 .../{ => e_invoice}/einv_item_template.json | 0 .../india/{ => e_invoice}/einv_template.json | 0 .../india/{ => e_invoice}/einv_validation.json | 0 .../regional/india/{ => e_invoice}/einvoice.js | 16 ++++++++-------- 8 files changed, 11 insertions(+), 11 deletions(-) rename erpnext/regional/india/{ => e_invoice}/e_invoice_utils.py (100%) rename erpnext/regional/india/{ => e_invoice}/einv_item_template.json (100%) rename erpnext/regional/india/{ => e_invoice}/einv_template.json (100%) rename erpnext/regional/india/{ => e_invoice}/einv_validation.json (100%) rename erpnext/regional/india/{ => e_invoice}/einvoice.js (88%) diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js index e97671ad6e0..ba672c9368d 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js @@ -1,5 +1,5 @@ {% include "erpnext/regional/india/taxes.js" %} -{% include "erpnext/regional/india/einvoice.js" %} +{% include "erpnext/regional/india/e_invoice/einvoice.js" %} erpnext.setup_auto_gst_taxation('Sales Invoice'); erpnext.setup_einvoice_actions('Sales Invoice') diff --git a/erpnext/hooks.py b/erpnext/hooks.py index d5b49a2ee22..fa0ab7535c6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -358,7 +358,7 @@ regional_overrides = { 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries', - 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice_utils.validate_einvoice_fields', + 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.e_invoice_utils.validate_einvoice_fields', }, 'United Arab Emirates': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data' diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js index 1b7efeeeddf..e9fb622b6b6 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js @@ -15,7 +15,7 @@ frappe.ui.form.on('E Invoice Settings', { frm.add_custom_button(__("Fetch Token"), () => { frm.call({ - method: 'erpnext.regional.india.e_invoice_utils.fetch_token', + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.fetch_token', freeze: true, callback: () => frm.refresh() }); diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py similarity index 100% rename from erpnext/regional/india/e_invoice_utils.py rename to erpnext/regional/india/e_invoice/e_invoice_utils.py diff --git a/erpnext/regional/india/einv_item_template.json b/erpnext/regional/india/e_invoice/einv_item_template.json similarity index 100% rename from erpnext/regional/india/einv_item_template.json rename to erpnext/regional/india/e_invoice/einv_item_template.json diff --git a/erpnext/regional/india/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json similarity index 100% rename from erpnext/regional/india/einv_template.json rename to erpnext/regional/india/e_invoice/einv_template.json diff --git a/erpnext/regional/india/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json similarity index 100% rename from erpnext/regional/india/einv_validation.json rename to erpnext/regional/india/e_invoice/einv_validation.json diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js similarity index 88% rename from erpnext/regional/india/einvoice.js rename to erpnext/regional/india/e_invoice/einvoice.js index f30e81a500c..8191bea82fc 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -12,7 +12,7 @@ erpnext.setup_einvoice_actions = (doctype) => { // "Generate IRN", // () => { // frappe.call({ - // method: 'erpnext.regional.india.e_invoice_utils.generate_irn', + // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.generate_irn', // args: { doctype: frm.doc.doctype, name: frm.doc.name }, // freeze: true, // callback: (res) => { @@ -43,7 +43,7 @@ erpnext.setup_einvoice_actions = (doctype) => { // primary_action: function() { // const data = d.get_values(); // frappe.call({ - // method: 'erpnext.regional.india.e_invoice_utils.cancel_irn', + // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.cancel_irn', // args: { irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark }, // freeze: true, // callback: () => { @@ -75,7 +75,7 @@ erpnext.setup_einvoice_actions = (doctype) => { // primary_action: function() { // const data = d.get_values(); // frappe.call({ - // method: 'erpnext.regional.india.e_invoice_utils.cancel_eway_bill', + // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.cancel_eway_bill', // args: { eway_bill: frm.doc.ewaybill, reason: data.reason.split('-')[0], remark: data.remark }, // freeze: true, // callback: () => { @@ -98,13 +98,13 @@ erpnext.setup_einvoice_actions = (doctype) => { "Download E-Invoice", () => { frappe.call({ - method: 'erpnext.regional.india.e_invoice_utils.make_e_invoice', + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.make_e_invoice', args: { doctype: frm.doc.doctype, name: frm.doc.name }, freeze: true, callback: (res) => { if (!res.exc) { const args = { - cmd: 'erpnext.regional.india.e_invoice_utils.download_einvoice', + cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_einvoice', einvoice: res.message.einvoice, name: frm.doc.name }; @@ -117,7 +117,7 @@ erpnext.setup_einvoice_actions = (doctype) => { "Upload Signed E-Invoice", () => { new frappe.ui.FileUploader({ - method: 'erpnext.regional.india.e_invoice_utils.upload_einvoice', + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_einvoice', allow_multiple: 0, doctype: frm.doc.doctype, docname: frm.doc.name, @@ -148,7 +148,7 @@ erpnext.setup_einvoice_actions = (doctype) => { primary_action: function() { const data = d.get_values(); const args = { - cmd: 'erpnext.regional.india.e_invoice_utils.download_cancel_einvoice', + cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_cancel_einvoice', irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark, name: frm.doc.name }; open_url_post(frappe.request.url, args); @@ -163,7 +163,7 @@ erpnext.setup_einvoice_actions = (doctype) => { "Upload Cancel JSON", () => { new frappe.ui.FileUploader({ - method: 'erpnext.regional.india.e_invoice_utils.upload_cancel_ack', + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_cancel_ack', allow_multiple: 0, doctype: frm.doc.doctype, docname: frm.doc.name, From 154c433a869bcd31578836b7555023baa28d390c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 10 Oct 2020 19:43:25 +0530 Subject: [PATCH 039/115] feat: QRCode Image and E-Invoice Print Format --- .../print_format/gst_e_invoice/__init__.py | 0 .../gst_e_invoice/gst_e_invoice.html | 147 ++++++++++++++++++ .../gst_e_invoice/gst_e_invoice.json | 24 +++ .../india/e_invoice/e_invoice_utils.py | 85 ++++++---- .../india/e_invoice/einv_template.json | 2 +- erpnext/regional/india/e_invoice/einvoice.js | 24 ++- erpnext/regional/india/setup.py | 18 ++- 7 files changed, 246 insertions(+), 54 deletions(-) create mode 100644 erpnext/accounts/print_format/gst_e_invoice/__init__.py create mode 100644 erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html create mode 100644 erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json diff --git a/erpnext/accounts/print_format/gst_e_invoice/__init__.py b/erpnext/accounts/print_format/gst_e_invoice/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html new file mode 100644 index 00000000000..15ea1c21744 --- /dev/null +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -0,0 +1,147 @@ +{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%} +{%- set einvoice = json.loads(doc.signed_einvoice) -%} + +
    +
    + +
    +
    +
    1. Transaction Details
    +
    +
    +
    +
    {{ einvoice.Irn }}
    +
    +
    +
    +
    {{ einvoice.AckNo }}
    +
    +
    +
    +
    {{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}
    +
    +
    +
    +
    {{ einvoice.TranDtls.SupTyp }}
    +
    +
    +
    +
    {{ einvoice.DocDtls.Typ }}
    +
    +
    +
    +
    {{ einvoice.DocDtls.No }}
    +
    +
    +
    + +
    +
    +
    +
    2. Party Details
    + {%- set seller = einvoice.SellerDtls -%} +
    +
    Seller
    +

    {{ seller.Gstin }}

    +

    {{ seller.LglNm }}

    +

    {{ seller.Addr1 }}

    + {%- if seller.Addr2 -%}

    {{ seller.Addr2 }}

    {% endif %} +

    {{ seller.Loc }}

    +

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

    + + {%- if einvoice.ShipDtls -%} + {%- set shipping = einvoice.ShipDtls -%} +
    Shipping
    +

    {{ shipping.Gstin }}

    +

    {{ shipping.LglNm }}

    +

    {{ shipping.Addr1 }}

    + {%- if shipping.Addr2 -%}

    {{ shipping.Addr2 }}

    {% endif %} +

    {{ shipping.Loc }}

    +

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

    + {% endif %} +
    + {%- set buyer = einvoice.BuyerDtls -%} +
    +
    Buyer
    +

    {{ buyer.Gstin }}

    +

    {{ buyer.LglNm }}

    +

    {{ buyer.Addr1 }}

    + {%- if buyer.Addr2 -%}

    {{ buyer.Addr2 }}

    {% endif %} +

    {{ buyer.Loc }}

    +

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

    +
    +
    +
    +
    3. Item Details
    + + + + + + + + + + + + + + + + + + + {% for item in einvoice.ItemList %} + + + + + + + + + + + + {% endfor %} + + +
    Sr. No.ItemHSN CodeQtyUOMRateDiscountTaxable AmountTax RateOther ChargesTotal
    {{ item.SlNo }}{{ item.PrdDesc }}{{ item.HsnCd }}{{ item.Qty }}{{ item.Unit }}{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}{{ item.GstRt + item.CesRt }} %{{ frappe.utils.fmt_money(0, None, "INR") }}{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}
    +
    +
    +
    4. Value Details
    + + + + + + + + + + + + + + + + + {%- set value_details = einvoice.ValDtls -%} + + + + + + + + + + + + + +
    Taxable AmountCGSTSGSTIGSTCESSState CESSDiscountOther ChargesRound OffTotal Value
    {{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}{{ frappe.utils.fmt_money(0, None, "INR") }}{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}{{ frappe.utils.fmt_money(0, None, "INR") }}{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}
    +
    +
    \ No newline at end of file diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json new file mode 100644 index 00000000000..0a5c8a27882 --- /dev/null +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json @@ -0,0 +1,24 @@ +{ + "align_labels_right": 1, + "creation": "2020-10-10 18:01:21.032914", + "custom_format": 0, + "default_print_language": "en-US", + "disabled": 0, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "html": "", + "idx": 0, + "line_breaks": 1, + "modified": "2020-10-10 18:01:21.032914", + "modified_by": "Administrator", + "module": "Accounts", + "name": "GST E-Invoice", + "owner": "Administrator", + "print_format_builder": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 1, + "standard": "Yes" +} \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py index 86f65285244..71ee07da68d 100644 --- a/erpnext/regional/india/e_invoice/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice/e_invoice_utils.py @@ -10,6 +10,7 @@ import json import base64 import frappe from Crypto.PublicKey import RSA +from pyqrcode import create as qrcreate from Crypto.Cipher import PKCS1_v1_5, AES from Crypto.Util.Padding import pad, unpad from frappe.model.document import Document @@ -116,12 +117,12 @@ def extract_token_and_sek(response, appkey): sek = aes_decrypt(enc_sek, appkey) return auth_token, token_expiry, sek -def attach_signed_json(invoice, data): +def attach_signed_invoice(doctype, name, data): f = frappe.get_doc({ "doctype": "File", - "file_name": invoice.name + "e_invoice.json", - "attached_to_doctype": invoice.doctype, - "attached_to_name": invoice.name, + "file_name": name + "e_invoice.json", + "attached_to_doctype": doctype, + "attached_to_name": name, "content": json.dumps(data), "is_private": True }).insert() @@ -147,8 +148,7 @@ def generate_irn(doctype, name): einv_creds = get_einv_credentials() headers = get_header(einv_creds) - invoice = frappe.get_doc(doctype, name) - e_invoice = make_e_invoice(invoice) + e_invoice = make_e_invoice(doctype, name) enc_e_invoice_json = aes_encrypt(e_invoice, einv_creds.sek) payload = dict(Data=enc_e_invoice_json) @@ -159,12 +159,15 @@ def generate_irn(doctype, name): enc_json = res.get('Data') json_str = aes_decrypt(enc_json, einv_creds.sek) - data = json.loads(json_str) - handle_irn_response(data) + signed_einvoice = json.loads(json_str) + handle_irn_response(signed_einvoice) - attach_signed_json(invoice, data['DecryptedSignedInvoice']) + update_einvoice_fields(doctype, name, signed_einvoice) - return data + attach_qrcode_image(doctype, name) + attach_signed_invoice(doctype, name, signed_einvoice['DecryptedSignedInvoice']) + + return signed_einvoice def get_irn_details(irn): einv_creds = get_einv_credentials() @@ -175,16 +178,10 @@ def get_irn_details(irn): res = make_get_request(endpoint, headers=headers) handle_err_response(res) - # enc_json = res.get('Data') - # json_str = aes_decrypt(enc_json, einv_creds.sek) - - # data = json.loads(json_str) - # handle_irn_response(data) - return res @frappe.whitelist() -def cancel_irn(irn, reason, remark=''): +def cancel_irn(doctype, name, irn, reason, remark=''): einv_creds = get_einv_credentials() endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/Cancel' @@ -197,6 +194,8 @@ def cancel_irn(irn, reason, remark=''): res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) handle_err_response(res) + frappe.db.set_value(doctype, name, 'irn_cancelled', 1) + return res @frappe.whitelist() @@ -277,15 +276,13 @@ def get_party_gstin_details(party_address): gstin = address.get('gstin') gstin_details = get_gstin_details(gstin) - # legal_name = address.get('address_title') legal_name = gstin_details.get('LegalName') trade_name = gstin_details.get('TradeName') - # location = address.get('city') - location = gstin_details.get('Loc') - state_code = address.get('gst_state_number') - pincode = cint(address.get('pincode')) - address_line1 = address.get('address_line1') - address_line2 = address.get('address_line2') + location = gstin_details.get('AddrLoc') + state_code = gstin_details.get('StateCode') + pincode = cint(gstin_details.get('AddrPncd')) + address_line1 = "{} {}".format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno')) + address_line2 = "{} {}".format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt')) email_id = address.get('email_id') phone = address.get('phone') if state_code == 97: @@ -527,6 +524,15 @@ def validate_einvoice(validations, e_invoice, error_msgs=[]): return error_msgs +def update_einvoice_fields(doctype, name, signed_einvoice): + enc_signed_invoice = signed_einvoice.get('SignedInvoice') + decrypted_signed_invoice = jwt_decrypt(enc_signed_invoice)['data'] + + frappe.db.set_value(doctype, name, 'irn', signed_einvoice.get('Irn')) + frappe.db.set_value(doctype, name, 'ewaybill', signed_einvoice.get('EwbNo')) + frappe.db.set_value(doctype, name, 'signed_qr_code', signed_einvoice.get('SignedQRCode').split('.')[1]) + frappe.db.set_value(doctype, name, 'signed_einvoice', decrypted_signed_invoice) + @frappe.whitelist() def download_einvoice(): data = frappe._dict(frappe.local.form_dict) @@ -545,13 +551,7 @@ def upload_einvoice(): doctype = data['doctype'] name = data['docname'] - enc_signed_invoice = signed_einvoice.get('SignedInvoice') - decrypted_signed_invoice = jwt_decrypt(enc_signed_invoice)['data'] - - frappe.db.set_value(doctype, name, 'irn', signed_einvoice.get('Irn')) - frappe.db.set_value(doctype, name, 'ewaybill', signed_einvoice.get('EwbNo')) - frappe.db.set_value(doctype, name, 'signed_qr_code', signed_einvoice.get('SignedQRCode')) - frappe.db.set_value(doctype, name, 'signed_einvoice', decrypted_signed_invoice) + update_einvoice_fields(doctype, name, signed_einvoice) @frappe.whitelist() def download_cancel_einvoice(): @@ -575,4 +575,25 @@ def upload_cancel_ack(): doctype = data['doctype'] name = data['docname'] - frappe.db.set_value(doctype, name, "irn_cancelled", 1) \ No newline at end of file + frappe.db.set_value(doctype, name, "irn_cancelled", 1) + +def attach_qrcode_image(doctype, name): + qrcode = frappe.db.get_value(doctype, name, 'signed_qr_code') + + if not qrcode: return + + _file = frappe.get_doc({ + "doctype": "File", + "file_name": "Signed_QR_{name}.png".format(name=name), + "attached_to_doctype": doctype, + "attached_to_name": name, + "attached_to_field": "qrcode_image", + "content": "qrcode" + }) + _file.save() + frappe.db.commit() + url = qrcreate(qrcode) + abs_file_path = os.path.abspath(_file.get_full_path()) + url.png(abs_file_path, scale=2) + + frappe.db.set_value(doctype, name, 'qrcode_image', _file.file_url) \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json index 15230fe22f9..0acc01cd386 100644 --- a/erpnext/regional/india/e_invoice/einv_template.json +++ b/erpnext/regional/india/e_invoice/einv_template.json @@ -1,5 +1,5 @@ {{ - "Version": "1.01", + "Version": "1.1", "TranDtls": {{ "TaxSch": "{trans_details.tax_scheme}", "SupTyp": "{trans_details.supply_type}", diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 8191bea82fc..076f13475d8 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -15,15 +15,7 @@ erpnext.setup_einvoice_actions = (doctype) => { // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.generate_irn', // args: { doctype: frm.doc.doctype, name: frm.doc.name }, // freeze: true, - // callback: (res) => { - // console.log(res.message); - // frm.set_value('irn', res.message['Irn']); - // frm.set_value('signed_einvoice', JSON.stringify(res.message['DecryptedSignedInvoice'])); - // frm.set_value('signed_qr_code', JSON.stringify(res.message['DecryptedSignedQRCode'])); - - // if (res.message['EwbNo']) frm.set_value('ewaybill', res.message['EwbNo']); - // frm.save(); - // } + // callback: () => frm.reload_doc() // }) // } // ) @@ -44,13 +36,15 @@ erpnext.setup_einvoice_actions = (doctype) => { // const data = d.get_values(); // frappe.call({ // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.cancel_irn', - // args: { irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark }, - // freeze: true, - // callback: () => { - // frm.set_value('irn_cancelled', 1); - // frm.save("Update"); - // d.hide() + // args: { + // doctype: frm.doc.doctype, + // name: frm.doc.name, + // irn: frm.doc.irn, + // reason: data.reason.split('-')[0], + // remark: data.remark // }, + // freeze: true, + // callback: () => frm.reload_doc() || d.hide(), // error: () => d.hide() // }) // }, diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index d5aba17a88f..fc8004b4752 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -377,14 +377,20 @@ def make_custom_fields(update=True): ] si_einvoice_fields = [ - dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, - depends_on='eval:in_list(["Register Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), - dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, + dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, + depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), + + dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), - dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, read_only=1), - dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, read_only=1), - dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, + + dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1) ] custom_fields = { From d6f3be0fc150c6bf412be31fd1f1ddca23716e07 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 10 Oct 2020 19:47:29 +0530 Subject: [PATCH 040/115] fix: bug --- erpnext/regional/india/e_invoice/e_invoice_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py index 71ee07da68d..b499bb53353 100644 --- a/erpnext/regional/india/e_invoice/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice/e_invoice_utils.py @@ -17,7 +17,7 @@ from frappe.model.document import Document from frappe import _, get_module_path, scrub from erpnext.regional.india.utils import get_gst_accounts from frappe.integrations.utils import make_post_request, make_get_request -from frappe.utils.data import get_datetime, cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime +from frappe.utils.data import get_datetime, cstr, cint,format_date, flt, time_diff_in_seconds, now_datetime def validate_einvoice_fields(doc): e_invoice_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable") @@ -120,7 +120,7 @@ def extract_token_and_sek(response, appkey): def attach_signed_invoice(doctype, name, data): f = frappe.get_doc({ "doctype": "File", - "file_name": name + "e_invoice.json", + "file_name": "E-INV--{}.json".format{name), "attached_to_doctype": doctype, "attached_to_name": name, "content": json.dumps(data), From 662a0fcfe975bbb2f66a02dd23e31d32ebde7b7e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 10 Oct 2020 19:48:38 +0530 Subject: [PATCH 041/115] fix: invalid syntax --- erpnext/regional/india/e_invoice/e_invoice_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py index b499bb53353..f5f4098c7cb 100644 --- a/erpnext/regional/india/e_invoice/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice/e_invoice_utils.py @@ -17,7 +17,7 @@ from frappe.model.document import Document from frappe import _, get_module_path, scrub from erpnext.regional.india.utils import get_gst_accounts from frappe.integrations.utils import make_post_request, make_get_request -from frappe.utils.data import get_datetime, cstr, cint,format_date, flt, time_diff_in_seconds, now_datetime +from frappe.utils.data import get_datetime, cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime def validate_einvoice_fields(doc): e_invoice_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable") @@ -120,7 +120,7 @@ def extract_token_and_sek(response, appkey): def attach_signed_invoice(doctype, name, data): f = frappe.get_doc({ "doctype": "File", - "file_name": "E-INV--{}.json".format{name), + "file_name": "E-INV--{}.json".format(name), "attached_to_doctype": doctype, "attached_to_name": name, "content": json.dumps(data), From 08b4736ee0d46202dd06ec3319b21bd429a08437 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 10 Oct 2020 21:51:53 +0530 Subject: [PATCH 042/115] chore: code cleanup --- .../india/e_invoice/e_invoice_utils.py | 272 ++++++----- .../india/e_invoice/einv_validation.json | 422 +++++++++--------- erpnext/regional/india/e_invoice/einvoice.js | 2 +- 3 files changed, 344 insertions(+), 352 deletions(-) diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py index f5f4098c7cb..044c2c88da2 100644 --- a/erpnext/regional/india/e_invoice/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice/e_invoice_utils.py @@ -9,6 +9,7 @@ import jwt import json import base64 import frappe +from six import string_types from Crypto.PublicKey import RSA from pyqrcode import create as qrcreate from Crypto.Cipher import PKCS1_v1_5, AES @@ -20,23 +21,23 @@ from frappe.integrations.utils import make_post_request, make_get_request from frappe.utils.data import get_datetime, cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime def validate_einvoice_fields(doc): - e_invoice_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable") - if not doc.doctype in ['Sales Invoice', 'Purchase Invoice'] or not e_invoice_enabled: return + einvoicing_enabled = frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable') + if not doc.doctype in ['Sales Invoice', 'Purchase Invoice'] or not einvoicing_enabled: return if doc.docstatus == 0 and doc._action == 'save' and doc.irn: - frappe.throw(_("You cannot edit the invoice after generating IRN"), title=_("Edit Not Allowed")) + frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: - frappe.throw(_("You must generate IRN before submitting the document."), title=_("Missing IRN")) + frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: - frappe.throw(_("You must cancel IRN before cancelling the document."), title=_("Cancel Not Allowed")) + frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed')) -def get_einv_credentials(for_token=False): - creds = frappe.get_doc("E Invoice Settings") - if not for_token and (not creds.token_expiry or time_diff_in_seconds(now_datetime(), creds.token_expiry) > 5.0): - fetch_token() - creds.load_from_db() - - return creds +def get_credentials(): + doc = frappe.get_doc('E Invoice Settings') + if not doc.token_expiry or time_diff_in_seconds(now_datetime(), doc.token_expiry) > 5.0: + fetch_token(doc) + doc.load_from_db() + + return doc def rsa_encrypt(msg, key): if not (isinstance(msg, bytes) or isinstance(msg, bytearray)): @@ -83,19 +84,20 @@ def get_header(creds): return headers @frappe.whitelist() -def fetch_token(): - einv_creds = get_einv_credentials(for_token=True) +def fetch_token(credentials=None): + if not credentials: + credentials = frappe.get_doc('E Invoice Settings') endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' headers = { 'content-type': 'application/json' } - headers.update(dict(client_id=einv_creds.client_id, client_secret=einv_creds.client_secret)) - payload = dict(UserName=einv_creds.username, ForceRefreshAccessToken=bool(einv_creds.auto_refresh_token)) + headers.update(dict(client_id=credentials.client_id, client_secret=credentials.client_secret)) + payload = dict(UserName=credentials.username, ForceRefreshAccessToken=bool(credentials.auto_refresh_token)) appkey = bytearray(os.urandom(32)) - enc_appkey = rsa_encrypt(appkey, einv_creds.public_key) + enc_appkey = rsa_encrypt(appkey, credentials.public_key) - password = einv_creds.get_password(fieldname='password') - enc_password = rsa_encrypt(password, einv_creds.public_key) + password = credentials.get_password(fieldname='password') + enc_password = rsa_encrypt(password, credentials.public_key) payload.update(dict(Password=enc_password, AppKey=enc_appkey)) @@ -104,10 +106,10 @@ def fetch_token(): auth_token, token_expiry, sek = extract_token_and_sek(res, appkey) - einv_creds.auth_token = auth_token - einv_creds.token_expiry = get_datetime(token_expiry) - einv_creds.sek = sek - einv_creds.save() + credentials.auth_token = auth_token + credentials.token_expiry = get_datetime(token_expiry) + credentials.sek = sek + credentials.save() def extract_token_and_sek(response, appkey): data = response.get('Data') @@ -119,45 +121,45 @@ def extract_token_and_sek(response, appkey): def attach_signed_invoice(doctype, name, data): f = frappe.get_doc({ - "doctype": "File", - "file_name": "E-INV--{}.json".format(name), - "attached_to_doctype": doctype, - "attached_to_name": name, - "content": json.dumps(data), - "is_private": True + 'doctype': 'File', + 'file_name': 'E-INV--{}.json'.format(name), + 'attached_to_doctype': doctype, + 'attached_to_name': name, + 'content': json.dumps(data), + 'is_private': True }).insert() def get_gstin_details(gstin): - einv_creds = get_einv_credentials() + credentials = get_credentials() endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/Master/gstin/{gstin}'.format(gstin=gstin) - headers = get_header(einv_creds) + headers = get_header(credentials) res = make_get_request(endpoint, headers=headers) handle_err_response(res) - enc_json = res.get('Data') - json_str = aes_decrypt(enc_json, einv_creds.sek) - data = json.loads(json_str) + enc_details = res.get('Data') + json_str = aes_decrypt(enc_details, credentials.sek) + details = json.loads(json_str) - return data + return details @frappe.whitelist() def generate_irn(doctype, name): endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice' - einv_creds = get_einv_credentials() - headers = get_header(einv_creds) + credentials = get_credentials() + headers = get_header(credentials) - e_invoice = make_e_invoice(doctype, name) + einvoice = make_einvoice(doctype, name) - enc_e_invoice_json = aes_encrypt(e_invoice, einv_creds.sek) - payload = dict(Data=enc_e_invoice_json) + enc_einvoice_json = aes_encrypt(einvoice, credentials.sek) + payload = dict(Data=enc_einvoice_json) res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) res = handle_err_response(res) enc_json = res.get('Data') - json_str = aes_decrypt(enc_json, einv_creds.sek) + json_str = aes_decrypt(enc_json, credentials.sek) signed_einvoice = json.loads(json_str) handle_irn_response(signed_einvoice) @@ -170,10 +172,10 @@ def generate_irn(doctype, name): return signed_einvoice def get_irn_details(irn): - einv_creds = get_einv_credentials() + credentials = get_credentials() endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/irn/{irn}'.format(irn=irn) - headers = get_header(einv_creds) + headers = get_header(credentials) res = make_get_request(endpoint, headers=headers) handle_err_response(res) @@ -182,13 +184,13 @@ def get_irn_details(irn): @frappe.whitelist() def cancel_irn(doctype, name, irn, reason, remark=''): - einv_creds = get_einv_credentials() + credentials = get_credentials() endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/Cancel' - headers = get_header(einv_creds) + headers = get_header(credentials) - cancel_e_inv = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark)) - enc_json = aes_encrypt(cancel_e_inv, einv_creds.sek) + cancel_einv = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark)) + enc_json = aes_encrypt(cancel_einv, credentials.sek) payload = dict(Data=enc_json) res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) @@ -199,18 +201,21 @@ def cancel_irn(doctype, name, irn, reason, remark=''): return res @frappe.whitelist() -def cancel_eway_bill(eway_bill, reason, remark=''): - einv_creds = get_einv_credentials() +def cancel_eway_bill(doctype, name, eway_bill, reason, remark=''): + credentials = get_credentials() endpoint = 'https://einv-apisandbox.nic.in/ewaybillapi/v1.03/ewayapi' - headers = get_header(einv_creds) + headers = get_header(credentials) cancel_eway_bill_json = json.dumps(dict(ewbNo=eway_bill, cancelRsnCode=reason, cancelRmrk=remark)) - enc_json = aes_encrypt(cancel_eway_bill_json, einv_creds.sek) - payload = dict(action="CANEWB", Data=enc_json) + enc_json = aes_encrypt(cancel_eway_bill_json, credentials.sek) + payload = dict(action='CANEWB', Data=enc_json) res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) handle_err_response(res) + frappe.db.set_value(doctype, name, 'ewaybill', '') + frappe.db.set_value(doctype, name, 'eway_bill_cancelled', 1) + return res def handle_irn_response(data): @@ -225,22 +230,28 @@ def handle_err_response(response): if response.get('Status') == 0: err_details = response.get('ErrorDetails') print(response) - err_msg = "" + error_msgs = [] for d in err_details: err_code = d.get('ErrorCode') + if err_code == '2150': irn = [d['Desc']['Irn'] for d in response.get('InfoDtls') if d['InfCd'] == 'DUPIRN'] response = get_irn_details(irn[0]) return response - err_msg += d.get('ErrorMessage') - err_msg += "
    " - frappe.throw(_(err_msg), title=_('API Request Failed')) + error_msgs.append(d.get('ErrorMessage')) + + if error_msgs: + if len(error_msgs) > 1: + li = ['
  • '+ d +'
  • ' for d in error_msgs] + frappe.throw(_("""
      {}
    """).format(''.join(li)), title=_('API Request Failed')) + else: + frappe.throw(_('{}').format(error_msgs[0]), title=_('API Request Failed')) return response def read_json(name): - file_path = os.path.join(os.path.dirname(__file__), "{name}.json".format(name=name)) + file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name)) with open(file_path, 'r') as f: return cstr(f.read()) @@ -267,12 +278,10 @@ def get_doc_details(invoice): invoice_name = invoice.name invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy') - return frappe._dict(dict( - invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date - )) + return frappe._dict(dict(invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date)) -def get_party_gstin_details(party_address): - address = frappe.get_all("Address", filters={"name": party_address}, fields=["*"])[0] +def get_party_gstin_details(address_name): + address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] gstin = address.get('gstin') gstin_details = get_gstin_details(gstin) @@ -281,8 +290,8 @@ def get_party_gstin_details(party_address): location = gstin_details.get('AddrLoc') state_code = gstin_details.get('StateCode') pincode = cint(gstin_details.get('AddrPncd')) - address_line1 = "{} {}".format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno')) - address_line2 = "{} {}".format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt')) + address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno')) + address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt')) email_id = address.get('email_id') phone = address.get('phone') if state_code == 97: @@ -294,9 +303,9 @@ def get_party_gstin_details(party_address): address_line2=address_line2, email=email_id, phone=phone )) -def get_overseas_address_details(party_address): +def get_overseas_address_details(address_name): address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value( - "Address", party_address, ["address_title", "address_line1", "address_line2", "city", "phone", "email_id"] + 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id'] ) return frappe._dict(dict( @@ -311,13 +320,13 @@ def get_item_list(invoice): gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] for d in invoice.items: - item_schema = read_json("einv_item_template") + item_schema = read_json('einv_item_template') item = frappe._dict(dict()) item.update(d.as_dict()) item.sr_no = d.idx item.description = d.item_name - item.is_service_item = "N" if frappe.db.get_value("Item", d.item_code, "is_stock_item") else "Y" - item.batch_expiry_date = frappe.db.get_value("Batch", d.batch_no, "expiry_date") if d.batch_no else None + item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y' + item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None item.unit_rate = item.base_price_list_rate if item.discount_amount else item.base_rate item.total_amount = item.unit_rate * item.qty @@ -345,10 +354,10 @@ def get_item_list(invoice): item.cgst_amount += item_tax_detail[1] item.total_value = item.base_amount + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount - e_inv_item = item_schema.format(item=item) - item_list.append(e_inv_item) + einv_item = item_schema.format(item=item) + item_list.append(einv_item) - return ", ".join(item_list) + return ', '.join(item_list) def get_value_details(invoice): gst_accounts = get_gst_accounts(invoice.company) @@ -379,7 +388,7 @@ def get_value_details(invoice): def get_payment_details(invoice): payee_name = invoice.company - mode_of_payment = ", ".join([d.mode_of_payment for d in invoice.payments]) + mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments]) paid_amount = invoice.base_paid_amount outstanding_amount = invoice.outstanding_amount @@ -389,14 +398,14 @@ def get_payment_details(invoice): )) def get_return_doc_reference(invoice): - invoice_date = frappe.db.get_value("Sales Invoice", invoice.return_against, "posting_date") + invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') return frappe._dict(dict( invoice_name=invoice.return_against, invoice_date=invoice_date )) def get_eway_bill_details(invoice): if not invoice.distance: - frappe.throw(_("Distance is mandatory for E-Way Bill generation"), title=_("Missing Values")) + frappe.throw(_('Distance is mandatory for E-Way Bill generation'), title=_('E Invoice Validation Failed')) mode_of_transport = { 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } @@ -413,13 +422,15 @@ def get_eway_bill_details(invoice): )) @frappe.whitelist() -def make_e_invoice(doctype, name): +def make_einvoice(doctype, name): invoice = frappe.get_doc(doctype, name) - schema = read_json("einv_template") - validations = json.loads(read_json("einv_validation")) + schema = read_json('einv_template') + validations = json.loads(read_json('einv_validation')) - trans_details = get_trans_details(invoice) + item_list = get_item_list(invoice) doc_details = get_doc_details(invoice) + value_details = get_value_details(invoice) + trans_details = get_trans_details(invoice) seller_details = get_party_gstin_details(invoice.company_address) if invoice.gst_category == 'Overseas': @@ -428,99 +439,87 @@ def make_e_invoice(doctype, name): buyer_details = get_party_gstin_details(invoice.customer_address) place_of_supply = invoice.place_of_supply.split('-')[0] buyer_details.update(dict(place_of_supply=place_of_supply)) - - item_list = get_item_list(invoice) - value_details = get_value_details(invoice) - shipping_details = frappe._dict({}) + shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: shipping_details = get_party_gstin_details(invoice.shipping_address_name) - payment_details = frappe._dict({}) if invoice.is_pos and invoice.base_paid_amount: payment_details = get_payment_details(invoice) - prev_doc_details = frappe._dict({}) if invoice.is_return and invoice.return_against: prev_doc_details = get_return_doc_reference(invoice) - dispatch_details = frappe._dict({}) - period_details = frappe._dict({}) - export_details = frappe._dict({}) - eway_bill_details = frappe._dict({}) if invoice.transporter: eway_bill_details = get_eway_bill_details(invoice) + + # not yet implemented + dispatch_details = period_details = export_details = frappe._dict({}) - e_invoice = schema.format( + einvoice = schema.format( trans_details=trans_details, doc_details=doc_details, dispatch_details=dispatch_details, seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details, item_list=item_list, value_details=value_details, payment_details=payment_details, period_details=period_details, prev_doc_details=prev_doc_details, export_details=export_details, eway_bill_details=eway_bill_details ) - e_invoice = json.loads(e_invoice) + einvoice = json.loads(einvoice) - error_msgs = validate_einvoice(validations, e_invoice, []) + error_msgs = validate_einvoice(validations, einvoice, []) if error_msgs: if len(error_msgs) > 1: - li = ["
  • "+ d +"
  • " for d in error_msgs] - frappe.throw(_("""
      {}
    """).format("".join(li)), title=_("E Invoice Validation Failed")) + li = ['
  • '+ d +'
  • ' for d in error_msgs] + frappe.throw("
      {}
    ".format(''.join(li)), title=_('E Invoice Validation Failed')) else: - frappe.throw(_("{}").format(error_msgs[0]), title=_("E Invoice Validation Failed")) + frappe.throw(error_msgs[0], title=_('E Invoice Validation Failed')) - return {'einvoice': json.dumps([e_invoice])} + return {'einvoice': json.dumps([einvoice])} -def validate_einvoice(validations, e_invoice, error_msgs=[]): - type_map = { - "string": cstr, - "number": cint, - "object": dict, - "array": list - } +def validate_einvoice(validations, einvoice, error_msgs=[]): + type_map = { 'string': 'str', 'number': 'int', 'object': 'dict', 'array': 'list' } - for field, value in validations.items(): - if isinstance(value, list): value = value[0] - - invoice_value = e_invoice.get(field) + for field, validation in validations.items(): + invoice_value = einvoice.get(field) if not invoice_value: continue - should_be_of_type = type_map[value.get('type').lower()] - if should_be_of_type == dict: - properties = value.get('properties') + should_be_of_type = type_map[validation.get('type').lower()] + if should_be_of_type in ['dict', 'list']: + child_validations = validation.get('properties') if isinstance(invoice_value, list): for d in invoice_value: - validate_einvoice(properties, d, error_msgs) + validate_einvoice(child_validations, d, error_msgs) else: - validate_einvoice(properties, invoice_value, error_msgs) + validate_einvoice(child_validations, invoice_value, error_msgs) # remove keys with empty dicts if not invoice_value: - e_invoice.pop(field, None) + einvoice.pop(field, None) continue - if invoice_value == "None": + if invoice_value == 'None': # remove keys with empty values - e_invoice.pop(field, None) + einvoice.pop(field, None) continue # convert to int or str - e_invoice[field] = should_be_of_type(invoice_value) - - should_be_of_len = value.get('maxLength') - should_be_greater_than = flt(value.get('minimum')) - should_be_less_than = flt(value.get('maximum')) - pattern_str = value.get('pattern') - pattern = re.compile(pattern_str or "") + function = eval('c' + should_be_of_type) + einvoice[field] = function(invoice_value) - field_label = value.get("label") or field + max_length = validation.get('maxLength') + minimum = flt(validation.get('minimum')) + maximum = flt(validation.get('maximum')) + pattern_str = validation.get('pattern') + pattern = re.compile(pattern_str or '') - if value.get('type').lower() == 'string' and len(invoice_value) > should_be_of_len: - error_msgs.append("{} should not exceed {} characters".format(field_label, should_be_of_len)) - if value.get('type').lower() == 'number' and not (flt(invoice_value) <= should_be_less_than): - error_msgs.append("{} should be less than {}".format(field_label, should_be_less_than)) + field_label = validation.get('label') or field + + if should_be_of_type == 'str' and len(invoice_value) > max_length: + error_msgs.append(_('{} should not exceed {} characters').format(field_label, max_length)) + if should_be_of_type == 'int' and not (flt(invoice_value) <= maximum): + error_msgs.append(_('{} should be less than {}').format(field_label, maximum)) if pattern_str and not pattern.match(invoice_value): - error_msgs.append(value.get('validationMsg')) + error_msgs.append(validation.get('validationMsg')) return error_msgs @@ -539,7 +538,7 @@ def download_einvoice(): einvoice = data['einvoice'] name = data['name'] - frappe.response['filename'] = "E-Invoice-" + name + ".json" + frappe.response['filename'] = 'E-Invoice-' + name + '.json' frappe.response['filecontent'] = einvoice frappe.response['content_type'] = 'application/json' frappe.response['type'] = 'download' @@ -563,7 +562,7 @@ def download_cancel_einvoice(): cancel_einvoice = json.dumps([dict(Irn=irn, CnlRsn=reason, CnlRem=remark)]) - frappe.response['filename'] = "Cancel E-Invoice " + name + ".json" + frappe.response['filename'] = 'Cancel E-Invoice ' + name + '.json' frappe.response['filecontent'] = cancel_einvoice frappe.response['content_type'] = 'application/json' frappe.response['type'] = 'download' @@ -575,7 +574,7 @@ def upload_cancel_ack(): doctype = data['doctype'] name = data['docname'] - frappe.db.set_value(doctype, name, "irn_cancelled", 1) + frappe.db.set_value(doctype, name, 'irn_cancelled', 1) def attach_qrcode_image(doctype, name): qrcode = frappe.db.get_value(doctype, name, 'signed_qr_code') @@ -583,12 +582,11 @@ def attach_qrcode_image(doctype, name): if not qrcode: return _file = frappe.get_doc({ - "doctype": "File", - "file_name": "Signed_QR_{name}.png".format(name=name), - "attached_to_doctype": doctype, - "attached_to_name": name, - "attached_to_field": "qrcode_image", - "content": "qrcode" + 'doctype': 'File', + 'file_name': 'Signed_QR_{name}.png'.format(name=name), + 'attached_to_doctype': doctype, + 'attached_to_name': name, + 'content': 'qrcode' }) _file.save() frappe.db.commit() diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json index 9f0ef5006c4..3f0767b8be8 100644 --- a/erpnext/regional/india/e_invoice/einv_validation.json +++ b/erpnext/regional/india/e_invoice/einv_validation.json @@ -277,209 +277,205 @@ }, "required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"] }, - "ItemList": [ - { - "type": "object", - "properties": { - "SlNo": { - "type": "string", - "minLength": 1, - "maxLength": 6 + "ItemList": { + "type": "Array", + "properties": { + "SlNo": { + "type": "string", + "minLength": 1, + "maxLength": 6 + }, + "PrdDesc": { + "type": "string", + "minLength": 3, + "maxLength": 300 + }, + "IsServc": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"] + }, + "HsnCd": { + "type": "string", + "minLength": 4, + "maxLength": 8 + }, + "Barcde": { + "type": "string", + "minLength": 3, + "maxLength": 30 + }, + "Qty": { + "type": "number", + "minimum": 0, + "maximum": 9999999999.999 + }, + "FreeQty": { + "type": "number", + "minimum": 0, + "maximum": 9999999999.999 + }, + "Unit": { + "type": "string", + "minLength": 3, + "maxLength": 8 + }, + "UnitPrice": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.999 + }, + "TotAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "Discount": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "PreTaxVal": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "AssAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "GstRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999 + }, + "IgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "CgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "SgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "CesRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999 + }, + "CesAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "CesNonAdvlAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "StateCesRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999 + }, + "StateCesAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "StateCesNonAdvlAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "OthChrg": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "TotItemVal": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "OrdLineRef": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "OrgCntry": { + "type": "string", + "minLength": 2, + "maxLength": 2 + }, + "PrdSlNo": { + "type": "string", + "minLength": 1, + "maxLength": 20 + }, + "BchDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 3, + "maxLength": 20 + }, + "ExpDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "validationMsg": "Expiry Date is invalid" + }, + "WrDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "validationMsg": "Warranty Date is invalid" + } }, - "PrdDesc": { - "type": "string", - "minLength": 3, - "maxLength": 300 - }, - "IsServc": { - "type": "string", - "minLength": 1, - "maxLength": 1, - "enum": ["Y", "N"] - }, - "HsnCd": { - "type": "string", - "minLength": 4, - "maxLength": 8 - }, - "Barcde": { - "type": "string", - "minLength": 3, - "maxLength": 30 - }, - "Qty": { - "type": "number", - "minimum": 0, - "maximum": 9999999999.999 - }, - "FreeQty": { - "type": "number", - "minimum": 0, - "maximum": 9999999999.999 - }, - "Unit": { - "type": "string", - "minLength": 3, - "maxLength": 8 - }, - "UnitPrice": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.999 - }, - "TotAmt": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.99 - }, - "Discount": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.99 - }, - "PreTaxVal": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.99 - }, - "AssAmt": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.99 - }, - "GstRt": { - "type": "number", - "minimum": 0, - "maximum": 999.999 - }, - "IgstAmt": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.99 - }, - "CgstAmt": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.99 - }, - "SgstAmt": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.99 - }, - "CesRt": { - "type": "number", - "minimum": 0, - "maximum": 999.999 - }, - "CesAmt": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.99 - }, - "CesNonAdvlAmt": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.99 - }, - "StateCesRt": { - "type": "number", - "minimum": 0, - "maximum": 999.999 - }, - "StateCesAmt": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.99 - }, - "StateCesNonAdvlAmt": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.99 - }, - "OthChrg": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.99 - }, - "TotItemVal": { - "type": "number", - "minimum": 0, - "maximum": 999999999999.99 - }, - "OrdLineRef": { - "type": "string", - "minLength": 1, - "maxLength": 50 - }, - "OrgCntry": { - "type": "string", - "minLength": 2, - "maxLength": 2 - }, - "PrdSlNo": { - "type": "string", - "minLength": 1, - "maxLength": 20 - }, - "BchDtls": { + "required": ["Nm"] + }, + "AttribDtls": { + "type": "Array", + "Attribute": { "type": "object", "properties": { "Nm": { "type": "string", - "minLength": 3, - "maxLength": 20 + "minLength": 1, + "maxLength": 100 }, - "ExpDt": { + "Val": { "type": "string", - "maxLength": 10, - "minLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", - "validationMsg": "Expiry Date is invalid" - }, - "WrDt": { - "type": "string", - "maxLength": 10, - "minLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", - "validationMsg": "Warranty Date is invalid" + "minLength": 1, + "maxLength": 100 } - }, - "required": ["Nm"] - }, - "AttribDtls": { - "type": "Array", - "Attribute": [ - { - "type": "object", - "properties": { - "Nm": { - "type": "string", - "minLength": 1, - "maxLength": 100 - }, - "Val": { - "type": "string", - "minLength": 1, - "maxLength": 100 - } - } - } - ] + } } - }, - "required": [ - "SlNo", - "IsServc", - "HsnCd", - "UnitPrice", - "TotAmt", - "AssAmt", - "GstRt", - "TotItemVal" - ] - } - ], + } + }, + "required": [ + "SlNo", + "IsServc", + "HsnCd", + "UnitPrice", + "TotAmt", + "AssAmt", + "GstRt", + "TotItemVal" + ] + }, "ValDtls": { "type": "object", "properties": { @@ -709,28 +705,26 @@ }, "AddlDocDtls": { "type": "Array", - "AddlDocument": [ - { - "type": "object", - "properties": { - "Url": { - "type": "string", - "minLength": 3, - "maxLength": 100 - }, - "Docs": { - "type": "string", - "minLength": 3, - "maxLength": 1000 - }, - "Info": { - "type": "string", - "minLength": 3, - "maxLength": 1000 - } + "AddlDocument": { + "type": "object", + "properties": { + "Url": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Docs": { + "type": "string", + "minLength": 3, + "maxLength": 1000 + }, + "Info": { + "type": "string", + "minLength": 3, + "maxLength": 1000 } } - ] + } }, "ExpDtls": { "type": "object", diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 076f13475d8..87e2031f883 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -92,7 +92,7 @@ erpnext.setup_einvoice_actions = (doctype) => { "Download E-Invoice", () => { frappe.call({ - method: 'erpnext.regional.india.e_invoice.e_invoice_utils.make_e_invoice', + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.make_einvoice', args: { doctype: frm.doc.doctype, name: frm.doc.name }, freeze: true, callback: (res) => { From 57dd4689e07381ba227cd5aa846a377a78f17d55 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 13 Oct 2020 15:54:25 +0530 Subject: [PATCH 043/115] chore: clean up e invoice actions --- .../india/e_invoice/e_invoice_utils.py | 6 +- erpnext/regional/india/e_invoice/einvoice.js | 317 ++++++++++-------- 2 files changed, 177 insertions(+), 146 deletions(-) diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py index 044c2c88da2..fe48bbe0af4 100644 --- a/erpnext/regional/india/e_invoice/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice/e_invoice_utils.py @@ -162,7 +162,7 @@ def generate_irn(doctype, name): json_str = aes_decrypt(enc_json, credentials.sek) signed_einvoice = json.loads(json_str) - handle_irn_response(signed_einvoice) + decrypt_irn_response(signed_einvoice) update_einvoice_fields(doctype, name, signed_einvoice) @@ -218,7 +218,7 @@ def cancel_eway_bill(doctype, name, eway_bill, reason, remark=''): return res -def handle_irn_response(data): +def decrypt_irn_response(data): enc_signed_invoice = data['SignedInvoice'] enc_signed_qr_code = data['SignedQRCode'] signed_invoice = jwt_decrypt(enc_signed_invoice)['data'] @@ -473,7 +473,7 @@ def make_einvoice(doctype, name): else: frappe.throw(error_msgs[0], title=_('E Invoice Validation Failed')) - return {'einvoice': json.dumps([einvoice])} + return json.dumps(einvoice) def validate_einvoice(validations, einvoice, error_msgs=[]): type_map = { 'string': 'str', 'number': 'int', 'object': 'dict', 'array': 'list' } diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 87e2031f883..bcdd6ef02ff 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -3,24 +3,157 @@ erpnext.setup_einvoice_actions = (doctype) => { refresh(frm) { const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable"); const supply_type = frm.doc.gst_category; - if (!einvoicing_enabled - || !['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type)) { - return; + const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type) + + if (!einvoicing_enabled || !valid_supply_type) return; + + const { docstatus, irn, irn_cancelled, ewaybill, eway_bill_cancelled, doctype, name, __unsaved } = frm.doc; + + if (docstatus == 0 && !irn && !__unsaved) { + frm.add_custom_button( + _("Generate IRN"), + () => { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.generate_irn', + args: { doctype: doctype, name: name }, + freeze: true, + callback: () => frm.reload_doc() + }) + }, + __("E Invoicing") + ); } + + if (docstatus == 1 && irn && !irn_cancelled) { + frm.add_custom_button( + __("Cancel IRN"), + () => { + const fields = [ + { + "label" : "Reason", + "fieldname": "reason", + "fieldtype": "Select", + "reqd": 1, + "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", + "fieldname": "remark", + "fieldtype": "Data", + "reqd": 1 + } + ]; + const d = new frappe.ui.Dialog({ + title: __("Cancel IRN"), + fields: fields, + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.cancel_irn', + args: { + doctype: doctype, + name: name, + irn: irn, + reason: data.reason.split('-')[0], + remark: data.remark + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }); + }, + primary_action_label: __('Submit') + }); + d.show(); + }, + __("E Invoicing") + ) + } + + if (docstatus == 1 && irn && !irn_cancelled && !eway_bill_cancelled) { + frm.add_custom_button( + __("Cancel E-Way Bill"), + () => { + const fields = [ + { + "label" : "Reason", + "fieldname": "reason", + "fieldtype": "Select", + "reqd": 1, + "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", + "fieldname": "remark", + "fieldtype": "Data", + "reqd": 1 + } + ] + const d = new frappe.ui.Dialog({ + title: __('Cancel E-Way Bill'), + fields: fields, + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.cancel_eway_bill', + args: { + doctype: doctype, + name: name, + eway_bill: ewaybill, + reason: data.reason.split('-')[0], + remark: data.remark + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }) + }, + primary_action_label: __('Submit') + }); + d.show(); + }, + __("E Invoicing") + ); + } + // if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) { // frm.add_custom_button( - // "Generate IRN", + // "Download E-Invoice", // () => { // frappe.call({ - // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.generate_irn', + // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.make_einvoice', // args: { doctype: frm.doc.doctype, name: frm.doc.name }, // freeze: true, - // callback: () => frm.reload_doc() + // callback: (res) => { + // if (!res.exc) { + // const args = { + // cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_einvoice', + // einvoice: res.message.einvoice, + // name: frm.doc.name + // }; + // open_url_post(frappe.request.url, args); + // } + // } // }) - // } - // ) + // }, "E-Invoicing"); + // frm.add_custom_button( + // "Upload Signed E-Invoice", + // () => { + // new frappe.ui.FileUploader({ + // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_einvoice', + // allow_multiple: 0, + // doctype: frm.doc.doctype, + // docname: frm.doc.name, + // on_success: (attachment, r) => { + // if (!r.exc) { + // frm.reload_doc(); + // } + // } + // }); + // }, "E-Invoicing"); // } - // if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) { // frm.add_custom_button( // "Cancel IRN", @@ -28,147 +161,45 @@ erpnext.setup_einvoice_actions = (doctype) => { // const d = new frappe.ui.Dialog({ // title: __('Cancel IRN'), // fields: [ - // { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", - // "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, - // { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 } + // { + // "label" : "Reason", "fieldname": "reason", + // "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", + // "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + // }, + // { + // "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 + // } // ], // primary_action: function() { // const data = d.get_values(); - // frappe.call({ - // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.cancel_irn', - // args: { - // doctype: frm.doc.doctype, - // name: frm.doc.name, - // irn: frm.doc.irn, - // reason: data.reason.split('-')[0], - // remark: data.remark - // }, - // freeze: true, - // callback: () => frm.reload_doc() || d.hide(), - // error: () => d.hide() - // }) + // const args = { + // cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_cancel_einvoice', + // irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark, name: frm.doc.name + // }; + // open_url_post(frappe.request.url, args); + // d.hide(); // }, - // primary_action_label: __('Submit') + // primary_action_label: __('Download JSON') // }); // d.show(); - // } - // ) - // } - - // if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled && !frm.doc.eway_bill_cancelled) { - // frm.add_custom_button( - // "Cancel E-Way Bill", - // () => { - // const d = new frappe.ui.Dialog({ - // title: __('Cancel E-Way Bill'), - // fields: [ - // { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", - // "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, - // { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 } - // ], - // primary_action: function() { - // const data = d.get_values(); - // frappe.call({ - // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.cancel_eway_bill', - // args: { eway_bill: frm.doc.ewaybill, reason: data.reason.split('-')[0], remark: data.remark }, - // freeze: true, - // callback: () => { - // frm.set_value('eway_bill_cancelled', 1); - // frm.save("Update"); - // d.hide() - // }, - // error: () => d.hide() - // }) - // }, - // primary_action_label: __('Submit') - // }); - // d.show(); - // } - // ) - // } - - if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) { - frm.add_custom_button( - "Download E-Invoice", - () => { - frappe.call({ - method: 'erpnext.regional.india.e_invoice.e_invoice_utils.make_einvoice', - args: { doctype: frm.doc.doctype, name: frm.doc.name }, - freeze: true, - callback: (res) => { - if (!res.exc) { - const args = { - cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_einvoice', - einvoice: res.message.einvoice, - name: frm.doc.name - }; - open_url_post(frappe.request.url, args); - } - } - }) - }, "E-Invoicing"); - frm.add_custom_button( - "Upload Signed E-Invoice", - () => { - new frappe.ui.FileUploader({ - method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_einvoice', - allow_multiple: 0, - doctype: frm.doc.doctype, - docname: frm.doc.name, - on_success: (attachment, r) => { - if (!r.exc) { - frm.reload_doc(); - } - } - }); - }, "E-Invoicing"); - } - if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) { - frm.add_custom_button( - "Cancel IRN", - () => { - const d = new frappe.ui.Dialog({ - title: __('Cancel IRN'), - fields: [ - { - "label" : "Reason", "fieldname": "reason", - "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", - "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] - }, - { - "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 - } - ], - primary_action: function() { - const data = d.get_values(); - const args = { - cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_cancel_einvoice', - irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark, name: frm.doc.name - }; - open_url_post(frappe.request.url, args); - d.hide(); - }, - primary_action_label: __('Download JSON') - }); - d.show(); - }, "E-Invoicing"); + // }, "E-Invoicing"); - frm.add_custom_button( - "Upload Cancel JSON", - () => { - new frappe.ui.FileUploader({ - method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_cancel_ack', - allow_multiple: 0, - doctype: frm.doc.doctype, - docname: frm.doc.name, - on_success: (attachment, r) => { - if (!r.exc) { - frm.reload_doc(); - } - } - }); - }, "E-Invoicing"); - } + // frm.add_custom_button( + // "Upload Cancel JSON", + // () => { + // new frappe.ui.FileUploader({ + // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_cancel_ack', + // allow_multiple: 0, + // doctype: frm.doc.doctype, + // docname: frm.doc.name, + // on_success: (attachment, r) => { + // if (!r.exc) { + // frm.reload_doc(); + // } + // } + // }); + // }, "E-Invoicing"); + // } } }) } \ No newline at end of file From e6646efef39b8efaedf2a7b638168e7db2b8caab Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 21 Oct 2020 19:09:27 +0530 Subject: [PATCH 044/115] fix: download & upload e-invoice --- .../gst_e_invoice/gst_e_invoice.html | 8 +- .../india/e_invoice/e_invoice_utils.py | 15 +- erpnext/regional/india/e_invoice/einvoice.js | 352 +++++++++--------- 3 files changed, 192 insertions(+), 183 deletions(-) diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html index 15ea1c21744..a900711af1b 100644 --- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -92,8 +92,8 @@ - - {% for item in einvoice.ItemList %} + {% for item in einvoice.ItemList %} + {{ item.SlNo }} {{ item.PrdDesc }} {{ item.HsnCd }} @@ -105,8 +105,8 @@ {{ item.GstRt + item.CesRt }} % {{ frappe.utils.fmt_money(0, None, "INR") }} {{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }} - {% endfor %} - + + {% endfor %} diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py index fe48bbe0af4..649f5eb9f3e 100644 --- a/erpnext/regional/india/e_invoice/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice/e_invoice_utils.py @@ -473,7 +473,7 @@ def make_einvoice(doctype, name): else: frappe.throw(error_msgs[0], title=_('E Invoice Validation Failed')) - return json.dumps(einvoice) + return {'einvoice': json.dumps([einvoice])} def validate_einvoice(validations, einvoice, error_msgs=[]): type_map = { 'string': 'str', 'number': 'int', 'object': 'dict', 'array': 'list' } @@ -503,8 +503,10 @@ def validate_einvoice(validations, einvoice, error_msgs=[]): continue # convert to int or str - function = eval('c' + should_be_of_type) - einvoice[field] = function(invoice_value) + if should_be_of_type == 'str': + einvoice[field] = str(invoice_value) + elif should_be_of_type == 'int': + einvoice[field] = flt(invoice_value, 3) max_length = validation.get('maxLength') minimum = flt(validation.get('minimum')) @@ -527,6 +529,12 @@ def update_einvoice_fields(doctype, name, signed_einvoice): enc_signed_invoice = signed_einvoice.get('SignedInvoice') decrypted_signed_invoice = jwt_decrypt(enc_signed_invoice)['data'] + if json.loads(decrypted_signed_invoice)['DocDtls']['No'] != name: + frappe.throw( + _("Document number of uploaded Signed E-Invoice doesn't matches with Sales Invoice"), + title=_("Inappropriate E-Invoice") + ) + frappe.db.set_value(doctype, name, 'irn', signed_einvoice.get('Irn')) frappe.db.set_value(doctype, name, 'ewaybill', signed_einvoice.get('EwbNo')) frappe.db.set_value(doctype, name, 'signed_qr_code', signed_einvoice.get('SignedQRCode').split('.')[1]) @@ -551,6 +559,7 @@ def upload_einvoice(): name = data['docname'] update_einvoice_fields(doctype, name, signed_einvoice) + attach_qrcode_image(doctype, name) @frappe.whitelist() def download_cancel_einvoice(): diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index bcdd6ef02ff..7373e831b0c 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -9,197 +9,197 @@ erpnext.setup_einvoice_actions = (doctype) => { const { docstatus, irn, irn_cancelled, ewaybill, eway_bill_cancelled, doctype, name, __unsaved } = frm.doc; - if (docstatus == 0 && !irn && !__unsaved) { - frm.add_custom_button( - _("Generate IRN"), - () => { - frappe.call({ - method: 'erpnext.regional.india.e_invoice.e_invoice_utils.generate_irn', - args: { doctype: doctype, name: name }, - freeze: true, - callback: () => frm.reload_doc() - }) - }, - __("E Invoicing") - ); - } - - if (docstatus == 1 && irn && !irn_cancelled) { - frm.add_custom_button( - __("Cancel IRN"), - () => { - const fields = [ - { - "label" : "Reason", - "fieldname": "reason", - "fieldtype": "Select", - "reqd": 1, - "default": "1-Duplicate", - "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] - }, - { - "label": "Remark", - "fieldname": "remark", - "fieldtype": "Data", - "reqd": 1 - } - ]; - const d = new frappe.ui.Dialog({ - title: __("Cancel IRN"), - fields: fields, - primary_action: function() { - const data = d.get_values(); - frappe.call({ - method: 'erpnext.regional.india.e_invoice.e_invoice_utils.cancel_irn', - args: { - doctype: doctype, - name: name, - irn: irn, - reason: data.reason.split('-')[0], - remark: data.remark - }, - freeze: true, - callback: () => frm.reload_doc() || d.hide(), - error: () => d.hide() - }); - }, - primary_action_label: __('Submit') - }); - d.show(); - }, - __("E Invoicing") - ) - } - - if (docstatus == 1 && irn && !irn_cancelled && !eway_bill_cancelled) { - frm.add_custom_button( - __("Cancel E-Way Bill"), - () => { - const fields = [ - { - "label" : "Reason", - "fieldname": "reason", - "fieldtype": "Select", - "reqd": 1, - "default": "1-Duplicate", - "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] - }, - { - "label": "Remark", - "fieldname": "remark", - "fieldtype": "Data", - "reqd": 1 - } - ] - const d = new frappe.ui.Dialog({ - title: __('Cancel E-Way Bill'), - fields: fields, - primary_action: function() { - const data = d.get_values(); - frappe.call({ - method: 'erpnext.regional.india.e_invoice.e_invoice_utils.cancel_eway_bill', - args: { - doctype: doctype, - name: name, - eway_bill: ewaybill, - reason: data.reason.split('-')[0], - remark: data.remark - }, - freeze: true, - callback: () => frm.reload_doc() || d.hide(), - error: () => d.hide() - }) - }, - primary_action_label: __('Submit') - }); - d.show(); - }, - __("E Invoicing") - ); - } - - // if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) { + // if (docstatus == 0 && !irn && !__unsaved) { // frm.add_custom_button( - // "Download E-Invoice", + // _("Generate IRN"), // () => { // frappe.call({ - // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.make_einvoice', - // args: { doctype: frm.doc.doctype, name: frm.doc.name }, + // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.generate_irn', + // args: { doctype: doctype, name: name }, // freeze: true, - // callback: (res) => { - // if (!res.exc) { - // const args = { - // cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_einvoice', - // einvoice: res.message.einvoice, - // name: frm.doc.name - // }; - // open_url_post(frappe.request.url, args); - // } - // } + // callback: () => frm.reload_doc() // }) - // }, "E-Invoicing"); - // frm.add_custom_button( - // "Upload Signed E-Invoice", - // () => { - // new frappe.ui.FileUploader({ - // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_einvoice', - // allow_multiple: 0, - // doctype: frm.doc.doctype, - // docname: frm.doc.name, - // on_success: (attachment, r) => { - // if (!r.exc) { - // frm.reload_doc(); - // } - // } - // }); - // }, "E-Invoicing"); + // }, + // __("E Invoicing") + // ); // } - // if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) { + + // if (docstatus == 1 && irn && !irn_cancelled) { // frm.add_custom_button( - // "Cancel IRN", + // __("Cancel IRN"), // () => { + // const fields = [ + // { + // "label" : "Reason", + // "fieldname": "reason", + // "fieldtype": "Select", + // "reqd": 1, + // "default": "1-Duplicate", + // "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + // }, + // { + // "label": "Remark", + // "fieldname": "remark", + // "fieldtype": "Data", + // "reqd": 1 + // } + // ]; // const d = new frappe.ui.Dialog({ - // title: __('Cancel IRN'), - // fields: [ - // { - // "label" : "Reason", "fieldname": "reason", - // "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", - // "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] - // }, - // { - // "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 - // } - // ], + // title: __("Cancel IRN"), + // fields: fields, // primary_action: function() { // const data = d.get_values(); - // const args = { - // cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_cancel_einvoice', - // irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark, name: frm.doc.name - // }; - // open_url_post(frappe.request.url, args); - // d.hide(); + // frappe.call({ + // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.cancel_irn', + // args: { + // doctype: doctype, + // name: name, + // irn: irn, + // reason: data.reason.split('-')[0], + // remark: data.remark + // }, + // freeze: true, + // callback: () => frm.reload_doc() || d.hide(), + // error: () => d.hide() + // }); // }, - // primary_action_label: __('Download JSON') + // primary_action_label: __('Submit') // }); // d.show(); - // }, "E-Invoicing"); - - // frm.add_custom_button( - // "Upload Cancel JSON", - // () => { - // new frappe.ui.FileUploader({ - // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_cancel_ack', - // allow_multiple: 0, - // doctype: frm.doc.doctype, - // docname: frm.doc.name, - // on_success: (attachment, r) => { - // if (!r.exc) { - // frm.reload_doc(); - // } - // } - // }); - // }, "E-Invoicing"); + // }, + // __("E Invoicing") + // ) // } + + // if (docstatus == 1 && irn && !irn_cancelled && !eway_bill_cancelled) { + // frm.add_custom_button( + // __("Cancel E-Way Bill"), + // () => { + // const fields = [ + // { + // "label" : "Reason", + // "fieldname": "reason", + // "fieldtype": "Select", + // "reqd": 1, + // "default": "1-Duplicate", + // "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + // }, + // { + // "label": "Remark", + // "fieldname": "remark", + // "fieldtype": "Data", + // "reqd": 1 + // } + // ] + // const d = new frappe.ui.Dialog({ + // title: __('Cancel E-Way Bill'), + // fields: fields, + // primary_action: function() { + // const data = d.get_values(); + // frappe.call({ + // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.cancel_eway_bill', + // args: { + // doctype: doctype, + // name: name, + // eway_bill: ewaybill, + // reason: data.reason.split('-')[0], + // remark: data.remark + // }, + // freeze: true, + // callback: () => frm.reload_doc() || d.hide(), + // error: () => d.hide() + // }) + // }, + // primary_action_label: __('Submit') + // }); + // d.show(); + // }, + // __("E Invoicing") + // ); + // } + + if (docstatus == 0 && !irn && !__unsaved) { + frm.add_custom_button( + "Download E-Invoice", + () => { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.make_einvoice', + args: { doctype, name }, + freeze: true, + callback: (res) => { + if (!res.exc) { + const args = { + cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_einvoice', + einvoice: res.message.einvoice, + name: name + }; + open_url_post(frappe.request.url, args); + } + } + }) + }, "E-Invoicing"); + frm.add_custom_button( + "Upload Signed E-Invoice", + () => { + new frappe.ui.FileUploader({ + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_einvoice', + allow_multiple: 0, + doctype: doctype, + docname: name, + on_success: (attachment, r) => { + if (!r.exc) { + frm.reload_doc(); + } + } + }); + }, "E-Invoicing"); + } + if (docstatus == 1 && irn && !irn_cancelled) { + frm.add_custom_button( + "Cancel IRN", + () => { + const d = new frappe.ui.Dialog({ + title: __('Cancel IRN'), + fields: [ + { + "label" : "Reason", "fieldname": "reason", + "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 + } + ], + primary_action: function() { + const data = d.get_values(); + const args = { + cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_cancel_einvoice', + irn: irn, reason: data.reason.split('-')[0], remark: data.remark, name: name + }; + open_url_post(frappe.request.url, args); + d.hide(); + }, + primary_action_label: __('Download JSON') + }); + d.show(); + }, "E-Invoicing"); + + frm.add_custom_button( + "Upload Cancel JSON", + () => { + new frappe.ui.FileUploader({ + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_cancel_ack', + allow_multiple: 0, + doctype: doctype, + docname: name, + on_success: (attachment, r) => { + if (!r.exc) { + frm.reload_doc(); + } + } + }); + }, "E-Invoicing"); + } } }) } \ No newline at end of file From cc704044bd818827ece699e0aa94e9c0d13547cc Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 21 Oct 2020 19:18:17 +0530 Subject: [PATCH 045/115] fix: print format --- erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html index a900711af1b..b4255038d39 100644 --- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -3,7 +3,7 @@
    - From e50804fb9dfd0b754db5879d53db381be9c6bcc5 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 22 Oct 2020 18:21:53 +0530 Subject: [PATCH 046/115] fix: validations --- .../india/e_invoice/e_invoice_utils.py | 104 +++++++++--------- erpnext/regional/india/e_invoice/einvoice.js | 2 +- 2 files changed, 51 insertions(+), 55 deletions(-) diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py index 649f5eb9f3e..46242e4a671 100644 --- a/erpnext/regional/india/e_invoice/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice/e_invoice_utils.py @@ -151,6 +151,7 @@ def generate_irn(doctype, name): headers = get_header(credentials) einvoice = make_einvoice(doctype, name) + einvoice = json.dumps(einvoice) enc_einvoice_json = aes_encrypt(einvoice, credentials.sek) payload = dict(Data=enc_einvoice_json) @@ -230,7 +231,7 @@ def handle_err_response(response): if response.get('Status') == 0: err_details = response.get('ErrorDetails') print(response) - error_msgs = [] + errors = [] for d in err_details: err_code = d.get('ErrorCode') @@ -239,14 +240,14 @@ def handle_err_response(response): response = get_irn_details(irn[0]) return response - error_msgs.append(d.get('ErrorMessage')) + errors.append(d.get('ErrorMessage')) - if error_msgs: - if len(error_msgs) > 1: - li = ['
  • '+ d +'
  • ' for d in error_msgs] + if errors: + if len(errors) > 1: + li = ['
  • '+ d +'
  • ' for d in errors] frappe.throw(_("""
      {}
    """).format(''.join(li)), title=_('API Request Failed')) else: - frappe.throw(_('{}').format(error_msgs[0]), title=_('API Request Failed')) + frappe.throw(_('{}').format(errors[0]), title=_('API Request Failed')) return response @@ -425,7 +426,6 @@ def get_eway_bill_details(invoice): def make_einvoice(doctype, name): invoice = frappe.get_doc(doctype, name) schema = read_json('einv_template') - validations = json.loads(read_json('einv_validation')) item_list = get_item_list(invoice) doc_details = get_doc_details(invoice) @@ -464,66 +464,62 @@ def make_einvoice(doctype, name): export_details=export_details, eway_bill_details=eway_bill_details ) einvoice = json.loads(einvoice) - - error_msgs = validate_einvoice(validations, einvoice, []) - if error_msgs: - if len(error_msgs) > 1: - li = ['
  • '+ d +'
  • ' for d in error_msgs] + + validations = json.loads(read_json('einv_validation')) + errors = validate_einvoice(validations, einvoice, []) + if errors: + if len(errors) > 1: + li = ['
  • '+ d +'
  • ' for d in errors] frappe.throw("
      {}
    ".format(''.join(li)), title=_('E Invoice Validation Failed')) else: - frappe.throw(error_msgs[0], title=_('E Invoice Validation Failed')) + frappe.throw(errors[0], title=_('E Invoice Validation Failed')) - return {'einvoice': json.dumps([einvoice])} + return einvoice -def validate_einvoice(validations, einvoice, error_msgs=[]): - type_map = { 'string': 'str', 'number': 'int', 'object': 'dict', 'array': 'list' } - - for field, validation in validations.items(): - invoice_value = einvoice.get(field) - if not invoice_value: - continue - - should_be_of_type = type_map[validation.get('type').lower()] - if should_be_of_type in ['dict', 'list']: - child_validations = validation.get('properties') - - if isinstance(invoice_value, list): - for d in invoice_value: - validate_einvoice(child_validations, d, error_msgs) - else: - validate_einvoice(child_validations, invoice_value, error_msgs) - # remove keys with empty dicts - if not invoice_value: - einvoice.pop(field, None) - continue - - if invoice_value == 'None': +def validate_einvoice(validations, einvoice, errors=[]): + for fieldname, field_validation in validations.items(): + value = einvoice.get(fieldname, None) + if value in (None, "None", {}, []): # remove keys with empty values - einvoice.pop(field, None) + einvoice.pop(fieldname, None) + continue + + value_type = field_validation.get("type").lower() + if value_type in ['object', 'array']: + child_validations = field_validation.get('properties') + + if isinstance(value, list): + for d in value: + validate_einvoice(child_validations, d, errors) + else: + validate_einvoice(child_validations, value, errors) + if not value: + # remove empty dicts + einvoice.pop(fieldname, None) continue # convert to int or str - if should_be_of_type == 'str': - einvoice[field] = str(invoice_value) - elif should_be_of_type == 'int': - einvoice[field] = flt(invoice_value, 3) + if value_type == 'string': + einvoice[fieldname] = str(value) + elif value_type == 'number': + einvoice[fieldname] = flt(value, 2) - max_length = validation.get('maxLength') - minimum = flt(validation.get('minimum')) - maximum = flt(validation.get('maximum')) - pattern_str = validation.get('pattern') + max_length = field_validation.get('maxLength') + minimum = flt(field_validation.get('minimum')) + maximum = flt(field_validation.get('maximum')) + pattern_str = field_validation.get('pattern') pattern = re.compile(pattern_str or '') - field_label = validation.get('label') or field + label = field_validation.get('label') or fieldname - if should_be_of_type == 'str' and len(invoice_value) > max_length: - error_msgs.append(_('{} should not exceed {} characters').format(field_label, max_length)) - if should_be_of_type == 'int' and not (flt(invoice_value) <= maximum): - error_msgs.append(_('{} should be less than {}').format(field_label, maximum)) - if pattern_str and not pattern.match(invoice_value): - error_msgs.append(validation.get('validationMsg')) + if value_type == 'string' and len(value) > max_length: + errors.append(_('{} should not exceed {} characters').format(fieldname_label, max_length)) + if value_type == 'number' and not (flt(value) <= maximum): + errors.append(_('{} should be less than {}').format(label, maximum)) + if pattern_str and not pattern.match(value): + errors.append(field_validation.get('validationMsg')) - return error_msgs + return errors def update_einvoice_fields(doctype, name, signed_einvoice): enc_signed_invoice = signed_einvoice.get('SignedInvoice') diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 7373e831b0c..b071919570d 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -130,7 +130,7 @@ erpnext.setup_einvoice_actions = (doctype) => { if (!res.exc) { const args = { cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_einvoice', - einvoice: res.message.einvoice, + einvoice: JSON.stringify([res.message]), name: name }; open_url_post(frappe.request.url, args); From c3719b46b201efdcdb62c98c8aa77a108d5176d4 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 23 Oct 2020 20:30:08 +0530 Subject: [PATCH 047/115] fix: add permissions on regional setup --- .../print_format/gst_e_invoice/gst_e_invoice.json | 4 ++-- .../e_invoice_settings/e_invoice_settings.json | 15 ++------------- erpnext/regional/india/setup.py | 5 +++-- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json index 0a5c8a27882..1001199a092 100644 --- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json @@ -3,7 +3,7 @@ "creation": "2020-10-10 18:01:21.032914", "custom_format": 0, "default_print_language": "en-US", - "disabled": 0, + "disabled": 1, "doc_type": "Sales Invoice", "docstatus": 0, "doctype": "Print Format", @@ -11,7 +11,7 @@ "html": "", "idx": 0, "line_breaks": 1, - "modified": "2020-10-10 18:01:21.032914", + "modified": "2020-10-23 19:54:40.634936", "modified_by": "Administrator", "module": "Accounts", "name": "GST E-Invoice", diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json index 3df9243925c..d9a1c4976a9 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json @@ -108,23 +108,12 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-09-29 17:05:33.512227", + "modified": "2020-10-23 19:55:11.417161", "modified_by": "Administrator", "module": "Regional", "name": "E Invoice Settings", "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], + "permissions": [], "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index fc8004b4752..4f1ca5012d1 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -77,7 +77,7 @@ def add_custom_roles_for_reports(): )).insert() def add_permissions(): - for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): + for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate', 'E Invoice Settings'): add_permission(doctype, 'All', 0) for role in ('Accounts Manager', 'Accounts User', 'System Manager'): add_permission(doctype, role, 0) @@ -93,9 +93,10 @@ def add_permissions(): def add_print_formats(): frappe.reload_doc("regional", "print_format", "gst_tax_invoice") frappe.reload_doc("accounts", "print_format", "gst_pos_invoice") + frappe.reload_doc("accounts", "print_format", "GST E-Invoice") frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where - name in('GST POS Invoice', 'GST Tax Invoice') """) + name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """) def make_custom_fields(update=True): hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', From ba1cd741897a62b9d516e2039822732ae05e62e8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 23 Oct 2020 21:19:55 +0530 Subject: [PATCH 048/115] feat: add patch --- erpnext/patches.txt | 3 +- .../patches/v12_0/setup_einvoice_fields.py | 31 +++++++++++++++++++ .../india/e_invoice/e_invoice_utils.py | 10 ++++-- 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 erpnext/patches/v12_0/setup_einvoice_fields.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index b5f31bafa7e..67dc8e38e95 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -677,4 +677,5 @@ erpnext.patches.v12_0.set_multi_uom_in_rfq erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v12_0.update_leave_application_status -erpnext.patches.v12_0.update_payment_entry_status \ No newline at end of file +erpnext.patches.v12_0.update_payment_entry_status +erpnext.patches.v12_0.setup_einvoice_fields diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py new file mode 100644 index 00000000000..e230eb0bf75 --- /dev/null +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -0,0 +1,31 @@ +from __future__ import unicode_literals +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +from erpnext.regional.india.setup import add_permissions, add_print_formats + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + custom_fields = { + 'Sales Invoice': [ + dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, + depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), + + dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, + depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), + + dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), + + dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1) + ] + } + create_custom_fields(custom_fields, update=True) + add_permissions() + add_print_formats() \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py index 46242e4a671..6155a04da29 100644 --- a/erpnext/regional/india/e_invoice/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice/e_invoice_utils.py @@ -16,8 +16,8 @@ from Crypto.Cipher import PKCS1_v1_5, AES from Crypto.Util.Padding import pad, unpad from frappe.model.document import Document from frappe import _, get_module_path, scrub -from erpnext.regional.india.utils import get_gst_accounts from frappe.integrations.utils import make_post_request, make_get_request +from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply from frappe.utils.data import get_datetime, cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime def validate_einvoice_fields(doc): @@ -33,6 +33,9 @@ def validate_einvoice_fields(doc): def get_credentials(): doc = frappe.get_doc('E Invoice Settings') + if not doc.enable: + frappe.throw(_("To setup E Invoicing you need to enable E Invoice Settings first."), title=_("E Invoicing Disabled")) + if not doc.token_expiry or time_diff_in_seconds(now_datetime(), doc.token_expiry) > 5.0: fetch_token(doc) doc.load_from_db() @@ -437,7 +440,8 @@ def make_einvoice(doctype, name): buyer_details = get_overseas_address_details(invoice.customer_address) else: buyer_details = get_party_gstin_details(invoice.customer_address) - place_of_supply = invoice.place_of_supply.split('-')[0] + place_of_supply = get_place_of_supply(invoice, doctype) or invoice.billing_address_gstin + place_of_supply = place_of_supply[:2] buyer_details.update(dict(place_of_supply=place_of_supply)) shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) @@ -513,7 +517,7 @@ def validate_einvoice(validations, einvoice, errors=[]): label = field_validation.get('label') or fieldname if value_type == 'string' and len(value) > max_length: - errors.append(_('{} should not exceed {} characters').format(fieldname_label, max_length)) + errors.append(_('{} should not exceed {} characters').format(label, max_length)) if value_type == 'number' and not (flt(value) <= maximum): errors.append(_('{} should be less than {}').format(label, maximum)) if pattern_str and not pattern.match(value): From 90a57c1f8c4bad210a4607023ea03cfa99885617 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 26 Oct 2020 12:13:00 +0530 Subject: [PATCH 049/115] fix: validate document name --- .../india/e_invoice/e_invoice_utils.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py index 6155a04da29..c4fff274c47 100644 --- a/erpnext/regional/india/e_invoice/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice/e_invoice_utils.py @@ -15,19 +15,33 @@ from pyqrcode import create as qrcreate from Crypto.Cipher import PKCS1_v1_5, AES from Crypto.Util.Padding import pad, unpad from frappe.model.document import Document -from frappe import _, get_module_path, scrub +from frappe import _, get_module_path, scrub, bold from frappe.integrations.utils import make_post_request, make_get_request from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply from frappe.utils.data import get_datetime, cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime def validate_einvoice_fields(doc): einvoicing_enabled = frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable') - if not doc.doctype in ['Sales Invoice', 'Purchase Invoice'] or not einvoicing_enabled: return + invalid_doctype = doc.doctype not in ['Sales Invoice', 'Purchase Invoice'] + invalid_supply_type = doc.gst_category not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] + + if invalid_doctype or invalid_supply_type or not einvoicing_enabled: return + + if doc.docstatus == 0 and doc._action == 'save': + if doc.irn: + frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) + if len(doc.name) > 16: + title = _('Document Name Too Long') + msg = (_('As you have E-Invoicing enabled, To be able to generate IRN for this invoice, document name {} exceed 16 letters. ') + .format(bold(_('should not')))) + msg += '

    ' + msg += (_('You {} modify your {} in order to have document name of {} length of 16. ') + .format(bold(_('must')), bold(_('naming series')), bold(_('maximum')))) + frappe.throw(msg, title=title) - if doc.docstatus == 0 and doc._action == 'save' and doc.irn: - frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) + elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed')) From 8616684c2495506d3bd97fe73e0df1fff228c4aa Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 26 Oct 2020 12:41:13 +0530 Subject: [PATCH 050/115] fix: return date --- erpnext/regional/india/e_invoice/e_invoice_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py index c4fff274c47..e1d380596b6 100644 --- a/erpnext/regional/india/e_invoice/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice/e_invoice_utils.py @@ -418,7 +418,7 @@ def get_payment_details(invoice): def get_return_doc_reference(invoice): invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') return frappe._dict(dict( - invoice_name=invoice.return_against, invoice_date=invoice_date + invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') )) def get_eway_bill_details(invoice): From e1e3dbcae226b6b0759bbc0124e344a3977781f3 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 26 Oct 2020 12:57:46 +0530 Subject: [PATCH 051/115] fix: credit note einvoice --- .../india/e_invoice/e_invoice_utils.py | 40 ++++++++++--------- .../india/e_invoice/einv_template.json | 4 +- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py index e1d380596b6..9c7b4d5c46f 100644 --- a/erpnext/regional/india/e_invoice/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice/e_invoice_utils.py @@ -339,16 +339,18 @@ def get_item_list(invoice): for d in invoice.items: item_schema = read_json('einv_item_template') - item = frappe._dict(dict()) + item = frappe._dict({}) item.update(d.as_dict()) item.sr_no = d.idx item.description = d.item_name item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y' item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None - item.unit_rate = item.base_price_list_rate if item.discount_amount else item.base_rate - item.total_amount = item.unit_rate * item.qty - item.discount_amount = item.discount_amount * item.qty + item.qty = abs(item.qty) + item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_rate) + item.total_amount = abs(item.unit_rate * item.qty) + item.discount_amount = abs(item.discount_amount * item.qty) + item.base_amount = abs(item.base_amount) item.tax_rate = 0 item.igst_amount = 0 item.cgst_amount = 0 @@ -360,18 +362,18 @@ def get_item_list(invoice): item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) if t.account_head in gst_accounts.cess_account: item.cess_rate += item_tax_detail[0] - item.cess_amount += item_tax_detail[1] + item.cess_amount += abs(item_tax_detail[1]) elif t.account_head in gst_accounts.igst_account: item.tax_rate += item_tax_detail[0] - item.igst_amount += item_tax_detail[1] + item.igst_amount += abs(item_tax_detail[1]) elif t.account_head in gst_accounts.sgst_account: item.tax_rate += item_tax_detail[0] - item.sgst_amount += item_tax_detail[1] + item.sgst_amount += abs(item_tax_detail[1]) elif t.account_head in gst_accounts.cgst_account: item.tax_rate += item_tax_detail[0] - item.cgst_amount += item_tax_detail[1] + item.cgst_amount += abs(item_tax_detail[1]) - item.total_value = item.base_amount + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount + item.total_value = abs(item.base_amount + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount) einv_item = item_schema.format(item=item) item_list.append(einv_item) @@ -382,11 +384,11 @@ def get_value_details(invoice): gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] value_details = frappe._dict(dict()) - value_details.base_net_total = invoice.base_net_total - value_details.invoice_discount_amt = invoice.discount_amount - value_details.round_off = invoice.base_rounding_adjustment - value_details.base_grand_total = invoice.base_rounded_total - value_details.grand_total = invoice.rounded_total + value_details.base_net_total = abs(invoice.base_net_total) + value_details.invoice_discount_amt = abs(invoice.discount_amount) + value_details.round_off = abs(invoice.base_rounding_adjustment) + value_details.base_grand_total = abs(invoice.base_rounded_total) + value_details.grand_total = abs(invoice.rounded_total) value_details.total_cgst_amt = 0 value_details.total_sgst_amt = 0 value_details.total_igst_amt = 0 @@ -394,13 +396,13 @@ def get_value_details(invoice): for t in invoice.taxes: if t.account_head in gst_accounts_list: if t.account_head in gst_accounts.cess_account: - value_details.total_cess_amt += t.base_tax_amount + value_details.total_cess_amt += abs(t.base_tax_amount) elif t.account_head in gst_accounts.igst_account: - value_details.total_igst_amt += t.base_tax_amount + value_details.total_igst_amt += abs(t.base_tax_amount) elif t.account_head in gst_accounts.sgst_account: - value_details.total_sgst_amt += t.base_tax_amount + value_details.total_sgst_amt += abs(t.base_tax_amount) elif t.account_head in gst_accounts.cgst_account: - value_details.total_cgst_amt += t.base_tax_amount + value_details.total_cgst_amt += abs(t.base_tax_amount) return value_details @@ -520,7 +522,7 @@ def validate_einvoice(validations, einvoice, errors=[]): if value_type == 'string': einvoice[fieldname] = str(value) elif value_type == 'number': - einvoice[fieldname] = flt(value, 2) + einvoice[fieldname] = flt(value, 2) if fieldname != 'Pin' else int(value) max_length = field_validation.get('maxLength') minimum = flt(field_validation.get('minimum')) diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json index 0acc01cd386..46741ca0337 100644 --- a/erpnext/regional/india/e_invoice/einv_template.json +++ b/erpnext/regional/india/e_invoice/einv_template.json @@ -83,10 +83,10 @@ "InvStDt": "{period_details.start_date}", "InvEndDt": "{period_details.end_date}" }}, - "PrecDocDtls": {{ + "PrecDocDtls": [{{ "InvNo": "{prev_doc_details.invoice_name}", "InvDt": "{prev_doc_details.invoice_date}" - }} + }}] }}, "ExpDtls": {{ "ShipBNo": "{export_details.bill_no}", From 9363725342fc05456e6f55d10094c87cc9a0e5f9 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 26 Oct 2020 13:18:33 +0530 Subject: [PATCH 052/115] fix: validations --- erpnext/regional/india/e_invoice/e_invoice_utils.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py index 9c7b4d5c46f..82ac20cd76d 100644 --- a/erpnext/regional/india/e_invoice/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice/e_invoice_utils.py @@ -396,13 +396,13 @@ def get_value_details(invoice): for t in invoice.taxes: if t.account_head in gst_accounts_list: if t.account_head in gst_accounts.cess_account: - value_details.total_cess_amt += abs(t.base_tax_amount) + value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) elif t.account_head in gst_accounts.igst_account: - value_details.total_igst_amt += abs(t.base_tax_amount) + value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount) elif t.account_head in gst_accounts.sgst_account: - value_details.total_sgst_amt += abs(t.base_tax_amount) + value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount) elif t.account_head in gst_accounts.cgst_account: - value_details.total_cgst_amt += abs(t.base_tax_amount) + value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount) return value_details @@ -499,7 +499,7 @@ def make_einvoice(doctype, name): def validate_einvoice(validations, einvoice, errors=[]): for fieldname, field_validation in validations.items(): value = einvoice.get(fieldname, None) - if value in (None, "None", {}, []): + if not value or value == "None": # remove keys with empty values einvoice.pop(fieldname, None) continue @@ -511,6 +511,9 @@ def validate_einvoice(validations, einvoice, errors=[]): if isinstance(value, list): for d in value: validate_einvoice(child_validations, d, errors) + if not d: + # remove empty dicts + einvoice.pop(fieldname, None) else: validate_einvoice(child_validations, value, errors) if not value: From 31ee8b4011187d2a75a190ac43dd5c6d1f92cbe5 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 26 Oct 2020 14:13:11 +0530 Subject: [PATCH 053/115] fix: error logging --- erpnext/regional/india/e_invoice/e_invoice_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/e_invoice_utils.py index 82ac20cd76d..4913569c395 100644 --- a/erpnext/regional/india/e_invoice/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice/e_invoice_utils.py @@ -247,7 +247,6 @@ def decrypt_irn_response(data): def handle_err_response(response): if response.get('Status') == 0: err_details = response.get('ErrorDetails') - print(response) errors = [] for d in err_details: err_code = d.get('ErrorCode') @@ -260,11 +259,12 @@ def handle_err_response(response): errors.append(d.get('ErrorMessage')) if errors: + frappe.log_error(title="E Invoice API Request Failed", message=json.dumps(errors, default=str, indent=4)) if len(errors) > 1: li = ['
  • '+ d +'
  • ' for d in errors] frappe.throw(_("""
      {}
    """).format(''.join(li)), title=_('API Request Failed')) else: - frappe.throw(_('{}').format(errors[0]), title=_('API Request Failed')) + frappe.throw(errors[0], title=_('API Request Failed')) return response @@ -488,6 +488,7 @@ def make_einvoice(doctype, name): validations = json.loads(read_json('einv_validation')) errors = validate_einvoice(validations, einvoice, []) if errors: + frappe.log_error(title="E Invoice Validation Failed", message=json.dumps(errors, default=str, indent=4)) if len(errors) > 1: li = ['
  • '+ d +'
  • ' for d in errors] frappe.throw("
      {}
    ".format(''.join(li)), title=_('E Invoice Validation Failed')) From 5eb430fe51af75a9d99f5b3bf947a5be405c10df Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 26 Oct 2020 17:19:51 +0530 Subject: [PATCH 054/115] fix: e_invoice module not found --- erpnext/hooks.py | 2 +- erpnext/regional/india/e_invoice/__init__.py | 0 .../regional/india/e_invoice/{e_invoice_utils.py => utils.py} | 0 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 erpnext/regional/india/e_invoice/__init__.py rename erpnext/regional/india/e_invoice/{e_invoice_utils.py => utils.py} (100%) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index fa0ab7535c6..81fad7df4ad 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -358,7 +358,7 @@ regional_overrides = { 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries', - 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.e_invoice_utils.validate_einvoice_fields', + 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields' }, 'United Arab Emirates': { 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data' diff --git a/erpnext/regional/india/e_invoice/__init__.py b/erpnext/regional/india/e_invoice/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/india/e_invoice/e_invoice_utils.py b/erpnext/regional/india/e_invoice/utils.py similarity index 100% rename from erpnext/regional/india/e_invoice/e_invoice_utils.py rename to erpnext/regional/india/e_invoice/utils.py From 12d9bc37877982297177c5af861f8f7dc5e646cf Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 26 Oct 2020 17:59:37 +0530 Subject: [PATCH 055/115] fix: add missing package --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index f807fa6c29d..20e43c44948 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ PyGithub==1.44.1 python-stdnum==1.12 Unidecode==1.1.1 WooCommerce==2.1.1 +pycryptodome==3.9.8 \ No newline at end of file From 37e4c91ec231585e6da2e2bcd5d39f5f9a5092d0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 28 Oct 2020 11:25:16 +0530 Subject: [PATCH 056/115] fix: rename e_invoice_utils.py --- .../doctype/e_invoice_settings/e_invoice_settings.js | 2 +- .../doctype/e_invoice_settings/e_invoice_settings.py | 4 +++- erpnext/regional/india/e_invoice/einvoice.js | 10 +++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js index e9fb622b6b6..bea16ffa7fd 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js @@ -15,7 +15,7 @@ frappe.ui.form.on('E Invoice Settings', { frm.add_custom_button(__("Fetch Token"), () => { frm.call({ - method: 'erpnext.regional.india.e_invoice.e_invoice_utils.fetch_token', + method: 'erpnext.regional.india.e_invoice.utils.fetch_token', freeze: true, callback: () => frm.refresh() }); diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py index e90d07edbd6..27f4a19b246 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -17,7 +17,9 @@ class EInvoiceSettings(Document): frappe.throw(_("{} is required").format(frappe.unscrub(d)), title=_("Missing Values")) def before_save(self): - if not self.public_key or self.has_value_changed('public_key_file'): + previous = self.get_doc_before_save() + public_file_changed = previous.get('public_key_file') != self.get('public_key_file') if previous else True + if not self.public_key or public_file_changed: self.public_key = self.read_key_file() def read_key_file(self): diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index b071919570d..e6fccbd7be4 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -123,13 +123,13 @@ erpnext.setup_einvoice_actions = (doctype) => { "Download E-Invoice", () => { frappe.call({ - method: 'erpnext.regional.india.e_invoice.e_invoice_utils.make_einvoice', + method: 'erpnext.regional.india.e_invoice.utils.make_einvoice', args: { doctype, name }, freeze: true, callback: (res) => { if (!res.exc) { const args = { - cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_einvoice', + cmd: 'erpnext.regional.india.e_invoice.utils.download_einvoice', einvoice: JSON.stringify([res.message]), name: name }; @@ -142,7 +142,7 @@ erpnext.setup_einvoice_actions = (doctype) => { "Upload Signed E-Invoice", () => { new frappe.ui.FileUploader({ - method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_einvoice', + method: 'erpnext.regional.india.e_invoice.utils.upload_einvoice', allow_multiple: 0, doctype: doctype, docname: name, @@ -173,7 +173,7 @@ erpnext.setup_einvoice_actions = (doctype) => { primary_action: function() { const data = d.get_values(); const args = { - cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_cancel_einvoice', + cmd: 'erpnext.regional.india.e_invoice.utils.download_cancel_einvoice', irn: irn, reason: data.reason.split('-')[0], remark: data.remark, name: name }; open_url_post(frappe.request.url, args); @@ -188,7 +188,7 @@ erpnext.setup_einvoice_actions = (doctype) => { "Upload Cancel JSON", () => { new frappe.ui.FileUploader({ - method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_cancel_ack', + method: 'erpnext.regional.india.e_invoice.utils.upload_cancel_ack', allow_multiple: 0, doctype: doctype, docname: name, From 271f35fef550e4fa4874ff5a52240dc4143e3c07 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 28 Oct 2020 12:38:58 +0530 Subject: [PATCH 057/115] fix: einvoice field validation --- erpnext/regional/india/e_invoice/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 4913569c395..8bca1f84e96 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -22,8 +22,8 @@ from frappe.utils.data import get_datetime, cstr, cint, format_date, flt, time_d def validate_einvoice_fields(doc): einvoicing_enabled = frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable') - invalid_doctype = doc.doctype not in ['Sales Invoice', 'Purchase Invoice'] - invalid_supply_type = doc.gst_category not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] + invalid_doctype = doc.doctype not in ['Sales Invoice'] + invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] if invalid_doctype or invalid_supply_type or not einvoicing_enabled: return From 9182c2e09d00757bd6bacd9c094ce330a8a8d15d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 28 Oct 2020 18:51:52 +0530 Subject: [PATCH 058/115] fix: patch --- erpnext/patches/v12_0/setup_einvoice_fields.py | 1 + erpnext/regional/india/e_invoice/utils.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index e230eb0bf75..fbb313a9fe1 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -8,6 +8,7 @@ def execute(): if not company: return + frappe.reload_doc("regional", "doctype", "e_invoice_settings") custom_fields = { 'Sales Invoice': [ dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 8bca1f84e96..103245ba12b 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -526,7 +526,7 @@ def validate_einvoice(validations, einvoice, errors=[]): if value_type == 'string': einvoice[fieldname] = str(value) elif value_type == 'number': - einvoice[fieldname] = flt(value, 2) if fieldname != 'Pin' else int(value) + einvoice[fieldname] = flt(value, 2) if fieldname != 'Pin' or fieldname != 'Distance' else int(value) max_length = field_validation.get('maxLength') minimum = flt(field_validation.get('minimum')) From 6c064a1f95c649faa82599b652f89e51348fc2ea Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sun, 1 Nov 2020 12:48:53 +0530 Subject: [PATCH 059/115] fix: invoice totals calculation --- .../india/e_invoice/einv_item_template.json | 5 +++++ .../regional/india/e_invoice/einv_template.json | 1 + erpnext/regional/india/e_invoice/utils.py | 16 +++++++++++----- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einv_item_template.json b/erpnext/regional/india/e_invoice/einv_item_template.json index f87b0f15f3c..23afd013d56 100644 --- a/erpnext/regional/india/e_invoice/einv_item_template.json +++ b/erpnext/regional/india/e_invoice/einv_item_template.json @@ -18,6 +18,11 @@ "SgstAmt": "{item.sgst_amount}", "CesRt": "{item.cess_rate}", "CesAmt": "{item.cess_amount}", + "CesNonAdvlAmt": "{item.cess_nadv_amount}", + "StateCesRt": "{item.state_cess_rate}", + "StateCesAmt": "{item.state_cess_amount}", + "StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}", + "OthChrg": "{item.other_charges}", "TotItemVal": "{item.total_value}", "BchDtls": {{ "Nm": "{item.batch_no}", diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json index 46741ca0337..bf6b855d2b8 100644 --- a/erpnext/regional/india/e_invoice/einv_template.json +++ b/erpnext/regional/india/e_invoice/einv_template.json @@ -66,6 +66,7 @@ "CesVal": "{value_details.total_cess_amt}", "Discount": "{value_details.invoice_discount_amt}", "RndOffAmt": "{value_details.round_off}", + "OthChrg": "{value_details.total_other_charges}", "TotInvVal": "{value_details.base_grand_total}", "TotInvValFc": "{value_details.grand_total}" }}, diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 103245ba12b..9e385d67117 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -347,19 +347,20 @@ def get_item_list(invoice): item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None item.qty = abs(item.qty) - item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_rate) + item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_net_rate) item.total_amount = abs(item.unit_rate * item.qty) item.discount_amount = abs(item.discount_amount * item.qty) - item.base_amount = abs(item.base_amount) + item.base_amount = abs(item.base_net_amount) item.tax_rate = 0 item.igst_amount = 0 item.cgst_amount = 0 item.sgst_amount = 0 item.cess_rate = 0 item.cess_amount = 0 + item.other_charges = 0 for t in invoice.taxes: + item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) if t.account_head in gst_accounts_list: - item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) if t.account_head in gst_accounts.cess_account: item.cess_rate += item_tax_detail[0] item.cess_amount += abs(item_tax_detail[1]) @@ -372,8 +373,10 @@ def get_item_list(invoice): elif t.account_head in gst_accounts.cgst_account: item.tax_rate += item_tax_detail[0] item.cgst_amount += abs(item_tax_detail[1]) + else: + item.other_charges += abs(item_tax_detail[1]) - item.total_value = abs(item.base_amount + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount) + item.total_value = abs(item.base_amount + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount + item.other_charges) einv_item = item_schema.format(item=item) item_list.append(einv_item) @@ -386,13 +389,14 @@ def get_value_details(invoice): value_details = frappe._dict(dict()) value_details.base_net_total = abs(invoice.base_net_total) value_details.invoice_discount_amt = abs(invoice.discount_amount) - value_details.round_off = abs(invoice.base_rounding_adjustment) + value_details.round_off = invoice.rounding_adjustment if invoice.rounding_adjustment > 0 else 0 value_details.base_grand_total = abs(invoice.base_rounded_total) value_details.grand_total = abs(invoice.rounded_total) value_details.total_cgst_amt = 0 value_details.total_sgst_amt = 0 value_details.total_igst_amt = 0 value_details.total_cess_amt = 0 + value_details.total_other_charges = 0 for t in invoice.taxes: if t.account_head in gst_accounts_list: if t.account_head in gst_accounts.cess_account: @@ -403,6 +407,8 @@ def get_value_details(invoice): value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount) elif t.account_head in gst_accounts.cgst_account: value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount) + else: + value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) return value_details From b77375c72b67967f3252234ecc8fad85366f621a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 2 Nov 2020 14:10:38 +0530 Subject: [PATCH 060/115] fix: other charges calculation --- erpnext/regional/india/e_invoice/utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 9e385d67117..34139e5db7d 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -373,8 +373,6 @@ def get_item_list(invoice): elif t.account_head in gst_accounts.cgst_account: item.tax_rate += item_tax_detail[0] item.cgst_amount += abs(item_tax_detail[1]) - else: - item.other_charges += abs(item_tax_detail[1]) item.total_value = abs(item.base_amount + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount + item.other_charges) einv_item = item_schema.format(item=item) @@ -388,8 +386,8 @@ def get_value_details(invoice): value_details = frappe._dict(dict()) value_details.base_net_total = abs(invoice.base_net_total) - value_details.invoice_discount_amt = abs(invoice.discount_amount) - value_details.round_off = invoice.rounding_adjustment if invoice.rounding_adjustment > 0 else 0 + value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount > 0 else 0 + value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount < 0 else 0) value_details.base_grand_total = abs(invoice.base_rounded_total) value_details.grand_total = abs(invoice.rounded_total) value_details.total_cgst_amt = 0 From c313f05a8ea8503faac3f70cb9e163e60933f1ed Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 2 Nov 2020 19:52:42 +0530 Subject: [PATCH 061/115] chore: improve document name validation message --- erpnext/regional/india/e_invoice/utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 34139e5db7d..a5d58e62781 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -32,11 +32,13 @@ def validate_einvoice_fields(doc): frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) if len(doc.name) > 16: title = _('Document Name Too Long') - msg = (_('As you have E-Invoicing enabled, To be able to generate IRN for this invoice, document name {} exceed 16 letters. ') - .format(bold(_('should not')))) + msg = _('As you have E-Invoicing enabled, To be able to generate IRN for this invoice, ') + msg += _('document name {} exceed 16 letters. ').format(bold(_('should not'))) msg += '

    ' - msg += (_('You {} modify your {} in order to have document name of {} length of 16. ') - .format(bold(_('must')), bold(_('naming series')), bold(_('maximum')))) + msg += _('You must {} your {} in order to have document name of {} length 16. ').format( + bold(_('modify')), bold(_('naming series')), bold(_('maximum')) + ) + msg += _('Please account for ammended document names too. ') frappe.throw(msg, title=title) elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: From 049836dff8590b0e74d517388d626868e7d631ab Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 3 Nov 2020 15:21:16 +0530 Subject: [PATCH 062/115] fix: qr code image string --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index a5d58e62781..fdc63ae570d 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -563,7 +563,7 @@ def update_einvoice_fields(doctype, name, signed_einvoice): frappe.db.set_value(doctype, name, 'irn', signed_einvoice.get('Irn')) frappe.db.set_value(doctype, name, 'ewaybill', signed_einvoice.get('EwbNo')) - frappe.db.set_value(doctype, name, 'signed_qr_code', signed_einvoice.get('SignedQRCode').split('.')[1]) + frappe.db.set_value(doctype, name, 'signed_qr_code', '.'.join(signed_einvoice.get('SignedQRCode').split('.')[1:])) frappe.db.set_value(doctype, name, 'signed_einvoice', decrypted_signed_invoice) @frappe.whitelist() From 86f87e9b53916fe1f4e4996efa81d85235c3dc5b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 11:19:20 +0530 Subject: [PATCH 063/115] feat: initialize GSP connector --- erpnext/regional/india/e_invoice/utils.py | 34 ++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index fdc63ae570d..e6bfe74af58 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -629,4 +629,36 @@ def attach_qrcode_image(doctype, name): abs_file_path = os.path.abspath(_file.get_full_path()) url.png(abs_file_path, scale=2) - frappe.db.set_value(doctype, name, 'qrcode_image', _file.file_url) \ No newline at end of file + frappe.db.set_value(doctype, name, 'qrcode_image', _file.file_url) + +class GSPConnector(): + def __init__(self): + self.credentials = frappe.get_cached_doc('E Invoice Settings') + + self.base_url = 'https://gsp.adaequare.com/' + self.authenticate_url = self.base_url + 'gsp/authenticate?grant_type=token' + + def get_auth_token(self): + if time_diff_in_seconds(self.credentials.token_expiry, now_datetime()) < 150.0: + self.fetch_auth_token() + + return self.credentials.auth_token + + def fetch_auth_token(self): + headers = { + 'gspappid': self.credentials.client_id, + 'gspappsecret': self.credentials.client_secret + } + + try: + res = make_post_request(self.authenticate_url, headers=headers) + self.credentials.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token')) + self.credentials.token_expiry = add_to_date(None, seconds=res.get('expires_in')) + self.credentials.save() + + except Exception as e: + self.log_error(e) + raise + + def log_error(self, exc): + print(exc) \ No newline at end of file From 54405a4b646f8f2e776e82b63f4dedf1010b9da7 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 11:19:50 +0530 Subject: [PATCH 064/115] chore: remove unwanted fields --- .../e_invoice_settings.json | 30 +++++-------------- .../e_invoice_settings/e_invoice_settings.py | 21 +------------ 2 files changed, 9 insertions(+), 42 deletions(-) diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json index d9a1c4976a9..8816cbde3e4 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json @@ -9,13 +9,11 @@ "section_break_2", "client_id", "client_secret", - "public_key_file", "public_key", "column_break_3", "gstin", "username", "password", - "auto_refresh_token", "auth_token", "token_expiry", "sek" @@ -45,12 +43,6 @@ "label": "Client Secret", "reqd": 1 }, - { - "fieldname": "public_key_file", - "fieldtype": "Attach", - "label": "Public Key", - "reqd": 1 - }, { "fieldname": "public_key", "fieldtype": "Long Text", @@ -79,36 +71,30 @@ "label": "Password", "reqd": 1 }, - { - "default": "0", - "description": "Token will be automatically refreshed 10 mins before expiry", - "fieldname": "auto_refresh_token", - "fieldtype": "Check", - "label": "Auto Refresh Token" - }, { "fieldname": "auth_token", "fieldtype": "Data", "hidden": 1, "read_only": 1 }, - { - "fieldname": "token_expiry", - "fieldtype": "Datetime", - "hidden": 1, - "read_only": 1 - }, { "fieldname": "sek", "fieldtype": "Data", "hidden": 1, "read_only": 1 + }, + { + "default": "0", + "fieldname": "token_expiry", + "fieldtype": "Datetime", + "hidden": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-10-23 19:55:11.417161", + "modified": "2020-11-03 21:23:50.277305", "modified_by": "Administrator", "module": "Regional", "name": "E Invoice Settings", diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py index 27f4a19b246..2a4f00365f3 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -3,26 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe -from frappe import _ -from frappe.utils.data import cstr from frappe.model.document import Document -from frappe.custom.doctype.property_setter.property_setter import make_property_setter class EInvoiceSettings(Document): - def validate(self): - mandatory_fields = ['client_id', 'client_secret', 'gstin', 'username', 'password', 'public_key_file'] - for d in mandatory_fields: - if not self.get(d): - frappe.throw(_("{} is required").format(frappe.unscrub(d)), title=_("Missing Values")) - - def before_save(self): - previous = self.get_doc_before_save() - public_file_changed = previous.get('public_key_file') != self.get('public_key_file') if previous else True - if not self.public_key or public_file_changed: - self.public_key = self.read_key_file() - - def read_key_file(self): - key_file = frappe.get_doc('File', dict(attached_to_name=self.doctype)) - with open(key_file.get_full_path(), 'rb') as f: - return cstr(f.read()) \ No newline at end of file + pass From ad19268b4cf161065d7e7eaf7c347e54b35ae3cd Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 11:23:01 +0530 Subject: [PATCH 065/115] fix: qr code generation --- erpnext/regional/india/e_invoice/utils.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index e6bfe74af58..5f3edbcc407 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -563,7 +563,7 @@ def update_einvoice_fields(doctype, name, signed_einvoice): frappe.db.set_value(doctype, name, 'irn', signed_einvoice.get('Irn')) frappe.db.set_value(doctype, name, 'ewaybill', signed_einvoice.get('EwbNo')) - frappe.db.set_value(doctype, name, 'signed_qr_code', '.'.join(signed_einvoice.get('SignedQRCode').split('.')[1:])) + frappe.db.set_value(doctype, name, 'signed_qr_code', signed_einvoice.get('SignedQRCode')) frappe.db.set_value(doctype, name, 'signed_einvoice', decrypted_signed_invoice) @frappe.whitelist() @@ -616,14 +616,15 @@ def attach_qrcode_image(doctype, name): if not qrcode: return - _file = frappe.get_doc({ - 'doctype': 'File', + _file = frappe.new_doc('File') + _file.update({ 'file_name': 'Signed_QR_{name}.png'.format(name=name), 'attached_to_doctype': doctype, 'attached_to_name': name, - 'content': 'qrcode' + 'content': 'qrcode', + 'is_private': 1 }) - _file.save() + _file.insert() frappe.db.commit() url = qrcreate(qrcode) abs_file_path = os.path.abspath(_file.get_full_path()) From 495ac7ba6b2895cd985e9ad7add4d450a381a9c0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 11:28:31 +0530 Subject: [PATCH 066/115] feat: fetch and cache GSTIN details --- erpnext/regional/india/e_invoice/utils.py | 74 ++++++++++++++++++++--- 1 file changed, 64 insertions(+), 10 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 5f3edbcc407..92736470553 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -300,16 +300,18 @@ def get_doc_details(invoice): return frappe._dict(dict(invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date)) -def get_party_gstin_details(address_name): - address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] +def get_party_details(address): + from erpnext.regional.doctype.e_invoice_settings.e_invoice_settings import GSPConnector + address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] gstin = address.get('gstin') - gstin_details = get_gstin_details(gstin) + + gstin_details = GSPConnector.get_gstin_details(gstin) legal_name = gstin_details.get('LegalName') trade_name = gstin_details.get('TradeName') - location = gstin_details.get('AddrLoc') + location = gstin_details.get('AddrLoc') or address.get('city') state_code = gstin_details.get('StateCode') - pincode = cint(gstin_details.get('AddrPncd')) + pincode = gstin_details.get('AddrPncd') address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno')) address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt')) email_id = address.get('email_id') @@ -376,7 +378,10 @@ def get_item_list(invoice): item.tax_rate += item_tax_detail[0] item.cgst_amount += abs(item_tax_detail[1]) - item.total_value = abs(item.base_amount + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount + item.other_charges) + item.total_value = abs( + item.base_amount + item.igst_amount + item.sgst_amount + + item.cgst_amount + item.cess_amount + item.other_charges + ) einv_item = item_schema.format(item=item) item_list.append(einv_item) @@ -456,19 +461,19 @@ def make_einvoice(doctype, name): doc_details = get_doc_details(invoice) value_details = get_value_details(invoice) trans_details = get_trans_details(invoice) - seller_details = get_party_gstin_details(invoice.company_address) + seller_details = get_party_details(invoice.company_address) if invoice.gst_category == 'Overseas': buyer_details = get_overseas_address_details(invoice.customer_address) else: - buyer_details = get_party_gstin_details(invoice.customer_address) + buyer_details = get_party_details(invoice.customer_address) place_of_supply = get_place_of_supply(invoice, doctype) or invoice.billing_address_gstin place_of_supply = place_of_supply[:2] buyer_details.update(dict(place_of_supply=place_of_supply)) shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: - shipping_details = get_party_gstin_details(invoice.shipping_address_name) + shipping_details = get_party_details(invoice.shipping_address_name) if invoice.is_pos and invoice.base_paid_amount: payment_details = get_payment_details(invoice) @@ -532,7 +537,7 @@ def validate_einvoice(validations, einvoice, errors=[]): if value_type == 'string': einvoice[fieldname] = str(value) elif value_type == 'number': - einvoice[fieldname] = flt(value, 2) if fieldname != 'Pin' or fieldname != 'Distance' else int(value) + einvoice[fieldname] = flt(value, 2) if fieldname not in ['Pin', 'Distance'] else int(value) max_length = field_validation.get('maxLength') minimum = flt(field_validation.get('minimum')) @@ -638,6 +643,7 @@ class GSPConnector(): self.base_url = 'https://gsp.adaequare.com/' self.authenticate_url = self.base_url + 'gsp/authenticate?grant_type=token' + self.gstin_details_url = self.base_url + 'test/enriched/ei/api/master/gstin' def get_auth_token(self): if time_diff_in_seconds(self.credentials.token_expiry, now_datetime()) < 150.0: @@ -660,6 +666,54 @@ class GSPConnector(): except Exception as e: self.log_error(e) raise + + def get_headers(self): + return { + 'content-type': 'application/json', + 'user_name': self.credentials.username, + 'password': self.credentials.get_password(), + 'gstin': self.credentials.gstin, + 'authorization': self.get_auth_token(), + 'requestid': str(base64.b64encode(os.urandom(18))), + } + + def fetch_gstin_details(self, gstin): + headers = self.get_headers() + + try: + params = '?gstin={gstin}'.format(gstin=gstin) + res = make_get_request(self.gstin_details_url + params, headers=headers) + if res.get('success'): + return res.get('result') + + except Exception as e: + self.log_error(e) + + return {} + + @staticmethod + def get_gstin_details(gstin): + '''fetch or get cached GSTIN details''' + + if not hasattr(frappe.local, 'gstin_cache'): + frappe.local.gstin_cache = {} + + key = gstin + details = frappe.local.gstin_cache.get(key) + if details: + return details + + details = frappe.cache().hget('gstin_cache', key) + if details: + frappe.local.gstin_cache[key] = details + return details + + gsp_connector = GSPConnector() + details = gsp_connector.fetch_gstin_details(gstin) + + frappe.local.gstin_cache[key] = details + frappe.cache().hset('gstin_cache', key, details) + return details def log_error(self, exc): print(exc) \ No newline at end of file From 3f9390c077be0436bc0c0948fec468b4fdfd2f59 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 11:39:39 +0530 Subject: [PATCH 067/115] feat: generate & cancel IRN --- erpnext/regional/india/e_invoice/utils.py | 58 ++++++++++++++++++----- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 92736470553..153493d6564 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -556,20 +556,17 @@ def validate_einvoice(validations, einvoice, errors=[]): return errors -def update_einvoice_fields(doctype, name, signed_einvoice): - enc_signed_invoice = signed_einvoice.get('SignedInvoice') - decrypted_signed_invoice = jwt_decrypt(enc_signed_invoice)['data'] +def update_invoice(invoice, res): + doctype = invoice.doctype + name = invoice.name - if json.loads(decrypted_signed_invoice)['DocDtls']['No'] != name: - frappe.throw( - _("Document number of uploaded Signed E-Invoice doesn't matches with Sales Invoice"), - title=_("Inappropriate E-Invoice") - ) + enc_signed_invoice = res.get('SignedInvoice') + dec_signed_invoice = jwt.decode(enc_signed_invoice)['data'] - frappe.db.set_value(doctype, name, 'irn', signed_einvoice.get('Irn')) - frappe.db.set_value(doctype, name, 'ewaybill', signed_einvoice.get('EwbNo')) - frappe.db.set_value(doctype, name, 'signed_qr_code', signed_einvoice.get('SignedQRCode')) - frappe.db.set_value(doctype, name, 'signed_einvoice', decrypted_signed_invoice) + frappe.db.set_value(doctype, name, 'irn', res.get('Irn')) + frappe.db.set_value(doctype, name, 'ewaybill', res.get('EwbNo')) + frappe.db.set_value(doctype, name, 'signed_invoice', dec_signed_invoice) + frappe.db.set_value(doctype, name, 'signed_qr_code', res.get('SignedQRCode')) @frappe.whitelist() def download_einvoice(): @@ -644,6 +641,8 @@ class GSPConnector(): self.base_url = 'https://gsp.adaequare.com/' self.authenticate_url = self.base_url + 'gsp/authenticate?grant_type=token' self.gstin_details_url = self.base_url + 'test/enriched/ei/api/master/gstin' + self.generate_irn_url = self.base_url + 'test/enriched/ei/api/invoice' + self.cancel_irn_url = self.base_url + 'test/enriched/ei/api/invoice/cancel' def get_auth_token(self): if time_diff_in_seconds(self.credentials.token_expiry, now_datetime()) < 150.0: @@ -715,5 +714,40 @@ class GSPConnector(): frappe.cache().hset('gstin_cache', key, details) return details + def generate_irn(self, docname): + headers = self.get_headers() + doctype = 'Sales Invoice' + einvoice = make_einvoice(doctype, docname) + data = json.dumps(einvoice) + + try: + res = make_post_request(self.generate_irn_url, headers=headers, data=data) + if res.get('success'): + update_invoice(invoice, res) + else: + # {'success': False, 'message': '3039 : Seller Details:Pincode-560009 does not belong to the state-1, 2177 : Invalid item unit code(s)-UNIT'} + self.log_error() + + except Exception as e: + self.log_error(e) + + def cancel_irn(self, docname, irn, reason, remark): + headers = self.get_headers() + doctype = 'Sales Invoice' + data = json.dumps({ + 'Irn': irn, + 'Cnlrsn': reason, + 'Cnlrem': remark + }) + + try: + res = make_post_request(self.cancel_irn_url, headers=headers, data=data) + if res.get('success'): + frappe.db.set_value(doctype, docname, 'irn_cancelled', 1) + # frappe.db.set_value(doctype, docname, 'cancelled_on', res.get('CancelDate')) + + except Exception as e: + self.log_error(e) + def log_error(self, exc): print(exc) \ No newline at end of file From b7ee2a44a06da020c1eee8fc4017aa5154aafe3d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 11:40:13 +0530 Subject: [PATCH 068/115] feat: cancel eway bill --- erpnext/regional/india/e_invoice/utils.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 153493d6564..7250c7e75a5 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -643,6 +643,7 @@ class GSPConnector(): self.gstin_details_url = self.base_url + 'test/enriched/ei/api/master/gstin' self.generate_irn_url = self.base_url + 'test/enriched/ei/api/invoice' self.cancel_irn_url = self.base_url + 'test/enriched/ei/api/invoice/cancel' + self.cancel_ewaybill_url = self.base_url + '/test/enriched/ei/api/ewayapi' def get_auth_token(self): if time_diff_in_seconds(self.credentials.token_expiry, now_datetime()) < 150.0: @@ -749,5 +750,23 @@ class GSPConnector(): except Exception as e: self.log_error(e) + def cancel_eway_bill(self, docname, eway_bill, reason, remark): + headers = self.get_headers() + doctype = 'Sales Invoice' + data = json.dumps({ + 'ewbNo': eway_bill, + 'cancelRsnCode': reason, + 'cancelRmrk': remark + }) + + try: + res = make_post_request(self.cancel_ewaybill_url, headers=headers, data=data) + if res.get('success'): + frappe.db.set_value(doctype, docname, 'ewaybill', '') + frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1) + + except Exception as e: + self.log_error(e) + def log_error(self, exc): print(exc) \ No newline at end of file From 778141c5691540388b45759579b8e7ebbc2cfb91 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 11:45:24 +0530 Subject: [PATCH 069/115] chore: remove unwanted fuctions --- erpnext/regional/india/e_invoice/einvoice.js | 257 ++++++------------ erpnext/regional/india/e_invoice/utils.py | 267 ------------------- 2 files changed, 87 insertions(+), 437 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index e6fccbd7be4..e0db6c44e69 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -9,196 +9,113 @@ erpnext.setup_einvoice_actions = (doctype) => { const { docstatus, irn, irn_cancelled, ewaybill, eway_bill_cancelled, doctype, name, __unsaved } = frm.doc; - // if (docstatus == 0 && !irn && !__unsaved) { - // frm.add_custom_button( - // _("Generate IRN"), - // () => { - // frappe.call({ - // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.generate_irn', - // args: { doctype: doctype, name: name }, - // freeze: true, - // callback: () => frm.reload_doc() - // }) - // }, - // __("E Invoicing") - // ); - // } - - // if (docstatus == 1 && irn && !irn_cancelled) { - // frm.add_custom_button( - // __("Cancel IRN"), - // () => { - // const fields = [ - // { - // "label" : "Reason", - // "fieldname": "reason", - // "fieldtype": "Select", - // "reqd": 1, - // "default": "1-Duplicate", - // "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] - // }, - // { - // "label": "Remark", - // "fieldname": "remark", - // "fieldtype": "Data", - // "reqd": 1 - // } - // ]; - // const d = new frappe.ui.Dialog({ - // title: __("Cancel IRN"), - // fields: fields, - // primary_action: function() { - // const data = d.get_values(); - // frappe.call({ - // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.cancel_irn', - // args: { - // doctype: doctype, - // name: name, - // irn: irn, - // reason: data.reason.split('-')[0], - // remark: data.remark - // }, - // freeze: true, - // callback: () => frm.reload_doc() || d.hide(), - // error: () => d.hide() - // }); - // }, - // primary_action_label: __('Submit') - // }); - // d.show(); - // }, - // __("E Invoicing") - // ) - // } - - // if (docstatus == 1 && irn && !irn_cancelled && !eway_bill_cancelled) { - // frm.add_custom_button( - // __("Cancel E-Way Bill"), - // () => { - // const fields = [ - // { - // "label" : "Reason", - // "fieldname": "reason", - // "fieldtype": "Select", - // "reqd": 1, - // "default": "1-Duplicate", - // "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] - // }, - // { - // "label": "Remark", - // "fieldname": "remark", - // "fieldtype": "Data", - // "reqd": 1 - // } - // ] - // const d = new frappe.ui.Dialog({ - // title: __('Cancel E-Way Bill'), - // fields: fields, - // primary_action: function() { - // const data = d.get_values(); - // frappe.call({ - // method: 'erpnext.regional.india.e_invoice.e_invoice_utils.cancel_eway_bill', - // args: { - // doctype: doctype, - // name: name, - // eway_bill: ewaybill, - // reason: data.reason.split('-')[0], - // remark: data.remark - // }, - // freeze: true, - // callback: () => frm.reload_doc() || d.hide(), - // error: () => d.hide() - // }) - // }, - // primary_action_label: __('Submit') - // }); - // d.show(); - // }, - // __("E Invoicing") - // ); - // } - if (docstatus == 0 && !irn && !__unsaved) { frm.add_custom_button( - "Download E-Invoice", + __("Generate IRN"), () => { frappe.call({ - method: 'erpnext.regional.india.e_invoice.utils.make_einvoice', - args: { doctype, name }, + method: 'erpnext.regional.india.e_invoice.utils.generate_irn', + args: { doctype: doctype, name: name }, freeze: true, - callback: (res) => { - if (!res.exc) { - const args = { - cmd: 'erpnext.regional.india.e_invoice.utils.download_einvoice', - einvoice: JSON.stringify([res.message]), - name: name - }; - open_url_post(frappe.request.url, args); - } - } + callback: () => frm.reload_doc() }) - }, "E-Invoicing"); - frm.add_custom_button( - "Upload Signed E-Invoice", - () => { - new frappe.ui.FileUploader({ - method: 'erpnext.regional.india.e_invoice.utils.upload_einvoice', - allow_multiple: 0, - doctype: doctype, - docname: name, - on_success: (attachment, r) => { - if (!r.exc) { - frm.reload_doc(); - } - } - }); - }, "E-Invoicing"); + }, + __("E Invoicing") + ); } + if (docstatus == 1 && irn && !irn_cancelled) { frm.add_custom_button( - "Cancel IRN", + __("Cancel IRN"), () => { + const fields = [ + { + "label" : "Reason", + "fieldname": "reason", + "fieldtype": "Select", + "reqd": 1, + "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", + "fieldname": "remark", + "fieldtype": "Data", + "reqd": 1 + } + ]; const d = new frappe.ui.Dialog({ - title: __('Cancel IRN'), - fields: [ - { - "label" : "Reason", "fieldname": "reason", - "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", - "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] - }, - { - "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 - } - ], + title: __("Cancel IRN"), + fields: fields, primary_action: function() { const data = d.get_values(); - const args = { - cmd: 'erpnext.regional.india.e_invoice.utils.download_cancel_einvoice', - irn: irn, reason: data.reason.split('-')[0], remark: data.remark, name: name - }; - open_url_post(frappe.request.url, args); - d.hide(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.cancel_irn', + args: { + doctype: doctype, + name: name, + irn: irn, + reason: data.reason.split('-')[0], + remark: data.remark + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }); }, - primary_action_label: __('Download JSON') + primary_action_label: __('Submit') }); d.show(); - }, "E-Invoicing"); - + }, + __("E Invoicing") + ) + } + + if (docstatus == 1 && irn && !irn_cancelled && !eway_bill_cancelled) { frm.add_custom_button( - "Upload Cancel JSON", + __("Cancel E-Way Bill"), () => { - new frappe.ui.FileUploader({ - method: 'erpnext.regional.india.e_invoice.utils.upload_cancel_ack', - allow_multiple: 0, - doctype: doctype, - docname: name, - on_success: (attachment, r) => { - if (!r.exc) { - frm.reload_doc(); - } + const fields = [ + { + "label" : "Reason", + "fieldname": "reason", + "fieldtype": "Select", + "reqd": 1, + "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", + "fieldname": "remark", + "fieldtype": "Data", + "reqd": 1 } + ] + const d = new frappe.ui.Dialog({ + title: __('Cancel E-Way Bill'), + fields: fields, + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', + args: { + doctype: doctype, + name: name, + eway_bill: ewaybill, + reason: data.reason.split('-')[0], + remark: data.remark + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }) + }, + primary_action_label: __('Submit') }); - }, "E-Invoicing"); + d.show(); + }, + __("E Invoicing") + ); } } }) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 7250c7e75a5..2a174202968 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -47,229 +47,6 @@ def validate_einvoice_fields(doc): elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed')) -def get_credentials(): - doc = frappe.get_doc('E Invoice Settings') - if not doc.enable: - frappe.throw(_("To setup E Invoicing you need to enable E Invoice Settings first."), title=_("E Invoicing Disabled")) - - if not doc.token_expiry or time_diff_in_seconds(now_datetime(), doc.token_expiry) > 5.0: - fetch_token(doc) - doc.load_from_db() - - return doc - -def rsa_encrypt(msg, key): - if not (isinstance(msg, bytes) or isinstance(msg, bytearray)): - msg = str.encode(msg) - - rsa_pub_key = RSA.import_key(key) - cipher = PKCS1_v1_5.new(rsa_pub_key) - enc_msg = cipher.encrypt(msg) - b64_enc_msg = base64.b64encode(enc_msg) - return b64_enc_msg.decode() - -def aes_decrypt(enc_msg, key): - encode_as_b64 = True - if not (isinstance(key, bytes) or isinstance(key, bytearray)): - key = base64.b64decode(key) - encode_as_b64 = False - - cipher = AES.new(key, AES.MODE_ECB) - b64_enc_msg = base64.b64decode(enc_msg) - msg_bytes = cipher.decrypt(b64_enc_msg) - msg_bytes = unpad(msg_bytes, AES.block_size) # due to ECB/PKCS5Padding - if encode_as_b64: - msg_bytes = base64.b64encode(msg_bytes) - return msg_bytes.decode() - -def aes_encrypt(msg, key): - if not (isinstance(key, bytes) or isinstance(key, bytearray)): - key = base64.b64decode(key) - - cipher = AES.new(key, AES.MODE_ECB) - bytes_msg = str.encode(msg) - padded_bytes_msg = pad(bytes_msg, AES.block_size) - enc_msg = cipher.encrypt(padded_bytes_msg) - b64_enc_msg = base64.b64encode(enc_msg) - return b64_enc_msg.decode() - -def jwt_decrypt(token): - return jwt.decode(token, verify=False) - -def get_header(creds): - headers = { 'content-type': 'application/json' } - headers.update(dict(client_id=creds.client_id, client_secret=creds.client_secret, user_name=creds.username)) - headers.update(dict(Gstin=creds.gstin, AuthToken=creds.auth_token)) - return headers - -@frappe.whitelist() -def fetch_token(credentials=None): - if not credentials: - credentials = frappe.get_doc('E Invoice Settings') - - endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' - headers = { 'content-type': 'application/json' } - headers.update(dict(client_id=credentials.client_id, client_secret=credentials.client_secret)) - payload = dict(UserName=credentials.username, ForceRefreshAccessToken=bool(credentials.auto_refresh_token)) - - appkey = bytearray(os.urandom(32)) - enc_appkey = rsa_encrypt(appkey, credentials.public_key) - - password = credentials.get_password(fieldname='password') - enc_password = rsa_encrypt(password, credentials.public_key) - - payload.update(dict(Password=enc_password, AppKey=enc_appkey)) - - res = make_post_request(endpoint, headers=headers, data=json.dumps({ 'data': payload })) - handle_err_response(res) - - auth_token, token_expiry, sek = extract_token_and_sek(res, appkey) - - credentials.auth_token = auth_token - credentials.token_expiry = get_datetime(token_expiry) - credentials.sek = sek - credentials.save() - -def extract_token_and_sek(response, appkey): - data = response.get('Data') - auth_token = data.get('AuthToken') - token_expiry = data.get('TokenExpiry') - enc_sek = data.get('Sek') - sek = aes_decrypt(enc_sek, appkey) - return auth_token, token_expiry, sek - -def attach_signed_invoice(doctype, name, data): - f = frappe.get_doc({ - 'doctype': 'File', - 'file_name': 'E-INV--{}.json'.format(name), - 'attached_to_doctype': doctype, - 'attached_to_name': name, - 'content': json.dumps(data), - 'is_private': True - }).insert() - -def get_gstin_details(gstin): - credentials = get_credentials() - - endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/Master/gstin/{gstin}'.format(gstin=gstin) - headers = get_header(credentials) - - res = make_get_request(endpoint, headers=headers) - handle_err_response(res) - - enc_details = res.get('Data') - json_str = aes_decrypt(enc_details, credentials.sek) - details = json.loads(json_str) - - return details - -@frappe.whitelist() -def generate_irn(doctype, name): - endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice' - credentials = get_credentials() - headers = get_header(credentials) - - einvoice = make_einvoice(doctype, name) - einvoice = json.dumps(einvoice) - - enc_einvoice_json = aes_encrypt(einvoice, credentials.sek) - payload = dict(Data=enc_einvoice_json) - - res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) - res = handle_err_response(res) - - enc_json = res.get('Data') - json_str = aes_decrypt(enc_json, credentials.sek) - - signed_einvoice = json.loads(json_str) - decrypt_irn_response(signed_einvoice) - - update_einvoice_fields(doctype, name, signed_einvoice) - - attach_qrcode_image(doctype, name) - attach_signed_invoice(doctype, name, signed_einvoice['DecryptedSignedInvoice']) - - return signed_einvoice - -def get_irn_details(irn): - credentials = get_credentials() - - endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/irn/{irn}'.format(irn=irn) - headers = get_header(credentials) - - res = make_get_request(endpoint, headers=headers) - handle_err_response(res) - - return res - -@frappe.whitelist() -def cancel_irn(doctype, name, irn, reason, remark=''): - credentials = get_credentials() - - endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/Cancel' - headers = get_header(credentials) - - cancel_einv = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark)) - enc_json = aes_encrypt(cancel_einv, credentials.sek) - payload = dict(Data=enc_json) - - res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) - handle_err_response(res) - - frappe.db.set_value(doctype, name, 'irn_cancelled', 1) - - return res - -@frappe.whitelist() -def cancel_eway_bill(doctype, name, eway_bill, reason, remark=''): - credentials = get_credentials() - endpoint = 'https://einv-apisandbox.nic.in/ewaybillapi/v1.03/ewayapi' - headers = get_header(credentials) - - cancel_eway_bill_json = json.dumps(dict(ewbNo=eway_bill, cancelRsnCode=reason, cancelRmrk=remark)) - enc_json = aes_encrypt(cancel_eway_bill_json, credentials.sek) - payload = dict(action='CANEWB', Data=enc_json) - - res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) - handle_err_response(res) - - frappe.db.set_value(doctype, name, 'ewaybill', '') - frappe.db.set_value(doctype, name, 'eway_bill_cancelled', 1) - - return res - -def decrypt_irn_response(data): - enc_signed_invoice = data['SignedInvoice'] - enc_signed_qr_code = data['SignedQRCode'] - signed_invoice = jwt_decrypt(enc_signed_invoice)['data'] - signed_qr_code = jwt_decrypt(enc_signed_qr_code)['data'] - data['DecryptedSignedInvoice'] = json.loads(signed_invoice) - data['DecryptedSignedQRCode'] = json.loads(signed_qr_code) - -def handle_err_response(response): - if response.get('Status') == 0: - err_details = response.get('ErrorDetails') - errors = [] - for d in err_details: - err_code = d.get('ErrorCode') - - if err_code == '2150': - irn = [d['Desc']['Irn'] for d in response.get('InfoDtls') if d['InfCd'] == 'DUPIRN'] - response = get_irn_details(irn[0]) - return response - - errors.append(d.get('ErrorMessage')) - - if errors: - frappe.log_error(title="E Invoice API Request Failed", message=json.dumps(errors, default=str, indent=4)) - if len(errors) > 1: - li = ['
  • '+ d +'
  • ' for d in errors] - frappe.throw(_("""
      {}
    """).format(''.join(li)), title=_('API Request Failed')) - else: - frappe.throw(errors[0], title=_('API Request Failed')) - - return response - def read_json(name): file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name)) with open(file_path, 'r') as f: @@ -568,50 +345,6 @@ def update_invoice(invoice, res): frappe.db.set_value(doctype, name, 'signed_invoice', dec_signed_invoice) frappe.db.set_value(doctype, name, 'signed_qr_code', res.get('SignedQRCode')) -@frappe.whitelist() -def download_einvoice(): - data = frappe._dict(frappe.local.form_dict) - einvoice = data['einvoice'] - name = data['name'] - - frappe.response['filename'] = 'E-Invoice-' + name + '.json' - frappe.response['filecontent'] = einvoice - frappe.response['content_type'] = 'application/json' - frappe.response['type'] = 'download' - -@frappe.whitelist() -def upload_einvoice(): - signed_einvoice = json.loads(frappe.local.uploaded_file) - data = frappe._dict(frappe.local.form_dict) - doctype = data['doctype'] - name = data['docname'] - - update_einvoice_fields(doctype, name, signed_einvoice) - attach_qrcode_image(doctype, name) - -@frappe.whitelist() -def download_cancel_einvoice(): - data = frappe._dict(frappe.local.form_dict) - name = data['name'] - irn = data['irn'] - reason = data['reason'] - remark = data['remark'] - - cancel_einvoice = json.dumps([dict(Irn=irn, CnlRsn=reason, CnlRem=remark)]) - - frappe.response['filename'] = 'Cancel E-Invoice ' + name + '.json' - frappe.response['filecontent'] = cancel_einvoice - frappe.response['content_type'] = 'application/json' - frappe.response['type'] = 'download' - -@frappe.whitelist() -def upload_cancel_ack(): - cancel_ack = json.loads(frappe.local.uploaded_file) - data = frappe._dict(frappe.local.form_dict) - doctype = data['doctype'] - name = data['docname'] - - frappe.db.set_value(doctype, name, 'irn_cancelled', 1) def attach_qrcode_image(doctype, name): qrcode = frappe.db.get_value(doctype, name, 'signed_qr_code') From 63bfa30bc176e761e819084cbff79bb924add724 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 11:54:01 +0530 Subject: [PATCH 070/115] chore: clean up einvoice actions --- erpnext/regional/india/e_invoice/einvoice.js | 188 +++++++++---------- 1 file changed, 90 insertions(+), 98 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index e0db6c44e69..65cac992ff4 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -10,112 +10,104 @@ erpnext.setup_einvoice_actions = (doctype) => { const { docstatus, irn, irn_cancelled, ewaybill, eway_bill_cancelled, doctype, name, __unsaved } = frm.doc; if (docstatus == 0 && !irn && !__unsaved) { - frm.add_custom_button( - __("Generate IRN"), - () => { - frappe.call({ - method: 'erpnext.regional.india.e_invoice.utils.generate_irn', - args: { doctype: doctype, name: name }, - freeze: true, - callback: () => frm.reload_doc() - }) - }, - __("E Invoicing") - ); + const action = () => { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.generate_irn', + args: { doctype: doctype, name: name }, + freeze: true, + callback: () => frm.reload_doc() + }) + }; + + frm.add_custom_button(__("Generate IRN"), action, __('E Invoicing')); } if (docstatus == 1 && irn && !irn_cancelled) { - frm.add_custom_button( - __("Cancel IRN"), - () => { - const fields = [ - { - "label" : "Reason", - "fieldname": "reason", - "fieldtype": "Select", - "reqd": 1, - "default": "1-Duplicate", - "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] - }, - { - "label": "Remark", - "fieldname": "remark", - "fieldtype": "Data", - "reqd": 1 - } - ]; - const d = new frappe.ui.Dialog({ - title: __("Cancel IRN"), - fields: fields, - primary_action: function() { - const data = d.get_values(); - frappe.call({ - method: 'erpnext.regional.india.e_invoice.utils.cancel_irn', - args: { - doctype: doctype, - name: name, - irn: irn, - reason: data.reason.split('-')[0], - remark: data.remark - }, - freeze: true, - callback: () => frm.reload_doc() || d.hide(), - error: () => d.hide() - }); - }, - primary_action_label: __('Submit') - }); - d.show(); + const fields = [ + { + "label" : "Reason", + "fieldname": "reason", + "fieldtype": "Select", + "reqd": 1, + "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, - __("E Invoicing") - ) + { + "label": "Remark", + "fieldname": "remark", + "fieldtype": "Data", + "reqd": 1 + } + ]; + const action = () => { + const d = new frappe.ui.Dialog({ + title: __("Cancel IRN"), + fields: fields, + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.cancel_irn', + args: { + doctype: doctype, + name: name, + irn: irn, + reason: data.reason.split('-')[0], + remark: data.remark + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }); + }, + primary_action_label: __('Submit') + }); + d.show(); + }; + frm.add_custom_button(__("Cancel IRN"), action, __("E Invoicing")); } if (docstatus == 1 && irn && !irn_cancelled && !eway_bill_cancelled) { - frm.add_custom_button( - __("Cancel E-Way Bill"), - () => { - const fields = [ - { - "label" : "Reason", - "fieldname": "reason", - "fieldtype": "Select", - "reqd": 1, - "default": "1-Duplicate", - "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] - }, - { - "label": "Remark", - "fieldname": "remark", - "fieldtype": "Data", - "reqd": 1 - } - ] - const d = new frappe.ui.Dialog({ - title: __('Cancel E-Way Bill'), - fields: fields, - primary_action: function() { - const data = d.get_values(); - frappe.call({ - method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', - args: { - doctype: doctype, - name: name, - eway_bill: ewaybill, - reason: data.reason.split('-')[0], - remark: data.remark - }, - freeze: true, - callback: () => frm.reload_doc() || d.hide(), - error: () => d.hide() - }) - }, - primary_action_label: __('Submit') - }); - d.show(); + const fields = [ + { + "label" : "Reason", + "fieldname": "reason", + "fieldtype": "Select", + "reqd": 1, + "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, - __("E Invoicing") - ); + { + "label": "Remark", + "fieldname": "remark", + "fieldtype": "Data", + "reqd": 1 + } + ]; + const action = () => { + const d = new frappe.ui.Dialog({ + title: __('Cancel E-Way Bill'), + fields: fields, + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', + args: { + doctype: doctype, + name: name, + eway_bill: ewaybill, + reason: data.reason.split('-')[0], + remark: data.remark + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }) + }, + primary_action_label: __('Submit') + }); + d.show(); + }; + frm.add_custom_button(__("Cancel E-Way Bill"), action, __("E Invoicing")); } } }) From eb9f216b4020a4a28d8c99137a9e0fc3549f2b98 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 11:58:34 +0530 Subject: [PATCH 071/115] fix: attach qrcode on irn generation --- erpnext/regional/india/e_invoice/utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 2a174202968..878bc11fb11 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -343,17 +343,18 @@ def update_invoice(invoice, res): frappe.db.set_value(doctype, name, 'irn', res.get('Irn')) frappe.db.set_value(doctype, name, 'ewaybill', res.get('EwbNo')) frappe.db.set_value(doctype, name, 'signed_invoice', dec_signed_invoice) - frappe.db.set_value(doctype, name, 'signed_qr_code', res.get('SignedQRCode')) + signed_qrcode = res.get('SignedQRCode') + frappe.db.set_value(doctype, name, 'signed_qr_code', signed_qrcode) -def attach_qrcode_image(doctype, name): - qrcode = frappe.db.get_value(doctype, name, 'signed_qr_code') + attach_qrcode_image(doctype, name, signed_qrcode) +def attach_qrcode_image(doctype, name, qrcode): if not qrcode: return _file = frappe.new_doc('File') _file.update({ - 'file_name': 'Signed_QR_{name}.png'.format(name=name), + 'file_name': f'QRCode_{name}.png', 'attached_to_doctype': doctype, 'attached_to_name': name, 'content': 'qrcode', From 6d5a8aea9af4f8a68e06419f5a244f620e31b649 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 12:35:05 +0530 Subject: [PATCH 072/115] fix: generate & cancel IRN --- erpnext/regional/india/e_invoice/einvoice.js | 5 +-- erpnext/regional/india/e_invoice/utils.py | 47 +++++++++++--------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 65cac992ff4..464b3b2e880 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -13,7 +13,7 @@ erpnext.setup_einvoice_actions = (doctype) => { const action = () => { frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.generate_irn', - args: { doctype: doctype, name: name }, + args: { docname: name }, freeze: true, callback: () => frm.reload_doc() }) @@ -48,8 +48,7 @@ erpnext.setup_einvoice_actions = (doctype) => { frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.cancel_irn', args: { - doctype: doctype, - name: name, + docname: name, irn: irn, reason: data.reason.split('-')[0], remark: data.remark diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 878bc11fb11..8e2b154ffd0 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -77,9 +77,7 @@ def get_doc_details(invoice): return frappe._dict(dict(invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date)) -def get_party_details(address): - from erpnext.regional.doctype.e_invoice_settings.e_invoice_settings import GSPConnector - +def get_party_details(address_name): address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] gstin = address.get('gstin') @@ -333,30 +331,27 @@ def validate_einvoice(validations, einvoice, errors=[]): return errors -def update_invoice(invoice, res): - doctype = invoice.doctype - name = invoice.name - +def update_invoice(doctype, docname, res): enc_signed_invoice = res.get('SignedInvoice') - dec_signed_invoice = jwt.decode(enc_signed_invoice)['data'] + dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data'] - frappe.db.set_value(doctype, name, 'irn', res.get('Irn')) - frappe.db.set_value(doctype, name, 'ewaybill', res.get('EwbNo')) - frappe.db.set_value(doctype, name, 'signed_invoice', dec_signed_invoice) + frappe.db.set_value(doctype, docname, 'irn', res.get('Irn')) + frappe.db.set_value(doctype, docname, 'ewaybill', res.get('EwbNo')) + frappe.db.set_value(doctype, docname, 'signed_einvoice', dec_signed_invoice) - signed_qrcode = res.get('SignedQRCode') - frappe.db.set_value(doctype, name, 'signed_qr_code', signed_qrcode) + signed_qr_code = res.get('SignedQRCode') + frappe.db.set_value(doctype, docname, 'signed_qr_code', signed_qr_code) - attach_qrcode_image(doctype, name, signed_qrcode) + attach_qrcode_image(doctype, docname, signed_qr_code) -def attach_qrcode_image(doctype, name, qrcode): +def attach_qrcode_image(doctype, docname, qrcode): if not qrcode: return _file = frappe.new_doc('File') _file.update({ - 'file_name': f'QRCode_{name}.png', + 'file_name': f'QRCode_{docname}.png', 'attached_to_doctype': doctype, - 'attached_to_name': name, + 'attached_to_name': docname, 'content': 'qrcode', 'is_private': 1 }) @@ -366,7 +361,7 @@ def attach_qrcode_image(doctype, name, qrcode): abs_file_path = os.path.abspath(_file.get_full_path()) url.png(abs_file_path, scale=2) - frappe.db.set_value(doctype, name, 'qrcode_image', _file.file_url) + frappe.db.set_value(doctype, docname, 'qrcode_image', _file.file_url) class GSPConnector(): def __init__(self): @@ -458,10 +453,10 @@ class GSPConnector(): try: res = make_post_request(self.generate_irn_url, headers=headers, data=data) if res.get('success'): - update_invoice(invoice, res) + update_invoice(doctype, docname, res.get('result')) else: # {'success': False, 'message': '3039 : Seller Details:Pincode-560009 does not belong to the state-1, 2177 : Invalid item unit code(s)-UNIT'} - self.log_error() + self.log_error(res) except Exception as e: self.log_error(e) @@ -503,4 +498,14 @@ class GSPConnector(): self.log_error(e) def log_error(self, exc): - print(exc) \ No newline at end of file + print(exc) + +@frappe.whitelist() +def generate_irn(docname): + gsp_connector = GSPConnector() + gsp_connector.generate_irn(docname) + +@frappe.whitelist() +def cancel_irn(docname, irn, reason, remark): + gsp_connector = GSPConnector() + gsp_connector.cancel_irn(docname, irn, reason, remark) \ No newline at end of file From ca944316b464e0ca02ce5f1391d6acb159a627ff Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 18:50:15 +0530 Subject: [PATCH 073/115] fix: show/hide eway bill fields --- erpnext/patches.txt | 2 +- erpnext/patches/v12_0/setup_einvoice_fields.py | 10 +++++++++- erpnext/regional/india/setup.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 67dc8e38e95..d08a9283a08 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -678,4 +678,4 @@ erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v12_0.update_leave_application_status erpnext.patches.v12_0.update_payment_entry_status -erpnext.patches.v12_0.setup_einvoice_fields +erpnext.patches.v12_0.setup_einvoice_fields #2020-11-04 diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index fbb313a9fe1..9d7cfc52eb4 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -29,4 +29,12 @@ def execute(): } create_custom_fields(custom_fields, update=True) add_permissions() - add_print_formats() \ No newline at end of file + add_print_formats() + + frappe.db.set_value('Custom Field', { 'fieldname': 'mode_of_transport' }, 'default', '') + frappe.db.set_value('Custom Field', { 'fieldname': 'distance' }, 'mandatory_depends_on', 'transporter') + frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'mandatory_depends_on', 'eval:doc.mode_of_transport == "Road"') + frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'depends_on', 'depends_on': 'eval:doc.mode_of_transport == "Road"') + frappe.db.set_value('Custom Field', { 'fieldname': 'gst_vehicle_type' }, 'mandatory_depends_on', 'eval:doc.mode_of_transport == "Road"') + frappe.db.set_value('Custom Field', { 'fieldname': 'lr_date' }, 'mandatory_depends_on', 'eval:in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)') + frappe.db.set_value('Custom Field', { 'fieldname': 'lr_no' }, 'mandatory_depends_on', 'eval:in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)') \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 4f1ca5012d1..bb0d2ff8013 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -303,6 +303,7 @@ def make_custom_fields(update=True): 'label': 'Vehicle No', 'fieldtype': 'Data', 'insert_after': 'lr_no', + 'depends_on': 'eval:(doc.mode_of_transport === "Road")', 'print_hide': 1, 'translatable': 0 }, @@ -333,7 +334,6 @@ def make_custom_fields(update=True): 'label': 'Mode of Transport', 'fieldtype': 'Select', 'options': '\nRoad\nAir\nRail\nShip', - 'default': 'Road', 'insert_after': 'transporter_name', 'print_hide': 1, 'translatable': 0 From fb9dde38f69ad8dc1efefc04592ddf34ca0b0e6c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 19:44:54 +0530 Subject: [PATCH 074/115] fix: valiations --- .../india/e_invoice/einv_item_template.json | 4 +- .../india/e_invoice/einv_validation.json | 448 +++++++++++------- erpnext/regional/india/e_invoice/utils.py | 48 +- 3 files changed, 317 insertions(+), 183 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einv_item_template.json b/erpnext/regional/india/e_invoice/einv_item_template.json index 23afd013d56..78e56518dff 100644 --- a/erpnext/regional/india/e_invoice/einv_item_template.json +++ b/erpnext/regional/india/e_invoice/einv_item_template.json @@ -8,9 +8,9 @@ "Qty": "{item.qty}", "FreeQty": "{item.free_qty}", "UnitPrice": "{item.unit_rate}", - "TotAmt": "{item.total_amount}", + "TotAmt": "{item.gross_amount}", "Discount": "{item.discount_amount}", - "AssAmt": "{item.base_amount}", + "AssAmt": "{item.taxable_value}", "PrdSlNo": "{item.serial_no}", "GstRt": "{item.tax_rate}", "IgstAmt": "{item.igst_amount}", diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json index 3f0767b8be8..86290cfe524 100644 --- a/erpnext/regional/india/e_invoice/einv_validation.json +++ b/erpnext/regional/india/e_invoice/einv_validation.json @@ -2,12 +2,14 @@ "Version": { "type": "string", "minLength": 1, - "maxLength": 6 + "maxLength": 6, + "description": "Version of the schema" }, "Irn": { "type": "string", "minLength": 64, - "maxLength": 64 + "maxLength": 64, + "description": "Invoice Reference Number" }, "TranDtls": { "type": "object", @@ -16,31 +18,37 @@ "type": "string", "minLength": 3, "maxLength": 10, - "enum": ["GST"] + "enum": ["GST"], + "description": "GST- Goods and Services Tax Scheme" }, "SupTyp": { "type": "string", "minLength": 3, "maxLength": 10, - "enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"] + "enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"], + "description": "Type of Supply: B2B-Business to Business, SEZWP - SEZ with payment, SEZWOP - SEZ without payment, EXPWP - Export with Payment, EXPWOP - Export without payment,DEXP - Deemed Export" }, "RegRev": { "type": "string", "minLength": 1, "maxLength": 1, - "enum": ["Y", "N"] + "enum": ["Y", "N"], + "description": "Y- whether the tax liability is payable under reverse charge" }, "EcmGstin": { "type": "string", "minLength": 15, "maxLength": 15, - "pattern": "([0-9]{2}[0-9A-Z]{13})" + "pattern": "([0-9]{2}[0-9A-Z]{13})", + "description": "E-Commerce GSTIN", + "validationMsg": "E-Commerce GSTIN is invalid" }, "IgstOnIntra": { "type": "string", "minLength": 1, "maxLength": 1, - "enum": ["Y", "N"] + "enum": ["Y", "N"], + "description": "Y- indicates the supply is intra state but chargeable to IGST" } }, "required": ["TaxSch", "SupTyp"] @@ -52,22 +60,23 @@ "type": "string", "minLength": 3, "maxLength": 3, - "enum": ["INV", "CRN", "DBN"] + "enum": ["INV", "CRN", "DBN"], + "description": "Document Type" }, "No": { "type": "string", "minLength": 1, "maxLength": 16, "pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$", - "label": "Document Name", - "validationMsg": "Document name should not be starting with 0, / and -" + "description": "Document Number", + "validationMsg": "Document Number should not be starting with 0, / and -" }, "Dt": { "type": "string", "minLength": 10, "maxLength": 10, "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", - "validationMsg": "Document Date is invalid" + "description": "Document Date" } }, "required": ["Typ", "No", "Dt"] @@ -80,52 +89,62 @@ "minLength": 15, "maxLength": 15, "pattern": "([0-9]{2}[0-9A-Z]{13})", - "validationMsg": "Seller GSTIN is invalid" + "description": "Supplier GSTIN", + "validationMsg": "Company GSTIN is invalid" }, "LglNm": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Legal Name" }, "TrdNm": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Tradename" }, "Addr1": { "type": "string", - "minLength": 3, - "maxLength": 100 + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" }, "Addr2": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Address Line 2" }, "Loc": { "type": "string", "minLength": 3, - "maxLength": 50 + "maxLength": 50, + "description": "Location" }, "Pin": { "type": "number", "minimum": 100000, - "maximum": 999999 + "maximum": 999999, + "description": "Pincode" }, "Stcd": { "type": "string", "minLength": 1, - "maxLength": 2 + "maxLength": 2, + "description": "Supplier State Code" }, "Ph": { "type": "string", "minLength": 6, - "maxLength": 12 + "maxLength": 12, + "description": "Phone" }, "Em": { "type": "string", "minLength": 6, - "maxLength": 100 + "maxLength": 100, + "description": "Email-Id" } }, "required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"] @@ -138,57 +157,68 @@ "minLength": 3, "maxLength": 15, "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", - "validationMsg": "Buyer GSTIN is invalid" + "description": "Buyer GSTIN", + "validationMsg": "Customer GSTIN is invalid" }, "LglNm": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Legal Name" }, "TrdNm": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Trade Name" }, "Pos": { "type": "string", "minLength": 1, - "maxLength": 2 + "maxLength": 2, + "description": "Place of Supply State code" }, "Addr1": { "type": "string", - "minLength": 3, - "maxLength": 100 + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" }, "Addr2": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Address Line 2" }, "Loc": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Location" }, "Pin": { "type": "number", "minimum": 100000, - "maximum": 999999 + "maximum": 999999, + "description": "Pincode" }, "Stcd": { "type": "string", "minLength": 1, - "maxLength": 2 + "maxLength": 2, + "description": "Buyer State Code" }, "Ph": { "type": "string", "minLength": 6, - "maxLength": 12 + "maxLength": 12, + "description": "Phone" }, "Em": { "type": "string", "minLength": 6, - "maxLength": 100 + "maxLength": 100, + "description": "Email-Id" } }, "required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"] @@ -199,32 +229,38 @@ "Nm": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Dispatch Address Name" }, "Addr1": { "type": "string", - "minLength": 3, - "maxLength": 100 + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" }, "Addr2": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Address Line 2" }, "Loc": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Location" }, "Pin": { "type": "number", "minimum": 100000, - "maximum": 999999 + "maximum": 999999, + "description": "Pincode" }, "Stcd": { "type": "string", "minLength": 1, - "maxLength": 2 + "maxLength": 2, + "description": "State Code" } }, "required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"] @@ -237,42 +273,50 @@ "maxLength": 15, "minLength": 3, "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", + "description": "Shipping Address GSTIN", "validationMsg": "Shipping Address GSTIN is invalid" }, "LglNm": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Legal Name" }, "TrdNm": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Trade Name" }, "Addr1": { "type": "string", - "minLength": 3, - "maxLength": 100 + "minLength": 1, + "maxLength": 100, + "description": "Address Line 1" }, "Addr2": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Address Line 2" }, "Loc": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Location" }, "Pin": { "type": "number", "minimum": 100000, - "maximum": 999999 + "maximum": 999999, + "description": "Pincode" }, "Stcd": { "type": "string", "minLength": 1, - "maxLength": 2 + "maxLength": 2, + "description": "State Code" } }, "required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"] @@ -283,143 +327,171 @@ "SlNo": { "type": "string", "minLength": 1, - "maxLength": 6 + "maxLength": 6, + "description": "Serial No. of Item" }, "PrdDesc": { "type": "string", "minLength": 3, - "maxLength": 300 + "maxLength": 300, + "description": "Item Name" }, "IsServc": { "type": "string", "minLength": 1, "maxLength": 1, - "enum": ["Y", "N"] + "enum": ["Y", "N"], + "description": "Is Service Item" }, "HsnCd": { "type": "string", "minLength": 4, - "maxLength": 8 + "maxLength": 8, + "description": "HSN Code" }, "Barcde": { "type": "string", "minLength": 3, - "maxLength": 30 + "maxLength": 30, + "description": "Barcode" }, "Qty": { "type": "number", "minimum": 0, - "maximum": 9999999999.999 + "maximum": 9999999999.999, + "description": "Quantity" }, "FreeQty": { "type": "number", "minimum": 0, - "maximum": 9999999999.999 + "maximum": 9999999999.999, + "description": "Free Quantity" }, "Unit": { "type": "string", "minLength": 3, - "maxLength": 8 + "maxLength": 8, + "description": "UOM" }, "UnitPrice": { "type": "number", "minimum": 0, - "maximum": 999999999999.999 + "maximum": 999999999999.999, + "description": "Rate" }, "TotAmt": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "Gross Amount" }, "Discount": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "Discount" }, "PreTaxVal": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "Pre tax value" }, "AssAmt": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "Taxable Value" }, "GstRt": { "type": "number", "minimum": 0, - "maximum": 999.999 + "maximum": 999.999, + "description": "GST Rate" }, "IgstAmt": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "IGST Amount" }, "CgstAmt": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "CGST Amount" }, "SgstAmt": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "SGST Amount" }, "CesRt": { "type": "number", "minimum": 0, - "maximum": 999.999 + "maximum": 999.999, + "description": "Cess Rate" }, "CesAmt": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "Cess Amount (Advalorem)" }, "CesNonAdvlAmt": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "Cess Amount (Non-Advalorem)" }, "StateCesRt": { "type": "number", "minimum": 0, - "maximum": 999.999 + "maximum": 999.999, + "description": "State CESS Rate" }, "StateCesAmt": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "State CESS Amount" }, "StateCesNonAdvlAmt": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "State CESS Amount (Non Advalorem)" }, "OthChrg": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "Other Charges" }, "TotItemVal": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "Total Item Value" }, "OrdLineRef": { "type": "string", "minLength": 1, - "maxLength": 50 + "maxLength": 50, + "description": "Order line reference" }, "OrgCntry": { "type": "string", "minLength": 2, - "maxLength": 2 + "maxLength": 2, + "description": "Origin Country" }, "PrdSlNo": { "type": "string", "minLength": 1, - "maxLength": 20 + "maxLength": 20, + "description": "Serial number" }, "BchDtls": { "type": "object", @@ -427,21 +499,22 @@ "Nm": { "type": "string", "minLength": 3, - "maxLength": 20 + "maxLength": 20, + "description": "Batch number" }, "ExpDt": { "type": "string", "maxLength": 10, "minLength": 10, "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", - "validationMsg": "Expiry Date is invalid" + "description": "Batch Expiry Date" }, "WrDt": { "type": "string", "maxLength": 10, "minLength": 10, "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", - "validationMsg": "Warranty Date is invalid" + "description": "Warranty Date" } }, "required": ["Nm"] @@ -454,12 +527,14 @@ "Nm": { "type": "string", "minLength": 1, - "maxLength": 100 + "maxLength": 100, + "description": "Attribute name of the item" }, "Val": { "type": "string", "minLength": 1, - "maxLength": 100 + "maxLength": 100, + "description": "Attribute value of the item" } } } @@ -482,57 +557,68 @@ "AssVal": { "type": "number", "minimum": 0, - "maximum": 99999999999999.99 + "maximum": 99999999999999.99, + "description": "Total Assessable value of all items" }, "CgstVal": { "type": "number", "maximum": 99999999999999.99, - "minimum": 0 + "minimum": 0, + "description": "Total CGST value of all items" }, "SgstVal": { "type": "number", "minimum": 0, - "maximum": 99999999999999.99 + "maximum": 99999999999999.99, + "description": "Total SGST value of all items" }, "IgstVal": { "type": "number", "minimum": 0, - "maximum": 99999999999999.99 + "maximum": 99999999999999.99, + "description": "Total IGST value of all items" }, "CesVal": { "type": "number", "minimum": 0, - "maximum": 99999999999999.99 + "maximum": 99999999999999.99, + "description": "Total CESS value of all items" }, "StCesVal": { "type": "number", "minimum": 0, - "maximum": 99999999999999.99 + "maximum": 99999999999999.99, + "description": "Total State CESS value of all items" }, "Discount": { "type": "number", "minimum": 0, - "maximum": 99999999999999.99 + "maximum": 99999999999999.99, + "description": "Invoice Discount" }, "OthChrg": { "type": "number", "minimum": 0, - "maximum": 99999999999999.99 + "maximum": 99999999999999.99, + "description": "Other Charges" }, "RndOffAmt": { "type": "number", - "minimum": 0, - "maximum": 99.99 + "minimum": -99.99, + "maximum": 99.99, + "description": "Rounded off Amount" }, "TotInvVal": { "type": "number", "minimum": 0, - "maximum": 99999999999999.99 + "maximum": 99999999999999.99, + "description": "Final Invoice Value " }, "TotInvValFc": { "type": "number", "minimum": 0, - "maximum": 99999999999999.99 + "maximum": 99999999999999.99, + "description": "Final Invoice value in Foreign Currency" } }, "required": ["AssVal", "TotInvVal"] @@ -543,57 +629,68 @@ "Nm": { "type": "string", "minLength": 1, - "maxLength": 100 + "maxLength": 100, + "description": "Payee Name" }, "AccDet": { "type": "string", "minLength": 1, - "maxLength": 18 + "maxLength": 18, + "description": "Bank Account Number of Payee" }, "Mode": { "type": "string", "minLength": 1, - "maxLength": 18 + "maxLength": 18, + "description": "Mode of Payment" }, "FinInsBr": { "type": "string", "minLength": 1, - "maxLength": 11 + "maxLength": 11, + "description": "Branch or IFSC code" }, "PayTerm": { "type": "string", "minLength": 1, - "maxLength": 100 + "maxLength": 100, + "description": "Terms of Payment" }, "PayInstr": { "type": "string", "minLength": 1, - "maxLength": 100 + "maxLength": 100, + "description": "Payment Instruction" }, "CrTrn": { "type": "string", "minLength": 1, - "maxLength": 100 + "maxLength": 100, + "description": "Credit Transfer" }, "DirDr": { "type": "string", "minLength": 1, - "maxLength": 100 + "maxLength": 100, + "description": "Direct Debit" }, "CrDay": { "type": "number", "minimum": 0, - "maximum": 9999 + "maximum": 9999, + "description": "Credit Days" }, "PaidAmt": { "type": "number", "minimum": 0, - "maximum": 99999999999999.99 + "maximum": 99999999999999.99, + "description": "Advance Amount" }, "PaymtDue": { "type": "number", "minimum": 0, - "maximum": 99999999999999.99 + "maximum": 99999999999999.99, + "description": "Outstanding Amount" } } }, @@ -604,7 +701,8 @@ "type": "string", "maxLength": 100, "minLength": 3, - "pattern": "^[0-9A-Za-z/-]{3,100}$" + "pattern": "^[0-9A-Za-z/-]{3,100}$", + "description": "Remarks/Note" }, "DocPerdDtls": { "type": "object", @@ -613,16 +711,18 @@ "type": "string", "maxLength": 10, "minLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Invoice Period Start Date" }, "InvEndDt": { "type": "string", "maxLength": 10, "minLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Invoice Period End Date" } }, - "required": ["InvStDt", "InvEndDt"] + "required": ["InvStDt ", "InvEndDt "] }, "PrecDocDtls": { "type": "object", @@ -631,22 +731,25 @@ "type": "string", "minLength": 1, "maxLength": 16, - "pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$" + "pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$", + "description": "Reference of Original Invoice" }, "InvDt": { "type": "string", "maxLength": 10, "minLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Date of Orginal Invoice" }, "OthRefNo": { "type": "string", "minLength": 1, - "maxLength": 20 + "maxLength": 20, + "description": "Other Reference" } - }, - "required": ["InvNo", "InvDt"] + } }, + "required": ["InvNo", "InvDt"], "ContrDtls": { "type": "object", "properties": { @@ -654,117 +757,132 @@ "type": "string", "minLength": 1, "maxLength": 20, - "pattern": "^([0-9A-Za-z/-]){1,20}$" + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Receipt Advice No." }, "RecAdvDt": { "type": "string", "minLength": 10, "maxLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Date of receipt advice" }, "TendRefr": { "type": "string", "minLength": 1, "maxLength": 20, - "pattern": "^([0-9A-Za-z/-]){1,20}$" + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Lot/Batch Reference No." }, "ContrRefr": { "type": "string", "minLength": 1, "maxLength": 20, - "pattern": "^([0-9A-Za-z/-]){1,20}$" + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Contract Reference Number" }, "ExtRefr": { "type": "string", "minLength": 1, "maxLength": 20, - "pattern": "^([0-9A-Za-z/-]){1,20}$" + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Any other reference" }, "ProjRefr": { "type": "string", "minLength": 1, "maxLength": 20, - "pattern": "^([0-9A-Za-z/-]){1,20}$" + "pattern": "^([0-9A-Za-z/-]){1,20}$", + "description": "Project Reference Number" }, "PORefr": { "type": "string", "minLength": 1, "maxLength": 16, - "pattern": "^([0-9A-Za-z/-]){1,16}$" + "pattern": "^([0-9A-Za-z/-]){1,16}$", + "description": "PO Reference Number" }, "PORefDt": { "type": "string", "minLength": 10, "maxLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "PO Reference date" } } } - }, - "required": ["InvStDt", "InvEndDt"] - }, - "AddlDocDtls": { - "type": "Array", - "AddlDocument": { - "type": "object", - "properties": { - "Url": { - "type": "string", - "minLength": 3, - "maxLength": 100 - }, - "Docs": { - "type": "string", - "minLength": 3, - "maxLength": 1000 - }, - "Info": { - "type": "string", - "minLength": 3, - "maxLength": 1000 - } - } } }, + "AddlDocDtls": { + "type": "Array", + "properties": { + "Url": { + "type": "string", + "minLength": 3, + "maxLength": 100, + "description": "Supporting document URL" + }, + "Docs": { + "type": "string", + "minLength": 3, + "maxLength": 1000, + "description": "Supporting document in Base64 Format" + }, + "Info": { + "type": "string", + "minLength": 3, + "maxLength": 1000, + "description": "Any additional information" + } + } + }, + "ExpDtls": { "type": "object", "properties": { "ShipBNo": { "type": "string", "minLength": 1, - "maxLength": 20 + "maxLength": 20, + "description": "Shipping Bill No." }, "ShipBDt": { "type": "string", "minLength": 10, "maxLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Shipping Bill Date" }, "Port": { "type": "string", "minLength": 2, "maxLength": 10, - "pattern": "^[0-9A-Za-z]{2,10}$" + "pattern": "^[0-9A-Za-z]{2,10}$", + "description": "Port Code. Refer the master" }, "RefClm": { "type": "string", "minLength": 1, - "maxLength": 1 + "maxLength": 1, + "description": "Claiming Refund. Y/N" }, "ForCur": { "type": "string", "minLength": 3, - "maxLength": 16 + "maxLength": 16, + "description": "Additional Currency Code. Refer the master" }, "CntCode": { "type": "string", "minLength": 2, - "maxLength": 2 + "maxLength": 2, + "description": "Country Code. Refer the master" }, "ExpDuty": { "type": "number", "minimum": 0, - "maximum": 999999999999.99 + "maximum": 999999999999.99, + "description": "Export Duty" } } }, @@ -774,46 +892,54 @@ "TransId": { "type": "string", "minLength": 15, - "maxLength": 15 + "maxLength": 15, + "description": "Transporter GSTIN" }, "TransName": { "type": "string", "minLength": 3, - "maxLength": 100 + "maxLength": 100, + "description": "Transporter Name" }, "TransMode": { "type": "string", "maxLength": 1, "minLength": 1, - "enum": ["1", 2, 3, 4] + "enum": ["1", "2", "3", "4"], + "description": "Mode of Transport" }, "Distance": { "type": "number", "minimum": 1, - "maximum": 9999 + "maximum": 9999, + "description": "Distance" }, "TransDocNo": { "type": "string", "minLength": 1, "maxLength": 15, - "pattern": "^([0-9A-Z/-]){1,15}$" + "pattern": "^([0-9A-Z/-]){1,15}$", + "description": "Tranport Document Number" }, "TransDocDt": { "type": "string", "minLength": 10, "maxLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "description": "Transport Document Date" }, "VehNo": { "type": "string", "minLength": 4, - "maxLength": 20 + "maxLength": 20, + "description": "Vehicle Number" }, "VehType": { "type": "string", "minLength": 1, "maxLength": 1, - "enum": ["O", "R"] + "enum": ["O", "R"], + "description": "Vehicle Type" } }, "required": ["Distance"] diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 8e2b154ffd0..11284852484 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -60,10 +60,16 @@ def get_trans_details(invoice): elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP' if not supply_type: - return _('Invalid invoice transaction category.') + rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export') + frappe.throw( + _('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export), + title=_('Invalid Supply Type') + ) return frappe._dict(dict( - tax_scheme='GST', supply_type=supply_type, reverse_charge=invoice.reverse_charge + tax_scheme='GST', + supply_type=supply_type, + reverse_charge=invoice.reverse_charge )) def get_doc_details(invoice): @@ -75,7 +81,11 @@ def get_doc_details(invoice): invoice_name = invoice.name invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy') - return frappe._dict(dict(invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date)) + return frappe._dict(dict( + invoice_type=invoice_type, + invoice_name=invoice_name, + invoice_date=invoice_date + )) def get_party_details(address_name): address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] @@ -127,16 +137,11 @@ def get_item_list(invoice): item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None item.qty = abs(item.qty) item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_net_rate) - item.total_amount = abs(item.unit_rate * item.qty) + item.gross_amount = abs(item.unit_rate * item.qty) item.discount_amount = abs(item.discount_amount * item.qty) - item.base_amount = abs(item.base_net_amount) - item.tax_rate = 0 - item.igst_amount = 0 - item.cgst_amount = 0 - item.sgst_amount = 0 - item.cess_rate = 0 - item.cess_amount = 0 - item.other_charges = 0 + item.taxable_value = abs(item.base_net_amount) + item.tax_rate = item.cess_rate = item.other_charges = 0 + item.cgst_amount = item.sgst_amount = item.igst_amount = item.cess_amount = 0 for t in invoice.taxes: item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) if t.account_head in gst_accounts_list: @@ -210,8 +215,8 @@ def get_return_doc_reference(invoice): )) def get_eway_bill_details(invoice): - if not invoice.distance: - frappe.throw(_('Distance is mandatory for E-Way Bill generation'), title=_('E Invoice Validation Failed')) + if invoice.is_return: + frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed')) mode_of_transport = { 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } @@ -220,7 +225,7 @@ def get_eway_bill_details(invoice): gstin=invoice.gst_transporter_id, name=invoice.transporter_name, mode_of_transport=mode_of_transport[invoice.mode_of_transport], - distance=invoice.distance, + distance=invoice.distance or 0, document_name=invoice.lr_no, document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'), vehicle_no=invoice.vehicle_no, @@ -232,6 +237,7 @@ def make_einvoice(doctype, name): invoice = frappe.get_doc(doctype, name) schema = read_json('einv_template') + trans_details = get_trans_details(invoice) item_list = get_item_list(invoice) doc_details = get_doc_details(invoice) value_details = get_value_details(invoice) @@ -272,7 +278,7 @@ def make_einvoice(doctype, name): einvoice = json.loads(einvoice) validations = json.loads(read_json('einv_validation')) - errors = validate_einvoice(validations, einvoice, []) + errors = validate_einvoice(validations, einvoice) if errors: frappe.log_error(title="E Invoice Validation Failed", message=json.dumps(errors, default=str, indent=4)) if len(errors) > 1: @@ -312,7 +318,9 @@ def validate_einvoice(validations, einvoice, errors=[]): if value_type == 'string': einvoice[fieldname] = str(value) elif value_type == 'number': - einvoice[fieldname] = flt(value, 2) if fieldname not in ['Pin', 'Distance'] else int(value) + is_integer = '.' not in str(field_validation.get('maximum')) + einvoice[fieldname] = flt(value, 2) if not is_integer else cint(value) + value = einvoice[fieldname] max_length = field_validation.get('maxLength') minimum = flt(field_validation.get('minimum')) @@ -320,12 +328,12 @@ def validate_einvoice(validations, einvoice, errors=[]): pattern_str = field_validation.get('pattern') pattern = re.compile(pattern_str or '') - label = field_validation.get('label') or fieldname + label = field_validation.get('description') or fieldname if value_type == 'string' and len(value) > max_length: errors.append(_('{} should not exceed {} characters').format(label, max_length)) - if value_type == 'number' and not (flt(value) <= maximum): - errors.append(_('{} should be less than {}').format(label, maximum)) + if value_type == 'number' and (value > maximum or value < minimum): + errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum)) if pattern_str and not pattern.match(value): errors.append(field_validation.get('validationMsg')) From 77b8ced4a4b128f70d11c6fc11ab3477034b2071 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 22:17:26 +0530 Subject: [PATCH 075/115] feat: generate eway bill from IRN --- .../patches/v12_0/setup_einvoice_fields.py | 3 +- erpnext/regional/india/e_invoice/einvoice.js | 120 +++++++++++++++++- erpnext/regional/india/e_invoice/utils.py | 56 +++++++- 3 files changed, 171 insertions(+), 8 deletions(-) diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index 9d7cfc52eb4..b72248c838d 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -37,4 +37,5 @@ def execute(): frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'depends_on', 'depends_on': 'eval:doc.mode_of_transport == "Road"') frappe.db.set_value('Custom Field', { 'fieldname': 'gst_vehicle_type' }, 'mandatory_depends_on', 'eval:doc.mode_of_transport == "Road"') frappe.db.set_value('Custom Field', { 'fieldname': 'lr_date' }, 'mandatory_depends_on', 'eval:in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)') - frappe.db.set_value('Custom Field', { 'fieldname': 'lr_no' }, 'mandatory_depends_on', 'eval:in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)') \ No newline at end of file + frappe.db.set_value('Custom Field', { 'fieldname': 'lr_no' }, 'mandatory_depends_on', 'eval:in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)') + frappe.db.set_value('Custom Field', { 'fieldname': 'ewaybill' }, 'read_only_depends_on', 'eval:doc.irn && doc.ewaybill') \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 464b3b2e880..d70a8ef089e 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -7,7 +7,7 @@ erpnext.setup_einvoice_actions = (doctype) => { if (!einvoicing_enabled || !valid_supply_type) return; - const { docstatus, irn, irn_cancelled, ewaybill, eway_bill_cancelled, doctype, name, __unsaved } = frm.doc; + const { docstatus, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; if (docstatus == 0 && !irn && !__unsaved) { const action = () => { @@ -22,7 +22,7 @@ erpnext.setup_einvoice_actions = (doctype) => { frm.add_custom_button(__("Generate IRN"), action, __('E Invoicing')); } - if (docstatus == 1 && irn && !irn_cancelled) { + if (docstatus == 1 && irn && !irn_cancelled && !ewaybill) { const fields = [ { "label" : "Reason", @@ -65,7 +65,118 @@ erpnext.setup_einvoice_actions = (doctype) => { frm.add_custom_button(__("Cancel IRN"), action, __("E Invoicing")); } - if (docstatus == 1 && irn && !irn_cancelled && !eway_bill_cancelled) { + if (irn && !irn_cancelled && !ewaybill) { + const fields = [ + { + 'fieldname': 'transporter', + 'label': 'Transporter', + 'fieldtype': 'Link', + 'options': 'Supplier', + 'default': frm.doc.transporter + }, + { + 'fieldname': 'gst_transporter_id', + 'label': 'GST Transporter ID', + 'fieldtype': 'Data', + 'fetch_from': 'transporter.gst_transporter_id', + 'default': frm.doc.gst_transporter_id + }, + { + 'fieldname': 'driver', + 'label': 'Driver', + 'fieldtype': 'Link', + 'options': 'Driver', + 'default': frm.doc.driver + }, + { + 'fieldname': 'lr_no', + 'label': 'Transport Receipt No', + 'fieldtype': 'Data', + 'default': frm.doc.lr_no + }, + { + 'fieldname': 'vehicle_no', + 'label': 'Vehicle No', + 'fieldtype': 'Data', + 'depends_on': 'eval:(doc.mode_of_transport === "Road")', + 'default': frm.doc.vehicle_no + }, + { + 'fieldname': 'distance', + 'label': 'Distance (in km)', + 'fieldtype': 'Float', + 'default': frm.doc.distance + }, + { + 'fieldname': 'transporter_col_break', + 'fieldtype': 'Column Break', + }, + { + 'fieldname': 'transporter_name', + 'label': 'Transporter Name', + 'fieldtype': 'Data', + 'fetch_from': 'transporter.name', + 'read_only': 1, + 'default': frm.doc.transporter_name + }, + { + 'fieldname': 'mode_of_transport', + 'label': 'Mode of Transport', + 'fieldtype': 'Select', + 'options': `\nRoad\nAir\nRail\nShip`, + 'default': frm.doc.mode_of_transport + }, + { + 'fieldname': 'driver_name', + 'label': 'Driver Name', + 'fieldtype': 'Data', + 'fetch_from': 'driver.full_name', + 'read_only': 1, + 'default': frm.doc.driver_name + }, + { + 'fieldname': 'lr_date', + 'label': 'Transport Receipt Date', + 'fieldtype': 'Date', + 'default': frm.doc.lr_date + }, + { + 'fieldname': 'gst_vehicle_type', + 'label': 'GST Vehicle Type', + 'fieldtype': 'Select', + 'options': `Regular\nOver Dimensional Cargo (ODC)`, + 'depends_on': 'eval:(doc.mode_of_transport === "Road")', + 'default': frm.doc.gst_vehicle_type + } + ] + + const action = () => { + const d = new frappe.ui.Dialog({ + title: __('Generate E-Way Bill'), + wide: 1, + fields: fields, + primary_action: function() { + const data = d.get_values(); + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.generate_eway_bill', + args: { + docname: name, irn, + ...data + }, + freeze: true, + callback: () => frm.reload_doc() || d.hide(), + error: () => d.hide() + }) + }, + primary_action_label: __('Submit') + }); + d.show(); + }; + + frm.add_custom_button(__("Generate E-Way Bill"), action, __("E Invoicing")); + } + + if (docstatus == 1 && irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { const fields = [ { "label" : "Reason", @@ -91,8 +202,7 @@ erpnext.setup_einvoice_actions = (doctype) => { frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', args: { - doctype: doctype, - name: name, + docname: name, eway_bill: ewaybill, reason: data.reason.split('-')[0], remark: data.remark diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 11284852484..c4da7b2585f 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -218,7 +218,7 @@ def get_eway_bill_details(invoice): if invoice.is_return: frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed')) - mode_of_transport = { 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } + mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } return frappe._dict(dict( @@ -381,6 +381,7 @@ class GSPConnector(): self.generate_irn_url = self.base_url + 'test/enriched/ei/api/invoice' self.cancel_irn_url = self.base_url + 'test/enriched/ei/api/invoice/cancel' self.cancel_ewaybill_url = self.base_url + '/test/enriched/ei/api/ewayapi' + self.generate_ewaybill_url = self.base_url + 'test/enriched/ei/api/ewaybill' def get_auth_token(self): if time_diff_in_seconds(self.credentials.token_expiry, now_datetime()) < 150.0: @@ -422,6 +423,8 @@ class GSPConnector(): res = make_get_request(self.gstin_details_url + params, headers=headers) if res.get('success'): return res.get('result') + else: + self.log_error(res) except Exception as e: self.log_error(e) @@ -483,6 +486,42 @@ class GSPConnector(): if res.get('success'): frappe.db.set_value(doctype, docname, 'irn_cancelled', 1) # frappe.db.set_value(doctype, docname, 'cancelled_on', res.get('CancelDate')) + else: + self.log_error(res) + + except Exception as e: + self.log_error(e) + + def generate_eway_bill(self, **kwargs): + args = frappe._dict(kwargs) + + headers = self.get_headers() + doctype = 'Sales Invoice' + docname = args.docname + eway_bill_details = get_eway_bill_details(args) + data = json.dumps({ + "Irn": args.irn, + "Distance": cint(eway_bill_details.distance), + "TransMode": eway_bill_details.mode_of_transport, + "TransId": eway_bill_details.gstin, + "TransName": eway_bill_details.transporter, + "TrnDocDt": eway_bill_details.document_date, + "TrnDocNo": eway_bill_details.document_name, + "VehNo": eway_bill_details.vehicle_no, + "VehType": eway_bill_details.vehicle_type + }) + + try: + res = make_post_request(self.generate_ewaybill_url, headers=headers, data=data) + if res.get('success'): + frappe.db.set_value(doctype, docname, 'ewaybill', res.get('result').get('EwbNo')) + frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 0) + for d in args: + if d in ['docname', 'cmd']: continue + # update eway bill details in sales invoice + frappe.db.set_value(doctype, docname, d, args[d]) + else: + self.log_error(res) except Exception as e: self.log_error(e) @@ -502,6 +541,9 @@ class GSPConnector(): frappe.db.set_value(doctype, docname, 'ewaybill', '') frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1) + else: + self.log_error(res) + except Exception as e: self.log_error(e) @@ -516,4 +558,14 @@ def generate_irn(docname): @frappe.whitelist() def cancel_irn(docname, irn, reason, remark): gsp_connector = GSPConnector() - gsp_connector.cancel_irn(docname, irn, reason, remark) \ No newline at end of file + gsp_connector.cancel_irn(docname, irn, reason, remark) + +@frappe.whitelist() +def generate_eway_bill(**kwargs): + gsp_connector = GSPConnector() + gsp_connector.generate_eway_bill(**kwargs) + +@frappe.whitelist() +def cancel_eway_bill(docname, eway_bill, reason, remark): + gsp_connector = GSPConnector() + gsp_connector.cancel_eway_bill(docname, eway_bill, reason, remark) \ No newline at end of file From e3c2f0a221ae0029fd6eecf62d8616de7a9f1b1b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 4 Nov 2020 22:22:59 +0530 Subject: [PATCH 076/115] chore: remove unwanted imports --- erpnext/regional/india/e_invoice/utils.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index c4da7b2585f..168ee38587f 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -9,16 +9,12 @@ import jwt import json import base64 import frappe -from six import string_types -from Crypto.PublicKey import RSA +from frappe import _, bold from pyqrcode import create as qrcreate -from Crypto.Cipher import PKCS1_v1_5, AES -from Crypto.Util.Padding import pad, unpad from frappe.model.document import Document -from frappe import _, get_module_path, scrub, bold from frappe.integrations.utils import make_post_request, make_get_request from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply -from frappe.utils.data import get_datetime, cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime +from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime def validate_einvoice_fields(doc): einvoicing_enabled = frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable') From 464fc1f087d800fc1fff796638bcd386144fc6eb Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 5 Nov 2020 10:49:21 +0530 Subject: [PATCH 077/115] chore: error logging --- erpnext/regional/india/e_invoice/utils.py | 28 ++++++++++++++--------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 168ee38587f..bda0cd8ef5c 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -237,8 +237,8 @@ def make_einvoice(doctype, name): item_list = get_item_list(invoice) doc_details = get_doc_details(invoice) value_details = get_value_details(invoice) - trans_details = get_trans_details(invoice) seller_details = get_party_details(invoice.company_address) + seller_details.update({ 'pincode': 193502 }) if invoice.gst_category == 'Overseas': buyer_details = get_overseas_address_details(invoice.customer_address) @@ -496,15 +496,15 @@ class GSPConnector(): docname = args.docname eway_bill_details = get_eway_bill_details(args) data = json.dumps({ - "Irn": args.irn, - "Distance": cint(eway_bill_details.distance), - "TransMode": eway_bill_details.mode_of_transport, - "TransId": eway_bill_details.gstin, - "TransName": eway_bill_details.transporter, - "TrnDocDt": eway_bill_details.document_date, - "TrnDocNo": eway_bill_details.document_name, - "VehNo": eway_bill_details.vehicle_no, - "VehType": eway_bill_details.vehicle_type + 'Irn': args.irn, + 'Distance': cint(eway_bill_details.distance), + 'TransMode': eway_bill_details.mode_of_transport, + 'TransId': eway_bill_details.gstin, + 'TransName': eway_bill_details.transporter, + 'TrnDocDt': eway_bill_details.document_date, + 'TrnDocNo': eway_bill_details.document_name, + 'VehNo': eway_bill_details.vehicle_no, + 'VehType': eway_bill_details.vehicle_type }) try: @@ -544,7 +544,13 @@ class GSPConnector(): self.log_error(e) def log_error(self, exc): - print(exc) + message = "\n".join(["Data:", json.dumps(exc), "--" * 50, "\nException:", traceback.format_exc()]) + frappe.log_error(title="E Invoicing Error", message=message) + link_to_error_list = '{1}'.format('Error Log', 'Error Log') + frappe.throw( + _('An error occurred while making API request. Please check {} for more information.').format(link_to_error_list), + title=_('E Invoice Request Failed') + ) @frappe.whitelist() def generate_irn(docname): From 589a49a38a89de69ac283698a88cc9b03931f3db Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 5 Nov 2020 11:34:22 +0530 Subject: [PATCH 078/115] feat: header & footer in GST E Invoice --- .../gst_e_invoice/gst_e_invoice.html | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html index b4255038d39..9827e00b71b 100644 --- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -2,12 +2,27 @@ {%- set einvoice = json.loads(doc.signed_einvoice) -%}
    -
    +
    + {% if letter_head and not no_letterhead %} +
    {{ letter_head }}
    + {% endif %}
    -
    + {% if print_settings.repeat_header_footer %} + + {% endif %} +
    1. Transaction Details
    From 6d63a1e90af9f4e8aa64e92cebf44af601db53fe Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 5 Nov 2020 11:35:34 +0530 Subject: [PATCH 079/115] chore: remove test pincode --- erpnext/regional/india/e_invoice/utils.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index bda0cd8ef5c..6fbca2d90c0 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -238,7 +238,6 @@ def make_einvoice(doctype, name): doc_details = get_doc_details(invoice) value_details = get_value_details(invoice) seller_details = get_party_details(invoice.company_address) - seller_details.update({ 'pincode': 193502 }) if invoice.gst_category == 'Overseas': buyer_details = get_overseas_address_details(invoice.customer_address) @@ -462,7 +461,6 @@ class GSPConnector(): if res.get('success'): update_invoice(doctype, docname, res.get('result')) else: - # {'success': False, 'message': '3039 : Seller Details:Pincode-560009 does not belong to the state-1, 2177 : Invalid item unit code(s)-UNIT'} self.log_error(res) except Exception as e: @@ -536,7 +534,6 @@ class GSPConnector(): if res.get('success'): frappe.db.set_value(doctype, docname, 'ewaybill', '') frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1) - else: self.log_error(res) From db4fec0314991fd3aa11923af0cc5d547605ebb4 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 5 Nov 2020 13:47:29 +0530 Subject: [PATCH 080/115] fix: invalid syntax --- erpnext/patches/v12_0/setup_einvoice_fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index b72248c838d..908fc8f5852 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -34,7 +34,7 @@ def execute(): frappe.db.set_value('Custom Field', { 'fieldname': 'mode_of_transport' }, 'default', '') frappe.db.set_value('Custom Field', { 'fieldname': 'distance' }, 'mandatory_depends_on', 'transporter') frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'mandatory_depends_on', 'eval:doc.mode_of_transport == "Road"') - frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'depends_on', 'depends_on': 'eval:doc.mode_of_transport == "Road"') + frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'depends_on': 'eval:doc.mode_of_transport == "Road"') frappe.db.set_value('Custom Field', { 'fieldname': 'gst_vehicle_type' }, 'mandatory_depends_on', 'eval:doc.mode_of_transport == "Road"') frappe.db.set_value('Custom Field', { 'fieldname': 'lr_date' }, 'mandatory_depends_on', 'eval:in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)') frappe.db.set_value('Custom Field', { 'fieldname': 'lr_no' }, 'mandatory_depends_on', 'eval:in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)') From 51ddf376daabebcc6ad090698f0c44d050c3433f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 5 Nov 2020 13:47:29 +0530 Subject: [PATCH 081/115] fix: invalid syntax --- erpnext/patches/v12_0/setup_einvoice_fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index 908fc8f5852..eb889ba0beb 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -34,7 +34,7 @@ def execute(): frappe.db.set_value('Custom Field', { 'fieldname': 'mode_of_transport' }, 'default', '') frappe.db.set_value('Custom Field', { 'fieldname': 'distance' }, 'mandatory_depends_on', 'transporter') frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'mandatory_depends_on', 'eval:doc.mode_of_transport == "Road"') - frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'depends_on': 'eval:doc.mode_of_transport == "Road"') + frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'depends_on', 'eval:doc.mode_of_transport == "Road"') frappe.db.set_value('Custom Field', { 'fieldname': 'gst_vehicle_type' }, 'mandatory_depends_on', 'eval:doc.mode_of_transport == "Road"') frappe.db.set_value('Custom Field', { 'fieldname': 'lr_date' }, 'mandatory_depends_on', 'eval:in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)') frappe.db.set_value('Custom Field', { 'fieldname': 'lr_no' }, 'mandatory_depends_on', 'eval:in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)') From 91580258d787835b8b023eee4f5291eef9225fc5 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 5 Nov 2020 13:56:04 +0530 Subject: [PATCH 082/115] fix: patch --- erpnext/patches/v12_0/setup_einvoice_fields.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index eb889ba0beb..dacdc86af53 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -32,10 +32,4 @@ def execute(): add_print_formats() frappe.db.set_value('Custom Field', { 'fieldname': 'mode_of_transport' }, 'default', '') - frappe.db.set_value('Custom Field', { 'fieldname': 'distance' }, 'mandatory_depends_on', 'transporter') - frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'mandatory_depends_on', 'eval:doc.mode_of_transport == "Road"') - frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'depends_on', 'eval:doc.mode_of_transport == "Road"') - frappe.db.set_value('Custom Field', { 'fieldname': 'gst_vehicle_type' }, 'mandatory_depends_on', 'eval:doc.mode_of_transport == "Road"') - frappe.db.set_value('Custom Field', { 'fieldname': 'lr_date' }, 'mandatory_depends_on', 'eval:in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)') - frappe.db.set_value('Custom Field', { 'fieldname': 'lr_no' }, 'mandatory_depends_on', 'eval:in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)') - frappe.db.set_value('Custom Field', { 'fieldname': 'ewaybill' }, 'read_only_depends_on', 'eval:doc.irn && doc.ewaybill') \ No newline at end of file + frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'depends_on', 'eval:doc.mode_of_transport == "Road"') \ No newline at end of file From 5d23c0ce8ec186471b2198aa4f3b6d3733de64cf Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 7 Nov 2020 14:04:39 +0530 Subject: [PATCH 083/115] feat: cess non advolem on einvoice item --- erpnext/regional/india/e_invoice/utils.py | 25 ++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 6fbca2d90c0..1a5bae470d4 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -136,14 +136,23 @@ def get_item_list(invoice): item.gross_amount = abs(item.unit_rate * item.qty) item.discount_amount = abs(item.discount_amount * item.qty) item.taxable_value = abs(item.base_net_amount) - item.tax_rate = item.cess_rate = item.other_charges = 0 - item.cgst_amount = item.sgst_amount = item.igst_amount = item.cess_amount = 0 + + for attr in [ + 'tax_rate', 'cess_rate', 'cess_nadv_amount', + 'cgst_amount', 'sgst_amount', 'igst_amount', + 'cess_amount', 'cess_nadv_amount', 'other_charges' + ]: + item[attr] = 0 + for t in invoice.taxes: item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) if t.account_head in gst_accounts_list: if t.account_head in gst_accounts.cess_account: - item.cess_rate += item_tax_detail[0] - item.cess_amount += abs(item_tax_detail[1]) + if t.charge_type == 'On Item Quantity': + item.cess_nadv_amount += abs(item_tax_detail[1]) + else: + item.cess_rate += item_tax_detail[0] + item.cess_amount += abs(item_tax_detail[1]) elif t.account_head in gst_accounts.igst_account: item.tax_rate += item_tax_detail[0] item.igst_amount += abs(item_tax_detail[1]) @@ -156,7 +165,7 @@ def get_item_list(invoice): item.total_value = abs( item.base_amount + item.igst_amount + item.sgst_amount + - item.cgst_amount + item.cess_amount + item.other_charges + item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges ) einv_item = item_schema.format(item=item) item_list.append(einv_item) @@ -170,9 +179,11 @@ def get_value_details(invoice): value_details = frappe._dict(dict()) value_details.base_net_total = abs(invoice.base_net_total) value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount > 0 else 0 + # discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount < 0 else 0) - value_details.base_grand_total = abs(invoice.base_rounded_total) - value_details.grand_total = abs(invoice.rounded_total) + disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') + value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else doc.base_rounded_total + value_details.grand_total = abs(invoice.grand_total) if disable_rounded else doc.rounded_total value_details.total_cgst_amt = 0 value_details.total_sgst_amt = 0 value_details.total_igst_amt = 0 From eeb99d1b983dccba0a60621344e81a46d9ab4f4f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 7 Nov 2020 14:05:24 +0530 Subject: [PATCH 084/115] chore: remove fetch token from e invocie settings --- .../e_invoice_settings/e_invoice_settings.js | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js index bea16ffa7fd..9ea43e70150 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js @@ -1,26 +1,4 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('E Invoice Settings', { - refresh: function(frm) { - if (!frm.doc.enable) return; - frm.trigger("show_fetch_token_btn"); - }, - - show_fetch_token_btn(frm) { - const { token_expiry } = frm.doc; - const now = frappe.datetime.now_datetime(); - const expiry_in_mins = moment(token_expiry).diff(now, "minute"); - if (expiry_in_mins <= 1 || !token_expiry) { - frm.add_custom_button(__("Fetch Token"), - () => { - frm.call({ - method: 'erpnext.regional.india.e_invoice.utils.fetch_token', - freeze: true, - callback: () => frm.refresh() - }); - } - ); - } - } -}); +frappe.ui.form.on('E Invoice Settings', {}); From 8693ac81a2127f65a9db27b0da99a677223e0cf8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 7 Nov 2020 17:53:22 +0530 Subject: [PATCH 085/115] fix: import format_date --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 1a5bae470d4..73e9edbb8b9 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -14,7 +14,7 @@ from pyqrcode import create as qrcreate from frappe.model.document import Document from frappe.integrations.utils import make_post_request, make_get_request from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply -from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime +from frappe.utils.data import cstr, cint, formatdate as format_date, flt, time_diff_in_seconds, now_datetime def validate_einvoice_fields(doc): einvoicing_enabled = frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable') From 70e07118936f3648f9343666d6a81fc4e3611ec6 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 7 Nov 2020 19:43:47 +0530 Subject: [PATCH 086/115] fix: imports --- erpnext/regional/india/e_invoice/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 73e9edbb8b9..292dab132d2 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -9,12 +9,13 @@ import jwt import json import base64 import frappe +import traceback from frappe import _, bold from pyqrcode import create as qrcreate from frappe.model.document import Document from frappe.integrations.utils import make_post_request, make_get_request from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply -from frappe.utils.data import cstr, cint, formatdate as format_date, flt, time_diff_in_seconds, now_datetime +from frappe.utils.data import cstr, cint, formatdate as format_date, flt, time_diff_in_seconds, now_datetime, add_to_date def validate_einvoice_fields(doc): einvoicing_enabled = frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable') From 020e6e980a0c7a9c330832862124440817561d96 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 7 Nov 2020 19:44:37 +0530 Subject: [PATCH 087/115] fix: error handling --- erpnext/regional/india/e_invoice/utils.py | 111 +++++++++++++++++----- 1 file changed, 88 insertions(+), 23 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 292dab132d2..54f18d37f00 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -183,8 +183,8 @@ def get_value_details(invoice): # discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount < 0 else 0) disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') - value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else doc.base_rounded_total - value_details.grand_total = abs(invoice.grand_total) if disable_rounded else doc.rounded_total + value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else invoice.base_rounded_total + value_details.grand_total = abs(invoice.grand_total) if disable_rounded else invoice.rounded_total value_details.total_cgst_amt = 0 value_details.total_sgst_amt = 0 value_details.total_igst_amt = 0 @@ -287,7 +287,12 @@ def make_einvoice(doctype, name): validations = json.loads(read_json('einv_validation')) errors = validate_einvoice(validations, einvoice) if errors: - frappe.log_error(title="E Invoice Validation Failed", message=json.dumps(errors, default=str, indent=4)) + message = "\n".join([ + "E Invoice: ", json.dumps(einvoice, default=str, indent=4), + "-" * 50, + "Errors: ", json.dumps(errors, default=str, indent=4) + ]) + frappe.log_error(title="E Invoice Validation Failed", message=message) if len(errors) > 1: li = ['
  • '+ d +'
  • ' for d in errors] frappe.throw("
      {}
    ".format(''.join(li)), title=_('E Invoice Validation Failed')) @@ -378,6 +383,8 @@ def attach_qrcode_image(doctype, docname, qrcode): frappe.db.set_value(doctype, docname, 'qrcode_image', _file.file_url) +class ResponseFailure(Exception): pass + class GSPConnector(): def __init__(self): self.credentials = frappe.get_cached_doc('E Invoice Settings') @@ -386,6 +393,7 @@ class GSPConnector(): self.authenticate_url = self.base_url + 'gsp/authenticate?grant_type=token' self.gstin_details_url = self.base_url + 'test/enriched/ei/api/master/gstin' self.generate_irn_url = self.base_url + 'test/enriched/ei/api/invoice' + self.irn_details_url = self.base_url + 'test/enriched/ei/api/invoice/irn' self.cancel_irn_url = self.base_url + 'test/enriched/ei/api/invoice/cancel' self.cancel_ewaybill_url = self.base_url + '/test/enriched/ei/api/ewayapi' self.generate_ewaybill_url = self.base_url + 'test/enriched/ei/api/ewaybill' @@ -401,16 +409,16 @@ class GSPConnector(): 'gspappid': self.credentials.client_id, 'gspappsecret': self.credentials.client_secret } - + res = {} try: res = make_post_request(self.authenticate_url, headers=headers) self.credentials.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token')) self.credentials.token_expiry = add_to_date(None, seconds=res.get('expires_in')) self.credentials.save() - except Exception as e: - self.log_error(e) - raise + except Exception: + self.log_error(res) + self.raise_error(True) def get_headers(self): return { @@ -432,11 +440,14 @@ class GSPConnector(): return res.get('result') else: self.log_error(res) - - except Exception as e: - self.log_error(e) + raise ResponseFailure - return {} + except ResponseFailure: + self.raise_error() + + except Exception: + self.log_error() + self.raise_error(True) @staticmethod def get_gstin_details(gstin): @@ -472,11 +483,43 @@ class GSPConnector(): res = make_post_request(self.generate_irn_url, headers=headers, data=data) if res.get('success'): update_invoice(doctype, docname, res.get('result')) + + elif '2150' in res.get('message'): + # IRN already generated + irn = res.get('result')[0].get('Desc').get('Irn') + irn_details = self.get_irn_details(irn) + if irn_details: + update_invoice(doctype, docname, irn_details) + else: self.log_error(res) + raise ResponseFailure + + except ResponseFailure: + self.raise_error() - except Exception as e: - self.log_error(e) + except Exception: + self.log_error(data) + self.raise_error(True) + + def get_irn_details(self, irn): + headers = self.get_headers() + + try: + params = '?irn={irn}'.format(irn=irn) + res = make_get_request(self.irn_details_url + params, headers=headers) + if res.get('success'): + return res.get('result') + else: + self.log_error(res) + raise ResponseFailure + + except ResponseFailure: + self.raise_error() + + except Exception: + self.log_error() + self.raise_error(True) def cancel_irn(self, docname, irn, reason, remark): headers = self.get_headers() @@ -494,9 +537,14 @@ class GSPConnector(): # frappe.db.set_value(doctype, docname, 'cancelled_on', res.get('CancelDate')) else: self.log_error(res) + raise ResponseFailure + + except ResponseFailure: + self.raise_error() - except Exception as e: - self.log_error(e) + except Exception: + self.log_error(data) + self.raise_error(True) def generate_eway_bill(self, **kwargs): args = frappe._dict(kwargs) @@ -528,9 +576,14 @@ class GSPConnector(): frappe.db.set_value(doctype, docname, d, args[d]) else: self.log_error(res) + raise ResponseFailure - except Exception as e: - self.log_error(e) + except ResponseFailure: + self.raise_error() + + except Exception: + self.log_error(data) + self.raise_error(True) def cancel_eway_bill(self, docname, eway_bill, reason, remark): headers = self.get_headers() @@ -548,17 +601,29 @@ class GSPConnector(): frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1) else: self.log_error(res) + raise ResponseFailure - except Exception as e: - self.log_error(e) + except ResponseFailure: + self.raise_error() - def log_error(self, exc): - message = "\n".join(["Data:", json.dumps(exc), "--" * 50, "\nException:", traceback.format_exc()]) + except Exception: + self.log_error(data) + self.raise_error(True) + + def log_error(self, data={}): + if not isinstance(data, dict): + data = json.loads(data) + + message = "\n".join(["Data:", json.dumps(data, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]) frappe.log_error(title="E Invoicing Error", message=message) + + def raise_error(self, raise_exception=False): link_to_error_list = '{1}'.format('Error Log', 'Error Log') - frappe.throw( + frappe.msgprint( _('An error occurred while making API request. Please check {} for more information.').format(link_to_error_list), - title=_('E Invoice Request Failed') + title=_('E Invoice Request Failed'), + raise_exception=raise_exception, + indicator='red' ) @frappe.whitelist() From 72ed0fbbb497e51e9f03dcde8d879dd9aff93af9 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 7 Nov 2020 21:00:38 +0530 Subject: [PATCH 088/115] fix: ewaybill could be modified after ewaybill generation --- erpnext/regional/india/e_invoice/einvoice.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index d70a8ef089e..62a2d2d5202 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -9,6 +9,10 @@ erpnext.setup_einvoice_actions = (doctype) => { const { docstatus, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; + if (ewaybill && irn) { + frm.set_df_property('ewaybill', 'read_only', 1); + } + if (docstatus == 0 && !irn && !__unsaved) { const action = () => { frappe.call({ From dc4b0921f6ca62daf8829b7fa5643f6ef5d6a883 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 7 Nov 2020 21:15:19 +0530 Subject: [PATCH 089/115] feat: update timeline on einvoice actions --- .../patches/v12_0/setup_einvoice_fields.py | 3 +- erpnext/regional/india/e_invoice/einvoice.js | 196 ++++++++++-------- erpnext/regional/india/e_invoice/utils.py | 161 ++++++++------ 3 files changed, 199 insertions(+), 161 deletions(-) diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index dacdc86af53..cbad581ab2a 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -32,4 +32,5 @@ def execute(): add_print_formats() frappe.db.set_value('Custom Field', { 'fieldname': 'mode_of_transport' }, 'default', '') - frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'depends_on', 'eval:doc.mode_of_transport == "Road"') \ No newline at end of file + frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'depends_on', 'eval:doc.mode_of_transport == "Road"') + frappe.db.set_value('Custom Field', { 'fieldname': 'ewaybill' }, 'depends_on', 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)') \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 62a2d2d5202..d4c473363d0 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -7,7 +7,13 @@ erpnext.setup_einvoice_actions = (doctype) => { if (!einvoicing_enabled || !valid_supply_type) return; - const { docstatus, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; + const { doctype, docstatus, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; + + const add_custom_button = (label, action) => { + if (!frm.custom_buttons[label]) { + frm.add_custom_button(label, action, __('E Invoicing')); + } + } if (ewaybill && irn) { frm.set_df_property('ewaybill', 'read_only', 1); @@ -17,13 +23,13 @@ erpnext.setup_einvoice_actions = (doctype) => { const action = () => { frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.generate_irn', - args: { docname: name }, + args: { doctype, docname: name }, freeze: true, callback: () => frm.reload_doc() }) }; - frm.add_custom_button(__("Generate IRN"), action, __('E Invoicing')); + add_custom_button(__("Generate IRN"), action); } if (docstatus == 1 && irn && !irn_cancelled && !ewaybill) { @@ -52,6 +58,7 @@ erpnext.setup_einvoice_actions = (doctype) => { frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.cancel_irn', args: { + doctype, docname: name, irn: irn, reason: data.reason.split('-')[0], @@ -66,105 +73,23 @@ erpnext.setup_einvoice_actions = (doctype) => { }); d.show(); }; - frm.add_custom_button(__("Cancel IRN"), action, __("E Invoicing")); + add_custom_button(__("Cancel IRN"), action); } if (irn && !irn_cancelled && !ewaybill) { - const fields = [ - { - 'fieldname': 'transporter', - 'label': 'Transporter', - 'fieldtype': 'Link', - 'options': 'Supplier', - 'default': frm.doc.transporter - }, - { - 'fieldname': 'gst_transporter_id', - 'label': 'GST Transporter ID', - 'fieldtype': 'Data', - 'fetch_from': 'transporter.gst_transporter_id', - 'default': frm.doc.gst_transporter_id - }, - { - 'fieldname': 'driver', - 'label': 'Driver', - 'fieldtype': 'Link', - 'options': 'Driver', - 'default': frm.doc.driver - }, - { - 'fieldname': 'lr_no', - 'label': 'Transport Receipt No', - 'fieldtype': 'Data', - 'default': frm.doc.lr_no - }, - { - 'fieldname': 'vehicle_no', - 'label': 'Vehicle No', - 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.mode_of_transport === "Road")', - 'default': frm.doc.vehicle_no - }, - { - 'fieldname': 'distance', - 'label': 'Distance (in km)', - 'fieldtype': 'Float', - 'default': frm.doc.distance - }, - { - 'fieldname': 'transporter_col_break', - 'fieldtype': 'Column Break', - }, - { - 'fieldname': 'transporter_name', - 'label': 'Transporter Name', - 'fieldtype': 'Data', - 'fetch_from': 'transporter.name', - 'read_only': 1, - 'default': frm.doc.transporter_name - }, - { - 'fieldname': 'mode_of_transport', - 'label': 'Mode of Transport', - 'fieldtype': 'Select', - 'options': `\nRoad\nAir\nRail\nShip`, - 'default': frm.doc.mode_of_transport - }, - { - 'fieldname': 'driver_name', - 'label': 'Driver Name', - 'fieldtype': 'Data', - 'fetch_from': 'driver.full_name', - 'read_only': 1, - 'default': frm.doc.driver_name - }, - { - 'fieldname': 'lr_date', - 'label': 'Transport Receipt Date', - 'fieldtype': 'Date', - 'default': frm.doc.lr_date - }, - { - 'fieldname': 'gst_vehicle_type', - 'label': 'GST Vehicle Type', - 'fieldtype': 'Select', - 'options': `Regular\nOver Dimensional Cargo (ODC)`, - 'depends_on': 'eval:(doc.mode_of_transport === "Road")', - 'default': frm.doc.gst_vehicle_type - } - ] - const action = () => { const d = new frappe.ui.Dialog({ title: __('Generate E-Way Bill'), wide: 1, - fields: fields, + fields: get_ewaybill_fields(frm), primary_action: function() { const data = d.get_values(); frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.generate_eway_bill', args: { - docname: name, irn, + doctype, + docname: name, + irn, ...data }, freeze: true, @@ -177,7 +102,7 @@ erpnext.setup_einvoice_actions = (doctype) => { d.show(); }; - frm.add_custom_button(__("Generate E-Way Bill"), action, __("E Invoicing")); + add_custom_button(__("Generate E-Way Bill"), action); } if (docstatus == 1 && irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { @@ -206,6 +131,7 @@ erpnext.setup_einvoice_actions = (doctype) => { frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', args: { + doctype, docname: name, eway_bill: ewaybill, reason: data.reason.split('-')[0], @@ -220,8 +146,94 @@ erpnext.setup_einvoice_actions = (doctype) => { }); d.show(); }; - frm.add_custom_button(__("Cancel E-Way Bill"), action, __("E Invoicing")); + add_custom_button(__("Cancel E-Way Bill"), action); } } }) +} + +const get_ewaybill_fields = (frm) => { + return [ + { + 'fieldname': 'transporter', + 'label': 'Transporter', + 'fieldtype': 'Link', + 'options': 'Supplier', + 'default': frm.doc.transporter + }, + { + 'fieldname': 'gst_transporter_id', + 'label': 'GST Transporter ID', + 'fieldtype': 'Data', + 'fetch_from': 'transporter.gst_transporter_id', + 'default': frm.doc.gst_transporter_id + }, + { + 'fieldname': 'driver', + 'label': 'Driver', + 'fieldtype': 'Link', + 'options': 'Driver', + 'default': frm.doc.driver + }, + { + 'fieldname': 'lr_no', + 'label': 'Transport Receipt No', + 'fieldtype': 'Data', + 'default': frm.doc.lr_no + }, + { + 'fieldname': 'vehicle_no', + 'label': 'Vehicle No', + 'fieldtype': 'Data', + 'depends_on': 'eval:(doc.mode_of_transport === "Road")', + 'default': frm.doc.vehicle_no + }, + { + 'fieldname': 'distance', + 'label': 'Distance (in km)', + 'fieldtype': 'Float', + 'default': frm.doc.distance + }, + { + 'fieldname': 'transporter_col_break', + 'fieldtype': 'Column Break', + }, + { + 'fieldname': 'transporter_name', + 'label': 'Transporter Name', + 'fieldtype': 'Data', + 'fetch_from': 'transporter.name', + 'read_only': 1, + 'default': frm.doc.transporter_name + }, + { + 'fieldname': 'mode_of_transport', + 'label': 'Mode of Transport', + 'fieldtype': 'Select', + 'options': `\nRoad\nAir\nRail\nShip`, + 'default': frm.doc.mode_of_transport + }, + { + 'fieldname': 'driver_name', + 'label': 'Driver Name', + 'fieldtype': 'Data', + 'fetch_from': 'driver.full_name', + 'read_only': 1, + 'default': frm.doc.driver_name + }, + { + 'fieldname': 'lr_date', + 'label': 'Transport Receipt Date', + 'fieldtype': 'Date', + 'default': frm.doc.lr_date + }, + { + 'fieldname': 'gst_vehicle_type', + 'label': 'GST Vehicle Type', + 'fieldtype': 'Select', + 'options': `Regular\nOver Dimensional Cargo (ODC)`, + 'depends_on': 'eval:(doc.mode_of_transport === "Road")', + 'default': frm.doc.gst_vehicle_type + } + ]; } \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 54f18d37f00..b38c5070cbd 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -240,9 +240,7 @@ def get_eway_bill_details(invoice): vehicle_type=vehicle_type[invoice.gst_vehicle_type] )) -@frappe.whitelist() -def make_einvoice(doctype, name): - invoice = frappe.get_doc(doctype, name) +def make_einvoice(invoice): schema = read_json('einv_template') trans_details = get_trans_details(invoice) @@ -255,7 +253,7 @@ def make_einvoice(doctype, name): buyer_details = get_overseas_address_details(invoice.customer_address) else: buyer_details = get_party_details(invoice.customer_address) - place_of_supply = get_place_of_supply(invoice, doctype) or invoice.billing_address_gstin + place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin place_of_supply = place_of_supply[:2] buyer_details.update(dict(place_of_supply=place_of_supply)) @@ -351,42 +349,10 @@ def validate_einvoice(validations, einvoice, errors=[]): return errors -def update_invoice(doctype, docname, res): - enc_signed_invoice = res.get('SignedInvoice') - dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data'] - - frappe.db.set_value(doctype, docname, 'irn', res.get('Irn')) - frappe.db.set_value(doctype, docname, 'ewaybill', res.get('EwbNo')) - frappe.db.set_value(doctype, docname, 'signed_einvoice', dec_signed_invoice) - - signed_qr_code = res.get('SignedQRCode') - frappe.db.set_value(doctype, docname, 'signed_qr_code', signed_qr_code) - - attach_qrcode_image(doctype, docname, signed_qr_code) - -def attach_qrcode_image(doctype, docname, qrcode): - if not qrcode: return - - _file = frappe.new_doc('File') - _file.update({ - 'file_name': f'QRCode_{docname}.png', - 'attached_to_doctype': doctype, - 'attached_to_name': docname, - 'content': 'qrcode', - 'is_private': 1 - }) - _file.insert() - frappe.db.commit() - url = qrcreate(qrcode) - abs_file_path = os.path.abspath(_file.get_full_path()) - url.png(abs_file_path, scale=2) - - frappe.db.set_value(doctype, docname, 'qrcode_image', _file.file_url) - class ResponseFailure(Exception): pass class GSPConnector(): - def __init__(self): + def __init__(self, doctype, docname): self.credentials = frappe.get_cached_doc('E Invoice Settings') self.base_url = 'https://gsp.adaequare.com/' @@ -397,6 +363,8 @@ class GSPConnector(): self.cancel_irn_url = self.base_url + 'test/enriched/ei/api/invoice/cancel' self.cancel_ewaybill_url = self.base_url + '/test/enriched/ei/api/ewayapi' self.generate_ewaybill_url = self.base_url + 'test/enriched/ei/api/ewaybill' + + self.invoice = frappe.get_cached_doc(doctype, docname) def get_auth_token(self): if time_diff_in_seconds(self.credentials.token_expiry, now_datetime()) < 150.0: @@ -473,23 +441,22 @@ class GSPConnector(): frappe.cache().hset('gstin_cache', key, details) return details - def generate_irn(self, docname): + def generate_irn(self): headers = self.get_headers() - doctype = 'Sales Invoice' - einvoice = make_einvoice(doctype, docname) + einvoice = make_einvoice(self.invoice) data = json.dumps(einvoice) try: res = make_post_request(self.generate_irn_url, headers=headers, data=data) if res.get('success'): - update_invoice(doctype, docname, res.get('result')) + self.set_einvoice_data(res.get('result')) elif '2150' in res.get('message'): # IRN already generated irn = res.get('result')[0].get('Desc').get('Irn') irn_details = self.get_irn_details(irn) if irn_details: - update_invoice(doctype, docname, irn_details) + self.set_einvoice_data(irn_details) else: self.log_error(res) @@ -521,9 +488,8 @@ class GSPConnector(): self.log_error() self.raise_error(True) - def cancel_irn(self, docname, irn, reason, remark): + def cancel_irn(self, irn, reason, remark): headers = self.get_headers() - doctype = 'Sales Invoice' data = json.dumps({ 'Irn': irn, 'Cnlrsn': reason, @@ -533,8 +499,14 @@ class GSPConnector(): try: res = make_post_request(self.cancel_irn_url, headers=headers, data=data) if res.get('success'): - frappe.db.set_value(doctype, docname, 'irn_cancelled', 1) - # frappe.db.set_value(doctype, docname, 'cancelled_on', res.get('CancelDate')) + self.invoice.irn_cancelled = 1 + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('IRN Cancelled - {}').format(remark) + } + self.update_invoice() + else: self.log_error(res) raise ResponseFailure @@ -550,8 +522,6 @@ class GSPConnector(): args = frappe._dict(kwargs) headers = self.get_headers() - doctype = 'Sales Invoice' - docname = args.docname eway_bill_details = get_eway_bill_details(args) data = json.dumps({ 'Irn': args.irn, @@ -568,12 +538,16 @@ class GSPConnector(): try: res = make_post_request(self.generate_ewaybill_url, headers=headers, data=data) if res.get('success'): - frappe.db.set_value(doctype, docname, 'ewaybill', res.get('result').get('EwbNo')) - frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 0) - for d in args: - if d in ['docname', 'cmd']: continue - # update eway bill details in sales invoice - frappe.db.set_value(doctype, docname, d, args[d]) + self.invoice.ewaybill = res.get('result').get('EwbNo') + self.invoice.eway_bill_cancelled = 0 + self.invoice.update(args) + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('E-Way Bill Generated') + } + self.update_invoice() + else: self.log_error(res) raise ResponseFailure @@ -585,7 +559,7 @@ class GSPConnector(): self.log_error(data) self.raise_error(True) - def cancel_eway_bill(self, docname, eway_bill, reason, remark): + def cancel_eway_bill(self, eway_bill, reason, remark): headers = self.get_headers() doctype = 'Sales Invoice' data = json.dumps({ @@ -597,8 +571,15 @@ class GSPConnector(): try: res = make_post_request(self.cancel_ewaybill_url, headers=headers, data=data) if res.get('success'): - frappe.db.set_value(doctype, docname, 'ewaybill', '') - frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1) + self.invoice.ewaybill = '' + self.invoice.eway_bill_cancelled = 1 + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('E-Way Bill Cancelled - {}').format(remark) + } + self.update_invoice() + else: self.log_error(res) raise ResponseFailure @@ -625,23 +606,67 @@ class GSPConnector(): raise_exception=raise_exception, indicator='red' ) + + def set_einvoice_data(self, res): + enc_signed_invoice = res.get('SignedInvoice') + dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data'] + + self.invoice.irn = res.get('Irn') + self.invoice.ewaybill = res.get('EwbNo') + self.invoice.signed_einvoice = dec_signed_invoice + self.invoice.signed_qr_code = res.get('SignedQRCode') + + self.attach_qrcode_image() + + self.invoice.flags.updater_reference = { + 'doctype': self.invoice.doctype, + 'docname': self.invoice.name, + 'label': _('IRN Generated') + } + self.update_invoice() + + def attach_qrcode_image(self): + qrcode = self.invoice.signed_qr_code + doctype = self.invoice.doctype + docname = self.invoice.name + + _file = frappe.new_doc('File') + _file.update({ + 'file_name': f'QRCode_{docname}.png', + 'attached_to_doctype': doctype, + 'attached_to_name': docname, + 'content': 'qrcode', + 'is_private': 1 + }) + _file.insert() + frappe.db.commit() + url = qrcreate(qrcode) + abs_file_path = os.path.abspath(_file.get_full_path()) + url.png(abs_file_path, scale=2) + + self.invoice.qrcode_image = _file.file_url + + def update_invoice(self): + self.invoice.flags.ignore_validate_update_after_submit = True + self.invoice.flags.ignore_validate = True + self.invoice.save() @frappe.whitelist() -def generate_irn(docname): - gsp_connector = GSPConnector() - gsp_connector.generate_irn(docname) +def generate_irn(doctype, docname): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.generate_irn() @frappe.whitelist() -def cancel_irn(docname, irn, reason, remark): - gsp_connector = GSPConnector() - gsp_connector.cancel_irn(docname, irn, reason, remark) +def cancel_irn(doctype, docname, irn, reason, remark): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.cancel_irn(irn, reason, remark) @frappe.whitelist() -def generate_eway_bill(**kwargs): - gsp_connector = GSPConnector() +def generate_eway_bill(doctype, docname, **kwargs): + gsp_connector = GSPConnector(doctype, docname) gsp_connector.generate_eway_bill(**kwargs) @frappe.whitelist() -def cancel_eway_bill(docname, eway_bill, reason, remark): - gsp_connector = GSPConnector() - gsp_connector.cancel_eway_bill(docname, eway_bill, reason, remark) \ No newline at end of file +def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.cancel_eway_bill(eway_bill, reason, remark) \ No newline at end of file From c82c955bc24a8e2bafe7a6ac66b761cc064c267d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 7 Nov 2020 22:44:12 +0530 Subject: [PATCH 090/115] fix: qrcode image size --- erpnext/regional/india/e_invoice/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index b38c5070cbd..fe9afa8d837 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -248,6 +248,7 @@ def make_einvoice(invoice): doc_details = get_doc_details(invoice) value_details = get_value_details(invoice) seller_details = get_party_details(invoice.company_address) + seller_details.update({ 'pincode': 193502 }) if invoice.gst_category == 'Overseas': buyer_details = get_overseas_address_details(invoice.customer_address) @@ -640,9 +641,9 @@ class GSPConnector(): }) _file.insert() frappe.db.commit() - url = qrcreate(qrcode) + url = qrcreate(qrcode, error='L') abs_file_path = os.path.abspath(_file.get_full_path()) - url.png(abs_file_path, scale=2) + url.png(abs_file_path, scale=2, quiet_zone=1) self.invoice.qrcode_image = _file.file_url From ee0b08ae9c4d6a7a74a71dc0a9753bead359ff96 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 24 Nov 2020 13:52:54 +0530 Subject: [PATCH 091/115] fix: exclude intra company transactions --- erpnext/regional/india/e_invoice/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index fe9afa8d837..9052f1bcf3f 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -21,8 +21,9 @@ def validate_einvoice_fields(doc): einvoicing_enabled = frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable') invalid_doctype = doc.doctype not in ['Sales Invoice'] invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] + company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') - if invalid_doctype or invalid_supply_type or not einvoicing_enabled: return + if invalid_doctype or invalid_supply_type or company_transaction or not einvoicing_enabled: return if doc.docstatus == 0 and doc._action == 'save': if doc.irn: From 9760eb9d5777e330895a3ac7d2060cda5943eccc Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 Nov 2020 13:17:02 +0530 Subject: [PATCH 092/115] fix: eway bill test --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 26f488d8c9a..f9f21a03c5f 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1909,6 +1909,7 @@ class TestSalesInvoice(unittest.TestCase): si.customer_address = "_Test Customer-Address for Eway bill-Shipping" si.vehicle_no = "KA12KA1234" si.gst_category = "Registered Regular" + si.mode_of_transport = 'Road' si.append("taxes", { "charge_type": "On Net Total", From 57e6759088c3e63c494110cc8b54fe0bd223bb3a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 2 Dec 2020 12:59:20 +0530 Subject: [PATCH 093/115] fix: ewaybill mandatory conditions --- erpnext/patches.txt | 2 +- erpnext/patches/v12_0/setup_einvoice_fields.py | 14 +++++++++++--- erpnext/regional/india/setup.py | 1 - 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d08a9283a08..e6066a9fead 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -678,4 +678,4 @@ erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v12_0.update_leave_application_status erpnext.patches.v12_0.update_payment_entry_status -erpnext.patches.v12_0.setup_einvoice_fields #2020-11-04 +erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02 diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index cbad581ab2a..6c7b3e8feb6 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -31,6 +31,14 @@ def execute(): add_permissions() add_print_formats() - frappe.db.set_value('Custom Field', { 'fieldname': 'mode_of_transport' }, 'default', '') - frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'depends_on', 'eval:doc.mode_of_transport == "Road"') - frappe.db.set_value('Custom Field', { 'fieldname': 'ewaybill' }, 'depends_on', 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)') \ No newline at end of file + t = { + 'mode_of_transport': [{'default': None}], + 'ewaybill': [ + {'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'} + ] + } + + for field, conditions in t.items(): + for c in conditions: + [(prop, value)] = c.items() + frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value) \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index bb0d2ff8013..317a0e242b7 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -303,7 +303,6 @@ def make_custom_fields(update=True): 'label': 'Vehicle No', 'fieldtype': 'Data', 'insert_after': 'lr_no', - 'depends_on': 'eval:(doc.mode_of_transport === "Road")', 'print_hide': 1, 'translatable': 0 }, From 5d6849ac7f221844a185d3ce947c895bf874b63b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 2 Dec 2020 17:51:51 +0530 Subject: [PATCH 094/115] chore: add tests --- .../sales_invoice/test_sales_invoice.py | 270 ++++++++++++------ erpnext/regional/india/e_invoice/utils.py | 50 ++-- 2 files changed, 206 insertions(+), 114 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index f9f21a03c5f..eee46f989f0 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1838,94 +1838,7 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(target_doc.supplier, "_Test Internal Supplier") def test_eway_bill_json(self): - if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Address for Eway bill", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gstin": "27AAECE4835E1ZR", - "gst_state": "Maharashtra", - "gst_state_number": "27", - "pincode": "401108" - }).insert() - - address.append("links", { - "link_doctype": "Company", - "link_name": "_Test Company" - }) - - address.save() - - if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Customer-Address for Eway bill", - "address_type": "Shipping", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+91 0000000000", - "gst_state": "Maharashtra", - "gst_state_number": "27", - "pincode": "410038" - }).insert() - - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test Customer" - }) - - address.save() - - gst_settings = frappe.get_doc("GST Settings") - - gst_account = frappe.get_all( - "GST Account", - fields=["cgst_account", "sgst_account", "igst_account"], - filters = {"company": "_Test Company"}) - - if not gst_account: - gst_settings.append("gst_accounts", { - "company": "_Test Company", - "cgst_account": "CGST - _TC", - "sgst_account": "SGST - _TC", - "igst_account": "IGST - _TC", - }) - - gst_settings.save() - - si = create_sales_invoice(do_not_save =1, rate = '60000') - - si.distance = 2000 - si.company_address = "_Test Address for Eway bill-Billing" - si.customer_address = "_Test Customer-Address for Eway bill-Shipping" - si.vehicle_no = "KA12KA1234" - si.gst_category = "Registered Regular" - si.mode_of_transport = 'Road' - - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "CGST - _TC", - "cost_center": "Main - _TC", - "description": "CGST @ 9.0", - "rate": 9 - }) - - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "SGST - _TC", - "cost_center": "Main - _TC", - "description": "SGST @ 9.0", - "rate": 9 - }) + si = make_sales_invoice_for_ewaybill() si.submit() @@ -1941,6 +1854,187 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(data['billLists'][0]['sgstValue'], 5400) self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) + + def test_einvoice_submission_without_irn(self): + # init + frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1) + country = frappe.flags.country + frappe.flags.country = 'India' + + si = make_sales_invoice_for_ewaybill() + self.assertRaises(frappe.ValidationError, si.submit) + + si.irn = 'test_irn' + si.submit() + + # reset + frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0) + frappe.flags.country = country + + def test_einvoice_json(self): + from erpnext.regional.india.e_invoice.utils import make_einvoice + + customer_gstin = '27AACCM7806M1Z3' + customer_gstin_dtls = { + 'LegalName': '_Test Customer', 'TradeName': '_Test Customer', 'AddrLoc': '_Test City', + 'StateCode': '27', 'AddrPncd': '410038', 'AddrBno': '_Test Bldg', + 'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street' + } + company_gstin = '27AAECE4835E1ZR' + company_gstin_dtls = { + 'LegalName': '_Test Company', 'TradeName': '_Test Company', 'AddrLoc': '_Test City', + 'StateCode': '27', 'AddrPncd': '401108', 'AddrBno': '_Test Bldg', + 'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street' + } + # set cache gstin details to avoid fetching details which will require connection to GSP servers + frappe.local.gstin_cache = {} + frappe.local.gstin_cache[customer_gstin] = customer_gstin_dtls + frappe.local.gstin_cache[company_gstin] = company_gstin_dtls + + si = make_sales_invoice_for_ewaybill() + si.naming_series = 'INV-2020-.#####' + si.items = [] + si.append("items", { + "item_code": "_Test Item", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 2, + "rate": 100, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + si.append("items", { + "item_code": "_Test Item 2", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 4, + "rate": 150, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }) + si.save() + + einvoice = make_einvoice(si) + + total_item_ass_value = sum([d['AssAmt'] for d in einvoice['ItemList']]) + total_item_cgst_value = sum([d['CgstAmt'] for d in einvoice['ItemList']]) + total_item_sgst_value = sum([d['SgstAmt'] for d in einvoice['ItemList']]) + total_item_igst_value = sum([d['IgstAmt'] for d in einvoice['ItemList']]) + total_item_value = sum([d['TotItemVal'] for d in einvoice['ItemList']]) + + self.assertEqual(einvoice['Version'], '1.1') + self.assertEqual(einvoice['ValDtls']['AssVal'], total_item_ass_value) + self.assertEqual(einvoice['ValDtls']['CgstVal'], total_item_cgst_value) + self.assertEqual(einvoice['ValDtls']['SgstVal'], total_item_sgst_value) + self.assertEqual(einvoice['ValDtls']['IgstVal'], total_item_igst_value) + self.assertEqual(einvoice['ValDtls']['TotInvVal'], total_item_value) + self.assertTrue(einvoice['EwbDtls']) + +def make_sales_invoice_for_ewaybill(): + if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): + address = frappe.get_doc({ + "address_line1": "_Test Address Line 1", + "address_title": "_Test Address for Eway bill", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+910000000000", + "gstin": "27AAECE4835E1ZR", + "gst_state": "Maharashtra", + "gst_state_number": "27", + "pincode": "401108" + }).insert() + + address.append("links", { + "link_doctype": "Company", + "link_name": "_Test Company" + }) + + address.save() + + if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'): + address = frappe.get_doc({ + "address_line1": "_Test Address Line 1", + "address_title": "_Test Customer-Address for Eway bill", + "address_type": "Shipping", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+910000000000", + "gstin": "27AACCM7806M1Z3", + "gst_state": "Maharashtra", + "gst_state_number": "27", + "pincode": "410038" + }).insert() + + address.append("links", { + "link_doctype": "Customer", + "link_name": "_Test Customer" + }) + + address.save() + + if not frappe.db.exists('Supplier', '_Test Transporter'): + supplier = frappe.get_doc({ + "doctype": "Supplier", + "supplier_name": "_Test Transporter", + "country": "India", + "supplier_group": "_Test Supplier Group", + "supplier_type": "Company", + "is_transporter": 1 + }).insert() + + gst_settings = frappe.get_doc("GST Settings") + + gst_account = frappe.get_all( + "GST Account", + fields=["cgst_account", "sgst_account", "igst_account"], + filters = {"company": "_Test Company"}) + + if not gst_account: + gst_settings.append("gst_accounts", { + "company": "_Test Company", + "cgst_account": "CGST - _TC", + "sgst_account": "SGST - _TC", + "igst_account": "IGST - _TC", + }) + + gst_settings.save() + + si = create_sales_invoice(do_not_save =1, rate = '60000') + + si.distance = 2000 + si.company_address = "_Test Address for Eway bill-Billing" + si.customer_address = "_Test Customer-Address for Eway bill-Shipping" + si.vehicle_no = "KA12KA1234" + si.gst_category = "Registered Regular" + si.mode_of_transport = 'Road' + si.transporter = '_Test Transporter' + + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "CGST - _TC", + "cost_center": "Main - _TC", + "description": "CGST @ 9.0", + "rate": 9 + }) + + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "SGST - _TC", + "cost_center": "Main - _TC", + "description": "SGST @ 9.0", + "rate": 9 + }) + + return si def test_item_tax_validity(self): item = frappe.get_doc("Item", "_Test Item 2") diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 9052f1bcf3f..087c5956dc0 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -71,10 +71,7 @@ def get_trans_details(invoice): )) def get_doc_details(invoice): - if invoice.doctype == 'Purchase Invoice' and invoice.is_return: - invoice_type = 'DBN' - else: - invoice_type = 'CRN' if invoice.is_return else 'INV' + invoice_type = 'CRN' if invoice.is_return else 'INV' invoice_name = invoice.name invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy') @@ -89,7 +86,7 @@ def get_party_details(address_name): address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] gstin = address.get('gstin') - gstin_details = GSPConnector.get_gstin_details(gstin) + gstin_details = get_gstin_details(gstin) legal_name = gstin_details.get('LegalName') trade_name = gstin_details.get('TradeName') location = gstin_details.get('AddrLoc') or address.get('city') @@ -98,7 +95,7 @@ def get_party_details(address_name): address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno')) address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt')) email_id = address.get('email_id') - phone = address.get('phone') + phone = address.get('phone').replace(" ", "")[-10:] # get last 10 digit if state_code == 97: pincode = 999999 @@ -108,6 +105,23 @@ def get_party_details(address_name): address_line2=address_line2, email=email_id, phone=phone )) +def get_gstin_details(gstin): + if not hasattr(frappe.local, 'gstin_cache'): + frappe.local.gstin_cache = {} + + key = gstin + details = frappe.local.gstin_cache.get(key) + if details: + return details + + details = frappe.cache().hget('gstin_cache', key) + if details: + frappe.local.gstin_cache[key] = details + return details + + if not details: + return GSPConnector.get_gstin_details(gstin) + def get_overseas_address_details(address_name): address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value( 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id'] @@ -180,9 +194,9 @@ def get_value_details(invoice): value_details = frappe._dict(dict()) value_details.base_net_total = abs(invoice.base_net_total) - value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount > 0 else 0 + value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount and invoice.discount_amount > 0 else 0 # discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off - value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount < 0 else 0) + value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0) disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else invoice.base_rounded_total value_details.grand_total = abs(invoice.grand_total) if disable_rounded else invoice.rounded_total @@ -293,11 +307,7 @@ def make_einvoice(invoice): "Errors: ", json.dumps(errors, default=str, indent=4) ]) frappe.log_error(title="E Invoice Validation Failed", message=message) - if len(errors) > 1: - li = ['
  • '+ d +'
  • ' for d in errors] - frappe.throw("
      {}
    ".format(''.join(li)), title=_('E Invoice Validation Failed')) - else: - frappe.throw(errors[0], title=_('E Invoice Validation Failed')) + frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1) return einvoice @@ -421,21 +431,9 @@ class GSPConnector(): @staticmethod def get_gstin_details(gstin): - '''fetch or get cached GSTIN details''' - - if not hasattr(frappe.local, 'gstin_cache'): - frappe.local.gstin_cache = {} + '''fetch and cache GSTIN details''' key = gstin - details = frappe.local.gstin_cache.get(key) - if details: - return details - - details = frappe.cache().hget('gstin_cache', key) - if details: - frappe.local.gstin_cache[key] = details - return details - gsp_connector = GSPConnector() details = gsp_connector.fetch_gstin_details(gstin) From 5327637b37215cb5f74a9f965a4c15d8c4b8cfac Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 3 Dec 2020 10:59:41 +0530 Subject: [PATCH 095/115] fix: returning condition --- erpnext/regional/india/e_invoice/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 087c5956dc0..772154f9a65 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -18,12 +18,12 @@ from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply from frappe.utils.data import cstr, cint, formatdate as format_date, flt, time_diff_in_seconds, now_datetime, add_to_date def validate_einvoice_fields(doc): - einvoicing_enabled = frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable') + einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')) invalid_doctype = doc.doctype not in ['Sales Invoice'] invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') - if invalid_doctype or invalid_supply_type or company_transaction or not einvoicing_enabled: return + if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction: return if doc.docstatus == 0 and doc._action == 'save': if doc.irn: From 42b028a3ba834b53d63bc5cd40b51260f89fef69 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 8 Dec 2020 14:59:02 +0530 Subject: [PATCH 096/115] feat: log e-invocing requests --- .../doctype/e_invoice_request_log/__init__.py | 0 .../e_invoice_request_log.js | 8 ++ .../e_invoice_request_log.json | 85 ++++++++++++++++++ .../e_invoice_request_log.py | 10 +++ .../test_e_invoice_request_log.py | 10 +++ erpnext/regional/india/e_invoice/utils.py | 87 +++++++++++++------ 6 files changed, 173 insertions(+), 27 deletions(-) create mode 100644 erpnext/regional/doctype/e_invoice_request_log/__init__.py create mode 100644 erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js create mode 100644 erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json create mode 100644 erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py create mode 100644 erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py diff --git a/erpnext/regional/doctype/e_invoice_request_log/__init__.py b/erpnext/regional/doctype/e_invoice_request_log/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js new file mode 100644 index 00000000000..7b7ba964e5e --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('E Invoice Request Log', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json new file mode 100644 index 00000000000..088d5cd7627 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json @@ -0,0 +1,85 @@ +{ + "actions": [], + "autoname": "EINV-REQ-.#####", + "creation": "2020-12-08 12:54:08.175992", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "url", + "headers", + "response", + "column_break_7", + "timestamp", + "reference_invoice", + "data" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User" + }, + { + "fieldname": "reference_invoice", + "fieldtype": "Link", + "label": "Reference Invoice", + "options": "Sales Invoice" + }, + { + "fieldname": "headers", + "fieldtype": "Code", + "label": "Headers", + "options": "JSON" + }, + { + "fieldname": "data", + "fieldtype": "Code", + "label": "Data", + "options": "JSON" + }, + { + "default": "Now", + "fieldname": "timestamp", + "fieldtype": "Datetime", + "label": "Timestamp" + }, + { + "fieldname": "response", + "fieldtype": "Code", + "label": "Response", + "options": "JSON" + }, + { + "fieldname": "url", + "fieldtype": "Data", + "label": "URL" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-12-08 14:50:33.820452", + "modified_by": "Administrator", + "module": "Regional", + "name": "E Invoice Request Log", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py new file mode 100644 index 00000000000..9150bdd9260 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class EInvoiceRequestLog(Document): + pass diff --git a/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py new file mode 100644 index 00000000000..c84e9a249bd --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestEInvoiceRequestLog(unittest.TestCase): + pass diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 772154f9a65..c49e3f7e7e8 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import os import re import jwt +import sys import json import base64 import frappe @@ -361,11 +362,12 @@ def validate_einvoice(validations, einvoice, errors=[]): return errors -class ResponseFailure(Exception): pass +class RequestFailed(Exception): pass class GSPConnector(): - def __init__(self, doctype, docname): + def __init__(self, doctype=None, docname=None): self.credentials = frappe.get_cached_doc('E Invoice Settings') + self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None self.base_url = 'https://gsp.adaequare.com/' self.authenticate_url = self.base_url + 'gsp/authenticate?grant_type=token' @@ -375,14 +377,31 @@ class GSPConnector(): self.cancel_irn_url = self.base_url + 'test/enriched/ei/api/invoice/cancel' self.cancel_ewaybill_url = self.base_url + '/test/enriched/ei/api/ewayapi' self.generate_ewaybill_url = self.base_url + 'test/enriched/ei/api/ewaybill' - - self.invoice = frappe.get_cached_doc(doctype, docname) def get_auth_token(self): if time_diff_in_seconds(self.credentials.token_expiry, now_datetime()) < 150.0: self.fetch_auth_token() return self.credentials.auth_token + + def make_request(self, request_type, url, headers=None, data=None): + function = make_post_request if request_type == 'post' else make_get_request + res = function(url, headers=headers, data=data) + self.log_request(url, headers, data, res) + return res + + def log_request(self, url, headers, data, res): + headers.update({ 'password': self.credentials.password }) + request_log = frappe.get_doc({ + "doctype": "E Invoice Request Log", + "user": frappe.session.user, + "reference_invoice": self.invoice.name if self.invoice else None, + "url": url, + "headers": json.dumps(headers, indent=4) if headers else None, + "data": json.dumps(data, indent=4) if data else None, + "response": json.dumps(res, indent=4) if res else None + }) + request_log.insert(ignore_permissions=True) def fetch_auth_token(self): headers = { @@ -391,7 +410,7 @@ class GSPConnector(): } res = {} try: - res = make_post_request(self.authenticate_url, headers=headers) + res = self.make_request('post', self.authenticate_url, headers) self.credentials.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token')) self.credentials.token_expiry = add_to_date(None, seconds=res.get('expires_in')) self.credentials.save() @@ -409,20 +428,20 @@ class GSPConnector(): 'authorization': self.get_auth_token(), 'requestid': str(base64.b64encode(os.urandom(18))), } - + def fetch_gstin_details(self, gstin): headers = self.get_headers() try: params = '?gstin={gstin}'.format(gstin=gstin) - res = make_get_request(self.gstin_details_url + params, headers=headers) + res = self.make_request('get', self.gstin_details_url + params, headers) if res.get('success'): return res.get('result') else: self.log_error(res) - raise ResponseFailure + raise RequestFailed - except ResponseFailure: + except RequestFailed: self.raise_error() except Exception: @@ -432,6 +451,8 @@ class GSPConnector(): @staticmethod def get_gstin_details(gstin): '''fetch and cache GSTIN details''' + if not hasattr(frappe.local, 'gstin_cache'): + frappe.local.gstin_cache = {} key = gstin gsp_connector = GSPConnector() @@ -447,7 +468,7 @@ class GSPConnector(): data = json.dumps(einvoice) try: - res = make_post_request(self.generate_irn_url, headers=headers, data=data) + res = self.make_request('post', self.generate_irn_url, headers, data) if res.get('success'): self.set_einvoice_data(res.get('result')) @@ -457,12 +478,15 @@ class GSPConnector(): irn_details = self.get_irn_details(irn) if irn_details: self.set_einvoice_data(irn_details) + else: + raise RequestFailed('IRN has already been generated for the invoice but cannot fetch details for the it. \ + Contact ERPNext support to resolve the issue.') else: self.log_error(res) - raise ResponseFailure + raise RequestFailed - except ResponseFailure: + except RequestFailed: self.raise_error() except Exception: @@ -474,14 +498,14 @@ class GSPConnector(): try: params = '?irn={irn}'.format(irn=irn) - res = make_get_request(self.irn_details_url + params, headers=headers) + res = self.make_request('get', self.irn_details_url + params, headers) if res.get('success'): return res.get('result') else: self.log_error(res) - raise ResponseFailure + raise RequestFailed - except ResponseFailure: + except RequestFailed: self.raise_error() except Exception: @@ -497,7 +521,7 @@ class GSPConnector(): }) try: - res = make_post_request(self.cancel_irn_url, headers=headers, data=data) + res = self.make_request('post', self.cancel_irn_url, headers, data) if res.get('success'): self.invoice.irn_cancelled = 1 self.invoice.flags.updater_reference = { @@ -509,9 +533,9 @@ class GSPConnector(): else: self.log_error(res) - raise ResponseFailure + raise RequestFailed - except ResponseFailure: + except RequestFailed: self.raise_error() except Exception: @@ -536,7 +560,7 @@ class GSPConnector(): }) try: - res = make_post_request(self.generate_ewaybill_url, headers=headers, data=data) + res = self.make_request('post', self.generate_ewaybill_url, headers, data) if res.get('success'): self.invoice.ewaybill = res.get('result').get('EwbNo') self.invoice.eway_bill_cancelled = 0 @@ -550,9 +574,9 @@ class GSPConnector(): else: self.log_error(res) - raise ResponseFailure + raise RequestFailed - except ResponseFailure: + except RequestFailed: self.raise_error() except Exception: @@ -569,7 +593,7 @@ class GSPConnector(): }) try: - res = make_post_request(self.cancel_ewaybill_url, headers=headers, data=data) + res = self.make_request('post', self.cancel_ewaybill_url, headers, data) if res.get('success'): self.invoice.ewaybill = '' self.invoice.eway_bill_cancelled = 1 @@ -582,9 +606,9 @@ class GSPConnector(): else: self.log_error(res) - raise ResponseFailure + raise RequestFailed - except ResponseFailure: + except RequestFailed: self.raise_error() except Exception: @@ -595,13 +619,22 @@ class GSPConnector(): if not isinstance(data, dict): data = json.loads(data) - message = "\n".join(["Data:", json.dumps(data, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]) + seperator = "--" * 50 + err_tb = traceback.format_exc() + err_msg = str(sys.exc_info()[1]) + data = json.dumps(data, default=str, indent=4) + + message = "\n".join([ + "Error", err_msg, seperator, + "Data:", data, seperator, + "Exception:", err_tb + ]) frappe.log_error(title="E Invoicing Error", message=message) def raise_error(self, raise_exception=False): - link_to_error_list = '{1}'.format('Error Log', 'Error Log') + link_to_error_list = 'Error Log' frappe.msgprint( - _('An error occurred while making API request. Please check {} for more information.').format(link_to_error_list), + _('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list), title=_('E Invoice Request Failed'), raise_exception=raise_exception, indicator='red' From 64e9275c4aa97ac5c4b811acc99597ed4a1121c6 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Dec 2020 12:17:23 +0530 Subject: [PATCH 097/115] chore: add ack date and ack no field for print formats --- erpnext/patches/v12_0/setup_einvoice_fields.py | 4 ++++ erpnext/regional/india/setup.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index 6c7b3e8feb6..4be4b03d2f8 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -13,6 +13,10 @@ def execute(): 'Sales Invoice': [ dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), + + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), + + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 317a0e242b7..3cfcaa4d9c8 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -379,6 +379,10 @@ def make_custom_fields(update=True): si_einvoice_fields = [ dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), + + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), + + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), From 390cac216059d86493f525734589abde709bb79e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 9 Dec 2020 12:29:25 +0530 Subject: [PATCH 098/115] fix: sider issues --- .../sales_invoice/test_sales_invoice.py | 2 +- erpnext/regional/india/e_invoice/einvoice.js | 20 +++++++++---------- erpnext/regional/india/e_invoice/utils.py | 3 --- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index eee46f989f0..32fca75efcf 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1982,7 +1982,7 @@ def make_sales_invoice_for_ewaybill(): address.save() if not frappe.db.exists('Supplier', '_Test Transporter'): - supplier = frappe.get_doc({ + frappe.get_doc({ "doctype": "Supplier", "supplier_name": "_Test Transporter", "country": "India", diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index d4c473363d0..ff5cef0443e 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -3,7 +3,7 @@ erpnext.setup_einvoice_actions = (doctype) => { refresh(frm) { const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable"); const supply_type = frm.doc.gst_category; - const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type) + const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type); if (!einvoicing_enabled || !valid_supply_type) return; @@ -13,7 +13,7 @@ erpnext.setup_einvoice_actions = (doctype) => { if (!frm.custom_buttons[label]) { frm.add_custom_button(label, action, __('E Invoicing')); } - } + }; if (ewaybill && irn) { frm.set_df_property('ewaybill', 'read_only', 1); @@ -26,7 +26,7 @@ erpnext.setup_einvoice_actions = (doctype) => { args: { doctype, docname: name }, freeze: true, callback: () => frm.reload_doc() - }) + }); }; add_custom_button(__("Generate IRN"), action); @@ -35,7 +35,7 @@ erpnext.setup_einvoice_actions = (doctype) => { if (docstatus == 1 && irn && !irn_cancelled && !ewaybill) { const fields = [ { - "label" : "Reason", + "label": "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, @@ -95,7 +95,7 @@ erpnext.setup_einvoice_actions = (doctype) => { freeze: true, callback: () => frm.reload_doc() || d.hide(), error: () => d.hide() - }) + }); }, primary_action_label: __('Submit') }); @@ -108,7 +108,7 @@ erpnext.setup_einvoice_actions = (doctype) => { if (docstatus == 1 && irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { const fields = [ { - "label" : "Reason", + "label": "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, @@ -140,7 +140,7 @@ erpnext.setup_einvoice_actions = (doctype) => { freeze: true, callback: () => frm.reload_doc() || d.hide(), error: () => d.hide() - }) + }); }, primary_action_label: __('Submit') }); @@ -149,8 +149,8 @@ erpnext.setup_einvoice_actions = (doctype) => { add_custom_button(__("Cancel E-Way Bill"), action); } } - }) -} + }); +}; const get_ewaybill_fields = (frm) => { return [ @@ -236,4 +236,4 @@ const get_ewaybill_fields = (frm) => { 'default': frm.doc.gst_vehicle_type } ]; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index c49e3f7e7e8..532e0955bc2 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -13,7 +13,6 @@ import frappe import traceback from frappe import _, bold from pyqrcode import create as qrcreate -from frappe.model.document import Document from frappe.integrations.utils import make_post_request, make_get_request from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply from frappe.utils.data import cstr, cint, formatdate as format_date, flt, time_diff_in_seconds, now_datetime, add_to_date @@ -89,7 +88,6 @@ def get_party_details(address_name): gstin_details = get_gstin_details(gstin) legal_name = gstin_details.get('LegalName') - trade_name = gstin_details.get('TradeName') location = gstin_details.get('AddrLoc') or address.get('city') state_code = gstin_details.get('StateCode') pincode = gstin_details.get('AddrPncd') @@ -585,7 +583,6 @@ class GSPConnector(): def cancel_eway_bill(self, eway_bill, reason, remark): headers = self.get_headers() - doctype = 'Sales Invoice' data = json.dumps({ 'ewbNo': eway_bill, 'cancelRsnCode': reason, From 5c74cdd0d2c3a448e63742eae8f702474e9d6ceb Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Dec 2020 16:40:23 +0530 Subject: [PATCH 099/115] fix: adjust qr code image size --- .../print_format/gst_e_invoice/gst_e_invoice.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html index 9827e00b71b..ec9be9aa64e 100644 --- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -24,7 +24,7 @@ {% endif %}
    1. Transaction Details
    -
    +
    {{ einvoice.Irn }}
    @@ -50,8 +50,12 @@
    {{ einvoice.DocDtls.No }}
    -
    - +
    + QRCode Image
    From cde452add7598e35d9933ca8107fdcae711f8e61 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Dec 2020 16:51:25 +0530 Subject: [PATCH 100/115] feat: show e-invoice preview before IRN generation --- erpnext/regional/india/e_invoice/einvoice.js | 75 +++++++++++++++++++- erpnext/regional/india/e_invoice/utils.py | 35 +++++---- 2 files changed, 95 insertions(+), 15 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index ff5cef0443e..7466de1ae19 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -22,11 +22,14 @@ erpnext.setup_einvoice_actions = (doctype) => { if (docstatus == 0 && !irn && !__unsaved) { const action = () => { frappe.call({ - method: 'erpnext.regional.india.e_invoice.utils.generate_irn', + method: 'erpnext.regional.india.e_invoice.utils.get_einvoice', args: { doctype, docname: name }, freeze: true, - callback: () => frm.reload_doc() - }); + callback: (res) => { + const einvoice = res.message; + show_einvoice_preview(frm, einvoice); + } + }) }; add_custom_button(__("Generate IRN"), action); @@ -236,4 +239,70 @@ const get_ewaybill_fields = (frm) => { 'default': frm.doc.gst_vehicle_type } ]; +}; + +const request_irn_generation = (frm, dialog) => { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.utils.generate_irn', + args: { doctype: frm.doc.doctype, docname: frm.doc.name }, + freeze: true, + callback: () => frm.reload_doc() || dialog.hide(), + error: () => dialog.hide() + }); +} + +const get_preview_dialog = (frm, action) => { + return new frappe.ui.Dialog({ + title: __("E Invoice Preview"), + wide: 1, + fields: [ + { + "label": "Preview", + "fieldname": "preview_html", + "fieldtype": "HTML" + } + ], + primary_action: () => action(frm), + primary_action_label: __('Generate IRN') + }); +} + +const show_einvoice_preview = (frm, einvoice) => { + const preview_dialog = get_preview_dialog(frm, request_irn_generation); + + // initialize empty e-invoice fields + einvoice.Irn = einvoice.AckNo = einvoice.AckDate = ''; + frm.doc.signed_einvoice = JSON.stringify(einvoice); + + // initialize preview wrapper + const $preview_wrapper = preview_dialog.get_field("preview_html").$wrapper; + $preview_wrapper.html( + `
    + +
    +
    ` + ); + + frappe.call({ + method: "frappe.www.printview.get_html_and_style", + args: { + doc: frm.doc, + print_format: "GST E-Invoice", + no_letterhead: 1 + }, + callback: function (r) { + if (!r.exc) { + $preview_wrapper.find(".print-format").html(r.message.html); + const style = ` + .print-format { font-size: 7.0pt; box-shadow: 0px 0px 5px rgba(0,0,0,0.2); padding: 0.30in; min-height: 80vh; } + .print-preview { min-height: 0px; } + .modal-dialog { width: 650px; } + ` + frappe.dom.set_style(style, "custom-print-style"); + preview_dialog.show(); + } + } + }); }; \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 532e0955bc2..ed7187846c7 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -94,7 +94,9 @@ def get_party_details(address_name): address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno')) address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt')) email_id = address.get('email_id') - phone = address.get('phone').replace(" ", "")[-10:] # get last 10 digit + phone = address.get('phone') + # get last 10 digit + phone = phone.replace(" ", "")[-10:] if phone else '' if state_code == 97: pincode = 999999 @@ -179,7 +181,7 @@ def get_item_list(invoice): item.cgst_amount += abs(item_tax_detail[1]) item.total_value = abs( - item.base_amount + item.igst_amount + item.sgst_amount + + item.taxable_value + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges ) einv_item = item_schema.format(item=item) @@ -197,8 +199,8 @@ def get_value_details(invoice): # discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0) disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') - value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else invoice.base_rounded_total - value_details.grand_total = abs(invoice.grand_total) if disable_rounded else invoice.rounded_total + value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else abs(invoice.base_rounded_total) + value_details.grand_total = abs(invoice.grand_total) if disable_rounded else abs(invoice.rounded_total) value_details.total_cgst_amt = 0 value_details.total_sgst_amt = 0 value_details.total_igst_amt = 0 @@ -301,12 +303,16 @@ def make_einvoice(invoice): errors = validate_einvoice(validations, einvoice) if errors: message = "\n".join([ - "E Invoice: ", json.dumps(einvoice, default=str, indent=4), + "E Invoice: ", json.dumps(einvoice, indent=4), "-" * 50, - "Errors: ", json.dumps(errors, default=str, indent=4) + "Errors: ", json.dumps(errors, indent=4) ]) frappe.log_error(title="E Invoice Validation Failed", message=message) - frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1) + if len(errors) > 1: + li = ['
  • '+ d +'
  • ' for d in errors] + frappe.throw("
      {}
    ".format(''.join(li)), title=_('E Invoice Validation Failed')) + else: + frappe.throw(errors[0], title=_('E Invoice Validation Failed')) return einvoice @@ -396,7 +402,7 @@ class GSPConnector(): "reference_invoice": self.invoice.name if self.invoice else None, "url": url, "headers": json.dumps(headers, indent=4) if headers else None, - "data": json.dumps(data, indent=4) if data else None, + "data": json.dumps(data, indent=4) if isinstance(data, dict) else data, "response": json.dumps(res, indent=4) if res else None }) request_log.insert(ignore_permissions=True) @@ -463,7 +469,7 @@ class GSPConnector(): def generate_irn(self): headers = self.get_headers() einvoice = make_einvoice(self.invoice) - data = json.dumps(einvoice) + data = json.dumps(einvoice, indent=4) try: res = self.make_request('post', self.generate_irn_url, headers, data) @@ -516,7 +522,7 @@ class GSPConnector(): 'Irn': irn, 'Cnlrsn': reason, 'Cnlrem': remark - }) + }, indent=4) try: res = self.make_request('post', self.cancel_irn_url, headers, data) @@ -555,7 +561,7 @@ class GSPConnector(): 'TrnDocNo': eway_bill_details.document_name, 'VehNo': eway_bill_details.vehicle_no, 'VehType': eway_bill_details.vehicle_type - }) + }, indent=4) try: res = self.make_request('post', self.generate_ewaybill_url, headers, data) @@ -587,7 +593,7 @@ class GSPConnector(): 'ewbNo': eway_bill, 'cancelRsnCode': reason, 'cancelRmrk': remark - }) + }, indent=4) try: res = self.make_request('post', self.cancel_ewaybill_url, headers, data) @@ -681,6 +687,11 @@ class GSPConnector(): self.invoice.flags.ignore_validate = True self.invoice.save() +@frappe.whitelist() +def get_einvoice(doctype, docname): + invoice = frappe.get_doc(doctype, docname) + return make_einvoice(invoice) + @frappe.whitelist() def generate_irn(doctype, docname): gsp_connector = GSPConnector(doctype, docname) From d67affdfae162adecd56b32d0fc5338c08383740 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Dec 2020 17:15:36 +0530 Subject: [PATCH 101/115] fix: minor ux issues --- erpnext/regional/india/e_invoice/einvoice.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 7466de1ae19..36ebb0e8baf 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -253,7 +253,7 @@ const request_irn_generation = (frm, dialog) => { const get_preview_dialog = (frm, action) => { return new frappe.ui.Dialog({ - title: __("E Invoice Preview"), + title: __("Preview"), wide: 1, fields: [ { @@ -271,7 +271,7 @@ const show_einvoice_preview = (frm, einvoice) => { const preview_dialog = get_preview_dialog(frm, request_irn_generation); // initialize empty e-invoice fields - einvoice.Irn = einvoice.AckNo = einvoice.AckDate = ''; + einvoice.Irn = einvoice.AckNo = ''; einvoice.AckDate = frappe.datetime.nowdate(); frm.doc.signed_einvoice = JSON.stringify(einvoice); // initialize preview wrapper @@ -296,9 +296,9 @@ const show_einvoice_preview = (frm, einvoice) => { if (!r.exc) { $preview_wrapper.find(".print-format").html(r.message.html); const style = ` - .print-format { font-size: 7.0pt; box-shadow: 0px 0px 5px rgba(0,0,0,0.2); padding: 0.30in; min-height: 80vh; } + .print-format { box-shadow: 0px 0px 5px rgba(0,0,0,0.2); padding: 0.30in; min-height: 80vh; } .print-preview { min-height: 0px; } - .modal-dialog { width: 650px; } + .modal-dialog { width: 720px; } ` frappe.dom.set_style(style, "custom-print-style"); preview_dialog.show(); From e6a07d5508017f662e971c36c02ac149597e2418 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Dec 2020 17:31:31 +0530 Subject: [PATCH 102/115] fix: dialog is undefined --- erpnext/regional/india/e_invoice/einvoice.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 36ebb0e8baf..916d436e3f9 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -246,13 +246,12 @@ const request_irn_generation = (frm, dialog) => { method: 'erpnext.regional.india.e_invoice.utils.generate_irn', args: { doctype: frm.doc.doctype, docname: frm.doc.name }, freeze: true, - callback: () => frm.reload_doc() || dialog.hide(), - error: () => dialog.hide() + callback: () => frm.reload_doc() }); } const get_preview_dialog = (frm, action) => { - return new frappe.ui.Dialog({ + const dialog = new frappe.ui.Dialog({ title: __("Preview"), wide: 1, fields: [ @@ -262,16 +261,17 @@ const get_preview_dialog = (frm, action) => { "fieldtype": "HTML" } ], - primary_action: () => action(frm), + primary_action: () => action(frm) || dialog.hide(), primary_action_label: __('Generate IRN') }); + return dialog; } const show_einvoice_preview = (frm, einvoice) => { const preview_dialog = get_preview_dialog(frm, request_irn_generation); - // initialize empty e-invoice fields - einvoice.Irn = einvoice.AckNo = ''; einvoice.AckDate = frappe.datetime.nowdate(); + // initialize e-invoice fields + einvoice["Irn"] = einvoice["AckNo"] = ''; einvoice["AckDt"] = frappe.datetime.nowdate(); frm.doc.signed_einvoice = JSON.stringify(einvoice); // initialize preview wrapper From 695e5519721c2c142d6e6f5d38943f02031cc91a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Dec 2020 18:59:01 +0530 Subject: [PATCH 103/115] fix: error handling --- erpnext/regional/india/e_invoice/utils.py | 80 ++++++++++++++++------- 1 file changed, 55 insertions(+), 25 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index ed7187846c7..dc932a15e24 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -308,14 +308,17 @@ def make_einvoice(invoice): "Errors: ", json.dumps(errors, indent=4) ]) frappe.log_error(title="E Invoice Validation Failed", message=message) - if len(errors) > 1: - li = ['
  • '+ d +'
  • ' for d in errors] - frappe.throw("
      {}
    ".format(''.join(li)), title=_('E Invoice Validation Failed')) - else: - frappe.throw(errors[0], title=_('E Invoice Validation Failed')) + throw_error_list(errors, _('E Invoice Validation Failed')) return einvoice +def throw_error_list(errors, title): + if len(errors) > 1: + li = ['
  • '+ d +'
  • ' for d in errors] + frappe.throw("
      {}
    ".format(''.join(li)), title=title) + else: + frappe.throw(errors[0], title=title) + def validate_einvoice(validations, einvoice, errors=[]): for fieldname, field_validation in validations.items(): value = einvoice.get(fieldname, None) @@ -406,6 +409,7 @@ class GSPConnector(): "response": json.dumps(res, indent=4) if res else None }) request_log.insert(ignore_permissions=True) + frappe.db.commit() def fetch_auth_token(self): headers = { @@ -487,11 +491,11 @@ class GSPConnector(): Contact ERPNext support to resolve the issue.') else: - self.log_error(res) raise RequestFailed except RequestFailed: - self.raise_error() + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) except Exception: self.log_error(data) @@ -506,11 +510,11 @@ class GSPConnector(): if res.get('success'): return res.get('result') else: - self.log_error(res) raise RequestFailed except RequestFailed: - self.raise_error() + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) except Exception: self.log_error() @@ -536,11 +540,11 @@ class GSPConnector(): self.update_invoice() else: - self.log_error(res) raise RequestFailed except RequestFailed: - self.raise_error() + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) except Exception: self.log_error(data) @@ -577,11 +581,11 @@ class GSPConnector(): self.update_invoice() else: - self.log_error(res) raise RequestFailed except RequestFailed: - self.raise_error() + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) except Exception: self.log_error(data) @@ -608,15 +612,37 @@ class GSPConnector(): self.update_invoice() else: - self.log_error(res) raise RequestFailed except RequestFailed: - self.raise_error() + errors = self.sanitize_error_message(res.get('message')) + self.raise_error(errors=errors) except Exception: self.log_error(data) self.raise_error(True) + + def sanitize_error_message(self, message): + ''' + message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, + 3095 : Supplier GSTIN is inactive' + we search for string between ':' to extract error messages + errors = [ + ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ', + ': Test' + ] + then we trim down the message by looping over errors + ''' + errors = re.findall(': [^:]+', message) + for idx, e in enumerate(errors): + # remove colons + errors[idx] = errors[idx].replace(':', '').strip() + # if not last + if idx != len(errors) - 1: + # remove last 7 chars eg: ', 3095 ' + errors[idx] = errors[idx][:-6] + + return errors def log_error(self, data={}): if not isinstance(data, dict): @@ -625,23 +651,27 @@ class GSPConnector(): seperator = "--" * 50 err_tb = traceback.format_exc() err_msg = str(sys.exc_info()[1]) - data = json.dumps(data, default=str, indent=4) + data = json.dumps(data, indent=4) message = "\n".join([ "Error", err_msg, seperator, "Data:", data, seperator, "Exception:", err_tb ]) - frappe.log_error(title="E Invoicing Error", message=message) + frappe.log_error(title=_('E Invoice Request Failed'), message=message) - def raise_error(self, raise_exception=False): - link_to_error_list = 'Error Log' - frappe.msgprint( - _('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list), - title=_('E Invoice Request Failed'), - raise_exception=raise_exception, - indicator='red' - ) + def raise_error(self, raise_exception=False, errors=[]): + title = _('E Invoice Request Failed') + if errors: + throw_error_list(errors, title) + else: + link_to_error_list = 'Error Log' + frappe.msgprint( + _('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list), + title=title, + raise_exception=raise_exception, + indicator='red' + ) def set_einvoice_data(self, res): enc_signed_invoice = res.get('SignedInvoice') From 9daf4418eab9bf88d7178c2ab183cc35329e4918 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Dec 2020 19:14:43 +0530 Subject: [PATCH 104/115] feat: add docs link to e invoice settings --- .../doctype/e_invoice_settings/e_invoice_settings.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js index 9ea43e70150..cc2d9f06d2d 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js @@ -1,4 +1,11 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('E Invoice Settings', {}); +frappe.ui.form.on('E Invoice Settings', { + refresh(frm) { + const docs_link = 'https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing'; + frm.dashboard.set_headline( + __("Read {0} for more information on E Invoicing features.", [`documentation`]) + ); + } +}); From c28379075a2ef8dd2bdcb56a7c61e18be6a24693 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Dec 2020 18:41:40 +0530 Subject: [PATCH 105/115] fix: remove test supplier pincode --- erpnext/regional/india/e_invoice/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index dc932a15e24..c6c1a921d71 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -264,7 +264,6 @@ def make_einvoice(invoice): doc_details = get_doc_details(invoice) value_details = get_value_details(invoice) seller_details = get_party_details(invoice.company_address) - seller_details.update({ 'pincode': 193502 }) if invoice.gst_category == 'Overseas': buyer_details = get_overseas_address_details(invoice.customer_address) From 6db07c027829c1f51917955271a93494be102e5b Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 22 Dec 2020 15:55:50 +0530 Subject: [PATCH 106/115] feat: multiple gstins for e invoicing --- .../e_invoice_settings.json | 68 +++---------------- .../e_invoice_settings/e_invoice_settings.py | 9 ++- .../doctype/e_invoice_user/__init__.py | 0 .../e_invoice_user/e_invoice_user.json | 48 +++++++++++++ .../doctype/e_invoice_user/e_invoice_user.py | 10 +++ erpnext/regional/india/e_invoice/utils.py | 36 +++++++--- 6 files changed, 101 insertions(+), 70 deletions(-) create mode 100644 erpnext/regional/doctype/e_invoice_user/__init__.py create mode 100644 erpnext/regional/doctype/e_invoice_user/e_invoice_user.json create mode 100644 erpnext/regional/doctype/e_invoice_user/e_invoice_user.py diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json index 8816cbde3e4..4dcb22a54c7 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json @@ -7,16 +7,9 @@ "field_order": [ "enable", "section_break_2", - "client_id", - "client_secret", - "public_key", - "column_break_3", - "gstin", - "username", - "password", + "credentials", "auth_token", - "token_expiry", - "sek" + "token_expiry" ], "fields": [ { @@ -30,47 +23,6 @@ "fieldname": "section_break_2", "fieldtype": "Section Break" }, - { - "fieldname": "client_id", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Client ID", - "reqd": 1 - }, - { - "fieldname": "client_secret", - "fieldtype": "Data", - "label": "Client Secret", - "reqd": 1 - }, - { - "fieldname": "public_key", - "fieldtype": "Long Text", - "hidden": 1, - "read_only": 1 - }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "fieldname": "gstin", - "fieldtype": "Data", - "label": "GSTIN", - "reqd": 1 - }, - { - "fieldname": "username", - "fieldtype": "Data", - "label": "Username", - "reqd": 1 - }, - { - "fieldname": "password", - "fieldtype": "Password", - "label": "Password", - "reqd": 1 - }, { "fieldname": "auth_token", "fieldtype": "Data", @@ -78,23 +30,23 @@ "read_only": 1 }, { - "fieldname": "sek", - "fieldtype": "Data", - "hidden": 1, - "read_only": 1 - }, - { - "default": "0", "fieldname": "token_expiry", "fieldtype": "Datetime", "hidden": 1, "read_only": 1 + }, + { + "fieldname": "credentials", + "fieldtype": "Table", + "label": "Credentials", + "mandatory_depends_on": "enable", + "options": "E Invoice User" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-11-03 21:23:50.277305", + "modified": "2020-12-22 15:34:57.280044", "modified_by": "Administrator", "module": "Regional", "name": "E Invoice Settings", diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py index 2a4f00365f3..c24ad886ea1 100644 --- a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -1,9 +1,14 @@ # -*- coding: utf-8 -*- # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - from __future__ import unicode_literals + +import frappe +from frappe import _ from frappe.model.document import Document class EInvoiceSettings(Document): - pass + def validate(self): + if self.enable and not self.credentials: + frappe.throw(_('You must add atleast one credentials to be able to use E Invoicing.')) + diff --git a/erpnext/regional/doctype/e_invoice_user/__init__.py b/erpnext/regional/doctype/e_invoice_user/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json new file mode 100644 index 00000000000..dd9d99773a3 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json @@ -0,0 +1,48 @@ +{ + "actions": [], + "creation": "2020-12-22 15:02:46.229474", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "gstin", + "username", + "password" + ], + "fields": [ + { + "fieldname": "gstin", + "fieldtype": "Data", + "in_list_view": 1, + "label": "GSTIN", + "reqd": 1 + }, + { + "fieldname": "username", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Username", + "reqd": 1 + }, + { + "fieldname": "password", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Password", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-12-22 15:10:53.466205", + "modified_by": "Administrator", + "module": "Regional", + "name": "E Invoice User", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py new file mode 100644 index 00000000000..056c54f069d --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class EInvoiceUser(Document): + pass diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index c6c1a921d71..4436c95438f 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -26,8 +26,8 @@ def validate_einvoice_fields(doc): if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction: return if doc.docstatus == 0 and doc._action == 'save': - if doc.irn: - frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) + # if doc.irn: + # frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) if len(doc.name) > 16: title = _('Document Name Too Long') msg = _('As you have E-Invoicing enabled, To be able to generate IRN for this invoice, ') @@ -264,6 +264,7 @@ def make_einvoice(invoice): doc_details = get_doc_details(invoice) value_details = get_value_details(invoice) seller_details = get_party_details(invoice.company_address) + seller_details.update({ 'pincode': '504273' }) if invoice.gst_category == 'Overseas': buyer_details = get_overseas_address_details(invoice.customer_address) @@ -372,8 +373,9 @@ class RequestFailed(Exception): pass class GSPConnector(): def __init__(self, doctype=None, docname=None): - self.credentials = frappe.get_cached_doc('E Invoice Settings') + self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None + self.credentials = self.get_credentials() self.base_url = 'https://gsp.adaequare.com/' self.authenticate_url = self.base_url + 'gsp/authenticate?grant_type=token' @@ -384,11 +386,25 @@ class GSPConnector(): self.cancel_ewaybill_url = self.base_url + '/test/enriched/ei/api/ewayapi' self.generate_ewaybill_url = self.base_url + 'test/enriched/ei/api/ewaybill' + def get_credentials(self): + if self.invoice: + gstin = self.get_seller_gstin() + credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin) + else: + credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None + return credentials + + def get_seller_gstin(self): + gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin') + if not gstin: + frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.')) + return gstin + def get_auth_token(self): - if time_diff_in_seconds(self.credentials.token_expiry, now_datetime()) < 150.0: + if time_diff_in_seconds(self.e_invoice_settings.token_expiry, now_datetime()) < 150.0: self.fetch_auth_token() - return self.credentials.auth_token + return self.e_invoice_settings.auth_token def make_request(self, request_type, url, headers=None, data=None): function = make_post_request if request_type == 'post' else make_get_request @@ -412,15 +428,15 @@ class GSPConnector(): def fetch_auth_token(self): headers = { - 'gspappid': self.credentials.client_id, - 'gspappsecret': self.credentials.client_secret + 'gspappid': frappe.conf.einvoice_client_id, + 'gspappsecret': frappe.conf.einvoice_client_secret } res = {} try: res = self.make_request('post', self.authenticate_url, headers) - self.credentials.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token')) - self.credentials.token_expiry = add_to_date(None, seconds=res.get('expires_in')) - self.credentials.save() + self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token')) + self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in')) + self.e_invoice_settings.save() except Exception: self.log_error(res) From 21461823f168f391166f6693e4ab3cd9e326743f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 23 Dec 2020 14:35:54 +0530 Subject: [PATCH 107/115] fix: uncomment test condition --- erpnext/regional/india/e_invoice/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 4436c95438f..896e3e72874 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -26,8 +26,8 @@ def validate_einvoice_fields(doc): if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction: return if doc.docstatus == 0 and doc._action == 'save': - # if doc.irn: - # frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) + if doc.irn: + frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) if len(doc.name) > 16: title = _('Document Name Too Long') msg = _('As you have E-Invoicing enabled, To be able to generate IRN for this invoice, ') From 1bbda81c9f1dd8c512f6670ccfaf689e520eff7c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 23 Dec 2020 15:19:22 +0530 Subject: [PATCH 108/115] fix: remove test pincode --- erpnext/regional/india/e_invoice/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 896e3e72874..36915206aab 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -264,7 +264,6 @@ def make_einvoice(invoice): doc_details = get_doc_details(invoice) value_details = get_value_details(invoice) seller_details = get_party_details(invoice.company_address) - seller_details.update({ 'pincode': '504273' }) if invoice.gst_category == 'Overseas': buyer_details = get_overseas_address_details(invoice.customer_address) From 81cbd0dc50a211e6c013bb33549c4e80280cc917 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 23 Dec 2020 15:21:50 +0530 Subject: [PATCH 109/115] fix: cannot cancel irn without submitting sales invoice --- erpnext/regional/india/e_invoice/einvoice.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 916d436e3f9..d794cd48184 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -19,7 +19,7 @@ erpnext.setup_einvoice_actions = (doctype) => { frm.set_df_property('ewaybill', 'read_only', 1); } - if (docstatus == 0 && !irn && !__unsaved) { + if (!irn && !__unsaved) { const action = () => { frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.get_einvoice', @@ -35,7 +35,7 @@ erpnext.setup_einvoice_actions = (doctype) => { add_custom_button(__("Generate IRN"), action); } - if (docstatus == 1 && irn && !irn_cancelled && !ewaybill) { + if (irn && !irn_cancelled && !ewaybill) { const fields = [ { "label": "Reason", @@ -108,7 +108,7 @@ erpnext.setup_einvoice_actions = (doctype) => { add_custom_button(__("Generate E-Way Bill"), action); } - if (docstatus == 1 && irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { + if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { const fields = [ { "label": "Reason", From 6d3fc0d02090e773edf0fdc573ad45d18f6515c8 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 23 Dec 2020 15:41:10 +0530 Subject: [PATCH 110/115] chore: code cleanup --- .../india/e_invoice/einv_template.json | 30 ++-- erpnext/regional/india/e_invoice/utils.py | 170 ++++++++++-------- 2 files changed, 111 insertions(+), 89 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json index bf6b855d2b8..e5751da5612 100644 --- a/erpnext/regional/india/e_invoice/einv_template.json +++ b/erpnext/regional/india/e_invoice/einv_template.json @@ -1,11 +1,11 @@ {{ "Version": "1.1", "TranDtls": {{ - "TaxSch": "{trans_details.tax_scheme}", - "SupTyp": "{trans_details.supply_type}", - "RegRev": "{trans_details.reverse_charge}", - "EcmGstin": "{trans_details.ecom_gstin}", - "IgstOnIntra": "{trans_details.igst_on_intra}" + "TaxSch": "{transaction_details.tax_scheme}", + "SupTyp": "{transaction_details.supply_type}", + "RegRev": "{transaction_details.reverse_charge}", + "EcmGstin": "{transaction_details.ecom_gstin}", + "IgstOnIntra": "{transaction_details.igst_on_intra}" }}, "DocDtls": {{ "Typ": "{doc_details.invoice_type}", @@ -59,16 +59,16 @@ {item_list} ], "ValDtls": {{ - "AssVal": "{value_details.base_net_total}", - "CgstVal": "{value_details.total_cgst_amt}", - "SgstVal": "{value_details.total_sgst_amt}", - "IgstVal": "{value_details.total_igst_amt}", - "CesVal": "{value_details.total_cess_amt}", - "Discount": "{value_details.invoice_discount_amt}", - "RndOffAmt": "{value_details.round_off}", - "OthChrg": "{value_details.total_other_charges}", - "TotInvVal": "{value_details.base_grand_total}", - "TotInvValFc": "{value_details.grand_total}" + "AssVal": "{invoice_value_details.base_net_total}", + "CgstVal": "{invoice_value_details.total_cgst_amt}", + "SgstVal": "{invoice_value_details.total_sgst_amt}", + "IgstVal": "{invoice_value_details.total_igst_amt}", + "CesVal": "{invoice_value_details.total_cess_amt}", + "Discount": "{invoice_value_details.invoice_discount_amt}", + "RndOffAmt": "{invoice_value_details.round_off}", + "OthChrg": "{invoice_value_details.total_other_charges}", + "TotInvVal": "{invoice_value_details.base_grand_total}", + "TotInvValFc": "{invoice_value_details.grand_total}" }}, "PayDtls": {{ "Nm": "{payment_details.payee_name}", diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 36915206aab..52f980676a5 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -29,15 +29,7 @@ def validate_einvoice_fields(doc): if doc.irn: frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) if len(doc.name) > 16: - title = _('Document Name Too Long') - msg = _('As you have E-Invoicing enabled, To be able to generate IRN for this invoice, ') - msg += _('document name {} exceed 16 letters. ').format(bold(_('should not'))) - msg += '

    ' - msg += _('You must {} your {} in order to have document name of {} length 16. ').format( - bold(_('modify')), bold(_('naming series')), bold(_('maximum')) - ) - msg += _('Please account for ammended document names too. ') - frappe.throw(msg, title=title) + raise_document_name_too_long_error() elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) @@ -45,12 +37,23 @@ def validate_einvoice_fields(doc): elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed')) +def raise_document_name_too_long_error(): + title = _('Document ID Too Long') + msg = _('As you have E-Invoicing enabled, to be able to generate IRN for this invoice, ') + msg += _('document id {} exceed 16 letters. ').format(bold(_('should not'))) + msg += '

    ' + msg += _('You must {} your {} in order to have document id of {} length 16. ').format( + bold(_('modify')), bold(_('naming series')), bold(_('maximum')) + ) + msg += _('Please account for ammended documents too. ') + frappe.throw(msg, title=title) + def read_json(name): file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name)) with open(file_path, 'r') as f: return cstr(f.read()) -def get_trans_details(invoice): +def get_transaction_details(invoice): supply_type = '' if invoice.gst_category == 'Registered Regular': supply_type = 'B2B' elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP' @@ -59,10 +62,8 @@ def get_trans_details(invoice): if not supply_type: rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export') - frappe.throw( - _('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export), - title=_('Invalid Supply Type') - ) + frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export), + title=_('Invalid Supply Type')) return frappe._dict(dict( tax_scheme='GST', @@ -97,7 +98,9 @@ def get_party_details(address_name): phone = address.get('phone') # get last 10 digit phone = phone.replace(" ", "")[-10:] if phone else '' + if state_code == 97: + # according to einvoice standard pincode = 999999 return frappe._dict(dict( @@ -136,90 +139,104 @@ def get_overseas_address_details(address_name): def get_item_list(invoice): item_list = [] - gst_accounts = get_gst_accounts(invoice.company) - gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] for d in invoice.items: - item_schema = read_json('einv_item_template') + einvoice_item_schema = read_json('einv_item_template') item = frappe._dict({}) item.update(d.as_dict()) + item.sr_no = d.idx - item.description = d.item_name - item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y' - item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None - item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None item.qty = abs(item.qty) + item.description = d.item_name + item.taxable_value = abs(item.base_net_amount) + item.discount_amount = abs(item.discount_amount * item.qty) item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_net_rate) item.gross_amount = abs(item.unit_rate * item.qty) - item.discount_amount = abs(item.discount_amount * item.qty) - item.taxable_value = abs(item.base_net_amount) - for attr in [ - 'tax_rate', 'cess_rate', 'cess_nadv_amount', - 'cgst_amount', 'sgst_amount', 'igst_amount', - 'cess_amount', 'cess_nadv_amount', 'other_charges' - ]: - item[attr] = 0 + item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None + item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None + item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y' - for t in invoice.taxes: - item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) - if t.account_head in gst_accounts_list: - if t.account_head in gst_accounts.cess_account: - if t.charge_type == 'On Item Quantity': - item.cess_nadv_amount += abs(item_tax_detail[1]) - else: - item.cess_rate += item_tax_detail[0] - item.cess_amount += abs(item_tax_detail[1]) - elif t.account_head in gst_accounts.igst_account: - item.tax_rate += item_tax_detail[0] - item.igst_amount += abs(item_tax_detail[1]) - elif t.account_head in gst_accounts.sgst_account: - item.tax_rate += item_tax_detail[0] - item.sgst_amount += abs(item_tax_detail[1]) - elif t.account_head in gst_accounts.cgst_account: - item.tax_rate += item_tax_detail[0] - item.cgst_amount += abs(item_tax_detail[1]) + item = update_item_taxes(invoice, item) item.total_value = abs( item.taxable_value + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges ) - einv_item = item_schema.format(item=item) + einv_item = einvoice_item_schema.format(item=item) item_list.append(einv_item) return ', '.join(item_list) -def get_value_details(invoice): +def update_item_taxes(invoice, item): gst_accounts = get_gst_accounts(invoice.company) gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] - value_details = frappe._dict(dict()) - value_details.base_net_total = abs(invoice.base_net_total) - value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount and invoice.discount_amount > 0 else 0 + for attr in [ + 'tax_rate', 'cess_rate', 'cess_nadv_amount', + 'cgst_amount', 'sgst_amount', 'igst_amount', + 'cess_amount', 'cess_nadv_amount', 'other_charges' + ]: + item[attr] = 0 + + for t in invoice.taxes: + item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) + if t.account_head in gst_accounts_list: + if t.account_head in gst_accounts.cess_account: + if t.charge_type == 'On Item Quantity': + item.cess_nadv_amount += abs(item_tax_detail[1]) + else: + item.cess_rate += item_tax_detail[0] + item.cess_amount += abs(item_tax_detail[1]) + elif t.account_head in gst_accounts.igst_account: + item.tax_rate += item_tax_detail[0] + item.igst_amount += abs(item_tax_detail[1]) + elif t.account_head in gst_accounts.sgst_account: + item.tax_rate += item_tax_detail[0] + item.sgst_amount += abs(item_tax_detail[1]) + elif t.account_head in gst_accounts.cgst_account: + item.tax_rate += item_tax_detail[0] + item.cgst_amount += abs(item_tax_detail[1]) + + return item + +def get_invoice_value_details(invoice): + invoice_value_details = frappe._dict(dict()) + invoice_value_details.base_net_total = abs(invoice.base_net_total) + invoice_value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount and invoice.discount_amount > 0 else 0 # discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off - value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0) + invoice_value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0) disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') - value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else abs(invoice.base_rounded_total) - value_details.grand_total = abs(invoice.grand_total) if disable_rounded else abs(invoice.rounded_total) - value_details.total_cgst_amt = 0 - value_details.total_sgst_amt = 0 - value_details.total_igst_amt = 0 - value_details.total_cess_amt = 0 - value_details.total_other_charges = 0 + invoice_value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else abs(invoice.base_rounded_total) + invoice_value_details.grand_total = abs(invoice.grand_total) if disable_rounded else abs(invoice.rounded_total) + + invoice_value_details = update_invoice_taxes(invoice, invoice_value_details) + + return invoice_value_details + +def update_invoice_taxes(invoice, invoice_value_details): + gst_accounts = get_gst_accounts(invoice.company) + gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] + + invoice_value_details.total_cgst_amt = 0 + invoice_value_details.total_sgst_amt = 0 + invoice_value_details.total_igst_amt = 0 + invoice_value_details.total_cess_amt = 0 + invoice_value_details.total_other_charges = 0 for t in invoice.taxes: if t.account_head in gst_accounts_list: if t.account_head in gst_accounts.cess_account: - value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) + invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) elif t.account_head in gst_accounts.igst_account: - value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount) + invoice_value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount) elif t.account_head in gst_accounts.sgst_account: - value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount) + invoice_value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount) elif t.account_head in gst_accounts.cgst_account: - value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount) + invoice_value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount) else: - value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) + invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) - return value_details + return invoice_value_details def get_payment_details(invoice): payee_name = invoice.company @@ -259,10 +276,10 @@ def get_eway_bill_details(invoice): def make_einvoice(invoice): schema = read_json('einv_template') - trans_details = get_trans_details(invoice) + transaction_details = get_transaction_details(invoice) item_list = get_item_list(invoice) doc_details = get_doc_details(invoice) - value_details = get_value_details(invoice) + invoice_value_details = get_invoice_value_details(invoice) seller_details = get_party_details(invoice.company_address) if invoice.gst_category == 'Overseas': @@ -290,9 +307,9 @@ def make_einvoice(invoice): dispatch_details = period_details = export_details = frappe._dict({}) einvoice = schema.format( - trans_details=trans_details, doc_details=doc_details, dispatch_details=dispatch_details, + transaction_details=transaction_details, doc_details=doc_details, dispatch_details=dispatch_details, seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details, - item_list=item_list, value_details=value_details, payment_details=payment_details, + item_list=item_list, invoice_value_details=invoice_value_details, payment_details=payment_details, period_details=period_details, prev_doc_details=prev_doc_details, export_details=export_details, eway_bill_details=eway_bill_details ) @@ -406,8 +423,11 @@ class GSPConnector(): return self.e_invoice_settings.auth_token def make_request(self, request_type, url, headers=None, data=None): - function = make_post_request if request_type == 'post' else make_get_request - res = function(url, headers=headers, data=data) + if request_type == 'post': + res = make_post_request(url, headers=headers, data=data) + else: + res = make_get_request(url, headers=headers, data=data) + self.log_request(url, headers, data, res) return res @@ -495,7 +515,8 @@ class GSPConnector(): self.set_einvoice_data(res.get('result')) elif '2150' in res.get('message'): - # IRN already generated + # IRN already generated but not updated in invoice + # Extract the IRN from the response description and fetch irn details irn = res.get('result')[0].get('Desc').get('Irn') irn_details = self.get_irn_details(irn) if irn_details: @@ -638,9 +659,10 @@ class GSPConnector(): def sanitize_error_message(self, message): ''' + On validation errors, response message looks something like this: message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 : Supplier GSTIN is inactive' - we search for string between ':' to extract error messages + we search for string between ':' to extract the error messages errors = [ ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ', ': Test' From c2a41371d26cf08926fcef88469cc5426d17ca0d Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Dec 2020 10:39:24 +0530 Subject: [PATCH 111/115] fix: sider issues --- erpnext/regional/india/e_invoice/einvoice.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index d794cd48184..5ecbb0ff502 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -4,10 +4,11 @@ erpnext.setup_einvoice_actions = (doctype) => { const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable"); const supply_type = frm.doc.gst_category; const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type); + const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin; - if (!einvoicing_enabled || !valid_supply_type) return; + if (!einvoicing_enabled || !valid_supply_type || company_transaction) return; - const { doctype, docstatus, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; + const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; const add_custom_button = (label, action) => { if (!frm.custom_buttons[label]) { @@ -29,7 +30,7 @@ erpnext.setup_einvoice_actions = (doctype) => { const einvoice = res.message; show_einvoice_preview(frm, einvoice); } - }) + }); }; add_custom_button(__("Generate IRN"), action); @@ -241,14 +242,14 @@ const get_ewaybill_fields = (frm) => { ]; }; -const request_irn_generation = (frm, dialog) => { +const request_irn_generation = (frm) => { frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.generate_irn', args: { doctype: frm.doc.doctype, docname: frm.doc.name }, freeze: true, callback: () => frm.reload_doc() }); -} +}; const get_preview_dialog = (frm, action) => { const dialog = new frappe.ui.Dialog({ @@ -265,7 +266,7 @@ const get_preview_dialog = (frm, action) => { primary_action_label: __('Generate IRN') }); return dialog; -} +}; const show_einvoice_preview = (frm, einvoice) => { const preview_dialog = get_preview_dialog(frm, request_irn_generation); @@ -298,8 +299,8 @@ const show_einvoice_preview = (frm, einvoice) => { const style = ` .print-format { box-shadow: 0px 0px 5px rgba(0,0,0,0.2); padding: 0.30in; min-height: 80vh; } .print-preview { min-height: 0px; } - .modal-dialog { width: 720px; } - ` + .modal-dialog { width: 720px; }`; + frappe.dom.set_style(style, "custom-print-style"); preview_dialog.show(); } From becae59af0b29ce1d8182dbe76d5ca5b51709825 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Dec 2020 21:12:06 +0530 Subject: [PATCH 112/115] fix: travis --- erpnext/regional/india/e_invoice/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 52f980676a5..1b746d5e10a 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -734,7 +734,7 @@ class GSPConnector(): _file = frappe.new_doc('File') _file.update({ - 'file_name': f'QRCode_{docname}.png', + 'file_name': 'QRCode_{}.png'.format(docname), 'attached_to_doctype': doctype, 'attached_to_name': docname, 'content': 'qrcode', From a683b14c642815c3e83aa04642dcca27f82cad6f Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 24 Dec 2020 21:10:16 +0530 Subject: [PATCH 113/115] fix: e invoice request log permissions --- .../e_invoice_request_log.json | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json index 088d5cd7627..5c1c79dc047 100644 --- a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json +++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json @@ -64,7 +64,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-08 14:50:33.820452", + "modified": "2020-12-24 21:09:38.882866", "modified_by": "Administrator", "module": "Regional", "name": "E Invoice Request Log", @@ -78,6 +78,24 @@ "report": 1, "role": "System Manager", "share": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1 } ], "sort_field": "modified", From d8193304431e70a51203c72b1ebe331b22ca46dd Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 28 Dec 2020 13:32:09 +0530 Subject: [PATCH 114/115] fix: invoice value set to zero if rounded total disabled --- erpnext/regional/india/e_invoice/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 1b746d5e10a..931bb40c6b9 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -206,9 +206,8 @@ def get_invoice_value_details(invoice): invoice_value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount and invoice.discount_amount > 0 else 0 # discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off invoice_value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0) - disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') - invoice_value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else abs(invoice.base_rounded_total) - invoice_value_details.grand_total = abs(invoice.grand_total) if disable_rounded else abs(invoice.rounded_total) + invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total) + invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total) invoice_value_details = update_invoice_taxes(invoice, invoice_value_details) From d94a97b6ad5547444768e30eefc5ac97c32b9852 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Dec 2020 13:35:04 +0530 Subject: [PATCH 115/115] fix: unit rate & gross amount calculation --- erpnext/regional/india/e_invoice/utils.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 931bb40c6b9..864ca0ea3d1 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -146,12 +146,12 @@ def get_item_list(invoice): item.update(d.as_dict()) item.sr_no = d.idx - item.qty = abs(item.qty) - item.description = d.item_name - item.taxable_value = abs(item.base_net_amount) item.discount_amount = abs(item.discount_amount * item.qty) - item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_net_rate) - item.gross_amount = abs(item.unit_rate * item.qty) + item.description = d.item_name + item.qty = abs(item.qty) + item.unit_rate = abs(item.base_net_amount / item.qty) + item.gross_amount = abs(item.base_net_amount) + item.taxable_value = abs(item.base_net_amount) item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None @@ -364,7 +364,8 @@ def validate_einvoice(validations, einvoice, errors=[]): einvoice[fieldname] = str(value) elif value_type == 'number': is_integer = '.' not in str(field_validation.get('maximum')) - einvoice[fieldname] = flt(value, 2) if not is_integer else cint(value) + precision = 3 if '.999' in str(field_validation.get('maximum')) else 2 + einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value) value = einvoice[fieldname] max_length = field_validation.get('maxLength')