feat: Generate & Cancel IRN from Sales Invoice
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
{% include "erpnext/regional/india/taxes.js" %}
|
{% include "erpnext/regional/india/taxes.js" %}
|
||||||
|
{% include "erpnext/regional/india/einvoice.js" %}
|
||||||
|
|
||||||
erpnext.setup_auto_gst_taxation('Sales Invoice');
|
erpnext.setup_auto_gst_taxation('Sales Invoice');
|
||||||
|
erpnext.setup_einvoice_actions('Sales Invoice')
|
||||||
|
|
||||||
frappe.ui.form.on("Sales Invoice", {
|
frappe.ui.form.on("Sales Invoice", {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
|
|||||||
@@ -225,9 +225,9 @@ class SalesInvoice(SellingController):
|
|||||||
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
||||||
|
|
||||||
def before_cancel(self):
|
def before_cancel(self):
|
||||||
|
super(SalesInvoice, self).before_cancel()
|
||||||
self.update_time_sheet(None)
|
self.update_time_sheet(None)
|
||||||
|
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
super(SalesInvoice, self).on_cancel()
|
super(SalesInvoice, self).on_cancel()
|
||||||
|
|
||||||
|
|||||||
@@ -108,8 +108,14 @@ class AccountsController(TransactionBase):
|
|||||||
self.validate_deferred_start_and_end_date()
|
self.validate_deferred_start_and_end_date()
|
||||||
|
|
||||||
validate_regional(self)
|
validate_regional(self)
|
||||||
|
|
||||||
|
validate_einvoice_fields(self)
|
||||||
|
|
||||||
if self.doctype != 'Material Request':
|
if self.doctype != 'Material Request':
|
||||||
apply_pricing_rule_on_transaction(self)
|
apply_pricing_rule_on_transaction(self)
|
||||||
|
|
||||||
|
def before_cancel(self):
|
||||||
|
validate_einvoice_fields(self)
|
||||||
|
|
||||||
def validate_deferred_start_and_end_date(self):
|
def validate_deferred_start_and_end_date(self):
|
||||||
for d in self.items:
|
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
|
@erpnext.allow_regional
|
||||||
def validate_regional(doc):
|
def validate_regional(doc):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@erpnext.allow_regional
|
||||||
|
def validate_einvoice_fields(doc):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -360,7 +360,8 @@ regional_overrides = {
|
|||||||
'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details',
|
'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_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.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': {
|
'United Arab Emirates': {
|
||||||
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data'
|
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data'
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ class EInvoiceSettings(Document):
|
|||||||
if not self.public_key or self.has_value_changed('public_key_file'):
|
if not self.public_key or self.has_value_changed('public_key_file'):
|
||||||
self.public_key = self.read_key_file()
|
self.public_key = self.read_key_file()
|
||||||
|
|
||||||
make_property_setter("Sales Invoice", "irn", "reqd", self.enable, "Data")
|
|
||||||
|
|
||||||
def read_key_file(self):
|
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, attached_to_field='public_key_file'))
|
||||||
with open(key_file.get_full_path(), 'rb') as f:
|
with open(key_file.get_full_path(), 'rb') as f:
|
||||||
|
|||||||
@@ -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.utils.data import get_datetime, cstr, cint, format_date
|
||||||
from frappe.integrations.utils import make_post_request, make_get_request
|
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():
|
def get_einv_credentials():
|
||||||
return frappe.get_doc("E Invoice Settings")
|
return frappe.get_doc("E Invoice Settings")
|
||||||
|
|
||||||
@@ -66,7 +74,7 @@ def get_header(creds):
|
|||||||
return headers
|
return headers
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def fetch_token(self):
|
def fetch_token():
|
||||||
einv_creds = get_einv_credentials()
|
einv_creds = get_einv_credentials()
|
||||||
|
|
||||||
endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth'
|
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)
|
sek = aes_decrypt(enc_sek, appkey)
|
||||||
return auth_token, token_expiry, sek
|
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):
|
def get_gstin_details(gstin):
|
||||||
einv_creds = get_einv_credentials()
|
einv_creds = get_einv_credentials()
|
||||||
|
|
||||||
@@ -115,19 +133,20 @@ def get_gstin_details(gstin):
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def generate_irn(invoice):
|
@frappe.whitelist()
|
||||||
einv_creds = get_einv_credentials()
|
def generate_irn(doctype, name):
|
||||||
|
|
||||||
endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice'
|
endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice'
|
||||||
|
einv_creds = get_einv_credentials()
|
||||||
headers = get_header(einv_creds)
|
headers = get_header(einv_creds)
|
||||||
|
|
||||||
|
invoice = frappe.get_doc(doctype, name)
|
||||||
e_invoice = make_e_invoice(invoice)
|
e_invoice = make_e_invoice(invoice)
|
||||||
|
|
||||||
enc_e_invoice_json = aes_encrypt(e_invoice, einv_creds.sek)
|
enc_e_invoice_json = aes_encrypt(e_invoice, einv_creds.sek)
|
||||||
payload = dict(Data=enc_e_invoice_json)
|
payload = dict(Data=enc_e_invoice_json)
|
||||||
|
|
||||||
res = make_post_request(endpoint, headers=headers, data=json.dumps(payload))
|
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')
|
enc_json = res.get('Data')
|
||||||
json_str = aes_decrypt(enc_json, einv_creds.sek)
|
json_str = aes_decrypt(enc_json, einv_creds.sek)
|
||||||
@@ -135,6 +154,8 @@ def generate_irn(invoice):
|
|||||||
data = json.loads(json_str)
|
data = json.loads(json_str)
|
||||||
handle_irn_response(data)
|
handle_irn_response(data)
|
||||||
|
|
||||||
|
attach_signed_json(invoice, data['DecryptedSignedInvoice'])
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def get_irn_details(irn):
|
def get_irn_details(irn):
|
||||||
@@ -146,19 +167,20 @@ def get_irn_details(irn):
|
|||||||
res = make_get_request(endpoint, headers=headers)
|
res = make_get_request(endpoint, headers=headers)
|
||||||
handle_err_response(res)
|
handle_err_response(res)
|
||||||
|
|
||||||
enc_json = res.get('Data')
|
# enc_json = res.get('Data')
|
||||||
json_str = aes_decrypt(enc_json, einv_creds.sek)
|
# json_str = aes_decrypt(enc_json, einv_creds.sek)
|
||||||
|
|
||||||
data = json.loads(json_str)
|
# data = json.loads(json_str)
|
||||||
handle_irn_response(data)
|
# handle_irn_response(data)
|
||||||
|
|
||||||
return data
|
return res
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
def cancel_irn(irn, reason, remark=''):
|
def cancel_irn(irn, reason, remark=''):
|
||||||
einv_creds = get_einv_credentials()
|
einv_creds = get_einv_credentials()
|
||||||
|
|
||||||
endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/Cancel'
|
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))
|
cancel_e_inv = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark))
|
||||||
enc_json = aes_encrypt(cancel_e_inv, einv_creds.sek)
|
enc_json = aes_encrypt(cancel_e_inv, einv_creds.sek)
|
||||||
@@ -183,10 +205,18 @@ def handle_err_response(response):
|
|||||||
print(response)
|
print(response)
|
||||||
err_msg = ""
|
err_msg = ""
|
||||||
for d in err_details:
|
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 += d.get('ErrorMessage')
|
||||||
err_msg += "<br>"
|
err_msg += "<br>"
|
||||||
frappe.throw(_(err_msg), title=_('API Request Failed'))
|
frappe.throw(_(err_msg), title=_('API Request Failed'))
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
def read_json(name):
|
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:
|
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(
|
gstin, address_line1, address_line2, phone, email_id = frappe.db.get_value(
|
||||||
"Address", party_address, ["gstin", "address_line1", "address_line2", "phone", "email_id"]
|
"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')
|
legal_name = gstin_details.get('LegalName')
|
||||||
trade_name = gstin_details.get('TradeName')
|
trade_name = gstin_details.get('TradeName')
|
||||||
location = gstin_details.get('AddrLoc')
|
location = gstin_details.get('AddrLoc')
|
||||||
@@ -405,9 +435,9 @@ def run_e_invoice_validations(validations, e_invoice):
|
|||||||
|
|
||||||
if isinstance(invoice_value, list):
|
if isinstance(invoice_value, list):
|
||||||
for d in invoice_value:
|
for d in invoice_value:
|
||||||
self.run_e_invoice_validations(properties, d)
|
run_e_invoice_validations(properties, d)
|
||||||
else:
|
else:
|
||||||
self.run_e_invoice_validations(properties, invoice_value)
|
run_e_invoice_validations(properties, invoice_value)
|
||||||
if not invoice_value:
|
if not invoice_value:
|
||||||
e_invoice.pop(field, None)
|
e_invoice.pop(field, None)
|
||||||
continue
|
continue
|
||||||
|
|||||||
59
erpnext/regional/india/einvoice.js
Normal file
59
erpnext/regional/india/einvoice.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -377,7 +377,10 @@ def make_custom_fields(update=True):
|
|||||||
]
|
]
|
||||||
|
|
||||||
si_einvoice_fields = [
|
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 = {
|
custom_fields = {
|
||||||
|
|||||||
Reference in New Issue
Block a user