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/einvoice.js" %}
|
||||
|
||||
erpnext.setup_auto_gst_taxation('Sales Invoice');
|
||||
erpnext.setup_einvoice_actions('Sales Invoice')
|
||||
|
||||
frappe.ui.form.on("Sales Invoice", {
|
||||
setup: function(frm) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 += "<br>"
|
||||
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
|
||||
|
||||
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 = [
|
||||
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 = {
|
||||
|
||||
Reference in New Issue
Block a user