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 184f98bd2ef..2ff5dd70c0c 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 9f582de90e6..3c628345a59 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -108,8 +108,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: @@ -1423,3 +1429,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 1dc6b6b4170..f70533ea38b 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -360,7 +360,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 = {