diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js
index 6336db16eb..f54bce8aac 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) {
diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js
index d9d6634c39..f01325d80b 100644
--- a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js
+++ b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js
@@ -36,4 +36,139 @@ frappe.listview_settings['Sales Invoice'].onload = function (list_view) {
};
list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
+
+ const generate_irns = () => {
+ const docnames = list_view.get_checked_items(true);
+ if (docnames && docnames.length) {
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.generate_einvoices',
+ args: { docnames },
+ freeze: true,
+ freeze_message: __('Generating E-Invoices...')
+ });
+ } else {
+ frappe.msgprint({
+ message: __('Please select at least one sales invoice to generate IRN'),
+ title: __('No Invoice Selected'),
+ indicator: 'red'
+ });
+ }
+ };
+
+ const cancel_irns = () => {
+ const docnames = list_view.get_checked_items(true);
+
+ const 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
+ }
+ ];
+
+ const d = new frappe.ui.Dialog({
+ title: __("Cancel IRN"),
+ fields: fields,
+ primary_action: function() {
+ const data = d.get_values();
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.cancel_irns',
+ args: {
+ doctype: list_view.doctype,
+ docnames,
+ reason: data.reason.split('-')[0],
+ remark: data.remark
+ },
+ freeze: true,
+ freeze_message: __('Cancelling E-Invoices...'),
+ });
+ d.hide();
+ },
+ primary_action_label: __('Submit')
+ });
+ d.show();
+ };
+
+ let einvoicing_enabled = false;
+ frappe.db.get_single_value("E Invoice Settings", "enable").then(enabled => {
+ einvoicing_enabled = enabled;
+ });
+
+ list_view.$result.on("change", "input[type=checkbox]", () => {
+ if (einvoicing_enabled) {
+ const docnames = list_view.get_checked_items(true);
+ // show/hide e-invoicing actions when no sales invoices are checked
+ if (docnames && docnames.length) {
+ // prevent adding actions twice if e-invoicing action group already exists
+ if (list_view.page.get_inner_group_button(__('E-Invoicing')).length == 0) {
+ list_view.page.add_inner_button(__('Generate IRNs'), generate_irns, __('E-Invoicing'));
+ list_view.page.add_inner_button(__('Cancel IRNs'), cancel_irns, __('E-Invoicing'));
+ }
+ } else {
+ list_view.page.remove_inner_button(__('Generate IRNs'), __('E-Invoicing'));
+ list_view.page.remove_inner_button(__('Cancel IRNs'), __('E-Invoicing'));
+ }
+ }
+ });
+
+ frappe.realtime.on("bulk_einvoice_generation_complete", (data) => {
+ const { failures, user, invoices } = data;
+
+ if (invoices.length != failures.length) {
+ frappe.msgprint({
+ message: __('{0} e-invoices generated successfully', [invoices.length]),
+ title: __('Bulk E-Invoice Generation Complete'),
+ indicator: 'orange'
+ });
+ }
+
+ if (failures && failures.length && user == frappe.session.user) {
+ let message = `
+ Failed to generate IRNs for following ${failures.length} sales invoices:
+
+ ${failures.map(d => `- ${d.docname}
`).join('')}
+
+ `;
+ frappe.msgprint({
+ message: message,
+ title: __('Bulk E-Invoice Generation Complete'),
+ indicator: 'orange'
+ });
+ }
+ });
+
+ frappe.realtime.on("bulk_einvoice_cancellation_complete", (data) => {
+ const { failures, user, invoices } = data;
+
+ if (invoices.length != failures.length) {
+ frappe.msgprint({
+ message: __('{0} e-invoices cancelled successfully', [invoices.length]),
+ title: __('Bulk E-Invoice Cancellation Complete'),
+ indicator: 'orange'
+ });
+ }
+
+ if (failures && failures.length && user == frappe.session.user) {
+ let message = `
+ Failed to cancel IRNs for following ${failures.length} sales invoices:
+
+ ${failures.map(d => `- ${d.docname}
`).join('')}
+
+ `;
+ frappe.msgprint({
+ message: message,
+ title: __('Bulk E-Invoice Cancellation Complete'),
+ indicator: 'orange'
+ });
+ }
+ });
};
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index f04e7eacb9..bc443581e4 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -294,6 +294,8 @@ class SalesInvoice(SellingController):
def before_cancel(self):
self.check_if_consolidated_invoice()
+
+ super(SalesInvoice, self).before_cancel()
self.update_time_sheet(None)
def on_cancel(self):
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 55e3853060..941061f2a2 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -2100,6 +2100,54 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['actualFromStateCode'],7)
self.assertEqual(data['billLists'][0]['fromStateCode'],27)
+ def test_einvoice_submission_without_irn(self):
+ # init
+ einvoice_settings = frappe.get_doc('E Invoice Settings')
+ einvoice_settings.enable = 1
+ einvoice_settings.applicable_from = nowdate()
+ einvoice_settings.append('credentials', {
+ 'company': '_Test Company',
+ 'gstin': '27AAECE4835E1ZR',
+ 'username': 'test',
+ 'password': 'test'
+ })
+ einvoice_settings.save()
+
+ country = frappe.flags.country
+ frappe.flags.country = 'India'
+
+ si = make_sales_invoice_for_ewaybill()
+ self.assertRaises(frappe.ValidationError, si.submit)
+
+ si.irn = 'test_irn'
+ si.submit()
+
+ # reset
+ einvoice_settings = frappe.get_doc('E Invoice Settings')
+ einvoice_settings.enable = 0
+ frappe.flags.country = country
+
+ def test_einvoice_json(self):
+ from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
+
+ si = get_sales_invoice_for_e_invoice()
+ si.discount_amount = 100
+ si.save()
+
+ einvoice = make_einvoice(si)
+ self.assertTrue(einvoice['EwbDtls'])
+ validate_totals(einvoice)
+
+ si.apply_discount_on = 'Net Total'
+ si.save()
+ einvoice = make_einvoice(si)
+ validate_totals(einvoice)
+
+ [d.set('included_in_print_rate', 1) for d in si.taxes]
+ si.save()
+ einvoice = make_einvoice(si)
+ validate_totals(einvoice)
+
def test_item_tax_net_range(self):
item = create_item("T Shirt")
diff --git a/erpnext/patches/v14_0/__init__.py b/erpnext/accounts/print_format/gst_e_invoice/__init__.py
similarity index 100%
rename from erpnext/patches/v14_0/__init__.py
rename to erpnext/accounts/print_format/gst_e_invoice/__init__.py
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 0000000000..e658049309
--- /dev/null
+++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
@@ -0,0 +1,173 @@
+{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
+{%- set einvoice = json.loads(doc.signed_einvoice) -%}
+
+
+
+ {% if print_settings.repeat_header_footer %}
+
+ {% endif %}
+
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 -%}
+
Shipped From
+
{{ 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 }}
+
+ {%- if einvoice.DispDtls -%}
+ {%- set dispatch = einvoice.DispDtls -%}
+
Dispatched From
+ {%- if dispatch.Gstin -%}
{{ dispatch.Gstin }}
{% endif %}
+
{{ dispatch.LglNm }}
+
{{ dispatch.Addr1 }}
+ {%- if dispatch.Addr2 -%}
{{ dispatch.Addr2 }}
{% endif %}
+
{{ dispatch.Loc }}
+
{{ frappe.db.get_value("Address", doc.dispatch_address_name, "gst_state") }} - {{ dispatch.Pin }}
+ {% endif %}
+
+
+
+
3. Item Details
+
+
+
+ Sr. No. |
+ Item |
+ HSN Code |
+ Qty |
+ UOM |
+ Rate |
+ Discount |
+ Taxable Amount |
+ Tax Rate |
+ Other Charges |
+ Total |
+
+
+
+ {% for item in einvoice.ItemList %}
+
+ {{ 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") }} |
+
+ {% endfor %}
+
+
+
+
+
4. Value Details
+
+
+
+ Taxable Amount |
+ CGST |
+ SGST |
+ IGST |
+ CESS |
+ State CESS |
+ Discount |
+ Other Charges |
+ Round Off |
+ Total Value |
+
+
+
+ {%- set value_details = einvoice.ValDtls -%}
+
+ {{ 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(value_details.OthChrg, None, "INR") }} |
+ {{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }} |
+ {{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }} |
+
+
+
+
+
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 0000000000..1001199a09
--- /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 4775f56a01..bab16a494d 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -167,9 +167,14 @@ class AccountsController(TransactionBase):
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 on_trash(self):
# delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'):
@@ -2151,3 +2156,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
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index d172da3f1f..04f5793836 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -443,6 +443,7 @@ regional_overrides = {
'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts',
'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.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields',
'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount',
'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code'
},
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index ad5062fc0e..b04eeb85a8 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -203,6 +203,7 @@ execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation")
erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020
erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020
erpnext.patches.v12_0.create_itc_reversal_custom_fields
+execute:frappe.reload_doc("regional", "doctype", "e_invoice_settings")
erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020
erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020
erpnext.patches.v12_0.add_taxjar_integration_field
@@ -223,6 +224,7 @@ erpnext.patches.v13_0.set_youtube_video_id
erpnext.patches.v13_0.set_app_name
erpnext.patches.v13_0.print_uom_after_quantity_patch
erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account
+erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.update_reason_for_resignation_in_employee
execute:frappe.delete_doc("Report", "Quoted Item Comparison")
@@ -242,6 +244,8 @@ erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
erpnext.patches.v13_0.update_vehicle_no_reqd_condition
+erpnext.patches.v12_0.add_einvoice_status_field #2021-03-17
+erpnext.patches.v12_0.add_einvoice_summary_report_permissions
erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae
@@ -255,6 +259,7 @@ erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 #17-01-2022
erpnext.patches.v13_0.update_shipment_status
erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting
+erpnext.patches.v12_0.add_ewaybill_validity_field
erpnext.patches.v13_0.germany_make_custom_fields
erpnext.patches.v13_0.germany_fill_debtor_creditor_number
erpnext.patches.v13_0.set_pos_closing_as_failed
@@ -271,6 +276,7 @@ erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
erpnext.patches.v13_0.update_subscription_status_in_memberships
erpnext.patches.v13_0.update_amt_in_work_order_required_items
+erpnext.patches.v12_0.show_einvoice_irn_cancelled_field
erpnext.patches.v13_0.delete_orphaned_tables
erpnext.patches.v13_0.update_export_type_for_gst #2021-08-16
erpnext.patches.v13_0.update_tds_check_field #3
@@ -281,7 +287,6 @@ erpnext.patches.v13_0.remove_bad_selling_defaults
erpnext.patches.v13_0.trim_whitespace_from_serial_nos # 16-01-2022
erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
-erpnext.patches.v13_0.einvoicing_deprecation_warning
execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Category")
erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
@@ -327,11 +332,12 @@ erpnext.patches.v13_0.delete_bank_reconciliation_detail
[post_model_sync]
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
-erpnext.patches.v14_0.delete_einvoicing_doctypes
erpnext.patches.v14_0.delete_shopify_doctypes
erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022
erpnext.patches.v14_0.delete_agriculture_doctypes
erpnext.patches.v14_0.rearrange_company_fields
erpnext.patches.v14_0.update_leave_notification_template
+erpnext.patches.v14_0.restore_einvoice_fields
erpnext.patches.v13_0.update_sane_transfer_against
+erpnext.patches.v12_0.add_company_link_to_einvoice_settings
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py
new file mode 100644
index 0000000000..e498b673fb
--- /dev/null
+++ b/erpnext/patches/v12_0/add_company_link_to_einvoice_settings.py
@@ -0,0 +1,18 @@
+from __future__ import unicode_literals
+
+import frappe
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company or not frappe.db.count('E Invoice User'):
+ return
+
+ frappe.reload_doc("regional", "doctype", "e_invoice_user")
+ for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']):
+ company_name = frappe.db.sql("""
+ select dl.link_name from `tabAddress` a, `tabDynamic Link` dl
+ where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company'
+ """, (creds.get('gstin')))
+ if company_name and len(company_name) > 0:
+ frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0])
diff --git a/erpnext/patches/v12_0/add_einvoice_status_field.py b/erpnext/patches/v12_0/add_einvoice_status_field.py
new file mode 100644
index 0000000000..aeff9ca841
--- /dev/null
+++ b/erpnext/patches/v12_0/add_einvoice_status_field.py
@@ -0,0 +1,72 @@
+from __future__ import unicode_literals
+
+import json
+
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ # move hidden einvoice fields to a different section
+ custom_fields = {
+ 'Sales Invoice': [
+ dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
+ print_hide=1, hidden=1),
+
+ dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
+ no_copy=1, print_hide=1),
+
+ dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
+
+ dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
+ no_copy=1, print_hide=1),
+
+ dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image',
+ options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON',
+ hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1)
+ ]
+ }
+ create_custom_fields(custom_fields, update=True)
+
+ if frappe.db.exists('E Invoice Settings') and frappe.db.get_single_value('E Invoice Settings', 'enable'):
+ frappe.db.sql('''
+ UPDATE `tabSales Invoice` SET einvoice_status = 'Pending'
+ WHERE
+ posting_date >= '2021-04-01'
+ AND ifnull(irn, '') = ''
+ AND ifnull(`billing_address_gstin`, '') != ifnull(`company_gstin`, '')
+ AND ifnull(gst_category, '') in ('Registered Regular', 'SEZ', 'Overseas', 'Deemed Export')
+ ''')
+
+ # set appropriate statuses
+ frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Generated'
+ WHERE ifnull(irn, '') != '' AND ifnull(irn_cancelled, 0) = 0''')
+
+ frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Cancelled'
+ WHERE ifnull(irn_cancelled, 0) = 1''')
+
+ # set correct acknowledgement in e-invoices
+ einvoices = frappe.get_all('Sales Invoice', {'irn': ['is', 'set']}, ['name', 'signed_einvoice'])
+
+ if einvoices:
+ for inv in einvoices:
+ signed_einvoice = inv.get('signed_einvoice')
+ if signed_einvoice:
+ signed_einvoice = json.loads(signed_einvoice)
+ frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_no', signed_einvoice.get('AckNo'), update_modified=False)
+ frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_date', signed_einvoice.get('AckDt'), update_modified=False)
diff --git a/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py
new file mode 100644
index 0000000000..e837786138
--- /dev/null
+++ b/erpnext/patches/v12_0/add_einvoice_summary_report_permissions.py
@@ -0,0 +1,20 @@
+from __future__ import unicode_literals
+
+import frappe
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ if frappe.db.exists('Report', 'E-Invoice Summary') and \
+ not frappe.db.get_value('Custom Role', dict(report='E-Invoice Summary')):
+ frappe.get_doc(dict(
+ doctype='Custom Role',
+ report='E-Invoice Summary',
+ roles= [
+ dict(role='Accounts User'),
+ dict(role='Accounts Manager')
+ ]
+ )).insert()
diff --git a/erpnext/patches/v12_0/add_ewaybill_validity_field.py b/erpnext/patches/v12_0/add_ewaybill_validity_field.py
new file mode 100644
index 0000000000..247140d21d
--- /dev/null
+++ b/erpnext/patches/v12_0/add_ewaybill_validity_field.py
@@ -0,0 +1,18 @@
+from __future__ import unicode_literals
+
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ custom_fields = {
+ 'Sales Invoice': [
+ dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1,
+ depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill')
+ ]
+ }
+ create_custom_fields(custom_fields, update=True)
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 0000000000..c17666add1
--- /dev/null
+++ b/erpnext/patches/v12_0/setup_einvoice_fields.py
@@ -0,0 +1,59 @@
+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
+
+ frappe.reload_doc("custom", "doctype", "custom_field")
+ frappe.reload_doc("regional", "doctype", "e_invoice_settings")
+ 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='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
+
+ dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
+
+ 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()
+
+ einvoice_cond = 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)'
+ t = {
+ 'mode_of_transport': [{'default': None}],
+ 'distance': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.transporter'}],
+ 'gst_vehicle_type': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
+ 'lr_date': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
+ 'lr_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
+ 'vehicle_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
+ 'ewaybill': [
+ {'read_only_depends_on': 'eval:doc.irn && doc.ewaybill'},
+ {'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'}
+ ]
+ }
+
+ for field, conditions in t.items():
+ for c in conditions:
+ [(prop, value)] = c.items()
+ frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value)
diff --git a/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py
new file mode 100644
index 0000000000..3f90a03020
--- /dev/null
+++ b/erpnext/patches/v12_0/show_einvoice_irn_cancelled_field.py
@@ -0,0 +1,14 @@
+from __future__ import unicode_literals
+
+import frappe
+
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ irn_cancelled_field = frappe.db.exists('Custom Field', {'dt': 'Sales Invoice', 'fieldname': 'irn_cancelled'})
+ if irn_cancelled_field:
+ frappe.db.set_value('Custom Field', irn_cancelled_field, 'depends_on', 'eval: doc.irn')
+ frappe.db.set_value('Custom Field', irn_cancelled_field, 'read_only', 0)
diff --git a/erpnext/patches/v13_0/einvoicing_deprecation_warning.py b/erpnext/patches/v13_0/einvoicing_deprecation_warning.py
deleted file mode 100644
index e123a55f5a..0000000000
--- a/erpnext/patches/v13_0/einvoicing_deprecation_warning.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import click
-
-
-def execute():
- click.secho(
- "Indian E-Invoicing integration is moved to a separate app and will be removed from ERPNext in version-14.\n"
- "Please install the app to continue using the integration: https://github.com/frappe/erpnext_gst_compliance",
- fg="yellow",
- )
diff --git a/erpnext/patches/v14_0/delete_einvoicing_doctypes.py b/erpnext/patches/v14_0/delete_einvoicing_doctypes.py
deleted file mode 100644
index a3a8149be3..0000000000
--- a/erpnext/patches/v14_0/delete_einvoicing_doctypes.py
+++ /dev/null
@@ -1,10 +0,0 @@
-import frappe
-
-
-def execute():
- frappe.delete_doc('DocType', 'E Invoice Settings', ignore_missing=True)
- frappe.delete_doc('DocType', 'E Invoice User', ignore_missing=True)
- frappe.delete_doc('Report', 'E-Invoice Summary', ignore_missing=True)
- frappe.delete_doc('Print Format', 'GST E-Invoice', ignore_missing=True)
- frappe.delete_doc('Custom Field', 'Sales Invoice-eway_bill_cancelled', ignore_missing=True)
- frappe.delete_doc('Custom Field', 'Sales Invoice-irn_cancelled', ignore_missing=True)
diff --git a/erpnext/patches/v14_0/restore_einvoice_fields.py b/erpnext/patches/v14_0/restore_einvoice_fields.py
new file mode 100644
index 0000000000..c4431fb9db
--- /dev/null
+++ b/erpnext/patches/v14_0/restore_einvoice_fields.py
@@ -0,0 +1,24 @@
+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():
+ # restores back the 2 custom fields that was deleted while removing e-invoicing from v14
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ custom_fields = {
+ 'Sales Invoice': [
+ 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'),
+ ]
+ }
+ create_custom_fields(custom_fields, update=True)
+ add_permissions()
+ add_print_formats()
diff --git a/erpnext/regional/doctype/e_invoice_request_log/__init__.py b/erpnext/regional/doctype/e_invoice_request_log/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js
new file mode 100644
index 0000000000..7b7ba964e5
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('E Invoice Request Log', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json
new file mode 100644
index 0000000000..3034370fea
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json
@@ -0,0 +1,102 @@
+{
+ "actions": [],
+ "autoname": "EINV-REQ-.#####",
+ "creation": "2020-12-08 12:54:08.175992",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "url",
+ "headers",
+ "response",
+ "column_break_7",
+ "timestamp",
+ "reference_invoice",
+ "data"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "User",
+ "options": "User"
+ },
+ {
+ "fieldname": "reference_invoice",
+ "fieldtype": "Data",
+ "label": "Reference Invoice"
+ },
+ {
+ "fieldname": "headers",
+ "fieldtype": "Code",
+ "label": "Headers",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "data",
+ "fieldtype": "Code",
+ "label": "Data",
+ "options": "JSON"
+ },
+ {
+ "default": "Now",
+ "fieldname": "timestamp",
+ "fieldtype": "Datetime",
+ "label": "Timestamp"
+ },
+ {
+ "fieldname": "response",
+ "fieldtype": "Code",
+ "label": "Response",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "url",
+ "fieldtype": "Data",
+ "label": "URL"
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-01-13 12:06:57.253111",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "E Invoice Request Log",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py
new file mode 100644
index 0000000000..c89552d782
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class EInvoiceRequestLog(Document):
+ pass
diff --git a/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py
new file mode 100644
index 0000000000..091cc88e45
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py
@@ -0,0 +1,11 @@
+# -*- 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 TestEInvoiceRequestLog(unittest.TestCase):
+ pass
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 0000000000..e69de29bb2
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 0000000000..54e488610d
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js
@@ -0,0 +1,11 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('E Invoice Settings', {
+ refresh(frm) {
+ const docs_link = 'https://docs.erpnext.com/docs/v13/user/manual/en/regional/india/setup-e-invoicing';
+ frm.dashboard.set_headline(
+ __("Read {0} for more information on E Invoicing features.", [`documentation`])
+ );
+ }
+});
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 0000000000..16b2963301
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
@@ -0,0 +1,97 @@
+{
+ "actions": [],
+ "creation": "2020-09-24 16:23:16.235722",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "enable",
+ "section_break_2",
+ "sandbox_mode",
+ "applicable_from",
+ "credentials",
+ "advanced_settings_section",
+ "client_id",
+ "column_break_8",
+ "client_secret",
+ "auth_token",
+ "token_expiry"
+ ],
+ "fields": [
+ {
+ "default": "0",
+ "fieldname": "enable",
+ "fieldtype": "Check",
+ "label": "Enable"
+ },
+ {
+ "depends_on": "enable",
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "auth_token",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "token_expiry",
+ "fieldtype": "Datetime",
+ "hidden": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "credentials",
+ "fieldtype": "Table",
+ "label": "Credentials",
+ "mandatory_depends_on": "enable",
+ "options": "E Invoice User"
+ },
+ {
+ "default": "0",
+ "fieldname": "sandbox_mode",
+ "fieldtype": "Check",
+ "label": "Sandbox Mode"
+ },
+ {
+ "fieldname": "applicable_from",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Applicable From",
+ "reqd": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "advanced_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Advanced Settings"
+ },
+ {
+ "fieldname": "client_id",
+ "fieldtype": "Data",
+ "label": "Client ID"
+ },
+ {
+ "fieldname": "client_secret",
+ "fieldtype": "Password",
+ "label": "Client Secret"
+ },
+ {
+ "fieldname": "column_break_8",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-11-16 19:50:28.029517",
+ "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 0000000000..342d583cc0
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+
+
+class EInvoiceSettings(Document):
+ def validate(self):
+ if self.enable and not self.credentials:
+ frappe.throw(_('You must add atleast one credentials to be able to use E Invoicing.'))
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 0000000000..10770deb0e
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py
@@ -0,0 +1,11 @@
+# -*- 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/doctype/e_invoice_user/__init__.py b/erpnext/regional/doctype/e_invoice_user/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
new file mode 100644
index 0000000000..a65b1ca7ca
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
@@ -0,0 +1,57 @@
+{
+ "actions": [],
+ "creation": "2020-12-22 15:02:46.229474",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "gstin",
+ "username",
+ "password"
+ ],
+ "fields": [
+ {
+ "fieldname": "gstin",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "GSTIN",
+ "reqd": 1
+ },
+ {
+ "fieldname": "username",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Username",
+ "reqd": 1
+ },
+ {
+ "fieldname": "password",
+ "fieldtype": "Password",
+ "in_list_view": 1,
+ "label": "Password",
+ "reqd": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-03-22 12:16:56.365616",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "E Invoice User",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py
new file mode 100644
index 0000000000..4e0e89c09a
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class EInvoiceUser(Document):
+ pass
diff --git a/erpnext/regional/india/e_invoice/__init__.py b/erpnext/regional/india/e_invoice/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
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 0000000000..78e56518df
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/einv_item_template.json
@@ -0,0 +1,31 @@
+{{
+ "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.gross_amount}",
+ "Discount": "{item.discount_amount}",
+ "AssAmt": "{item.taxable_value}",
+ "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}",
+ "CesNonAdvlAmt": "{item.cess_nadv_amount}",
+ "StateCesRt": "{item.state_cess_rate}",
+ "StateCesAmt": "{item.state_cess_amount}",
+ "StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}",
+ "OthChrg": "{item.other_charges}",
+ "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 0000000000..c2a28f2049
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/einv_template.json
@@ -0,0 +1,110 @@
+{{
+ "Version": "1.1",
+ "TranDtls": {{
+ "TaxSch": "{transaction_details.tax_scheme}",
+ "SupTyp": "{transaction_details.supply_type}",
+ "RegRev": "{transaction_details.reverse_charge}",
+ "EcmGstin": "{transaction_details.ecom_gstin}",
+ "IgstOnIntra": "{transaction_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.legal_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": "{invoice_value_details.base_total}",
+ "CgstVal": "{invoice_value_details.total_cgst_amt}",
+ "SgstVal": "{invoice_value_details.total_sgst_amt}",
+ "IgstVal": "{invoice_value_details.total_igst_amt}",
+ "CesVal": "{invoice_value_details.total_cess_amt}",
+ "Discount": "{invoice_value_details.invoice_discount_amt}",
+ "RndOffAmt": "{invoice_value_details.round_off}",
+ "OthChrg": "{invoice_value_details.total_other_charges}",
+ "TotInvVal": "{invoice_value_details.base_grand_total}",
+ "TotInvValFc": "{invoice_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 0000000000..f4a3542a60
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/einv_validation.json
@@ -0,0 +1,957 @@
+{
+ "Version": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 6,
+ "description": "Version of the schema"
+ },
+ "Irn": {
+ "type": "string",
+ "minLength": 64,
+ "maxLength": 64,
+ "description": "Invoice Reference Number"
+ },
+ "TranDtls": {
+ "type": "object",
+ "properties": {
+ "TaxSch": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 10,
+ "enum": ["GST"],
+ "description": "GST- Goods and Services Tax Scheme"
+ },
+ "SupTyp": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 10,
+ "enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"],
+ "description": "Type of Supply: B2B-Business to Business, SEZWP - SEZ with payment, SEZWOP - SEZ without payment, EXPWP - Export with Payment, EXPWOP - Export without payment,DEXP - Deemed Export"
+ },
+ "RegRev": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "enum": ["Y", "N"],
+ "description": "Y- whether the tax liability is payable under reverse charge"
+ },
+ "EcmGstin": {
+ "type": "string",
+ "minLength": 15,
+ "maxLength": 15,
+ "pattern": "([0-9]{2}[0-9A-Z]{13})",
+ "description": "E-Commerce GSTIN",
+ "validationMsg": "E-Commerce GSTIN is invalid"
+ },
+ "IgstOnIntra": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "enum": ["Y", "N"],
+ "description": "Y- indicates the supply is intra state but chargeable to IGST"
+ }
+ },
+ "required": ["TaxSch", "SupTyp"]
+ },
+ "DocDtls": {
+ "type": "object",
+ "properties": {
+ "Typ": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 3,
+ "enum": ["INV", "CRN", "DBN"],
+ "description": "Document Type"
+ },
+ "No": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 16,
+ "pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$",
+ "description": "Document Number",
+ "validationMsg": "Document Number 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]",
+ "description": "Document Date"
+ }
+ },
+ "required": ["Typ", "No", "Dt"]
+ },
+ "SellerDtls": {
+ "type": "object",
+ "properties": {
+ "Gstin": {
+ "type": "string",
+ "minLength": 15,
+ "maxLength": 15,
+ "pattern": "([0-9]{2}[0-9A-Z]{13})",
+ "description": "Supplier GSTIN",
+ "validationMsg": "Company GSTIN is invalid"
+ },
+ "LglNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Legal Name"
+ },
+ "TrdNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Tradename"
+ },
+ "Addr1": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Address Line 1"
+ },
+ "Addr2": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Address Line 2"
+ },
+ "Loc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 50,
+ "description": "Location"
+ },
+ "Pin": {
+ "type": "number",
+ "minimum": 100000,
+ "maximum": 999999,
+ "description": "Pincode"
+ },
+ "Stcd": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "Supplier State Code"
+ },
+ "Ph": {
+ "type": "string",
+ "minLength": 6,
+ "maxLength": 12,
+ "description": "Phone"
+ },
+ "Em": {
+ "type": "string",
+ "minLength": 6,
+ "maxLength": 100,
+ "description": "Email-Id"
+ }
+ },
+ "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)$",
+ "description": "Buyer GSTIN",
+ "validationMsg": "Customer GSTIN is invalid"
+ },
+ "LglNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Legal Name"
+ },
+ "TrdNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Trade Name"
+ },
+ "Pos": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "Place of Supply State code"
+ },
+ "Addr1": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Address Line 1"
+ },
+ "Addr2": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Address Line 2"
+ },
+ "Loc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Location"
+ },
+ "Pin": {
+ "type": "number",
+ "minimum": 100000,
+ "maximum": 999999,
+ "description": "Pincode"
+ },
+ "Stcd": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "Buyer State Code"
+ },
+ "Ph": {
+ "type": "string",
+ "minLength": 6,
+ "maxLength": 12,
+ "description": "Phone"
+ },
+ "Em": {
+ "type": "string",
+ "minLength": 6,
+ "maxLength": 100,
+ "description": "Email-Id"
+ }
+ },
+ "required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"]
+ },
+ "DispDtls": {
+ "type": "object",
+ "properties": {
+ "Nm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Dispatch Address Name"
+ },
+ "Addr1": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Address Line 1"
+ },
+ "Addr2": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Address Line 2"
+ },
+ "Loc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Location"
+ },
+ "Pin": {
+ "type": "number",
+ "minimum": 100000,
+ "maximum": 999999,
+ "description": "Pincode"
+ },
+ "Stcd": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "State Code"
+ }
+ },
+ "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)$",
+ "description": "Shipping Address GSTIN",
+ "validationMsg": "Shipping Address GSTIN is invalid"
+ },
+ "LglNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Legal Name"
+ },
+ "TrdNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Trade Name"
+ },
+ "Addr1": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Address Line 1"
+ },
+ "Addr2": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Address Line 2"
+ },
+ "Loc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Location"
+ },
+ "Pin": {
+ "type": "number",
+ "minimum": 100000,
+ "maximum": 999999,
+ "description": "Pincode"
+ },
+ "Stcd": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "State Code"
+ }
+ },
+ "required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"]
+ },
+ "ItemList": {
+ "type": "Array",
+ "properties": {
+ "SlNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 6,
+ "description": "Serial No. of Item"
+ },
+ "PrdDesc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "description": "Item Name"
+ },
+ "IsServc": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "enum": ["Y", "N"],
+ "description": "Is Service Item"
+ },
+ "HsnCd": {
+ "type": "string",
+ "minLength": 4,
+ "maxLength": 8,
+ "description": "HSN Code"
+ },
+ "Barcde": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 30,
+ "description": "Barcode"
+ },
+ "Qty": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 9999999999.999,
+ "description": "Quantity"
+ },
+ "FreeQty": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 9999999999.999,
+ "description": "Free Quantity"
+ },
+ "Unit": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 8,
+ "description": "UOM"
+ },
+ "UnitPrice": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.999,
+ "description": "Rate"
+ },
+ "TotAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Gross Amount"
+ },
+ "Discount": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Discount"
+ },
+ "PreTaxVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Pre tax value"
+ },
+ "AssAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Taxable Value"
+ },
+ "GstRt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999.999,
+ "description": "GST Rate"
+ },
+ "IgstAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "IGST Amount"
+ },
+ "CgstAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "CGST Amount"
+ },
+ "SgstAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "SGST Amount"
+ },
+ "CesRt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999.999,
+ "description": "Cess Rate"
+ },
+ "CesAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Cess Amount (Advalorem)"
+ },
+ "CesNonAdvlAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Cess Amount (Non-Advalorem)"
+ },
+ "StateCesRt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999.999,
+ "description": "State CESS Rate"
+ },
+ "StateCesAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "State CESS Amount"
+ },
+ "StateCesNonAdvlAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "State CESS Amount (Non Advalorem)"
+ },
+ "OthChrg": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Other Charges"
+ },
+ "TotItemVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Total Item Value"
+ },
+ "OrdLineRef": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 50,
+ "description": "Order line reference"
+ },
+ "OrgCntry": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 2,
+ "description": "Origin Country"
+ },
+ "PrdSlNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "description": "Serial number"
+ },
+ "BchDtls": {
+ "type": "object",
+ "properties": {
+ "Nm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 20,
+ "description": "Batch number"
+ },
+ "ExpDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Batch Expiry Date"
+ },
+ "WrDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Warranty Date"
+ }
+ },
+ "required": ["Nm"]
+ },
+ "AttribDtls": {
+ "type": "Array",
+ "Attribute": {
+ "type": "object",
+ "properties": {
+ "Nm": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Attribute name of the item"
+ },
+ "Val": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Attribute value of the item"
+ }
+ }
+ }
+ }
+ },
+ "required": [
+ "SlNo",
+ "IsServc",
+ "HsnCd",
+ "UnitPrice",
+ "TotAmt",
+ "AssAmt",
+ "GstRt",
+ "TotItemVal"
+ ]
+ },
+ "ValDtls": {
+ "type": "object",
+ "properties": {
+ "AssVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total Assessable value of all items"
+ },
+ "CgstVal": {
+ "type": "number",
+ "maximum": 99999999999999.99,
+ "minimum": 0,
+ "description": "Total CGST value of all items"
+ },
+ "SgstVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total SGST value of all items"
+ },
+ "IgstVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total IGST value of all items"
+ },
+ "CesVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total CESS value of all items"
+ },
+ "StCesVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total State CESS value of all items"
+ },
+ "Discount": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Invoice Discount"
+ },
+ "OthChrg": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Other Charges"
+ },
+ "RndOffAmt": {
+ "type": "number",
+ "minimum": -99.99,
+ "maximum": 99.99,
+ "description": "Rounded off Amount"
+ },
+ "TotInvVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Final Invoice Value "
+ },
+ "TotInvValFc": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Final Invoice value in Foreign Currency"
+ }
+ },
+ "required": ["AssVal", "TotInvVal"]
+ },
+ "PayDtls": {
+ "type": "object",
+ "properties": {
+ "Nm": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Payee Name"
+ },
+ "AccDet": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 18,
+ "description": "Bank Account Number of Payee"
+ },
+ "Mode": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 18,
+ "description": "Mode of Payment"
+ },
+ "FinInsBr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 11,
+ "description": "Branch or IFSC code"
+ },
+ "PayTerm": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Terms of Payment"
+ },
+ "PayInstr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Payment Instruction"
+ },
+ "CrTrn": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Credit Transfer"
+ },
+ "DirDr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Direct Debit"
+ },
+ "CrDay": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 9999,
+ "description": "Credit Days"
+ },
+ "PaidAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Advance Amount"
+ },
+ "PaymtDue": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Outstanding Amount"
+ }
+ }
+ },
+ "RefDtls": {
+ "type": "object",
+ "properties": {
+ "InvRm": {
+ "type": "string",
+ "maxLength": 100,
+ "minLength": 3,
+ "pattern": "^[0-9A-Za-z/-]{3,100}$",
+ "description": "Remarks/Note"
+ },
+ "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]",
+ "description": "Invoice Period Start Date"
+ },
+ "InvEndDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Invoice Period End Date"
+ }
+ },
+ "required": ["InvStDt ", "InvEndDt "]
+ },
+ "PrecDocDtls": {
+ "type": "object",
+ "properties": {
+ "InvNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 16,
+ "pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$",
+ "description": "Reference of Original Invoice"
+ },
+ "InvDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Date of Orginal Invoice"
+ },
+ "OthRefNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "description": "Other Reference"
+ }
+ }
+ },
+ "required": ["InvNo", "InvDt"],
+ "ContrDtls": {
+ "type": "object",
+ "properties": {
+ "RecAdvRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Receipt Advice No."
+ },
+ "RecAdvDt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Date of receipt advice"
+ },
+ "TendRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Lot/Batch Reference No."
+ },
+ "ContrRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Contract Reference Number"
+ },
+ "ExtRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Any other reference"
+ },
+ "ProjRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Project Reference Number"
+ },
+ "PORefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 16,
+ "pattern": "^([0-9A-Za-z/-]){1,16}$",
+ "description": "PO Reference Number"
+ },
+ "PORefDt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "PO Reference date"
+ }
+ }
+ }
+ }
+ },
+ "AddlDocDtls": {
+ "type": "Array",
+ "properties": {
+ "Url": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Supporting document URL"
+ },
+ "Docs": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 1000,
+ "description": "Supporting document in Base64 Format"
+ },
+ "Info": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 1000,
+ "description": "Any additional information"
+ }
+ }
+ },
+
+ "ExpDtls": {
+ "type": "object",
+ "properties": {
+ "ShipBNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "description": "Shipping Bill No."
+ },
+ "ShipBDt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Shipping Bill Date"
+ },
+ "Port": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 10,
+ "pattern": "^[0-9A-Za-z]{2,10}$",
+ "description": "Port Code. Refer the master"
+ },
+ "RefClm": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "description": "Claiming Refund. Y/N"
+ },
+ "ForCur": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 16,
+ "description": "Additional Currency Code. Refer the master"
+ },
+ "CntCode": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 2,
+ "description": "Country Code. Refer the master"
+ },
+ "ExpDuty": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Export Duty"
+ }
+ }
+ },
+ "EwbDtls": {
+ "type": "object",
+ "properties": {
+ "TransId": {
+ "type": "string",
+ "minLength": 15,
+ "maxLength": 15,
+ "description": "Transporter GSTIN"
+ },
+ "TransName": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Transporter Name"
+ },
+ "TransMode": {
+ "type": "string",
+ "maxLength": 1,
+ "minLength": 1,
+ "enum": ["1", "2", "3", "4"],
+ "description": "Mode of Transport"
+ },
+ "Distance": {
+ "type": "number",
+ "minimum": 1,
+ "maximum": 9999,
+ "description": "Distance"
+ },
+ "TransDocNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 15,
+ "pattern": "^([0-9A-Z/-]){1,15}$",
+ "description": "Tranport Document Number",
+ "validationMsg": "Transport Receipt No is invalid"
+ },
+ "TransDocDt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Transport Document Date"
+ },
+ "VehNo": {
+ "type": "string",
+ "minLength": 4,
+ "maxLength": 20,
+ "description": "Vehicle Number"
+ },
+ "VehType": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "enum": ["O", "R"],
+ "description": "Vehicle Type"
+ }
+ },
+ "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 0000000000..348f0c6fee
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -0,0 +1,292 @@
+erpnext.setup_einvoice_actions = (doctype) => {
+ frappe.ui.form.on(doctype, {
+ async refresh(frm) {
+ if (frm.doc.docstatus == 2) return;
+
+ const res = await frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility',
+ args: { doc: frm.doc }
+ });
+ const invoice_eligible = res.message;
+
+ if (!invoice_eligible) return;
+
+ const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
+
+ const add_custom_button = (label, action) => {
+ if (!frm.custom_buttons[label]) {
+ frm.add_custom_button(label, action, __('E Invoicing'));
+ }
+ };
+
+ if (!irn && !__unsaved) {
+ const action = () => {
+ if (frm.doc.__unsaved) {
+ frappe.throw(__('Please save the document to generate IRN.'));
+ }
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.get_einvoice',
+ args: { doctype, docname: name },
+ freeze: true,
+ callback: (res) => {
+ const einvoice = res.message;
+ show_einvoice_preview(frm, einvoice);
+ }
+ });
+ };
+
+ add_custom_button(__("Generate IRN"), action);
+ }
+
+ if (irn && !irn_cancelled && !ewaybill) {
+ const 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
+ }
+ ];
+ const action = () => {
+ const d = new frappe.ui.Dialog({
+ title: __("Cancel IRN"),
+ fields: fields,
+ primary_action: function() {
+ const data = d.get_values();
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.cancel_irn',
+ args: {
+ doctype,
+ docname: name,
+ irn: irn,
+ reason: data.reason.split('-')[0],
+ remark: data.remark
+ },
+ freeze: true,
+ callback: () => frm.reload_doc() || d.hide(),
+ error: () => d.hide()
+ });
+ },
+ primary_action_label: __('Submit')
+ });
+ d.show();
+ };
+ add_custom_button(__("Cancel IRN"), action);
+ }
+
+ if (irn && !irn_cancelled && !ewaybill) {
+ const action = () => {
+ const d = new frappe.ui.Dialog({
+ title: __('Generate E-Way Bill'),
+ size: "large",
+ fields: get_ewaybill_fields(frm),
+ primary_action: function() {
+ const data = d.get_values();
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.generate_eway_bill',
+ args: {
+ doctype,
+ docname: name,
+ irn,
+ ...data
+ },
+ freeze: true,
+ callback: () => frm.reload_doc() || d.hide(),
+ error: () => d.hide()
+ });
+ },
+ primary_action_label: __('Submit')
+ });
+ d.show();
+ };
+
+ add_custom_button(__("Generate E-Way Bill"), action);
+ }
+
+ if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
+ const action = () => {
+ let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
+ message += '
';
+ message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
+
+ const dialog = frappe.msgprint({
+ title: __('Update E-Way Bill Cancelled Status?'),
+ message: message,
+ indicator: 'orange',
+ primary_action: {
+ action: function() {
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
+ args: { doctype, docname: name },
+ freeze: true,
+ callback: () => frm.reload_doc() || dialog.hide()
+ });
+ }
+ },
+ primary_action_label: __('Yes')
+ });
+ };
+ add_custom_button(__("Cancel E-Way Bill"), action);
+ }
+ }
+ });
+};
+
+const get_ewaybill_fields = (frm) => {
+ return [
+ {
+ 'fieldname': 'transporter',
+ 'label': 'Transporter',
+ 'fieldtype': 'Link',
+ 'options': 'Supplier',
+ 'default': frm.doc.transporter
+ },
+ {
+ 'fieldname': 'gst_transporter_id',
+ 'label': 'GST Transporter ID',
+ 'fieldtype': 'Data',
+ 'fetch_from': 'transporter.gst_transporter_id',
+ 'default': frm.doc.gst_transporter_id
+ },
+ {
+ 'fieldname': 'driver',
+ 'label': 'Driver',
+ 'fieldtype': 'Link',
+ 'options': 'Driver',
+ 'default': frm.doc.driver
+ },
+ {
+ 'fieldname': 'lr_no',
+ 'label': 'Transport Receipt No',
+ 'fieldtype': 'Data',
+ 'default': frm.doc.lr_no
+ },
+ {
+ 'fieldname': 'vehicle_no',
+ 'label': 'Vehicle No',
+ 'fieldtype': 'Data',
+ 'default': frm.doc.vehicle_no
+ },
+ {
+ 'fieldname': 'distance',
+ 'label': 'Distance (in km)',
+ 'fieldtype': 'Float',
+ 'default': frm.doc.distance
+ },
+ {
+ 'fieldname': 'transporter_col_break',
+ 'fieldtype': 'Column Break',
+ },
+ {
+ 'fieldname': 'transporter_name',
+ 'label': 'Transporter Name',
+ 'fieldtype': 'Data',
+ 'fetch_from': 'transporter.name',
+ 'read_only': 1,
+ 'default': frm.doc.transporter_name
+ },
+ {
+ 'fieldname': 'mode_of_transport',
+ 'label': 'Mode of Transport',
+ 'fieldtype': 'Select',
+ 'options': `\nRoad\nAir\nRail\nShip`,
+ 'default': frm.doc.mode_of_transport
+ },
+ {
+ 'fieldname': 'driver_name',
+ 'label': 'Driver Name',
+ 'fieldtype': 'Data',
+ 'fetch_from': 'driver.full_name',
+ 'read_only': 1,
+ 'default': frm.doc.driver_name
+ },
+ {
+ 'fieldname': 'lr_date',
+ 'label': 'Transport Receipt Date',
+ 'fieldtype': 'Date',
+ 'default': frm.doc.lr_date
+ },
+ {
+ 'fieldname': 'gst_vehicle_type',
+ 'label': 'GST Vehicle Type',
+ 'fieldtype': 'Select',
+ 'options': `Regular\nOver Dimensional Cargo (ODC)`,
+ 'depends_on': 'eval:(doc.mode_of_transport === "Road")',
+ 'default': frm.doc.gst_vehicle_type
+ }
+ ];
+};
+
+const request_irn_generation = (frm) => {
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.generate_irn',
+ args: { doctype: frm.doc.doctype, docname: frm.doc.name },
+ freeze: true,
+ callback: () => frm.reload_doc()
+ });
+};
+
+const get_preview_dialog = (frm, action) => {
+ const dialog = new frappe.ui.Dialog({
+ title: __("Preview"),
+ size: "large",
+ fields: [
+ {
+ "label": "Preview",
+ "fieldname": "preview_html",
+ "fieldtype": "HTML"
+ }
+ ],
+ primary_action: () => action(frm) || dialog.hide(),
+ primary_action_label: __('Generate IRN')
+ });
+ return dialog;
+};
+
+const show_einvoice_preview = (frm, einvoice) => {
+ const preview_dialog = get_preview_dialog(frm, request_irn_generation);
+
+ // initialize e-invoice fields
+ einvoice["Irn"] = einvoice["AckNo"] = ''; einvoice["AckDt"] = frappe.datetime.nowdate();
+ frm.doc.signed_einvoice = JSON.stringify(einvoice);
+
+ // initialize preview wrapper
+ const $preview_wrapper = preview_dialog.get_field("preview_html").$wrapper;
+ $preview_wrapper.html(
+ ``
+ );
+
+ frappe.call({
+ method: "frappe.www.printview.get_html_and_style",
+ args: {
+ doc: frm.doc,
+ print_format: "GST E-Invoice",
+ no_letterhead: 1
+ },
+ callback: function (r) {
+ if (!r.exc) {
+ $preview_wrapper.find(".print-format").html(r.message.html);
+ const style = `
+ .print-format { box-shadow: 0px 0px 5px rgba(0,0,0,0.2); padding: 0.30in; min-height: 80vh; }
+ .print-preview { min-height: 0px; }
+ .modal-dialog { width: 720px; }`;
+
+ frappe.dom.set_style(style, "custom-print-style");
+ preview_dialog.show();
+ }
+ }
+ });
+};
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
new file mode 100644
index 0000000000..e3f7e90ff3
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -0,0 +1,1171 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import base64
+import io
+import json
+import os
+import re
+import sys
+import traceback
+
+import frappe
+import jwt
+from frappe import _, bold
+from frappe.core.page.background_jobs.background_jobs import get_info
+from frappe.integrations.utils import make_get_request, make_post_request
+from frappe.utils.background_jobs import enqueue
+from frappe.utils.data import (
+ add_to_date,
+ cint,
+ cstr,
+ flt,
+ format_date,
+ get_link_to_form,
+ getdate,
+ now_datetime,
+ time_diff_in_hours,
+ time_diff_in_seconds,
+)
+from frappe.utils.scheduler import is_scheduler_inactive
+from pyqrcode import create as qrcreate
+
+from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
+
+
+@frappe.whitelist()
+def validate_eligibility(doc):
+ if isinstance(doc, str):
+ doc = json.loads(doc)
+
+ invalid_doctype = doc.get('doctype') != 'Sales Invoice'
+ if invalid_doctype:
+ return False
+
+ einvoicing_enabled = cint(frappe.db.get_single_value('E Invoice Settings', 'enable'))
+ if not einvoicing_enabled:
+ return False
+
+ einvoicing_eligible_from = frappe.db.get_single_value('E Invoice Settings', 'applicable_from') or '2021-04-01'
+ if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from):
+ return False
+
+ invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') })
+ invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
+ company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
+
+ # if export invoice, then taxes can be empty
+ # invoice can only be ineligible if no taxes applied and is not an export invoice
+ no_taxes_applied = not doc.get('taxes') and not doc.get('gst_category') == 'Overseas'
+ has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst'))
+
+ if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item:
+ return False
+
+ return True
+
+def validate_einvoice_fields(doc):
+ invoice_eligible = validate_eligibility(doc)
+
+ if not invoice_eligible:
+ 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:
+ raise_document_name_too_long_error()
+
+ doc.einvoice_status = 'Pending'
+
+ 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.irn and 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 raise_document_name_too_long_error():
+ title = _('Document ID Too Long')
+ msg = _('As you have E-Invoicing enabled, to be able to generate IRN for this invoice')
+ msg += ', '
+ msg += _('document id {} exceed 16 letters.').format(bold(_('should not')))
+ msg += '
'
+ msg += _('You must {} your {} in order to have document id of {} length 16.').format(
+ bold(_('modify')), bold(_('naming series')), bold(_('maximum'))
+ )
+ msg += _('Please account for ammended documents too.')
+ frappe.throw(msg, title=title)
+
+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_transaction_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:
+ rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export')
+ frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export),
+ title=_('Invalid Supply Type'))
+
+ return frappe._dict(dict(
+ tax_scheme='GST',
+ supply_type=supply_type,
+ reverse_charge=invoice.reverse_charge
+ ))
+
+def get_doc_details(invoice):
+ if getdate(invoice.posting_date) < getdate('2021-01-01'):
+ frappe.throw(_('IRN generation is not allowed for invoices dated before 1st Jan 2021'), title=_('Not Allowed'))
+
+ 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 validate_address_fields(address, skip_gstin_validation):
+ if ((not address.gstin and not skip_gstin_validation)
+ or not address.city
+ or not address.pincode
+ or not address.address_title
+ or not address.address_line1
+ or not address.gst_state_number):
+
+ frappe.throw(
+ msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name),
+ title=_('Missing Address Fields')
+ )
+
+ if address.address_line2 and len(address.address_line2) < 2:
+ # to prevent "The field Address 2 must be a string with a minimum length of 3 and a maximum length of 100"
+ address.address_line2 = ""
+
+def get_party_details(address_name, skip_gstin_validation=False):
+ addr = frappe.get_doc('Address', address_name)
+
+ validate_address_fields(addr, skip_gstin_validation)
+
+ if addr.gst_state_number == 97:
+ # according to einvoice standard
+ addr.pincode = 999999
+
+ party_address_details = frappe._dict(dict(
+ legal_name=sanitize_for_json(addr.address_title),
+ location=sanitize_for_json(addr.city),
+ pincode=addr.pincode, gstin=addr.gstin,
+ state_code=addr.gst_state_number,
+ address_line1=sanitize_for_json(addr.address_line1),
+ address_line2=sanitize_for_json(addr.address_line2)
+ ))
+
+ return party_address_details
+
+def get_overseas_address_details(address_name):
+ address_title, address_line1, address_line2, city = frappe.db.get_value(
+ 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city']
+ )
+
+ if not address_title or not address_line1 or not city:
+ frappe.throw(
+ msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format(
+ get_link_to_form('Address', address_name)
+ ),
+ title=_('Missing Address Fields')
+ )
+
+ return frappe._dict(dict(
+ gstin='URP',
+ legal_name=sanitize_for_json(address_title),
+ location=city,
+ address_line1=sanitize_for_json(address_line1),
+ address_line2=sanitize_for_json(address_line2),
+ pincode=999999, state_code=96, place_of_supply=96
+ ))
+
+def get_item_list(invoice):
+ item_list = []
+
+ for d in invoice.items:
+ einvoice_item_schema = read_json('einv_item_template')
+ item = frappe._dict({})
+ item.update(d.as_dict())
+
+ item.sr_no = d.idx
+ item.description = sanitize_for_json(d.item_name)
+
+ item.qty = abs(item.qty)
+ if flt(item.qty) != 0.0:
+ item.unit_rate = abs(item.taxable_value / item.qty)
+ else:
+ item.unit_rate = abs(item.taxable_value)
+ item.gross_amount = abs(item.taxable_value)
+ item.taxable_value = abs(item.taxable_value)
+ item.discount_amount = 0
+
+ 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.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N'
+ item.serial_no = ""
+
+ item = update_item_taxes(invoice, item)
+
+ item.total_value = abs(
+ item.taxable_value + item.igst_amount + item.sgst_amount +
+ item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges
+ )
+ einv_item = einvoice_item_schema.format(item=item)
+ item_list.append(einv_item)
+
+ return ', '.join(item_list)
+
+def update_item_taxes(invoice, item):
+ gst_accounts = get_gst_accounts(invoice.company)
+ gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
+
+ for attr in [
+ 'tax_rate', 'cess_rate', 'cess_nadv_amount',
+ 'cgst_amount', 'sgst_amount', 'igst_amount',
+ 'cess_amount', 'cess_nadv_amount', 'other_charges'
+ ]:
+ item[attr] = 0
+
+ for t in invoice.taxes:
+ is_applicable = t.tax_amount and t.account_head in gst_accounts_list
+ if is_applicable:
+ # this contains item wise tax rate & tax amount (incl. discount)
+ item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code or item.item_name)
+
+ item_tax_rate = item_tax_detail[0]
+ # item tax amount excluding discount amount
+ item_tax_amount = (item_tax_rate / 100) * item.taxable_value
+
+ if t.account_head in gst_accounts.cess_account:
+ item_tax_amount_after_discount = item_tax_detail[1]
+ if t.charge_type == 'On Item Quantity':
+ item.cess_nadv_amount += abs(item_tax_amount_after_discount)
+ else:
+ item.cess_rate += item_tax_rate
+ item.cess_amount += abs(item_tax_amount_after_discount)
+
+ for tax_type in ['igst', 'cgst', 'sgst']:
+ if t.account_head in gst_accounts[f'{tax_type}_account']:
+ item.tax_rate += item_tax_rate
+ item[f'{tax_type}_amount'] += abs(item_tax_amount)
+ else:
+ # TODO: other charges per item
+ pass
+
+ return item
+
+def get_invoice_value_details(invoice):
+ invoice_value_details = frappe._dict(dict())
+ invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
+ invoice_value_details.invoice_discount_amt = 0
+
+ invoice_value_details.round_off = invoice.base_rounding_adjustment
+ invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
+ invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
+
+ invoice_value_details = update_invoice_taxes(invoice, invoice_value_details)
+
+ return invoice_value_details
+
+def update_invoice_taxes(invoice, invoice_value_details):
+ gst_accounts = get_gst_accounts(invoice.company)
+ gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
+
+ invoice_value_details.total_cgst_amt = 0
+ invoice_value_details.total_sgst_amt = 0
+ invoice_value_details.total_igst_amt = 0
+ invoice_value_details.total_cess_amt = 0
+ invoice_value_details.total_other_charges = 0
+ considered_rows = []
+
+ for t in invoice.taxes:
+ tax_amount = t.base_tax_amount_after_discount_amount
+ if t.account_head in gst_accounts_list:
+ if t.account_head in gst_accounts.cess_account:
+ # using after discount amt since item also uses after discount amt for cess calc
+ invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount)
+
+ for tax_type in ['igst', 'cgst', 'sgst']:
+ if t.account_head in gst_accounts[f'{tax_type}_account']:
+
+ invoice_value_details[f'total_{tax_type}_amt'] += abs(tax_amount)
+ update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows)
+ else:
+ invoice_value_details.total_other_charges += abs(tax_amount)
+
+ return invoice_value_details
+
+def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows):
+ prev_row_id = cint(tax_row.row_id) - 1
+ if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows:
+ if tax_row.charge_type == 'On Previous Row Amount':
+ amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount
+ invoice_value_details.total_other_charges -= abs(amount)
+ considered_rows.append(prev_row_id)
+ if tax_row.charge_type == 'On Previous Row Total':
+ amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total
+ invoice_value_details.total_other_charges -= abs(amount)
+ considered_rows.append(prev_row_id)
+
+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 invoice.is_return:
+ frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'),
+ title=_('Invalid Fields'))
+
+
+ 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 or 0,
+ 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]
+ ))
+
+def validate_mandatory_fields(invoice):
+ if not invoice.company_address:
+ frappe.throw(
+ _('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'),
+ title=_('Missing Fields')
+ )
+ if not invoice.customer_address:
+ frappe.throw(
+ _('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'),
+ title=_('Missing Fields')
+ )
+ if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
+ frappe.throw(
+ _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
+ title=_('Missing Fields')
+ )
+ if invoice.gst_category != 'Overseas' and not frappe.db.get_value('Address', invoice.customer_address, 'gstin'):
+ frappe.throw(
+ _('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'),
+ title=_('Missing Fields')
+ )
+
+def validate_totals(einvoice):
+ item_list = einvoice['ItemList']
+ value_details = einvoice['ValDtls']
+
+ total_item_ass_value = 0
+ total_item_cgst_value = 0
+ total_item_sgst_value = 0
+ total_item_igst_value = 0
+ total_item_value = 0
+ for item in item_list:
+ total_item_ass_value += flt(item['AssAmt'])
+ total_item_cgst_value += flt(item['CgstAmt'])
+ total_item_sgst_value += flt(item['SgstAmt'])
+ total_item_igst_value += flt(item['IgstAmt'])
+ total_item_value += flt(item['TotItemVal'])
+
+ if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1:
+ frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx))
+
+ if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1:
+ frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.'))
+
+ if abs(flt(value_details['CgstVal']) + flt(value_details['SgstVal']) - total_item_cgst_value - total_item_sgst_value) > 1:
+ frappe.throw(_('CGST + SGST value of the items is not equal to total CGST + SGST value. Please review taxes for any correction.'))
+
+ if abs(flt(value_details['IgstVal']) - total_item_igst_value) > 1:
+ frappe.throw(_('IGST value of all items is not equal to total IGST value. Please review taxes for any correction.'))
+
+ if abs(
+ flt(value_details['TotInvVal']) + flt(value_details['Discount']) -
+ flt(value_details['OthChrg']) - flt(value_details['RndOffAmt']) -
+ total_item_value
+ ) > 1:
+ frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.'))
+
+ calculated_invoice_value = \
+ flt(value_details['AssVal']) + flt(value_details['CgstVal']) \
+ + flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \
+ + flt(value_details['OthChrg']) + flt(value_details['RndOffAmt']) - flt(value_details['Discount'])
+
+ if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1:
+ frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.'))
+
+def make_einvoice(invoice):
+ validate_mandatory_fields(invoice)
+
+ schema = read_json('einv_template')
+
+ transaction_details = get_transaction_details(invoice)
+ item_list = get_item_list(invoice)
+ doc_details = get_doc_details(invoice)
+ invoice_value_details = get_invoice_value_details(invoice)
+ seller_details = get_party_details(invoice.company_address)
+
+ if invoice.gst_category == 'Overseas':
+ buyer_details = get_overseas_address_details(invoice.customer_address)
+ else:
+ buyer_details = get_party_details(invoice.customer_address)
+ place_of_supply = get_place_of_supply(invoice, invoice.doctype)
+ if place_of_supply:
+ place_of_supply = place_of_supply.split('-')[0]
+ else:
+ place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2]
+ buyer_details.update(dict(place_of_supply=place_of_supply))
+
+ seller_details.update(dict(legal_name=invoice.company))
+ buyer_details.update(dict(legal_name=invoice.customer_name or invoice.customer))
+
+ 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:
+ if invoice.gst_category == 'Overseas':
+ shipping_details = get_overseas_address_details(invoice.shipping_address_name)
+ else:
+ shipping_details = get_party_details(invoice.shipping_address_name, skip_gstin_validation=True)
+
+ dispatch_details = frappe._dict({})
+ if invoice.dispatch_address_name:
+ dispatch_details = get_party_details(invoice.dispatch_address_name, skip_gstin_validation=True)
+
+ 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 and not invoice.is_return:
+ eway_bill_details = get_eway_bill_details(invoice)
+
+ # not yet implemented
+ period_details = export_details = frappe._dict({})
+
+ einvoice = schema.format(
+ transaction_details=transaction_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, invoice_value_details=invoice_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
+ )
+
+ try:
+ einvoice = safe_json_load(einvoice)
+ einvoice = santize_einvoice_fields(einvoice)
+ except Exception:
+ show_link_to_error_log(invoice, einvoice)
+
+ try:
+ validate_totals(einvoice)
+ except Exception:
+ log_error(einvoice)
+ raise
+
+ return einvoice
+
+def show_link_to_error_log(invoice, einvoice):
+ err_log = log_error(einvoice)
+ link_to_error_log = get_link_to_form('Error Log', err_log.name, 'Error Log')
+ frappe.throw(
+ _('An error occurred while creating e-invoice for {}. Please check {} for more information.').format(
+ invoice.name, link_to_error_log),
+ title=_('E Invoice Creation Failed')
+ )
+
+def log_error(data=None):
+ if isinstance(data, str):
+ data = json.loads(data)
+
+ seperator = "--" * 50
+ err_tb = traceback.format_exc()
+ err_msg = str(sys.exc_info()[1])
+ data = json.dumps(data, indent=4)
+
+ message = "\n".join([
+ "Error", err_msg, seperator,
+ "Data:", data, seperator,
+ "Exception:", err_tb
+ ])
+ return frappe.log_error(title=_('E Invoice Request Failed'), message=message)
+
+def santize_einvoice_fields(einvoice):
+ int_fields = ["Pin","Distance","CrDay"]
+ float_fields = ["Qty","FreeQty","UnitPrice","TotAmt","Discount","PreTaxVal","AssAmt","GstRt","IgstAmt","CgstAmt","SgstAmt","CesRt","CesAmt","CesNonAdvlAmt","StateCesRt","StateCesAmt","StateCesNonAdvlAmt","OthChrg","TotItemVal","AssVal","CgstVal","SgstVal","IgstVal","CesVal","StCesVal","Discount","OthChrg","RndOffAmt","TotInvVal","TotInvValFc","PaidAmt","PaymtDue","ExpDuty",]
+ copy = einvoice.copy()
+ for key, value in copy.items():
+ if isinstance(value, list):
+ for idx, d in enumerate(value):
+ santized_dict = santize_einvoice_fields(d)
+ if santized_dict:
+ einvoice[key][idx] = santized_dict
+ else:
+ einvoice[key].pop(idx)
+
+ if not einvoice[key]:
+ einvoice.pop(key, None)
+
+ elif isinstance(value, dict):
+ santized_dict = santize_einvoice_fields(value)
+ if santized_dict:
+ einvoice[key] = santized_dict
+ else:
+ einvoice.pop(key, None)
+
+ elif not value or value == "None":
+ einvoice.pop(key, None)
+
+ elif key in float_fields:
+ einvoice[key] = flt(value, 2)
+
+ elif key in int_fields:
+ einvoice[key] = cint(value)
+
+ return einvoice
+
+def safe_json_load(json_string):
+ try:
+ return json.loads(json_string)
+ except json.JSONDecodeError as e:
+ # print a snippet of 40 characters around the location where error occured
+ pos = e.pos
+ start, end = max(0, pos-20), min(len(json_string)-1, pos+20)
+ snippet = json_string[start:end]
+ frappe.throw(_("Error in input data. Please check for any special characters near following input:
{}").format(snippet))
+
+class RequestFailed(Exception):
+ pass
+class CancellationNotAllowed(Exception):
+ pass
+
+class GSPConnector():
+ def __init__(self, doctype=None, docname=None):
+ self.doctype = doctype
+ self.docname = docname
+
+ self.set_invoice()
+ self.set_credentials()
+
+ # authenticate url is same for sandbox & live
+ self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token'
+ self.base_url = 'https://gsp.adaequare.com' if not self.e_invoice_settings.sandbox_mode else 'https://gsp.adaequare.com/test'
+
+ self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel'
+ self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
+ self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
+ self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
+ self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB'
+ self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
+
+ def set_invoice(self):
+ self.invoice = None
+ if self.doctype and self.docname:
+ self.invoice = frappe.get_cached_doc(self.doctype, self.docname)
+
+ def set_credentials(self):
+ self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings')
+
+ if not self.e_invoice_settings.enable:
+ frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings")))
+
+ if self.invoice:
+ gstin = self.get_seller_gstin()
+ credentials_for_gstin = [d for d in self.e_invoice_settings.credentials if d.gstin == gstin]
+ if credentials_for_gstin:
+ self.credentials = credentials_for_gstin[0]
+ else:
+ frappe.throw(_('Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings'))
+ else:
+ self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
+
+ def get_seller_gstin(self):
+ gstin = frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
+ if not gstin:
+ frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.'))
+ return gstin
+
+ def get_auth_token(self):
+ if time_diff_in_seconds(self.e_invoice_settings.token_expiry, now_datetime()) < 150.0:
+ self.fetch_auth_token()
+
+ return self.e_invoice_settings.auth_token
+
+ def make_request(self, request_type, url, headers=None, data=None):
+ if request_type == 'post':
+ res = make_post_request(url, headers=headers, data=data)
+ else:
+ res = make_get_request(url, headers=headers, data=data)
+
+ self.log_request(url, headers, data, res)
+ return res
+
+ def log_request(self, url, headers, data, res):
+ headers.update({ 'password': self.credentials.password })
+ request_log = frappe.get_doc({
+ "doctype": "E Invoice Request Log",
+ "user": frappe.session.user,
+ "reference_invoice": self.invoice.name if self.invoice else None,
+ "url": url,
+ "headers": json.dumps(headers, indent=4) if headers else None,
+ "data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
+ "response": json.dumps(res, indent=4) if res else None
+ })
+ request_log.save(ignore_permissions=True)
+ frappe.db.commit()
+
+ def get_client_credentials(self):
+ if self.e_invoice_settings.client_id and self.e_invoice_settings.client_secret:
+ return self.e_invoice_settings.client_id, self.e_invoice_settings.get_password('client_secret')
+
+ return frappe.conf.einvoice_client_id, frappe.conf.einvoice_client_secret
+
+ def fetch_auth_token(self):
+ client_id, client_secret = self.get_client_credentials()
+ headers = {
+ 'gspappid': client_id,
+ 'gspappsecret': client_secret
+ }
+ res = {}
+ try:
+ res = self.make_request('post', self.authenticate_url, headers)
+ self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token'))
+ self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in'))
+ self.e_invoice_settings.save(ignore_permissions=True)
+ self.e_invoice_settings.reload()
+
+ except Exception:
+ log_error(res)
+ self.raise_error(True)
+
+ def get_headers(self):
+ return {
+ 'content-type': 'application/json',
+ 'user_name': self.credentials.username,
+ 'password': self.credentials.get_password(),
+ 'gstin': self.credentials.gstin,
+ 'authorization': self.get_auth_token(),
+ 'requestid': str(base64.b64encode(os.urandom(18))),
+ }
+
+ def fetch_gstin_details(self, gstin):
+ headers = self.get_headers()
+
+ try:
+ params = '?gstin={gstin}'.format(gstin=gstin)
+ res = self.make_request('get', self.gstin_details_url + params, headers)
+ if res.get('success'):
+ return res.get('result')
+ else:
+ log_error(res)
+ raise RequestFailed
+
+ except RequestFailed:
+ self.raise_error()
+
+ except Exception:
+ log_error()
+ self.raise_error(True)
+ @staticmethod
+ def get_gstin_details(gstin):
+ '''fetch and cache GSTIN details'''
+ if not hasattr(frappe.local, 'gstin_cache'):
+ frappe.local.gstin_cache = {}
+
+ key = gstin
+ gsp_connector = GSPConnector()
+ details = gsp_connector.fetch_gstin_details(gstin)
+
+ frappe.local.gstin_cache[key] = details
+ frappe.cache().hset('gstin_cache', key, details)
+ return details
+
+ def generate_irn(self):
+ data = {}
+ try:
+ headers = self.get_headers()
+ einvoice = make_einvoice(self.invoice)
+ data = json.dumps(einvoice, indent=4)
+ res = self.make_request('post', self.generate_irn_url, headers, data)
+
+ if res.get('success'):
+ self.set_einvoice_data(res.get('result'))
+
+ elif '2150' in res.get('message'):
+ # IRN already generated but not updated in invoice
+ # Extract the IRN from the response description and fetch irn details
+ irn = res.get('result')[0].get('Desc').get('Irn')
+ irn_details = self.get_irn_details(irn)
+ if irn_details:
+ self.set_einvoice_data(irn_details)
+ else:
+ raise RequestFailed('IRN has already been generated for the invoice but cannot fetch details for the it. \
+ Contact ERPNext support to resolve the issue.')
+
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.set_failed_status(errors=errors)
+ self.raise_error(errors=errors)
+
+ except Exception as e:
+ self.set_failed_status(errors=str(e))
+ log_error(data)
+ self.raise_error(True)
+
+ @staticmethod
+ def bulk_generate_irn(invoices):
+ gsp_connector = GSPConnector()
+ gsp_connector.doctype = 'Sales Invoice'
+
+ failed = []
+
+ for invoice in invoices:
+ try:
+ gsp_connector.docname = invoice
+ gsp_connector.set_invoice()
+ gsp_connector.set_credentials()
+ gsp_connector.generate_irn()
+
+ except Exception as e:
+ failed.append({
+ 'docname': invoice,
+ 'message': str(e)
+ })
+
+ return failed
+
+ def get_irn_details(self, irn):
+ headers = self.get_headers()
+
+ try:
+ params = '?irn={irn}'.format(irn=irn)
+ res = self.make_request('get', self.irn_details_url + params, headers)
+ if res.get('success'):
+ return res.get('result')
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.raise_error(errors=errors)
+
+ except Exception:
+ log_error()
+ self.raise_error(True)
+
+ def cancel_irn(self, irn, reason, remark):
+ data, res = {}, {}
+ try:
+ # validate cancellation
+ if time_diff_in_hours(now_datetime(), self.invoice.ack_date) > 24:
+ frappe.throw(_('E-Invoice cannot be cancelled after 24 hours of IRN generation.'), title=_('Not Allowed'), exc=CancellationNotAllowed)
+ if not irn:
+ frappe.throw(_('IRN not found. You must generate IRN before cancelling.'), title=_('Not Allowed'), exc=CancellationNotAllowed)
+
+ headers = self.get_headers()
+ data = json.dumps({
+ 'Irn': irn,
+ 'Cnlrsn': reason,
+ 'Cnlrem': remark
+ }, indent=4)
+
+ res = self.make_request('post', self.cancel_irn_url, headers, data)
+ if res.get('success') or '9999' in res.get('message'):
+ self.invoice.irn_cancelled = 1
+ self.invoice.irn_cancel_date = res.get('result')['CancelDate'] if res.get('result') else ""
+ self.invoice.einvoice_status = 'Cancelled'
+ self.invoice.flags.updater_reference = {
+ 'doctype': self.invoice.doctype,
+ 'docname': self.invoice.name,
+ 'label': _('IRN Cancelled - {}').format(remark)
+ }
+ self.update_invoice()
+
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.set_failed_status(errors=errors)
+ self.raise_error(errors=errors)
+
+ except CancellationNotAllowed as e:
+ self.set_failed_status(errors=str(e))
+ self.raise_error(errors=str(e))
+
+ except Exception as e:
+ self.set_failed_status(errors=str(e))
+ log_error(data)
+ self.raise_error(True)
+
+ @staticmethod
+ def bulk_cancel_irn(invoices, reason, remark):
+ gsp_connector = GSPConnector()
+ gsp_connector.doctype = 'Sales Invoice'
+
+ failed = []
+
+ for invoice in invoices:
+ try:
+ gsp_connector.docname = invoice
+ gsp_connector.set_invoice()
+ gsp_connector.set_credentials()
+ irn = gsp_connector.invoice.irn
+ gsp_connector.cancel_irn(irn, reason, remark)
+
+ except Exception as e:
+ failed.append({
+ 'docname': invoice,
+ 'message': str(e)
+ })
+
+ return failed
+
+ def generate_eway_bill(self, **kwargs):
+ args = frappe._dict(kwargs)
+
+ headers = self.get_headers()
+ eway_bill_details = get_eway_bill_details(args)
+ data = json.dumps({
+ 'Irn': args.irn,
+ 'Distance': cint(eway_bill_details.distance),
+ 'TransMode': eway_bill_details.mode_of_transport,
+ 'TransId': eway_bill_details.gstin,
+ 'TransName': eway_bill_details.transporter,
+ 'TrnDocDt': eway_bill_details.document_date,
+ 'TrnDocNo': eway_bill_details.document_name,
+ 'VehNo': eway_bill_details.vehicle_no,
+ 'VehType': eway_bill_details.vehicle_type
+ }, indent=4)
+
+ try:
+ res = self.make_request('post', self.generate_ewaybill_url, headers, data)
+ if res.get('success'):
+ self.invoice.ewaybill = res.get('result').get('EwbNo')
+ self.invoice.eway_bill_validity = res.get('result').get('EwbValidTill')
+ self.invoice.eway_bill_cancelled = 0
+ self.invoice.update(args)
+ self.invoice.flags.updater_reference = {
+ 'doctype': self.invoice.doctype,
+ 'docname': self.invoice.name,
+ 'label': _('E-Way Bill Generated')
+ }
+ self.update_invoice()
+
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.raise_error(errors=errors)
+
+ except Exception:
+ log_error(data)
+ self.raise_error(True)
+
+ def cancel_eway_bill(self, eway_bill, reason, remark):
+ headers = self.get_headers()
+ data = json.dumps({
+ 'ewbNo': eway_bill,
+ 'cancelRsnCode': reason,
+ 'cancelRmrk': remark
+ }, indent=4)
+ headers["username"] = headers["user_name"]
+ del headers["user_name"]
+ try:
+ res = self.make_request('post', self.cancel_ewaybill_url, headers, data)
+ if res.get('success'):
+ self.invoice.ewaybill = ''
+ self.invoice.eway_bill_cancelled = 1
+ self.invoice.flags.updater_reference = {
+ 'doctype': self.invoice.doctype,
+ 'docname': self.invoice.name,
+ 'label': _('E-Way Bill Cancelled - {}').format(remark)
+ }
+ self.update_invoice()
+
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.raise_error(errors=errors)
+
+ except Exception:
+ log_error(data)
+ self.raise_error(True)
+
+ def sanitize_error_message(self, message):
+ '''
+ On validation errors, response message looks something like this:
+ message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable,
+ 3095 : Supplier GSTIN is inactive'
+ we search for string between ':' to extract the error messages
+ errors = [
+ ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ',
+ ': Test'
+ ]
+ then we trim down the message by looping over errors
+ '''
+ if not message:
+ return []
+
+ errors = re.findall(': [^:]+', message)
+ for idx, e in enumerate(errors):
+ # remove colons
+ errors[idx] = errors[idx].replace(':', '').strip()
+ # if not last
+ if idx != len(errors) - 1:
+ # remove last 7 chars eg: ', 3095 '
+ errors[idx] = errors[idx][:-6]
+
+ return errors
+
+ def raise_error(self, raise_exception=False, errors=None):
+ if errors is None:
+ errors = []
+ title = _('E Invoice Request Failed')
+ if errors:
+ frappe.throw(errors, title=title, as_list=1)
+ else:
+ link_to_error_list = 'Error Log'
+ frappe.msgprint(
+ _('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list),
+ title=title,
+ raise_exception=raise_exception,
+ indicator='red'
+ )
+
+ def set_einvoice_data(self, res):
+ enc_signed_invoice = res.get('SignedInvoice')
+ dec_signed_invoice = jwt.decode(enc_signed_invoice, options={"verify_signature": False})['data']
+
+ self.invoice.irn = res.get('Irn')
+ self.invoice.ewaybill = res.get('EwbNo')
+ self.invoice.eway_bill_validity = res.get('EwbValidTill')
+ self.invoice.ack_no = res.get('AckNo')
+ self.invoice.ack_date = res.get('AckDt')
+ self.invoice.signed_einvoice = dec_signed_invoice
+ self.invoice.ack_no = res.get('AckNo')
+ self.invoice.ack_date = res.get('AckDt')
+ self.invoice.signed_qr_code = res.get('SignedQRCode')
+ self.invoice.einvoice_status = 'Generated'
+
+ self.attach_qrcode_image()
+
+ self.invoice.flags.updater_reference = {
+ 'doctype': self.invoice.doctype,
+ 'docname': self.invoice.name,
+ 'label': _('IRN Generated')
+ }
+ self.update_invoice()
+
+ def attach_qrcode_image(self):
+ qrcode = self.invoice.signed_qr_code
+ doctype = self.invoice.doctype
+ docname = self.invoice.name
+ filename = 'QRCode_{}.png'.format(docname).replace(os.path.sep, "__")
+
+ qr_image = io.BytesIO()
+ url = qrcreate(qrcode, error='L')
+ url.png(qr_image, scale=2, quiet_zone=1)
+ _file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": filename,
+ "attached_to_doctype": doctype,
+ "attached_to_name": docname,
+ "attached_to_field": "qrcode_image",
+ "is_private": 0,
+ "content": qr_image.getvalue()})
+ _file.save()
+ frappe.db.commit()
+ self.invoice.qrcode_image = _file.file_url
+
+ def update_invoice(self):
+ self.invoice.flags.ignore_validate_update_after_submit = True
+ self.invoice.flags.ignore_validate = True
+ self.invoice.save()
+
+ def set_failed_status(self, errors=None):
+ frappe.db.rollback()
+ self.invoice.einvoice_status = 'Failed'
+ self.invoice.failure_description = self.get_failure_message(errors) if errors else ""
+ self.update_invoice()
+ frappe.db.commit()
+
+ def get_failure_message(self, errors):
+ if isinstance(errors, list):
+ errors = ', '.join(errors)
+ return errors
+
+def sanitize_for_json(string):
+ """Escape JSON specific characters from a string."""
+
+ # json.dumps adds double-quotes to the string. Indexing to remove them.
+ return json.dumps(string)[1:-1]
+
+@frappe.whitelist()
+def get_einvoice(doctype, docname):
+ invoice = frappe.get_doc(doctype, docname)
+ return make_einvoice(invoice)
+
+@frappe.whitelist()
+def generate_irn(doctype, docname):
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.generate_irn()
+
+@frappe.whitelist()
+def cancel_irn(doctype, docname, irn, reason, remark):
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.cancel_irn(irn, reason, remark)
+
+@frappe.whitelist()
+def generate_eway_bill(doctype, docname, **kwargs):
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.generate_eway_bill(**kwargs)
+
+@frappe.whitelist()
+def cancel_eway_bill(doctype, docname):
+ # TODO: uncomment when eway_bill api from Adequare is enabled
+ # gsp_connector = GSPConnector(doctype, docname)
+ # gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
+
+ frappe.db.set_value(doctype, docname, 'ewaybill', '')
+ frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1)
+
+@frappe.whitelist()
+def generate_einvoices(docnames):
+ docnames = json.loads(docnames) or []
+
+ if len(docnames) < 10:
+ failures = GSPConnector.bulk_generate_irn(docnames)
+ frappe.local.message_log = []
+
+ if failures:
+ show_bulk_action_failure_message(failures)
+
+ success = len(docnames) - len(failures)
+ frappe.msgprint(
+ _('{} e-invoices generated successfully').format(success),
+ title=_('Bulk E-Invoice Generation Complete')
+ )
+
+ else:
+ enqueue_bulk_action(schedule_bulk_generate_irn, docnames=docnames)
+
+def schedule_bulk_generate_irn(docnames):
+ failures = GSPConnector.bulk_generate_irn(docnames)
+ frappe.local.message_log = []
+
+ frappe.publish_realtime("bulk_einvoice_generation_complete", {
+ "user": frappe.session.user,
+ "failures": failures,
+ "invoices": docnames
+ })
+
+def show_bulk_action_failure_message(failures):
+ for doc in failures:
+ docname = '{0}'.format(doc.get('docname'))
+ message = doc.get('message').replace("'", '"')
+ if message[0] == '[':
+ errors = json.loads(message)
+ error_list = ''.join(['{}'.format(err) for err in errors])
+ message = '''{} has following errors:
+ '''.format(docname, error_list)
+ else:
+ message = '{} - {}'.format(docname, message)
+
+ frappe.msgprint(
+ message,
+ title=_('Bulk E-Invoice Generation Complete'),
+ indicator='red'
+ )
+
+@frappe.whitelist()
+def cancel_irns(docnames, reason, remark):
+ docnames = json.loads(docnames) or []
+
+ if len(docnames) < 10:
+ failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark)
+ frappe.local.message_log = []
+
+ if failures:
+ show_bulk_action_failure_message(failures)
+
+ success = len(docnames) - len(failures)
+ frappe.msgprint(
+ _('{} e-invoices cancelled successfully').format(success),
+ title=_('Bulk E-Invoice Cancellation Complete')
+ )
+ else:
+ enqueue_bulk_action(schedule_bulk_cancel_irn, docnames=docnames, reason=reason, remark=remark)
+
+def schedule_bulk_cancel_irn(docnames, reason, remark):
+ failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark)
+ frappe.local.message_log = []
+
+ frappe.publish_realtime("bulk_einvoice_cancellation_complete", {
+ "user": frappe.session.user,
+ "failures": failures,
+ "invoices": docnames
+ })
+
+def enqueue_bulk_action(job, **kwargs):
+ check_scheduler_status()
+
+ enqueue(
+ job,
+ **kwargs,
+ queue="long",
+ timeout=10000,
+ event="processing_bulk_einvoice_action",
+ now=frappe.conf.developer_mode or frappe.flags.in_test,
+ )
+
+ if job == schedule_bulk_generate_irn:
+ msg = _('E-Invoices will be generated in a background process.')
+ else:
+ msg = _('E-Invoices will be cancelled in a background process.')
+
+ frappe.msgprint(msg, alert=1)
+
+def check_scheduler_status():
+ if is_scheduler_inactive() and not frappe.flags.in_test:
+ frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
+
+def job_already_enqueued(job_name):
+ enqueued_jobs = [d.get("job_name") for d in get_info()]
+ if job_name in enqueued_jobs:
+ return True
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 4b9942121a..074bd527e2 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -60,7 +60,7 @@ def create_hsn_codes(data, code_field):
def add_custom_roles_for_reports():
for report_name in ('GST Sales Register', 'GST Purchase Register',
- 'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill'):
+ 'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill', 'E-Invoice Summary'):
if not frappe.db.get_value('Custom Role', dict(report=report_name)):
frappe.get_doc(dict(
@@ -99,7 +99,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)
@@ -115,9 +115,11 @@ 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.set_value("Print Format", "GST POS Invoice", "disabled", 0)
frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0)
+ frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0)
def make_property_setters(patch=False):
# GST rules do not allow for an invoice no. bigger than 16 characters
@@ -453,7 +455,7 @@ def get_custom_fields():
'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,
@@ -481,6 +483,46 @@ def get_custom_fields():
fetch_from='customer_address.gstin', print_hide=1, read_only=1)
]
+ 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', allow_on_submit=1, insert_after='customer'),
+
+ dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1,
+ depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill'),
+
+ 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='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
+ print_hide=1, hidden=1),
+
+ dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
+ no_copy=1, print_hide=1),
+
+ dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
+
+ dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
+ no_copy=1, print_hide=1),
+
+ dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code',
+ no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image',
+ options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON',
+ hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1)
+ ]
+
custom_fields = {
'Address': [
dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data',
@@ -493,7 +535,7 @@ def get_custom_fields():
'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,
'POS Invoice': sales_invoice_gst_fields,
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category,
'Payment Entry': payment_entry_fields,
diff --git a/erpnext/regional/report/e_invoice_summary/__init__.py b/erpnext/regional/report/e_invoice_summary/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.js b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.js
new file mode 100644
index 0000000000..4713217d83
--- /dev/null
+++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.js
@@ -0,0 +1,55 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["E-Invoice Summary"] = {
+ "filters": [
+ {
+ "fieldtype": "Link",
+ "options": "Company",
+ "reqd": 1,
+ "fieldname": "company",
+ "label": __("Company"),
+ "default": frappe.defaults.get_user_default("Company"),
+ },
+ {
+ "fieldtype": "Link",
+ "options": "Customer",
+ "fieldname": "customer",
+ "label": __("Customer")
+ },
+ {
+ "fieldtype": "Date",
+ "reqd": 1,
+ "fieldname": "from_date",
+ "label": __("From Date"),
+ "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ },
+ {
+ "fieldtype": "Date",
+ "reqd": 1,
+ "fieldname": "to_date",
+ "label": __("To Date"),
+ "default": frappe.datetime.get_today(),
+ },
+ {
+ "fieldtype": "Select",
+ "fieldname": "status",
+ "label": __("Status"),
+ "options": "\nPending\nGenerated\nCancelled\nFailed"
+ }
+ ],
+
+ "formatter": function (value, row, column, data, default_formatter) {
+ value = default_formatter(value, row, column, data);
+
+ if (column.fieldname == "einvoice_status" && value) {
+ if (value == 'Pending') value = `${value}`;
+ else if (value == 'Generated') value = `${value}`;
+ else if (value == 'Cancelled') value = `${value}`;
+ else if (value == 'Failed') value = `${value}`;
+ }
+
+ return value;
+ }
+};
diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json
new file mode 100644
index 0000000000..d0000ad50d
--- /dev/null
+++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.json
@@ -0,0 +1,28 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-03-12 11:23:37.312294",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "json": "{}",
+ "letter_head": "Logo",
+ "modified": "2021-03-13 12:36:48.689413",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "E-Invoice Summary",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Sales Invoice",
+ "report_name": "E-Invoice Summary",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Administrator"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py
new file mode 100644
index 0000000000..2110c44447
--- /dev/null
+++ b/erpnext/regional/report/e_invoice_summary/e_invoice_summary.py
@@ -0,0 +1,110 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe import _
+
+
+def execute(filters=None):
+ validate_filters(filters)
+
+ columns = get_columns()
+ data = get_data(filters)
+
+ return columns, data
+
+def validate_filters(filters=None):
+ if filters is None:
+ filters = {}
+ filters = frappe._dict(filters)
+
+ if not filters.company:
+ frappe.throw(_('{} is mandatory for generating E-Invoice Summary Report').format(_('Company')), title=_('Invalid Filter'))
+ if filters.company:
+ # validate if company has e-invoicing enabled
+ pass
+ if not filters.from_date or not filters.to_date:
+ frappe.throw(_('From Date & To Date is mandatory for generating E-Invoice Summary Report'), title=_('Invalid Filter'))
+ if filters.from_date > filters.to_date:
+ frappe.throw(_('From Date must be before To Date'), title=_('Invalid Filter'))
+
+def get_data(filters=None):
+ if filters is None:
+ filters = {}
+ query_filters = {
+ 'posting_date': ['between', [filters.from_date, filters.to_date]],
+ 'einvoice_status': ['is', 'set'],
+ 'company': filters.company
+ }
+ if filters.customer:
+ query_filters['customer'] = filters.customer
+ if filters.status:
+ query_filters['einvoice_status'] = filters.status
+
+ data = frappe.get_all(
+ 'Sales Invoice',
+ filters=query_filters,
+ fields=[d.get('fieldname') for d in get_columns()]
+ )
+
+ return data
+
+def get_columns():
+ return [
+ {
+ "fieldtype": "Date",
+ "fieldname": "posting_date",
+ "label": _("Posting Date"),
+ "width": 0
+ },
+ {
+ "fieldtype": "Link",
+ "fieldname": "name",
+ "label": _("Sales Invoice"),
+ "options": "Sales Invoice",
+ "width": 140
+ },
+ {
+ "fieldtype": "Data",
+ "fieldname": "einvoice_status",
+ "label": _("Status"),
+ "width": 100
+ },
+ {
+ "fieldtype": "Link",
+ "fieldname": "customer",
+ "options": "Customer",
+ "label": _("Customer")
+ },
+ {
+ "fieldtype": "Check",
+ "fieldname": "is_return",
+ "label": _("Is Return"),
+ "width": 85
+ },
+ {
+ "fieldtype": "Data",
+ "fieldname": "ack_no",
+ "label": "Ack. No.",
+ "width": 145
+ },
+ {
+ "fieldtype": "Data",
+ "fieldname": "ack_date",
+ "label": "Ack. Date",
+ "width": 165
+ },
+ {
+ "fieldtype": "Data",
+ "fieldname": "irn",
+ "label": _("IRN No."),
+ "width": 250
+ },
+ {
+ "fieldtype": "Currency",
+ "options": "Company:company:default_currency",
+ "fieldname": "base_grand_total",
+ "label": _("Grand Total"),
+ "width": 120
+ }
+ ]