From acd5929ac343ba7994d087464f4e4860b3f68642 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 27 Oct 2020 20:37:20 +0530 Subject: [PATCH] feat: e invoicing * feat: e-invoice * fix: validations * fix: add permissions on regional setup * feat: add patch * fix: validate document name * fix: return date * fix: credit note einvoice * fix: validations * chore: remove extras * fix: error logging * fix: e_invoice module not found * fix: add missing package * fix: travis --- .../doctype/sales_invoice/regional/india.js | 6 +- .../doctype/sales_invoice/sales_invoice.py | 2 +- .../print_format/gst_e_invoice/__init__.py | 0 .../gst_e_invoice/gst_e_invoice.html | 147 ++++ .../gst_e_invoice/gst_e_invoice.json | 24 + erpnext/controllers/accounts_controller.py | 10 + erpnext/hooks.py | 3 +- erpnext/patches.txt | 3 +- .../patches/v12_0/setup_einvoice_fields.py | 31 + .../doctype/e_invoice_settings/__init__.py | 0 .../e_invoice_settings/e_invoice_settings.js | 26 + .../e_invoice_settings.json | 120 +++ .../e_invoice_settings/e_invoice_settings.py | 26 + .../test_e_invoice_settings.py | 10 + erpnext/regional/india/e_invoice/__init__.py | 0 .../india/e_invoice/einv_item_template.json | 26 + .../india/e_invoice/einv_template.json | 109 +++ .../india/e_invoice/einv_validation.json | 830 ++++++++++++++++++ erpnext/regional/india/e_invoice/einvoice.js | 96 ++ erpnext/regional/india/e_invoice/utils.py | 626 +++++++++++++ erpnext/regional/india/setup.py | 26 +- requirements.txt | 1 + 22 files changed, 2112 insertions(+), 10 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 create mode 100644 erpnext/patches/v12_0/setup_einvoice_fields.py create mode 100644 erpnext/regional/doctype/e_invoice_settings/__init__.py create mode 100644 erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js create mode 100644 erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json create mode 100644 erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py create mode 100644 erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py create mode 100644 erpnext/regional/india/e_invoice/__init__.py create mode 100644 erpnext/regional/india/e_invoice/einv_item_template.json create mode 100644 erpnext/regional/india/e_invoice/einv_template.json create mode 100644 erpnext/regional/india/e_invoice/einv_validation.json create mode 100644 erpnext/regional/india/e_invoice/einvoice.js create mode 100644 erpnext/regional/india/e_invoice/utils.py diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js index 1ed4b92e7a4..f54bce8aac7 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/e_invoice/einvoice.js" %} erpnext.setup_auto_gst_taxation('Sales Invoice'); +erpnext.setup_einvoice_actions('Sales Invoice') frappe.ui.form.on("Sales Invoice", { setup: function(frm) { @@ -46,8 +48,6 @@ frappe.ui.form.on("Sales Invoice", { }, __("Create")); } - }, + } }); - - diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index d4d40653e77..498f7097293 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/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..b4255038d39 --- /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..1001199a092 --- /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": 1, + "doc_type": "Sales Invoice", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "html": "", + "idx": 0, + "line_breaks": 1, + "modified": "2020-10-23 19:54:40.634936", + "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/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 31045a97671..908349fe611 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -106,8 +106,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: @@ -1408,3 +1414,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 \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 5270e7beea2..81fad7df4ad 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -357,7 +357,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/patches.txt b/erpnext/patches.txt index afb6db35f27..b5f13d11962 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -677,4 +677,5 @@ erpnext.patches.v12_0.set_multi_uom_in_rfq erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v12_0.update_leave_application_status -erpnext.patches.v12_0.update_payment_entry_status \ No newline at end of file +erpnext.patches.v12_0.update_payment_entry_status +erpnext.patches.v12_0.setup_einvoice_fields \ No newline at end of file diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py new file mode 100644 index 00000000000..e230eb0bf75 --- /dev/null +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -0,0 +1,31 @@ +from __future__ import unicode_literals +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +from erpnext.regional.india.setup import add_permissions, add_print_formats + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + custom_fields = { + 'Sales Invoice': [ + 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='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) + ] + } + create_custom_fields(custom_fields, update=True) + add_permissions() + add_print_formats() \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_settings/__init__.py b/erpnext/regional/doctype/e_invoice_settings/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js new file mode 100644 index 00000000000..e9fb622b6b6 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js @@ -0,0 +1,26 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('E Invoice Settings', { + refresh: function(frm) { + if (!frm.doc.enable) return; + frm.trigger("show_fetch_token_btn"); + }, + + show_fetch_token_btn(frm) { + const { token_expiry } = frm.doc; + const now = frappe.datetime.now_datetime(); + const expiry_in_mins = moment(token_expiry).diff(now, "minute"); + if (expiry_in_mins <= 1 || !token_expiry) { + frm.add_custom_button(__("Fetch Token"), + () => { + frm.call({ + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.fetch_token', + freeze: true, + callback: () => frm.refresh() + }); + } + ); + } + } +}); diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json new file mode 100644 index 00000000000..d9a1c4976a9 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json @@ -0,0 +1,120 @@ +{ + "actions": [], + "creation": "2020-09-24 16:23:16.235722", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enable", + "section_break_2", + "client_id", + "client_secret", + "public_key_file", + "public_key", + "column_break_3", + "gstin", + "username", + "password", + "auto_refresh_token", + "auth_token", + "token_expiry", + "sek" + ], + "fields": [ + { + "default": "0", + "fieldname": "enable", + "fieldtype": "Check", + "label": "Enable" + }, + { + "depends_on": "enable", + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "client_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Client ID", + "reqd": 1 + }, + { + "fieldname": "client_secret", + "fieldtype": "Data", + "label": "Client Secret", + "reqd": 1 + }, + { + "fieldname": "public_key_file", + "fieldtype": "Attach", + "label": "Public Key", + "reqd": 1 + }, + { + "fieldname": "public_key", + "fieldtype": "Long Text", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "gstin", + "fieldtype": "Data", + "label": "GSTIN", + "reqd": 1 + }, + { + "fieldname": "username", + "fieldtype": "Data", + "label": "Username", + "reqd": 1 + }, + { + "fieldname": "password", + "fieldtype": "Password", + "label": "Password", + "reqd": 1 + }, + { + "default": "0", + "description": "Token will be automatically refreshed 10 mins before expiry", + "fieldname": "auto_refresh_token", + "fieldtype": "Check", + "label": "Auto Refresh Token" + }, + { + "fieldname": "auth_token", + "fieldtype": "Data", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "token_expiry", + "fieldtype": "Datetime", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "sek", + "fieldtype": "Data", + "hidden": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2020-10-23 19:55:11.417161", + "modified_by": "Administrator", + "module": "Regional", + "name": "E Invoice Settings", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py new file mode 100644 index 00000000000..e90d07edbd6 --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils.data import cstr +from frappe.model.document import Document +from frappe.custom.doctype.property_setter.property_setter import make_property_setter + +class EInvoiceSettings(Document): + def validate(self): + mandatory_fields = ['client_id', 'client_secret', 'gstin', 'username', 'password', 'public_key_file'] + for d in mandatory_fields: + if not self.get(d): + frappe.throw(_("{} is required").format(frappe.unscrub(d)), title=_("Missing Values")) + + def before_save(self): + if not self.public_key or self.has_value_changed('public_key_file'): + self.public_key = self.read_key_file() + + def read_key_file(self): + key_file = frappe.get_doc('File', dict(attached_to_name=self.doctype)) + with open(key_file.get_full_path(), 'rb') as f: + return cstr(f.read()) \ No newline at end of file diff --git a/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py new file mode 100644 index 00000000000..a11ce63ee6c --- /dev/null +++ b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestEInvoiceSettings(unittest.TestCase): + pass diff --git a/erpnext/regional/india/e_invoice/__init__.py b/erpnext/regional/india/e_invoice/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/india/e_invoice/einv_item_template.json b/erpnext/regional/india/e_invoice/einv_item_template.json new file mode 100644 index 00000000000..f87b0f15f3c --- /dev/null +++ b/erpnext/regional/india/e_invoice/einv_item_template.json @@ -0,0 +1,26 @@ +{{ + "SlNo": "{item.sr_no}", + "PrdDesc": "{item.description}", + "IsServc": "{item.is_service_item}", + "HsnCd": "{item.gst_hsn_code}", + "Barcde": "{item.barcode}", + "Unit": "{item.uom}", + "Qty": "{item.qty}", + "FreeQty": "{item.free_qty}", + "UnitPrice": "{item.unit_rate}", + "TotAmt": "{item.total_amount}", + "Discount": "{item.discount_amount}", + "AssAmt": "{item.base_amount}", + "PrdSlNo": "{item.serial_no}", + "GstRt": "{item.tax_rate}", + "IgstAmt": "{item.igst_amount}", + "CgstAmt": "{item.cgst_amount}", + "SgstAmt": "{item.sgst_amount}", + "CesRt": "{item.cess_rate}", + "CesAmt": "{item.cess_amount}", + "TotItemVal": "{item.total_value}", + "BchDtls": {{ + "Nm": "{item.batch_no}", + "ExpDt": "{item.batch_expiry_date}" + }} +}} \ 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 new file mode 100644 index 00000000000..46741ca0337 --- /dev/null +++ b/erpnext/regional/india/e_invoice/einv_template.json @@ -0,0 +1,109 @@ +{{ + "Version": "1.1", + "TranDtls": {{ + "TaxSch": "{trans_details.tax_scheme}", + "SupTyp": "{trans_details.supply_type}", + "RegRev": "{trans_details.reverse_charge}", + "EcmGstin": "{trans_details.ecom_gstin}", + "IgstOnIntra": "{trans_details.igst_on_intra}" + }}, + "DocDtls": {{ + "Typ": "{doc_details.invoice_type}", + "No": "{doc_details.invoice_name}", + "Dt": "{doc_details.invoice_date}" + }}, + "SellerDtls": {{ + "Gstin": "{seller_details.gstin}", + "LglNm": "{seller_details.legal_name}", + "TrdNm": "{seller_details.trade_name}", + "Loc": "{seller_details.location}", + "Pin": "{seller_details.pincode}", + "Stcd": "{seller_details.state_code}", + "Addr1": "{seller_details.address_line1}", + "Addr2": "{seller_details.address_line2}", + "Ph": "{seller_details.phone}", + "Em": "{seller_details.email}" + }}, + "BuyerDtls": {{ + "Gstin": "{buyer_details.gstin}", + "LglNm": "{buyer_details.legal_name}", + "TrdNm": "{buyer_details.trade_name}", + "Addr1": "{buyer_details.address_line1}", + "Addr2": "{buyer_details.address_line2}", + "Loc": "{buyer_details.location}", + "Pin": "{buyer_details.pincode}", + "Stcd": "{buyer_details.state_code}", + "Ph": "{buyer_details.phone}", + "Em": "{buyer_details.email}", + "Pos": "{buyer_details.place_of_supply}" + }}, + "DispDtls": {{ + "Nm": "{dispatch_details.company_name}", + "Addr1": "{dispatch_details.address_line1}", + "Addr2": "{dispatch_details.address_line2}", + "Loc": "{dispatch_details.location}", + "Pin": "{dispatch_details.pincode}", + "Stcd": "{dispatch_details.state_code}" + }}, + "ShipDtls": {{ + "Gstin": "{shipping_details.gstin}", + "LglNm": "{shipping_details.legal_name}", + "TrdNm": "{shipping_details.trader_name}", + "Addr1": "{shipping_details.address_line1}", + "Addr2": "{shipping_details.address_line2}", + "Loc": "{shipping_details.location}", + "Pin": "{shipping_details.pincode}", + "Stcd": "{shipping_details.state_code}" + }}, + "ItemList": [ + {item_list} + ], + "ValDtls": {{ + "AssVal": "{value_details.base_net_total}", + "CgstVal": "{value_details.total_cgst_amt}", + "SgstVal": "{value_details.total_sgst_amt}", + "IgstVal": "{value_details.total_igst_amt}", + "CesVal": "{value_details.total_cess_amt}", + "Discount": "{value_details.invoice_discount_amt}", + "RndOffAmt": "{value_details.round_off}", + "TotInvVal": "{value_details.base_grand_total}", + "TotInvValFc": "{value_details.grand_total}" + }}, + "PayDtls": {{ + "Nm": "{payment_details.payee_name}", + "AccDet": "{payment_details.account_no}", + "Mode": "{payment_details.mode_of_payment}", + "FinInsBr": "{payment_details.ifsc_code}", + "PayTerm": "{payment_details.terms}", + "PaidAmt": "{payment_details.paid_amount}", + "PaymtDue": "{payment_details.outstanding_amount}" + }}, + "RefDtls": {{ + "DocPerdDtls": {{ + "InvStDt": "{period_details.start_date}", + "InvEndDt": "{period_details.end_date}" + }}, + "PrecDocDtls": [{{ + "InvNo": "{prev_doc_details.invoice_name}", + "InvDt": "{prev_doc_details.invoice_date}" + }}] + }}, + "ExpDtls": {{ + "ShipBNo": "{export_details.bill_no}", + "ShipBDt": "{export_details.bill_date}", + "Port": "{export_details.port}", + "ForCur": "{export_details.foreign_curr_code}", + "CntCode": "{export_details.country_code}", + "ExpDuty": "{export_details.export_duty}" + }}, + "EwbDtls": {{ + "TransId": "{eway_bill_details.gstin}", + "TransName": "{eway_bill_details.name}", + "TransMode": "{eway_bill_details.mode_of_transport}", + "Distance": "{eway_bill_details.distance}", + "TransDocNo": "{eway_bill_details.document_name}", + "TransDocDt": "{eway_bill_details.document_date}", + "VehNo": "{eway_bill_details.vehicle_no}", + "VehType": "{eway_bill_details.vehicle_type}" + }} +}} \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json new file mode 100644 index 00000000000..3f0767b8be8 --- /dev/null +++ b/erpnext/regional/india/e_invoice/einv_validation.json @@ -0,0 +1,830 @@ +{ + "Version": { + "type": "string", + "minLength": 1, + "maxLength": 6 + }, + "Irn": { + "type": "string", + "minLength": 64, + "maxLength": 64 + }, + "TranDtls": { + "type": "object", + "properties": { + "TaxSch": { + "type": "string", + "minLength": 3, + "maxLength": 10, + "enum": ["GST"] + }, + "SupTyp": { + "type": "string", + "minLength": 3, + "maxLength": 10, + "enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"] + }, + "RegRev": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"] + }, + "EcmGstin": { + "type": "string", + "minLength": 15, + "maxLength": 15, + "pattern": "([0-9]{2}[0-9A-Z]{13})" + }, + "IgstOnIntra": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"] + } + }, + "required": ["TaxSch", "SupTyp"] + }, + "DocDtls": { + "type": "object", + "properties": { + "Typ": { + "type": "string", + "minLength": 3, + "maxLength": 3, + "enum": ["INV", "CRN", "DBN"] + }, + "No": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$", + "label": "Document Name", + "validationMsg": "Document name should not be starting with 0, / and -" + }, + "Dt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "validationMsg": "Document Date is invalid" + } + }, + "required": ["Typ", "No", "Dt"] + }, + "SellerDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "minLength": 15, + "maxLength": 15, + "pattern": "([0-9]{2}[0-9A-Z]{13})", + "validationMsg": "Seller GSTIN is invalid" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr1": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 50 + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999 + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2 + }, + "Ph": { + "type": "string", + "minLength": 6, + "maxLength": 12 + }, + "Em": { + "type": "string", + "minLength": 6, + "maxLength": 100 + } + }, + "required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "BuyerDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "minLength": 3, + "maxLength": 15, + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", + "validationMsg": "Buyer GSTIN is invalid" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Pos": { + "type": "string", + "minLength": 1, + "maxLength": 2 + }, + "Addr1": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999 + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2 + }, + "Ph": { + "type": "string", + "minLength": 6, + "maxLength": 12 + }, + "Em": { + "type": "string", + "minLength": 6, + "maxLength": 100 + } + }, + "required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"] + }, + "DispDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr1": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999 + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2 + } + }, + "required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "ShipDtls": { + "type": "object", + "properties": { + "Gstin": { + "type": "string", + "maxLength": 15, + "minLength": 3, + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", + "validationMsg": "Shipping Address GSTIN is invalid" + }, + "LglNm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "TrdNm": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr1": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Addr2": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Loc": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Pin": { + "type": "number", + "minimum": 100000, + "maximum": 999999 + }, + "Stcd": { + "type": "string", + "minLength": 1, + "maxLength": 2 + } + }, + "required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"] + }, + "ItemList": { + "type": "Array", + "properties": { + "SlNo": { + "type": "string", + "minLength": 1, + "maxLength": 6 + }, + "PrdDesc": { + "type": "string", + "minLength": 3, + "maxLength": 300 + }, + "IsServc": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["Y", "N"] + }, + "HsnCd": { + "type": "string", + "minLength": 4, + "maxLength": 8 + }, + "Barcde": { + "type": "string", + "minLength": 3, + "maxLength": 30 + }, + "Qty": { + "type": "number", + "minimum": 0, + "maximum": 9999999999.999 + }, + "FreeQty": { + "type": "number", + "minimum": 0, + "maximum": 9999999999.999 + }, + "Unit": { + "type": "string", + "minLength": 3, + "maxLength": 8 + }, + "UnitPrice": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.999 + }, + "TotAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "Discount": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "PreTaxVal": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "AssAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "GstRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999 + }, + "IgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "CgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "SgstAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "CesRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999 + }, + "CesAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "CesNonAdvlAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "StateCesRt": { + "type": "number", + "minimum": 0, + "maximum": 999.999 + }, + "StateCesAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "StateCesNonAdvlAmt": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "OthChrg": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "TotItemVal": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + }, + "OrdLineRef": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "OrgCntry": { + "type": "string", + "minLength": 2, + "maxLength": 2 + }, + "PrdSlNo": { + "type": "string", + "minLength": 1, + "maxLength": 20 + }, + "BchDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 3, + "maxLength": 20 + }, + "ExpDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "validationMsg": "Expiry Date is invalid" + }, + "WrDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "validationMsg": "Warranty Date is invalid" + } + }, + "required": ["Nm"] + }, + "AttribDtls": { + "type": "Array", + "Attribute": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "Val": { + "type": "string", + "minLength": 1, + "maxLength": 100 + } + } + } + } + }, + "required": [ + "SlNo", + "IsServc", + "HsnCd", + "UnitPrice", + "TotAmt", + "AssAmt", + "GstRt", + "TotItemVal" + ] + }, + "ValDtls": { + "type": "object", + "properties": { + "AssVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "CgstVal": { + "type": "number", + "maximum": 99999999999999.99, + "minimum": 0 + }, + "SgstVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "IgstVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "CesVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "StCesVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "Discount": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "OthChrg": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "RndOffAmt": { + "type": "number", + "minimum": 0, + "maximum": 99.99 + }, + "TotInvVal": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "TotInvValFc": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + } + }, + "required": ["AssVal", "TotInvVal"] + }, + "PayDtls": { + "type": "object", + "properties": { + "Nm": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "AccDet": { + "type": "string", + "minLength": 1, + "maxLength": 18 + }, + "Mode": { + "type": "string", + "minLength": 1, + "maxLength": 18 + }, + "FinInsBr": { + "type": "string", + "minLength": 1, + "maxLength": 11 + }, + "PayTerm": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "PayInstr": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "CrTrn": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "DirDr": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "CrDay": { + "type": "number", + "minimum": 0, + "maximum": 9999 + }, + "PaidAmt": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + }, + "PaymtDue": { + "type": "number", + "minimum": 0, + "maximum": 99999999999999.99 + } + } + }, + "RefDtls": { + "type": "object", + "properties": { + "InvRm": { + "type": "string", + "maxLength": 100, + "minLength": 3, + "pattern": "^[0-9A-Za-z/-]{3,100}$" + }, + "DocPerdDtls": { + "type": "object", + "properties": { + "InvStDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + }, + "InvEndDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + } + }, + "required": ["InvStDt", "InvEndDt"] + }, + "PrecDocDtls": { + "type": "object", + "properties": { + "InvNo": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$" + }, + "InvDt": { + "type": "string", + "maxLength": 10, + "minLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + }, + "OthRefNo": { + "type": "string", + "minLength": 1, + "maxLength": 20 + } + }, + "required": ["InvNo", "InvDt"] + }, + "ContrDtls": { + "type": "object", + "properties": { + "RecAdvRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "RecAdvDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + }, + "TendRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "ContrRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "ExtRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "ProjRefr": { + "type": "string", + "minLength": 1, + "maxLength": 20, + "pattern": "^([0-9A-Za-z/-]){1,20}$" + }, + "PORefr": { + "type": "string", + "minLength": 1, + "maxLength": 16, + "pattern": "^([0-9A-Za-z/-]){1,16}$" + }, + "PORefDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + } + } + } + }, + "required": ["InvStDt", "InvEndDt"] + }, + "AddlDocDtls": { + "type": "Array", + "AddlDocument": { + "type": "object", + "properties": { + "Url": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "Docs": { + "type": "string", + "minLength": 3, + "maxLength": 1000 + }, + "Info": { + "type": "string", + "minLength": 3, + "maxLength": 1000 + } + } + } + }, + "ExpDtls": { + "type": "object", + "properties": { + "ShipBNo": { + "type": "string", + "minLength": 1, + "maxLength": 20 + }, + "ShipBDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + }, + "Port": { + "type": "string", + "minLength": 2, + "maxLength": 10, + "pattern": "^[0-9A-Za-z]{2,10}$" + }, + "RefClm": { + "type": "string", + "minLength": 1, + "maxLength": 1 + }, + "ForCur": { + "type": "string", + "minLength": 3, + "maxLength": 16 + }, + "CntCode": { + "type": "string", + "minLength": 2, + "maxLength": 2 + }, + "ExpDuty": { + "type": "number", + "minimum": 0, + "maximum": 999999999999.99 + } + } + }, + "EwbDtls": { + "type": "object", + "properties": { + "TransId": { + "type": "string", + "minLength": 15, + "maxLength": 15 + }, + "TransName": { + "type": "string", + "minLength": 3, + "maxLength": 100 + }, + "TransMode": { + "type": "string", + "maxLength": 1, + "minLength": 1, + "enum": ["1", 2, 3, 4] + }, + "Distance": { + "type": "number", + "minimum": 1, + "maximum": 9999 + }, + "TransDocNo": { + "type": "string", + "minLength": 1, + "maxLength": 15, + "pattern": "^([0-9A-Z/-]){1,15}$" + }, + "TransDocDt": { + "type": "string", + "minLength": 10, + "maxLength": 10, + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + }, + "VehNo": { + "type": "string", + "minLength": 4, + "maxLength": 20 + }, + "VehType": { + "type": "string", + "minLength": 1, + "maxLength": 1, + "enum": ["O", "R"] + } + }, + "required": ["Distance"] + }, + "required": [ + "Version", + "TranDtls", + "DocDtls", + "SellerDtls", + "BuyerDtls", + "ItemList", + "ValDtls" + ] +} diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js new file mode 100644 index 00000000000..4627b96795f --- /dev/null +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -0,0 +1,96 @@ +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"); + const supply_type = frm.doc.gst_category; + const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type) + + if (!einvoicing_enabled || !valid_supply_type) return; + + const { docstatus, irn, irn_cancelled, doctype, name, __unsaved } = frm.doc; + + if (docstatus == 0 && !irn && !__unsaved) { + frm.add_custom_button( + "Download E-Invoice", + () => { + frappe.call({ + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.make_einvoice', + args: { doctype, name }, + freeze: true, + callback: (res) => { + if (!res.exc) { + const args = { + cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_einvoice', + einvoice: JSON.stringify([res.message]), + name: name + }; + open_url_post(frappe.request.url, args); + } + } + }) + }, "E-Invoicing"); + frm.add_custom_button( + "Upload Signed E-Invoice", + () => { + new frappe.ui.FileUploader({ + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_einvoice', + allow_multiple: 0, + doctype: doctype, + docname: name, + on_success: (attachment, r) => { + if (!r.exc) { + frm.reload_doc(); + } + } + }); + }, "E-Invoicing"); + } + if (docstatus == 1 && irn && !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 Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 + } + ], + primary_action: function() { + const data = d.get_values(); + const args = { + cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_cancel_einvoice', + irn: irn, reason: data.reason.split('-')[0], remark: data.remark, name: name + }; + open_url_post(frappe.request.url, args); + d.hide(); + }, + primary_action_label: __('Download JSON') + }); + d.show(); + }, "E-Invoicing"); + + frm.add_custom_button( + "Upload Cancel JSON", + () => { + new frappe.ui.FileUploader({ + method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_cancel_ack', + allow_multiple: 0, + doctype: doctype, + docname: name, + on_success: (attachment, r) => { + if (!r.exc) { + frm.reload_doc(); + } + } + }); + }, "E-Invoicing"); + } + } + }) +} \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py new file mode 100644 index 00000000000..f6ed33ddb91 --- /dev/null +++ b/erpnext/regional/india/e_invoice/utils.py @@ -0,0 +1,626 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import os +import re +import jwt +import json +import base64 +import frappe +from six import string_types +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 +from frappe import _, get_module_path, scrub, bold +from frappe.integrations.utils import make_post_request, make_get_request +from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply +from frappe.utils.data import get_datetime, cstr, cint, formatdate as format_date, flt, time_diff_in_seconds, now_datetime + +def validate_einvoice_fields(doc): + einvoicing_enabled = frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable') + invalid_doctype = doc.doctype not in ['Sales Invoice', 'Purchase Invoice'] + invalid_supply_type = doc.gst_category not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] + + if invalid_doctype or invalid_supply_type or not einvoicing_enabled: return + + if doc.docstatus == 0 and doc._action == 'save': + if doc.irn: + frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed')) + if len(doc.name) > 16: + title = _('Document Name Too Long') + msg = (_('As you have E-Invoicing enabled, To be able to generate IRN for this invoice, document name {} exceed 16 letters. ') + .format(bold(_('should not')))) + msg += '

' + msg += (_('You {} modify your {} in order to have document name of {} length of 16. ') + .format(bold(_('must')), bold(_('naming series')), bold(_('maximum')))) + frappe.throw(msg, title=title) + + elif 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=_('Cancel Not Allowed')) + +def get_credentials(): + doc = frappe.get_doc('E Invoice Settings') + if not doc.enable: + frappe.throw(_("To setup E Invoicing you need to enable E Invoice Settings first."), title=_("E Invoicing Disabled")) + + if not doc.token_expiry or time_diff_in_seconds(now_datetime(), doc.token_expiry) > 5.0: + fetch_token(doc) + doc.load_from_db() + + return doc + +def rsa_encrypt(msg, key): + if not (isinstance(msg, bytes) or isinstance(msg, bytearray)): + msg = str.encode(msg) + + rsa_pub_key = RSA.import_key(key) + cipher = PKCS1_v1_5.new(rsa_pub_key) + enc_msg = cipher.encrypt(msg) + b64_enc_msg = base64.b64encode(enc_msg) + return b64_enc_msg.decode() + +def aes_decrypt(enc_msg, key): + encode_as_b64 = True + if not (isinstance(key, bytes) or isinstance(key, bytearray)): + key = base64.b64decode(key) + encode_as_b64 = False + + cipher = AES.new(key, AES.MODE_ECB) + b64_enc_msg = base64.b64decode(enc_msg) + msg_bytes = cipher.decrypt(b64_enc_msg) + msg_bytes = unpad(msg_bytes, AES.block_size) # due to ECB/PKCS5Padding + if encode_as_b64: + msg_bytes = base64.b64encode(msg_bytes) + return msg_bytes.decode() + +def aes_encrypt(msg, key): + if not (isinstance(key, bytes) or isinstance(key, bytearray)): + key = base64.b64decode(key) + + cipher = AES.new(key, AES.MODE_ECB) + bytes_msg = str.encode(msg) + padded_bytes_msg = pad(bytes_msg, AES.block_size) + enc_msg = cipher.encrypt(padded_bytes_msg) + b64_enc_msg = base64.b64encode(enc_msg) + return b64_enc_msg.decode() + +def jwt_decrypt(token): + return jwt.decode(token, verify=False) + +def get_header(creds): + headers = { 'content-type': 'application/json' } + headers.update(dict(client_id=creds.client_id, client_secret=creds.client_secret, user_name=creds.username)) + headers.update(dict(Gstin=creds.gstin, AuthToken=creds.auth_token)) + return headers + +@frappe.whitelist() +def fetch_token(credentials=None): + if not credentials: + credentials = frappe.get_doc('E Invoice Settings') + + endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' + headers = { 'content-type': 'application/json' } + headers.update(dict(client_id=credentials.client_id, client_secret=credentials.client_secret)) + payload = dict(UserName=credentials.username, ForceRefreshAccessToken=bool(credentials.auto_refresh_token)) + + appkey = bytearray(os.urandom(32)) + enc_appkey = rsa_encrypt(appkey, credentials.public_key) + + password = credentials.get_password(fieldname='password') + enc_password = rsa_encrypt(password, credentials.public_key) + + payload.update(dict(Password=enc_password, AppKey=enc_appkey)) + + res = make_post_request(endpoint, headers=headers, data=json.dumps({ 'data': payload })) + handle_err_response(res) + + auth_token, token_expiry, sek = extract_token_and_sek(res, appkey) + + credentials.auth_token = auth_token + credentials.token_expiry = get_datetime(token_expiry) + credentials.sek = sek + credentials.save() + +def extract_token_and_sek(response, appkey): + data = response.get('Data') + auth_token = data.get('AuthToken') + token_expiry = data.get('TokenExpiry') + enc_sek = data.get('Sek') + sek = aes_decrypt(enc_sek, appkey) + return auth_token, token_expiry, sek + +def attach_signed_invoice(doctype, name, data): + f = frappe.get_doc({ + 'doctype': 'File', + 'file_name': 'E-INV--{}.json'.format(name), + 'attached_to_doctype': doctype, + 'attached_to_name': name, + 'content': json.dumps(data), + 'is_private': True + }).insert() + +def get_gstin_details(gstin): + credentials = get_credentials() + + endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/Master/gstin/{gstin}'.format(gstin=gstin) + headers = get_header(credentials) + + res = make_get_request(endpoint, headers=headers) + handle_err_response(res) + + enc_details = res.get('Data') + json_str = aes_decrypt(enc_details, credentials.sek) + details = json.loads(json_str) + + return details + +@frappe.whitelist() +def generate_irn(doctype, name): + endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice' + credentials = get_credentials() + headers = get_header(credentials) + + einvoice = make_einvoice(doctype, name) + einvoice = json.dumps(einvoice) + + enc_einvoice_json = aes_encrypt(einvoice, credentials.sek) + payload = dict(Data=enc_einvoice_json) + + res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) + res = handle_err_response(res) + + enc_json = res.get('Data') + json_str = aes_decrypt(enc_json, credentials.sek) + + signed_einvoice = json.loads(json_str) + decrypt_irn_response(signed_einvoice) + + update_einvoice_fields(doctype, name, signed_einvoice) + + attach_qrcode_image(doctype, name) + attach_signed_invoice(doctype, name, signed_einvoice['DecryptedSignedInvoice']) + + return signed_einvoice + +def get_irn_details(irn): + credentials = get_credentials() + + endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/irn/{irn}'.format(irn=irn) + headers = get_header(credentials) + + res = make_get_request(endpoint, headers=headers) + handle_err_response(res) + + return res + +@frappe.whitelist() +def cancel_irn(doctype, name, irn, reason, remark=''): + credentials = get_credentials() + + endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/Cancel' + headers = get_header(credentials) + + cancel_einv = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark)) + enc_json = aes_encrypt(cancel_einv, credentials.sek) + payload = dict(Data=enc_json) + + 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() +def cancel_eway_bill(doctype, name, eway_bill, reason, remark=''): + credentials = get_credentials() + endpoint = 'https://einv-apisandbox.nic.in/ewaybillapi/v1.03/ewayapi' + headers = get_header(credentials) + + cancel_eway_bill_json = json.dumps(dict(ewbNo=eway_bill, cancelRsnCode=reason, cancelRmrk=remark)) + enc_json = aes_encrypt(cancel_eway_bill_json, credentials.sek) + payload = dict(action='CANEWB', Data=enc_json) + + res = make_post_request(endpoint, headers=headers, data=json.dumps(payload)) + handle_err_response(res) + + frappe.db.set_value(doctype, name, 'ewaybill', '') + frappe.db.set_value(doctype, name, 'eway_bill_cancelled', 1) + + return res + +def decrypt_irn_response(data): + enc_signed_invoice = data['SignedInvoice'] + enc_signed_qr_code = data['SignedQRCode'] + signed_invoice = jwt_decrypt(enc_signed_invoice)['data'] + signed_qr_code = jwt_decrypt(enc_signed_qr_code)['data'] + data['DecryptedSignedInvoice'] = json.loads(signed_invoice) + data['DecryptedSignedQRCode'] = json.loads(signed_qr_code) + +def handle_err_response(response): + if response.get('Status') == 0: + err_details = response.get('ErrorDetails') + errors = [] + 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 + + errors.append(d.get('ErrorMessage')) + + if errors: + frappe.log_error(title="E Invoice API Request Failed", message=json.dumps(errors, default=str, indent=4)) + if len(errors) > 1: + li = ['
  • '+ d +'
  • ' for d in errors] + frappe.throw(_("""""").format(''.join(li)), title=_('API Request Failed')) + else: + frappe.throw(errors[0], 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: + return cstr(f.read()) + +def get_trans_details(invoice): + supply_type = '' + if invoice.gst_category == 'Registered Regular': supply_type = 'B2B' + elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP' + elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP' + elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP' + + if not supply_type: + return _('Invalid invoice transaction category.') + + return frappe._dict(dict( + tax_scheme='GST', supply_type=supply_type, reverse_charge=invoice.reverse_charge + )) + +def get_doc_details(invoice): + if invoice.doctype == 'Purchase Invoice' and invoice.is_return: + invoice_type = 'DBN' + else: + invoice_type = 'CRN' if invoice.is_return else 'INV' + + invoice_name = invoice.name + invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy') + + return frappe._dict(dict(invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date)) + +def get_party_gstin_details(address_name): + address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] + + gstin = address.get('gstin') + gstin_details = get_gstin_details(gstin) + legal_name = gstin_details.get('LegalName') + trade_name = gstin_details.get('TradeName') + 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: + pincode = 999999 + + return frappe._dict(dict( + gstin=gstin, legal_name=legal_name, location=location, + pincode=pincode, state_code=state_code, address_line1=address_line1, + address_line2=address_line2, email=email_id, phone=phone + )) + +def get_overseas_address_details(address_name): + address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value( + 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id'] + ) + + return frappe._dict(dict( + gstin='URP', legal_name=address_title, address_line1=address_line1, + address_line2=address_line2, email=email_id, phone=phone, + pincode=999999, state_code=96, place_of_supply=96, location=city + )) + +def get_item_list(invoice): + item_list = [] + gst_accounts = get_gst_accounts(invoice.company) + gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] + + for d in invoice.items: + item_schema = read_json('einv_item_template') + item = frappe._dict({}) + item.update(d.as_dict()) + item.sr_no = d.idx + item.description = d.item_name + item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y' + item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None + item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None + item.qty = abs(item.qty) + item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_rate) + item.total_amount = abs(item.unit_rate * item.qty) + item.discount_amount = abs(item.discount_amount * item.qty) + item.base_amount = abs(item.base_amount) + item.tax_rate = 0 + item.igst_amount = 0 + item.cgst_amount = 0 + item.sgst_amount = 0 + item.cess_rate = 0 + item.cess_amount = 0 + for t in invoice.taxes: + if t.account_head in gst_accounts_list: + item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) + if t.account_head in gst_accounts.cess_account: + item.cess_rate += item_tax_detail[0] + item.cess_amount += abs(item_tax_detail[1]) + elif t.account_head in gst_accounts.igst_account: + item.tax_rate += item_tax_detail[0] + item.igst_amount += abs(item_tax_detail[1]) + elif t.account_head in gst_accounts.sgst_account: + item.tax_rate += item_tax_detail[0] + item.sgst_amount += abs(item_tax_detail[1]) + elif t.account_head in gst_accounts.cgst_account: + item.tax_rate += item_tax_detail[0] + item.cgst_amount += abs(item_tax_detail[1]) + + item.total_value = abs(item.base_amount + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount) + einv_item = item_schema.format(item=item) + item_list.append(einv_item) + + return ', '.join(item_list) + +def get_value_details(invoice): + gst_accounts = get_gst_accounts(invoice.company) + gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d] + + value_details = frappe._dict(dict()) + value_details.base_net_total = abs(invoice.base_net_total) + value_details.invoice_discount_amt = abs(invoice.discount_amount) + value_details.round_off = 0 + value_details.base_grand_total = abs(invoice.base_rounded_total) + value_details.grand_total = abs(invoice.rounded_total) + value_details.total_cgst_amt = 0 + value_details.total_sgst_amt = 0 + value_details.total_igst_amt = 0 + value_details.total_cess_amt = 0 + for t in invoice.taxes: + if t.account_head in gst_accounts_list: + if t.account_head in gst_accounts.cess_account: + value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) + elif t.account_head in gst_accounts.igst_account: + value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount) + elif t.account_head in gst_accounts.sgst_account: + value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount) + elif t.account_head in gst_accounts.cgst_account: + value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount) + + return value_details + +def get_payment_details(invoice): + payee_name = invoice.company + mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments]) + paid_amount = invoice.base_paid_amount + outstanding_amount = invoice.outstanding_amount + + return frappe._dict(dict( + payee_name=payee_name, mode_of_payment=mode_of_payment, + paid_amount=paid_amount, outstanding_amount=outstanding_amount + )) + +def get_return_doc_reference(invoice): + invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') + return frappe._dict(dict( + invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') + )) + +def get_eway_bill_details(invoice): + if not invoice.distance: + frappe.throw(_('Distance is mandatory for E-Way Bill generation'), title=_('E Invoice Validation Failed')) + + mode_of_transport = { 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } + vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } + + return frappe._dict(dict( + gstin=invoice.gst_transporter_id, + name=invoice.transporter_name, + mode_of_transport=mode_of_transport[invoice.mode_of_transport], + distance=invoice.distance, + document_name=invoice.lr_no, + document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'), + vehicle_no=invoice.vehicle_no, + vehicle_type=vehicle_type[invoice.gst_vehicle_type] + )) + +@frappe.whitelist() +def make_einvoice(doctype, name): + invoice = frappe.get_doc(doctype, name) + schema = read_json('einv_template') + + item_list = get_item_list(invoice) + doc_details = get_doc_details(invoice) + value_details = get_value_details(invoice) + trans_details = get_trans_details(invoice) + seller_details = get_party_gstin_details(invoice.company_address) + + if invoice.gst_category == 'Overseas': + buyer_details = get_overseas_address_details(invoice.customer_address) + else: + buyer_details = get_party_gstin_details(invoice.customer_address) + place_of_supply = get_place_of_supply(invoice, doctype) or invoice.billing_address_gstin + place_of_supply = place_of_supply[:2] + buyer_details.update(dict(place_of_supply=place_of_supply)) + + shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) + if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: + shipping_details = get_party_gstin_details(invoice.shipping_address_name) + + if invoice.is_pos and invoice.base_paid_amount: + payment_details = get_payment_details(invoice) + + if invoice.is_return and invoice.return_against: + prev_doc_details = get_return_doc_reference(invoice) + + if invoice.transporter: + eway_bill_details = get_eway_bill_details(invoice) + + # not yet implemented + dispatch_details = period_details = export_details = frappe._dict({}) + + einvoice = schema.format( + trans_details=trans_details, doc_details=doc_details, dispatch_details=dispatch_details, + seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details, + item_list=item_list, value_details=value_details, payment_details=payment_details, + period_details=period_details, prev_doc_details=prev_doc_details, + export_details=export_details, eway_bill_details=eway_bill_details + ) + einvoice = json.loads(einvoice) + + validations = json.loads(read_json('einv_validation')) + errors = validate_einvoice(validations, einvoice, []) + if errors: + frappe.log_error(title="E Invoice Validation Failed", message=json.dumps(errors, default=str, indent=4)) + if len(errors) > 1: + li = ['
  • '+ d +'
  • ' for d in errors] + frappe.throw("".format(''.join(li)), title=_('E Invoice Validation Failed')) + else: + frappe.throw(errors[0], title=_('E Invoice Validation Failed')) + + return einvoice + +def validate_einvoice(validations, einvoice, errors=[]): + for fieldname, field_validation in validations.items(): + value = einvoice.get(fieldname, None) + if not value or value == "None": + # remove keys with empty values + einvoice.pop(fieldname, None) + continue + + value_type = field_validation.get("type").lower() + if value_type in ['object', 'array']: + child_validations = field_validation.get('properties') + + if isinstance(value, list): + for d in value: + validate_einvoice(child_validations, d, errors) + if not d: + # remove empty dicts + einvoice.pop(fieldname, None) + else: + validate_einvoice(child_validations, value, errors) + if not value: + # remove empty dicts + einvoice.pop(fieldname, None) + continue + + # convert to int or str + if value_type == 'string': + einvoice[fieldname] = str(value) + elif value_type == 'number': + einvoice[fieldname] = flt(value, 2) if fieldname != 'Pin' else int(value) + + max_length = field_validation.get('maxLength') + minimum = flt(field_validation.get('minimum')) + maximum = flt(field_validation.get('maximum')) + pattern_str = field_validation.get('pattern') + pattern = re.compile(pattern_str or '') + + label = field_validation.get('label') or fieldname + + if value_type == 'string' and len(value) > max_length: + errors.append(_('{} should not exceed {} characters').format(label, max_length)) + if value_type == 'number' and not (flt(value) <= maximum): + errors.append(_('{} should be less than {}').format(label, maximum)) + if pattern_str and not pattern.match(value): + errors.append(field_validation.get('validationMsg')) + + return errors + +def update_einvoice_fields(doctype, name, signed_einvoice): + enc_signed_invoice = signed_einvoice.get('SignedInvoice') + decrypted_signed_invoice = jwt_decrypt(enc_signed_invoice)['data'] + + if json.loads(decrypted_signed_invoice)['DocDtls']['No'] != name: + frappe.throw( + _("Document number of uploaded Signed E-Invoice doesn't matches with Sales Invoice"), + title=_("Inappropriate E-Invoice") + ) + + 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) + einvoice = data['einvoice'] + name = data['name'] + + frappe.response['filename'] = 'E-Invoice-' + name + '.json' + frappe.response['filecontent'] = einvoice + frappe.response['content_type'] = 'application/json' + frappe.response['type'] = 'download' + +@frappe.whitelist() +def upload_einvoice(): + signed_einvoice = json.loads(frappe.local.uploaded_file) + data = frappe._dict(frappe.local.form_dict) + doctype = data['doctype'] + name = data['docname'] + + update_einvoice_fields(doctype, name, signed_einvoice) + attach_qrcode_image(doctype, name) + +@frappe.whitelist() +def download_cancel_einvoice(): + data = frappe._dict(frappe.local.form_dict) + name = data['name'] + irn = data['irn'] + reason = data['reason'] + remark = data['remark'] + + cancel_einvoice = json.dumps([dict(Irn=irn, CnlRsn=reason, CnlRem=remark)]) + + frappe.response['filename'] = 'Cancel E-Invoice ' + name + '.json' + frappe.response['filecontent'] = cancel_einvoice + frappe.response['content_type'] = 'application/json' + frappe.response['type'] = 'download' + +@frappe.whitelist() +def upload_cancel_ack(): + cancel_ack = json.loads(frappe.local.uploaded_file) + data = frappe._dict(frappe.local.form_dict) + doctype = data['doctype'] + name = data['docname'] + + 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, + '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/setup.py b/erpnext/regional/india/setup.py index 77a466fdff7..4f1ca5012d1 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -77,7 +77,7 @@ def add_custom_roles_for_reports(): )).insert() def add_permissions(): - for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): + for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate', 'E Invoice Settings'): add_permission(doctype, 'All', 0) for role in ('Accounts Manager', 'Accounts User', 'System Manager'): add_permission(doctype, role, 0) @@ -93,9 +93,10 @@ def add_permissions(): def add_print_formats(): frappe.reload_doc("regional", "print_format", "gst_tax_invoice") frappe.reload_doc("accounts", "print_format", "gst_pos_invoice") + frappe.reload_doc("accounts", "print_format", "GST E-Invoice") frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where - name in('GST POS Invoice', 'GST Tax Invoice') """) + name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """) def make_custom_fields(update=True): hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', @@ -369,13 +370,30 @@ def make_custom_fields(update=True): 'fieldname': 'ewaybill', 'label': 'e-Way Bill No.', 'fieldtype': 'Data', - 'depends_on': 'eval:(doc.docstatus === 1)', + 'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)', 'allow_on_submit': 1, 'insert_after': 'tax_id', 'translatable': 0 } ] + si_einvoice_fields = [ + 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='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 = { 'Address': [ dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data', @@ -388,7 +406,7 @@ def make_custom_fields(update=True): 'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields, 'Purchase Order': purchase_invoice_gst_fields, 'Purchase Receipt': purchase_invoice_gst_fields, - 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields, + 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields, 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields, 'Sales Order': sales_invoice_gst_fields, 'Tax Category': inter_state_gst_field, diff --git a/requirements.txt b/requirements.txt index f807fa6c29d..20e43c44948 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ PyGithub==1.44.1 python-stdnum==1.12 Unidecode==1.1.1 WooCommerce==2.1.1 +pycryptodome==3.9.8 \ No newline at end of file