From 154c433a869bcd31578836b7555023baa28d390c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 10 Oct 2020 19:43:25 +0530 Subject: [PATCH] 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 = {