feat: Generate & Cancel IRN from Sales Invoice

This commit is contained in:
Saqib Ansari
2020-09-29 22:30:02 +05:30
committed by Saurabh
parent fe054511c8
commit b12d08a85b
8 changed files with 122 additions and 19 deletions

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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

View File

@@ -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'

View File

@@ -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:

View File

@@ -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

View 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();
}
)
}
}
})
}

View File

@@ -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 = {