Revert "feat: e invoicing with JSON files" (#23925)

This commit is contained in:
Suraj Shetty
2020-11-17 11:44:55 +05:30
committed by GitHub
parent 780982dcc7
commit f202a49d80
22 changed files with 10 additions and 2112 deletions

View File

@@ -1,8 +1,6 @@
{% 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) {
@@ -48,6 +46,8 @@ frappe.ui.form.on("Sales Invoice", {
}, __("Create"));
}
}
},
});

View File

@@ -225,9 +225,9 @@ class SalesInvoice(SellingController):
frappe.throw(_("At least one mode of payment is required for POS invoice."))
def before_cancel(self):
super(SalesInvoice, self).before_cancel()
self.update_time_sheet(None)
def on_cancel(self):
super(SalesInvoice, self).on_cancel()

View File

@@ -1,147 +0,0 @@
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
<div class="page-break">
<div id="header-html" class="hidden-pdf">
<div class="print-heading">
<h2>E-Invoice</h2>
</div>
</div>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd;">
<h5 class="font-bold" style="margin-left: 15px; margin-top: 0px;">1. Transaction Details</h5>
<div class="col-xs-8 column-break">
<div class="row data-field">
<div class="col-xs-4"><label>IRN</label></div>
<div class="col-xs-8 value">{{ einvoice.Irn }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. No</label></div>
<div class="col-xs-8 value">{{ einvoice.AckNo }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. Date</label></div>
<div class="col-xs-8 value">{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Category</label></div>
<div class="col-xs-8 value">{{ einvoice.TranDtls.SupTyp }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document Type</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.Typ }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document No</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
</div>
</div>
<div class="col-xs-4 column-break">
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
</div>
</div>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
<h5 class="font-bold" style="margin-left: 15px; margin-bottom: 0px;">2. Party Details</h5>
{%- set seller = einvoice.SellerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Seller</h5>
<p>{{ seller.Gstin }}</p>
<p>{{ seller.LglNm }}</p>
<p>{{ seller.Addr1 }}</p>
{%- if seller.Addr2 -%} <p>{{ seller.Addr2 }}</p> {% endif %}
<p>{{ seller.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}</p>
{%- if einvoice.ShipDtls -%}
{%- set shipping = einvoice.ShipDtls -%}
<h5 style="margin-bottom: 5px;">Shipping</h5>
<p>{{ shipping.Gstin }}</p>
<p>{{ shipping.LglNm }}</p>
<p>{{ shipping.Addr1 }}</p>
{%- if shipping.Addr2 -%} <p>{{ shipping.Addr2 }}</p> {% endif %}
<p>{{ shipping.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}</p>
{% endif %}
</div>
{%- set buyer = einvoice.BuyerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Buyer</h5>
<p>{{ buyer.Gstin }}</p>
<p>{{ buyer.LglNm }}</p>
<p>{{ buyer.Addr1 }}</p>
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
<p>{{ buyer.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
</div>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-bottom: 0px;">3. Item Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left" style="width: 3%;">Sr. No.</th>
<th class="text-left">Item</th>
<th class="text-left" style="width: 10%;">HSN Code</th>
<th class="text-left" style="width: 5%;">Qty</th>
<th class="text-left" style="width: 5%;">UOM</th>
<th class="text-left">Rate</th>
<th class="text-left" style="width: 5%;">Discount</th>
<th class="text-left">Taxable Amount</th>
<th class="text-left" style="width: 7%;">Tax Rate</th>
<th class="text-left" style="width: 5%;">Other Charges</th>
<th class="text-left">Total</th>
</tr>
</thead>
<tbody>
{% for item in einvoice.ItemList %}
<tr>
<td class="text-left" style="width: 3%;">{{ item.SlNo }}</td>
<td class="text-left">{{ item.PrdDesc }}</td>
<td class="text-left" style="width: 10%;">{{ item.HsnCd }}</td>
<td class="text-right" style="width: 5%;">{{ item.Qty }}</td>
<td class="text-left" style="width: 5%;">{{ item.Unit }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}</td>
<td class="text-right" style="width: 7%;">{{ item.GstRt + item.CesRt }} %</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-bottom: 0px;">4. Value Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left">Taxable Amount</th>
<th class="text-left">CGST</th>
<th class="text-left"">SGST</th>
<th class="text-left">IGST</th>
<th class="text-left">CESS</th>
<th class="text-left" style="width: 10%;">State CESS</th>
<th class="text-left">Discount</th>
<th class="text-left" style="width: 10%;">Other Charges</th>
<th class="text-left" style="width: 10%;">Round Off</th>
<th class="text-left">Total Value</th>
</tr>
</thead>
<tbody>
{%- set value_details = einvoice.ValDtls -%}
<tr>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -1,24 +0,0 @@
{
"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"
}

View File

@@ -106,14 +106,8 @@ class AccountsController(TransactionBase):
self.validate_deferred_start_and_end_date()
validate_regional(self)
validate_einvoice_fields(self)
if self.doctype != 'Material Request':
apply_pricing_rule_on_transaction(self)
def before_cancel(self):
validate_einvoice_fields(self)
def validate_deferred_start_and_end_date(self):
for d in self.items:
@@ -1412,7 +1406,3 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
@erpnext.allow_regional
def validate_regional(doc):
pass
@erpnext.allow_regional
def validate_einvoice_fields(doc):
pass

View File

@@ -357,8 +357,7 @@ regional_overrides = {
'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details',
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries',
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields'
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries'
},
'United Arab Emirates': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data'

View File

@@ -677,5 +677,4 @@ erpnext.patches.v12_0.set_multi_uom_in_rfq
erpnext.patches.v12_0.update_state_code_for_daman_and_diu
erpnext.patches.v12_0.rename_lost_reason_detail
erpnext.patches.v12_0.update_leave_application_status
erpnext.patches.v12_0.update_payment_entry_status
erpnext.patches.v12_0.setup_einvoice_fields
erpnext.patches.v12_0.update_payment_entry_status

View File

@@ -1,31 +0,0 @@
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.regional.india.setup import add_permissions, add_print_formats
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
custom_fields = {
'Sales Invoice': [
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
]
}
create_custom_fields(custom_fields, update=True)
add_permissions()
add_print_formats()

View File

@@ -1,26 +0,0 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('E Invoice Settings', {
refresh: function(frm) {
if (!frm.doc.enable) return;
frm.trigger("show_fetch_token_btn");
},
show_fetch_token_btn(frm) {
const { token_expiry } = frm.doc;
const now = frappe.datetime.now_datetime();
const expiry_in_mins = moment(token_expiry).diff(now, "minute");
if (expiry_in_mins <= 1 || !token_expiry) {
frm.add_custom_button(__("Fetch Token"),
() => {
frm.call({
method: 'erpnext.regional.india.e_invoice.e_invoice_utils.fetch_token',
freeze: true,
callback: () => frm.refresh()
});
}
);
}
}
});

View File

@@ -1,120 +0,0 @@
{
"actions": [],
"creation": "2020-09-24 16:23:16.235722",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enable",
"section_break_2",
"client_id",
"client_secret",
"public_key_file",
"public_key",
"column_break_3",
"gstin",
"username",
"password",
"auto_refresh_token",
"auth_token",
"token_expiry",
"sek"
],
"fields": [
{
"default": "0",
"fieldname": "enable",
"fieldtype": "Check",
"label": "Enable"
},
{
"depends_on": "enable",
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"fieldname": "client_id",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Client ID",
"reqd": 1
},
{
"fieldname": "client_secret",
"fieldtype": "Data",
"label": "Client Secret",
"reqd": 1
},
{
"fieldname": "public_key_file",
"fieldtype": "Attach",
"label": "Public Key",
"reqd": 1
},
{
"fieldname": "public_key",
"fieldtype": "Long Text",
"hidden": 1,
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "gstin",
"fieldtype": "Data",
"label": "GSTIN",
"reqd": 1
},
{
"fieldname": "username",
"fieldtype": "Data",
"label": "Username",
"reqd": 1
},
{
"fieldname": "password",
"fieldtype": "Password",
"label": "Password",
"reqd": 1
},
{
"default": "0",
"description": "Token will be automatically refreshed 10 mins before expiry",
"fieldname": "auto_refresh_token",
"fieldtype": "Check",
"label": "Auto Refresh Token"
},
{
"fieldname": "auth_token",
"fieldtype": "Data",
"hidden": 1,
"read_only": 1
},
{
"fieldname": "token_expiry",
"fieldtype": "Datetime",
"hidden": 1,
"read_only": 1
},
{
"fieldname": "sek",
"fieldtype": "Data",
"hidden": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-10-23 19:55:11.417161",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice Settings",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,26 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils.data import cstr
from frappe.model.document import Document
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
class EInvoiceSettings(Document):
def validate(self):
mandatory_fields = ['client_id', 'client_secret', 'gstin', 'username', 'password', 'public_key_file']
for d in mandatory_fields:
if not self.get(d):
frappe.throw(_("{} is required").format(frappe.unscrub(d)), title=_("Missing Values"))
def before_save(self):
if not self.public_key or self.has_value_changed('public_key_file'):
self.public_key = self.read_key_file()
def read_key_file(self):
key_file = frappe.get_doc('File', dict(attached_to_name=self.doctype))
with open(key_file.get_full_path(), 'rb') as f:
return cstr(f.read())

View File

@@ -1,10 +0,0 @@
# -*- 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

View File

@@ -1,26 +0,0 @@
{{
"SlNo": "{item.sr_no}",
"PrdDesc": "{item.description}",
"IsServc": "{item.is_service_item}",
"HsnCd": "{item.gst_hsn_code}",
"Barcde": "{item.barcode}",
"Unit": "{item.uom}",
"Qty": "{item.qty}",
"FreeQty": "{item.free_qty}",
"UnitPrice": "{item.unit_rate}",
"TotAmt": "{item.total_amount}",
"Discount": "{item.discount_amount}",
"AssAmt": "{item.base_amount}",
"PrdSlNo": "{item.serial_no}",
"GstRt": "{item.tax_rate}",
"IgstAmt": "{item.igst_amount}",
"CgstAmt": "{item.cgst_amount}",
"SgstAmt": "{item.sgst_amount}",
"CesRt": "{item.cess_rate}",
"CesAmt": "{item.cess_amount}",
"TotItemVal": "{item.total_value}",
"BchDtls": {{
"Nm": "{item.batch_no}",
"ExpDt": "{item.batch_expiry_date}"
}}
}}

View File

@@ -1,109 +0,0 @@
{{
"Version": "1.1",
"TranDtls": {{
"TaxSch": "{trans_details.tax_scheme}",
"SupTyp": "{trans_details.supply_type}",
"RegRev": "{trans_details.reverse_charge}",
"EcmGstin": "{trans_details.ecom_gstin}",
"IgstOnIntra": "{trans_details.igst_on_intra}"
}},
"DocDtls": {{
"Typ": "{doc_details.invoice_type}",
"No": "{doc_details.invoice_name}",
"Dt": "{doc_details.invoice_date}"
}},
"SellerDtls": {{
"Gstin": "{seller_details.gstin}",
"LglNm": "{seller_details.legal_name}",
"TrdNm": "{seller_details.trade_name}",
"Loc": "{seller_details.location}",
"Pin": "{seller_details.pincode}",
"Stcd": "{seller_details.state_code}",
"Addr1": "{seller_details.address_line1}",
"Addr2": "{seller_details.address_line2}",
"Ph": "{seller_details.phone}",
"Em": "{seller_details.email}"
}},
"BuyerDtls": {{
"Gstin": "{buyer_details.gstin}",
"LglNm": "{buyer_details.legal_name}",
"TrdNm": "{buyer_details.trade_name}",
"Addr1": "{buyer_details.address_line1}",
"Addr2": "{buyer_details.address_line2}",
"Loc": "{buyer_details.location}",
"Pin": "{buyer_details.pincode}",
"Stcd": "{buyer_details.state_code}",
"Ph": "{buyer_details.phone}",
"Em": "{buyer_details.email}",
"Pos": "{buyer_details.place_of_supply}"
}},
"DispDtls": {{
"Nm": "{dispatch_details.company_name}",
"Addr1": "{dispatch_details.address_line1}",
"Addr2": "{dispatch_details.address_line2}",
"Loc": "{dispatch_details.location}",
"Pin": "{dispatch_details.pincode}",
"Stcd": "{dispatch_details.state_code}"
}},
"ShipDtls": {{
"Gstin": "{shipping_details.gstin}",
"LglNm": "{shipping_details.legal_name}",
"TrdNm": "{shipping_details.trader_name}",
"Addr1": "{shipping_details.address_line1}",
"Addr2": "{shipping_details.address_line2}",
"Loc": "{shipping_details.location}",
"Pin": "{shipping_details.pincode}",
"Stcd": "{shipping_details.state_code}"
}},
"ItemList": [
{item_list}
],
"ValDtls": {{
"AssVal": "{value_details.base_net_total}",
"CgstVal": "{value_details.total_cgst_amt}",
"SgstVal": "{value_details.total_sgst_amt}",
"IgstVal": "{value_details.total_igst_amt}",
"CesVal": "{value_details.total_cess_amt}",
"Discount": "{value_details.invoice_discount_amt}",
"RndOffAmt": "{value_details.round_off}",
"TotInvVal": "{value_details.base_grand_total}",
"TotInvValFc": "{value_details.grand_total}"
}},
"PayDtls": {{
"Nm": "{payment_details.payee_name}",
"AccDet": "{payment_details.account_no}",
"Mode": "{payment_details.mode_of_payment}",
"FinInsBr": "{payment_details.ifsc_code}",
"PayTerm": "{payment_details.terms}",
"PaidAmt": "{payment_details.paid_amount}",
"PaymtDue": "{payment_details.outstanding_amount}"
}},
"RefDtls": {{
"DocPerdDtls": {{
"InvStDt": "{period_details.start_date}",
"InvEndDt": "{period_details.end_date}"
}},
"PrecDocDtls": [{{
"InvNo": "{prev_doc_details.invoice_name}",
"InvDt": "{prev_doc_details.invoice_date}"
}}]
}},
"ExpDtls": {{
"ShipBNo": "{export_details.bill_no}",
"ShipBDt": "{export_details.bill_date}",
"Port": "{export_details.port}",
"ForCur": "{export_details.foreign_curr_code}",
"CntCode": "{export_details.country_code}",
"ExpDuty": "{export_details.export_duty}"
}},
"EwbDtls": {{
"TransId": "{eway_bill_details.gstin}",
"TransName": "{eway_bill_details.name}",
"TransMode": "{eway_bill_details.mode_of_transport}",
"Distance": "{eway_bill_details.distance}",
"TransDocNo": "{eway_bill_details.document_name}",
"TransDocDt": "{eway_bill_details.document_date}",
"VehNo": "{eway_bill_details.vehicle_no}",
"VehType": "{eway_bill_details.vehicle_type}"
}}
}}

View File

@@ -1,830 +0,0 @@
{
"Version": {
"type": "string",
"minLength": 1,
"maxLength": 6
},
"Irn": {
"type": "string",
"minLength": 64,
"maxLength": 64
},
"TranDtls": {
"type": "object",
"properties": {
"TaxSch": {
"type": "string",
"minLength": 3,
"maxLength": 10,
"enum": ["GST"]
},
"SupTyp": {
"type": "string",
"minLength": 3,
"maxLength": 10,
"enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"]
},
"RegRev": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"]
},
"EcmGstin": {
"type": "string",
"minLength": 15,
"maxLength": 15,
"pattern": "([0-9]{2}[0-9A-Z]{13})"
},
"IgstOnIntra": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"]
}
},
"required": ["TaxSch", "SupTyp"]
},
"DocDtls": {
"type": "object",
"properties": {
"Typ": {
"type": "string",
"minLength": 3,
"maxLength": 3,
"enum": ["INV", "CRN", "DBN"]
},
"No": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$",
"label": "Document Name",
"validationMsg": "Document name should not be starting with 0, / and -"
},
"Dt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"validationMsg": "Document Date is invalid"
}
},
"required": ["Typ", "No", "Dt"]
},
"SellerDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"minLength": 15,
"maxLength": 15,
"pattern": "([0-9]{2}[0-9A-Z]{13})",
"validationMsg": "Seller GSTIN is invalid"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr1": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 50
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2
},
"Ph": {
"type": "string",
"minLength": 6,
"maxLength": 12
},
"Em": {
"type": "string",
"minLength": 6,
"maxLength": 100
}
},
"required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"]
},
"BuyerDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"minLength": 3,
"maxLength": 15,
"pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
"validationMsg": "Buyer GSTIN is invalid"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Pos": {
"type": "string",
"minLength": 1,
"maxLength": 2
},
"Addr1": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2
},
"Ph": {
"type": "string",
"minLength": 6,
"maxLength": 12
},
"Em": {
"type": "string",
"minLength": 6,
"maxLength": 100
}
},
"required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"]
},
"DispDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr1": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2
}
},
"required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"]
},
"ShipDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"maxLength": 15,
"minLength": 3,
"pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
"validationMsg": "Shipping Address GSTIN is invalid"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr1": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2
}
},
"required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"]
},
"ItemList": {
"type": "Array",
"properties": {
"SlNo": {
"type": "string",
"minLength": 1,
"maxLength": 6
},
"PrdDesc": {
"type": "string",
"minLength": 3,
"maxLength": 300
},
"IsServc": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"]
},
"HsnCd": {
"type": "string",
"minLength": 4,
"maxLength": 8
},
"Barcde": {
"type": "string",
"minLength": 3,
"maxLength": 30
},
"Qty": {
"type": "number",
"minimum": 0,
"maximum": 9999999999.999
},
"FreeQty": {
"type": "number",
"minimum": 0,
"maximum": 9999999999.999
},
"Unit": {
"type": "string",
"minLength": 3,
"maxLength": 8
},
"UnitPrice": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.999
},
"TotAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"Discount": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"PreTaxVal": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"AssAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"GstRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999
},
"IgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"CgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"SgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"CesRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999
},
"CesAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"CesNonAdvlAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"StateCesRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999
},
"StateCesAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"StateCesNonAdvlAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"OthChrg": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"TotItemVal": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
},
"OrdLineRef": {
"type": "string",
"minLength": 1,
"maxLength": 50
},
"OrgCntry": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"PrdSlNo": {
"type": "string",
"minLength": 1,
"maxLength": 20
},
"BchDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 3,
"maxLength": 20
},
"ExpDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"validationMsg": "Expiry Date is invalid"
},
"WrDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"validationMsg": "Warranty Date is invalid"
}
},
"required": ["Nm"]
},
"AttribDtls": {
"type": "Array",
"Attribute": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"Val": {
"type": "string",
"minLength": 1,
"maxLength": 100
}
}
}
}
},
"required": [
"SlNo",
"IsServc",
"HsnCd",
"UnitPrice",
"TotAmt",
"AssAmt",
"GstRt",
"TotItemVal"
]
},
"ValDtls": {
"type": "object",
"properties": {
"AssVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"CgstVal": {
"type": "number",
"maximum": 99999999999999.99,
"minimum": 0
},
"SgstVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"IgstVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"CesVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"StCesVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"Discount": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"OthChrg": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"RndOffAmt": {
"type": "number",
"minimum": 0,
"maximum": 99.99
},
"TotInvVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"TotInvValFc": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
}
},
"required": ["AssVal", "TotInvVal"]
},
"PayDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"AccDet": {
"type": "string",
"minLength": 1,
"maxLength": 18
},
"Mode": {
"type": "string",
"minLength": 1,
"maxLength": 18
},
"FinInsBr": {
"type": "string",
"minLength": 1,
"maxLength": 11
},
"PayTerm": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"PayInstr": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"CrTrn": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"DirDr": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"CrDay": {
"type": "number",
"minimum": 0,
"maximum": 9999
},
"PaidAmt": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
},
"PaymtDue": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99
}
}
},
"RefDtls": {
"type": "object",
"properties": {
"InvRm": {
"type": "string",
"maxLength": 100,
"minLength": 3,
"pattern": "^[0-9A-Za-z/-]{3,100}$"
},
"DocPerdDtls": {
"type": "object",
"properties": {
"InvStDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"InvEndDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
}
},
"required": ["InvStDt", "InvEndDt"]
},
"PrecDocDtls": {
"type": "object",
"properties": {
"InvNo": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$"
},
"InvDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"OthRefNo": {
"type": "string",
"minLength": 1,
"maxLength": 20
}
},
"required": ["InvNo", "InvDt"]
},
"ContrDtls": {
"type": "object",
"properties": {
"RecAdvRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"RecAdvDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"TendRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"ContrRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"ExtRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"ProjRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$"
},
"PORefr": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^([0-9A-Za-z/-]){1,16}$"
},
"PORefDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
}
}
}
},
"required": ["InvStDt", "InvEndDt"]
},
"AddlDocDtls": {
"type": "Array",
"AddlDocument": {
"type": "object",
"properties": {
"Url": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"Docs": {
"type": "string",
"minLength": 3,
"maxLength": 1000
},
"Info": {
"type": "string",
"minLength": 3,
"maxLength": 1000
}
}
}
},
"ExpDtls": {
"type": "object",
"properties": {
"ShipBNo": {
"type": "string",
"minLength": 1,
"maxLength": 20
},
"ShipBDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"Port": {
"type": "string",
"minLength": 2,
"maxLength": 10,
"pattern": "^[0-9A-Za-z]{2,10}$"
},
"RefClm": {
"type": "string",
"minLength": 1,
"maxLength": 1
},
"ForCur": {
"type": "string",
"minLength": 3,
"maxLength": 16
},
"CntCode": {
"type": "string",
"minLength": 2,
"maxLength": 2
},
"ExpDuty": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99
}
}
},
"EwbDtls": {
"type": "object",
"properties": {
"TransId": {
"type": "string",
"minLength": 15,
"maxLength": 15
},
"TransName": {
"type": "string",
"minLength": 3,
"maxLength": 100
},
"TransMode": {
"type": "string",
"maxLength": 1,
"minLength": 1,
"enum": ["1", 2, 3, 4]
},
"Distance": {
"type": "number",
"minimum": 1,
"maximum": 9999
},
"TransDocNo": {
"type": "string",
"minLength": 1,
"maxLength": 15,
"pattern": "^([0-9A-Z/-]){1,15}$"
},
"TransDocDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]"
},
"VehNo": {
"type": "string",
"minLength": 4,
"maxLength": 20
},
"VehType": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["O", "R"]
}
},
"required": ["Distance"]
},
"required": [
"Version",
"TranDtls",
"DocDtls",
"SellerDtls",
"BuyerDtls",
"ItemList",
"ValDtls"
]
}

View File

@@ -1,96 +0,0 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
refresh(frm) {
const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable");
const supply_type = frm.doc.gst_category;
const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type)
if (!einvoicing_enabled || !valid_supply_type) return;
const { docstatus, irn, irn_cancelled, doctype, name, __unsaved } = frm.doc;
if (docstatus == 0 && !irn && !__unsaved) {
frm.add_custom_button(
"Download E-Invoice",
() => {
frappe.call({
method: 'erpnext.regional.india.e_invoice.e_invoice_utils.make_einvoice',
args: { doctype, name },
freeze: true,
callback: (res) => {
if (!res.exc) {
const args = {
cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_einvoice',
einvoice: JSON.stringify([res.message]),
name: name
};
open_url_post(frappe.request.url, args);
}
}
})
}, "E-Invoicing");
frm.add_custom_button(
"Upload Signed E-Invoice",
() => {
new frappe.ui.FileUploader({
method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_einvoice',
allow_multiple: 0,
doctype: doctype,
docname: name,
on_success: (attachment, r) => {
if (!r.exc) {
frm.reload_doc();
}
}
});
}, "E-Invoicing");
}
if (docstatus == 1 && irn && !irn_cancelled) {
frm.add_custom_button(
"Cancel IRN",
() => {
const d = new frappe.ui.Dialog({
title: __('Cancel IRN'),
fields: [
{
"label" : "Reason", "fieldname": "reason",
"fieldtype": "Select", "reqd": 1, "default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1
}
],
primary_action: function() {
const data = d.get_values();
const args = {
cmd: 'erpnext.regional.india.e_invoice.e_invoice_utils.download_cancel_einvoice',
irn: irn, reason: data.reason.split('-')[0], remark: data.remark, name: name
};
open_url_post(frappe.request.url, args);
d.hide();
},
primary_action_label: __('Download JSON')
});
d.show();
}, "E-Invoicing");
frm.add_custom_button(
"Upload Cancel JSON",
() => {
new frappe.ui.FileUploader({
method: 'erpnext.regional.india.e_invoice.e_invoice_utils.upload_cancel_ack',
allow_multiple: 0,
doctype: doctype,
docname: name,
on_success: (attachment, r) => {
if (!r.exc) {
frm.reload_doc();
}
}
});
}, "E-Invoicing");
}
}
})
}

View File

@@ -1,626 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import os
import re
import jwt
import json
import base64
import frappe
from six import string_types
from Crypto.PublicKey import RSA
from pyqrcode import create as qrcreate
from Crypto.Cipher import PKCS1_v1_5, AES
from Crypto.Util.Padding import pad, unpad
from frappe.model.document import Document
from frappe import _, get_module_path, scrub, bold
from frappe.integrations.utils import make_post_request, make_get_request
from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
from frappe.utils.data import get_datetime, cstr, cint, formatdate as format_date, flt, time_diff_in_seconds, now_datetime
def validate_einvoice_fields(doc):
einvoicing_enabled = frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')
invalid_doctype = doc.doctype not in ['Sales Invoice', 'Purchase Invoice']
invalid_supply_type = doc.gst_category not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
if invalid_doctype or invalid_supply_type or not einvoicing_enabled: return
if doc.docstatus == 0 and doc._action == 'save':
if doc.irn:
frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed'))
if len(doc.name) > 16:
title = _('Document Name Too Long')
msg = (_('As you have E-Invoicing enabled, To be able to generate IRN for this invoice, document name {} exceed 16 letters. ')
.format(bold(_('should not'))))
msg += '<br><br>'
msg += (_('You {} modify your {} in order to have document name of {} length of 16. ')
.format(bold(_('must')), bold(_('naming series')), bold(_('maximum'))))
frappe.throw(msg, title=title)
elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn:
frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN'))
elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled:
frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed'))
def get_credentials():
doc = frappe.get_doc('E Invoice Settings')
if not doc.enable:
frappe.throw(_("To setup E Invoicing you need to enable E Invoice Settings first."), title=_("E Invoicing Disabled"))
if not doc.token_expiry or time_diff_in_seconds(now_datetime(), doc.token_expiry) > 5.0:
fetch_token(doc)
doc.load_from_db()
return doc
def rsa_encrypt(msg, key):
if not (isinstance(msg, bytes) or isinstance(msg, bytearray)):
msg = str.encode(msg)
rsa_pub_key = RSA.import_key(key)
cipher = PKCS1_v1_5.new(rsa_pub_key)
enc_msg = cipher.encrypt(msg)
b64_enc_msg = base64.b64encode(enc_msg)
return b64_enc_msg.decode()
def aes_decrypt(enc_msg, key):
encode_as_b64 = True
if not (isinstance(key, bytes) or isinstance(key, bytearray)):
key = base64.b64decode(key)
encode_as_b64 = False
cipher = AES.new(key, AES.MODE_ECB)
b64_enc_msg = base64.b64decode(enc_msg)
msg_bytes = cipher.decrypt(b64_enc_msg)
msg_bytes = unpad(msg_bytes, AES.block_size) # due to ECB/PKCS5Padding
if encode_as_b64:
msg_bytes = base64.b64encode(msg_bytes)
return msg_bytes.decode()
def aes_encrypt(msg, key):
if not (isinstance(key, bytes) or isinstance(key, bytearray)):
key = base64.b64decode(key)
cipher = AES.new(key, AES.MODE_ECB)
bytes_msg = str.encode(msg)
padded_bytes_msg = pad(bytes_msg, AES.block_size)
enc_msg = cipher.encrypt(padded_bytes_msg)
b64_enc_msg = base64.b64encode(enc_msg)
return b64_enc_msg.decode()
def jwt_decrypt(token):
return jwt.decode(token, verify=False)
def get_header(creds):
headers = { 'content-type': 'application/json' }
headers.update(dict(client_id=creds.client_id, client_secret=creds.client_secret, user_name=creds.username))
headers.update(dict(Gstin=creds.gstin, AuthToken=creds.auth_token))
return headers
@frappe.whitelist()
def fetch_token(credentials=None):
if not credentials:
credentials = frappe.get_doc('E Invoice Settings')
endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth'
headers = { 'content-type': 'application/json' }
headers.update(dict(client_id=credentials.client_id, client_secret=credentials.client_secret))
payload = dict(UserName=credentials.username, ForceRefreshAccessToken=bool(credentials.auto_refresh_token))
appkey = bytearray(os.urandom(32))
enc_appkey = rsa_encrypt(appkey, credentials.public_key)
password = credentials.get_password(fieldname='password')
enc_password = rsa_encrypt(password, credentials.public_key)
payload.update(dict(Password=enc_password, AppKey=enc_appkey))
res = make_post_request(endpoint, headers=headers, data=json.dumps({ 'data': payload }))
handle_err_response(res)
auth_token, token_expiry, sek = extract_token_and_sek(res, appkey)
credentials.auth_token = auth_token
credentials.token_expiry = get_datetime(token_expiry)
credentials.sek = sek
credentials.save()
def extract_token_and_sek(response, appkey):
data = response.get('Data')
auth_token = data.get('AuthToken')
token_expiry = data.get('TokenExpiry')
enc_sek = data.get('Sek')
sek = aes_decrypt(enc_sek, appkey)
return auth_token, token_expiry, sek
def attach_signed_invoice(doctype, name, data):
f = frappe.get_doc({
'doctype': 'File',
'file_name': 'E-INV--{}.json'.format(name),
'attached_to_doctype': doctype,
'attached_to_name': name,
'content': json.dumps(data),
'is_private': True
}).insert()
def get_gstin_details(gstin):
credentials = get_credentials()
endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/Master/gstin/{gstin}'.format(gstin=gstin)
headers = get_header(credentials)
res = make_get_request(endpoint, headers=headers)
handle_err_response(res)
enc_details = res.get('Data')
json_str = aes_decrypt(enc_details, credentials.sek)
details = json.loads(json_str)
return details
@frappe.whitelist()
def generate_irn(doctype, name):
endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice'
credentials = get_credentials()
headers = get_header(credentials)
einvoice = make_einvoice(doctype, name)
einvoice = json.dumps(einvoice)
enc_einvoice_json = aes_encrypt(einvoice, credentials.sek)
payload = dict(Data=enc_einvoice_json)
res = make_post_request(endpoint, headers=headers, data=json.dumps(payload))
res = handle_err_response(res)
enc_json = res.get('Data')
json_str = aes_decrypt(enc_json, credentials.sek)
signed_einvoice = json.loads(json_str)
decrypt_irn_response(signed_einvoice)
update_einvoice_fields(doctype, name, signed_einvoice)
attach_qrcode_image(doctype, name)
attach_signed_invoice(doctype, name, signed_einvoice['DecryptedSignedInvoice'])
return signed_einvoice
def get_irn_details(irn):
credentials = get_credentials()
endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/irn/{irn}'.format(irn=irn)
headers = get_header(credentials)
res = make_get_request(endpoint, headers=headers)
handle_err_response(res)
return res
@frappe.whitelist()
def cancel_irn(doctype, name, irn, reason, remark=''):
credentials = get_credentials()
endpoint = 'https://einv-apisandbox.nic.in/eicore/v1.03/Invoice/Cancel'
headers = get_header(credentials)
cancel_einv = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark))
enc_json = aes_encrypt(cancel_einv, credentials.sek)
payload = dict(Data=enc_json)
res = make_post_request(endpoint, headers=headers, data=json.dumps(payload))
handle_err_response(res)
frappe.db.set_value(doctype, name, 'irn_cancelled', 1)
return res
@frappe.whitelist()
def cancel_eway_bill(doctype, name, eway_bill, reason, remark=''):
credentials = get_credentials()
endpoint = 'https://einv-apisandbox.nic.in/ewaybillapi/v1.03/ewayapi'
headers = get_header(credentials)
cancel_eway_bill_json = json.dumps(dict(ewbNo=eway_bill, cancelRsnCode=reason, cancelRmrk=remark))
enc_json = aes_encrypt(cancel_eway_bill_json, credentials.sek)
payload = dict(action='CANEWB', Data=enc_json)
res = make_post_request(endpoint, headers=headers, data=json.dumps(payload))
handle_err_response(res)
frappe.db.set_value(doctype, name, 'ewaybill', '')
frappe.db.set_value(doctype, name, 'eway_bill_cancelled', 1)
return res
def decrypt_irn_response(data):
enc_signed_invoice = data['SignedInvoice']
enc_signed_qr_code = data['SignedQRCode']
signed_invoice = jwt_decrypt(enc_signed_invoice)['data']
signed_qr_code = jwt_decrypt(enc_signed_qr_code)['data']
data['DecryptedSignedInvoice'] = json.loads(signed_invoice)
data['DecryptedSignedQRCode'] = json.loads(signed_qr_code)
def handle_err_response(response):
if response.get('Status') == 0:
err_details = response.get('ErrorDetails')
errors = []
for d in err_details:
err_code = d.get('ErrorCode')
if err_code == '2150':
irn = [d['Desc']['Irn'] for d in response.get('InfoDtls') if d['InfCd'] == 'DUPIRN']
response = get_irn_details(irn[0])
return response
errors.append(d.get('ErrorMessage'))
if errors:
frappe.log_error(title="E Invoice API Request Failed", message=json.dumps(errors, default=str, indent=4))
if len(errors) > 1:
li = ['<li>'+ d +'</li>' for d in errors]
frappe.throw(_("""<ul style='padding-left: 20px'>{}</ul>""").format(''.join(li)), title=_('API Request Failed'))
else:
frappe.throw(errors[0], title=_('API Request Failed'))
return response
def read_json(name):
file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name))
with open(file_path, 'r') as f:
return cstr(f.read())
def get_trans_details(invoice):
supply_type = ''
if invoice.gst_category == 'Registered Regular': supply_type = 'B2B'
elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP'
elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP'
elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP'
if not supply_type:
return _('Invalid invoice transaction category.')
return frappe._dict(dict(
tax_scheme='GST', supply_type=supply_type, reverse_charge=invoice.reverse_charge
))
def get_doc_details(invoice):
if invoice.doctype == 'Purchase Invoice' and invoice.is_return:
invoice_type = 'DBN'
else:
invoice_type = 'CRN' if invoice.is_return else 'INV'
invoice_name = invoice.name
invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy')
return frappe._dict(dict(invoice_type=invoice_type, invoice_name=invoice_name, invoice_date=invoice_date))
def get_party_gstin_details(address_name):
address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
gstin = address.get('gstin')
gstin_details = get_gstin_details(gstin)
legal_name = gstin_details.get('LegalName')
trade_name = gstin_details.get('TradeName')
location = gstin_details.get('AddrLoc')
state_code = gstin_details.get('StateCode')
pincode = cint(gstin_details.get('AddrPncd'))
address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno'))
address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt'))
email_id = address.get('email_id')
phone = address.get('phone')
if state_code == 97:
pincode = 999999
return frappe._dict(dict(
gstin=gstin, legal_name=legal_name, location=location,
pincode=pincode, state_code=state_code, address_line1=address_line1,
address_line2=address_line2, email=email_id, phone=phone
))
def get_overseas_address_details(address_name):
address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value(
'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id']
)
return frappe._dict(dict(
gstin='URP', legal_name=address_title, address_line1=address_line1,
address_line2=address_line2, email=email_id, phone=phone,
pincode=999999, state_code=96, place_of_supply=96, location=city
))
def get_item_list(invoice):
item_list = []
gst_accounts = get_gst_accounts(invoice.company)
gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
for d in invoice.items:
item_schema = read_json('einv_item_template')
item = frappe._dict({})
item.update(d.as_dict())
item.sr_no = d.idx
item.description = d.item_name
item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y'
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
item.qty = abs(item.qty)
item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_rate)
item.total_amount = abs(item.unit_rate * item.qty)
item.discount_amount = abs(item.discount_amount * item.qty)
item.base_amount = abs(item.base_amount)
item.tax_rate = 0
item.igst_amount = 0
item.cgst_amount = 0
item.sgst_amount = 0
item.cess_rate = 0
item.cess_amount = 0
for t in invoice.taxes:
if t.account_head in gst_accounts_list:
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
if t.account_head in gst_accounts.cess_account:
item.cess_rate += item_tax_detail[0]
item.cess_amount += abs(item_tax_detail[1])
elif t.account_head in gst_accounts.igst_account:
item.tax_rate += item_tax_detail[0]
item.igst_amount += abs(item_tax_detail[1])
elif t.account_head in gst_accounts.sgst_account:
item.tax_rate += item_tax_detail[0]
item.sgst_amount += abs(item_tax_detail[1])
elif t.account_head in gst_accounts.cgst_account:
item.tax_rate += item_tax_detail[0]
item.cgst_amount += abs(item_tax_detail[1])
item.total_value = abs(item.base_amount + item.igst_amount + item.sgst_amount + item.cgst_amount + item.cess_amount)
einv_item = item_schema.format(item=item)
item_list.append(einv_item)
return ', '.join(item_list)
def get_value_details(invoice):
gst_accounts = get_gst_accounts(invoice.company)
gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
value_details = frappe._dict(dict())
value_details.base_net_total = abs(invoice.base_net_total)
value_details.invoice_discount_amt = abs(invoice.discount_amount)
value_details.round_off = 0
value_details.base_grand_total = abs(invoice.base_rounded_total)
value_details.grand_total = abs(invoice.rounded_total)
value_details.total_cgst_amt = 0
value_details.total_sgst_amt = 0
value_details.total_igst_amt = 0
value_details.total_cess_amt = 0
for t in invoice.taxes:
if t.account_head in gst_accounts_list:
if t.account_head in gst_accounts.cess_account:
value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount)
elif t.account_head in gst_accounts.igst_account:
value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount)
elif t.account_head in gst_accounts.sgst_account:
value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount)
elif t.account_head in gst_accounts.cgst_account:
value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount)
return value_details
def get_payment_details(invoice):
payee_name = invoice.company
mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
paid_amount = invoice.base_paid_amount
outstanding_amount = invoice.outstanding_amount
return frappe._dict(dict(
payee_name=payee_name, mode_of_payment=mode_of_payment,
paid_amount=paid_amount, outstanding_amount=outstanding_amount
))
def get_return_doc_reference(invoice):
invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
return frappe._dict(dict(
invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
))
def get_eway_bill_details(invoice):
if not invoice.distance:
frappe.throw(_('Distance is mandatory for E-Way Bill generation'), title=_('E Invoice Validation Failed'))
mode_of_transport = { 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
return frappe._dict(dict(
gstin=invoice.gst_transporter_id,
name=invoice.transporter_name,
mode_of_transport=mode_of_transport[invoice.mode_of_transport],
distance=invoice.distance,
document_name=invoice.lr_no,
document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'),
vehicle_no=invoice.vehicle_no,
vehicle_type=vehicle_type[invoice.gst_vehicle_type]
))
@frappe.whitelist()
def make_einvoice(doctype, name):
invoice = frappe.get_doc(doctype, name)
schema = read_json('einv_template')
item_list = get_item_list(invoice)
doc_details = get_doc_details(invoice)
value_details = get_value_details(invoice)
trans_details = get_trans_details(invoice)
seller_details = get_party_gstin_details(invoice.company_address)
if invoice.gst_category == 'Overseas':
buyer_details = get_overseas_address_details(invoice.customer_address)
else:
buyer_details = get_party_gstin_details(invoice.customer_address)
place_of_supply = get_place_of_supply(invoice, doctype) or invoice.billing_address_gstin
place_of_supply = place_of_supply[:2]
buyer_details.update(dict(place_of_supply=place_of_supply))
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
shipping_details = get_party_gstin_details(invoice.shipping_address_name)
if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice)
if invoice.is_return and invoice.return_against:
prev_doc_details = get_return_doc_reference(invoice)
if invoice.transporter:
eway_bill_details = get_eway_bill_details(invoice)
# not yet implemented
dispatch_details = period_details = export_details = frappe._dict({})
einvoice = schema.format(
trans_details=trans_details, doc_details=doc_details, dispatch_details=dispatch_details,
seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details,
item_list=item_list, value_details=value_details, payment_details=payment_details,
period_details=period_details, prev_doc_details=prev_doc_details,
export_details=export_details, eway_bill_details=eway_bill_details
)
einvoice = json.loads(einvoice)
validations = json.loads(read_json('einv_validation'))
errors = validate_einvoice(validations, einvoice, [])
if errors:
frappe.log_error(title="E Invoice Validation Failed", message=json.dumps(errors, default=str, indent=4))
if len(errors) > 1:
li = ['<li>'+ d +'</li>' for d in errors]
frappe.throw("<ul style='padding-left: 20px'>{}</ul>".format(''.join(li)), title=_('E Invoice Validation Failed'))
else:
frappe.throw(errors[0], title=_('E Invoice Validation Failed'))
return einvoice
def validate_einvoice(validations, einvoice, errors=[]):
for fieldname, field_validation in validations.items():
value = einvoice.get(fieldname, None)
if not value or value == "None":
# remove keys with empty values
einvoice.pop(fieldname, None)
continue
value_type = field_validation.get("type").lower()
if value_type in ['object', 'array']:
child_validations = field_validation.get('properties')
if isinstance(value, list):
for d in value:
validate_einvoice(child_validations, d, errors)
if not d:
# remove empty dicts
einvoice.pop(fieldname, None)
else:
validate_einvoice(child_validations, value, errors)
if not value:
# remove empty dicts
einvoice.pop(fieldname, None)
continue
# convert to int or str
if value_type == 'string':
einvoice[fieldname] = str(value)
elif value_type == 'number':
einvoice[fieldname] = flt(value, 2) if fieldname != 'Pin' else int(value)
max_length = field_validation.get('maxLength')
minimum = flt(field_validation.get('minimum'))
maximum = flt(field_validation.get('maximum'))
pattern_str = field_validation.get('pattern')
pattern = re.compile(pattern_str or '')
label = field_validation.get('label') or fieldname
if value_type == 'string' and len(value) > max_length:
errors.append(_('{} should not exceed {} characters').format(label, max_length))
if value_type == 'number' and not (flt(value) <= maximum):
errors.append(_('{} should be less than {}').format(label, maximum))
if pattern_str and not pattern.match(value):
errors.append(field_validation.get('validationMsg'))
return errors
def update_einvoice_fields(doctype, name, signed_einvoice):
enc_signed_invoice = signed_einvoice.get('SignedInvoice')
decrypted_signed_invoice = jwt_decrypt(enc_signed_invoice)['data']
if json.loads(decrypted_signed_invoice)['DocDtls']['No'] != name:
frappe.throw(
_("Document number of uploaded Signed E-Invoice doesn't matches with Sales Invoice"),
title=_("Inappropriate E-Invoice")
)
frappe.db.set_value(doctype, name, 'irn', signed_einvoice.get('Irn'))
frappe.db.set_value(doctype, name, 'ewaybill', signed_einvoice.get('EwbNo'))
frappe.db.set_value(doctype, name, 'signed_qr_code', signed_einvoice.get('SignedQRCode').split('.')[1])
frappe.db.set_value(doctype, name, 'signed_einvoice', decrypted_signed_invoice)
@frappe.whitelist()
def download_einvoice():
data = frappe._dict(frappe.local.form_dict)
einvoice = data['einvoice']
name = data['name']
frappe.response['filename'] = 'E-Invoice-' + name + '.json'
frappe.response['filecontent'] = einvoice
frappe.response['content_type'] = 'application/json'
frappe.response['type'] = 'download'
@frappe.whitelist()
def upload_einvoice():
signed_einvoice = json.loads(frappe.local.uploaded_file)
data = frappe._dict(frappe.local.form_dict)
doctype = data['doctype']
name = data['docname']
update_einvoice_fields(doctype, name, signed_einvoice)
attach_qrcode_image(doctype, name)
@frappe.whitelist()
def download_cancel_einvoice():
data = frappe._dict(frappe.local.form_dict)
name = data['name']
irn = data['irn']
reason = data['reason']
remark = data['remark']
cancel_einvoice = json.dumps([dict(Irn=irn, CnlRsn=reason, CnlRem=remark)])
frappe.response['filename'] = 'Cancel E-Invoice ' + name + '.json'
frappe.response['filecontent'] = cancel_einvoice
frappe.response['content_type'] = 'application/json'
frappe.response['type'] = 'download'
@frappe.whitelist()
def upload_cancel_ack():
cancel_ack = json.loads(frappe.local.uploaded_file)
data = frappe._dict(frappe.local.form_dict)
doctype = data['doctype']
name = data['docname']
frappe.db.set_value(doctype, name, 'irn_cancelled', 1)
def attach_qrcode_image(doctype, name):
qrcode = frappe.db.get_value(doctype, name, 'signed_qr_code')
if not qrcode: return
_file = frappe.get_doc({
'doctype': 'File',
'file_name': 'Signed_QR_{name}.png'.format(name=name),
'attached_to_doctype': doctype,
'attached_to_name': name,
'content': 'qrcode'
})
_file.save()
frappe.db.commit()
url = qrcreate(qrcode)
abs_file_path = os.path.abspath(_file.get_full_path())
url.png(abs_file_path, scale=2)
frappe.db.set_value(doctype, name, 'qrcode_image', _file.file_url)

View File

@@ -77,7 +77,7 @@ def add_custom_roles_for_reports():
)).insert()
def add_permissions():
for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate', 'E Invoice Settings'):
for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'):
add_permission(doctype, 'All', 0)
for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
add_permission(doctype, role, 0)
@@ -93,10 +93,9 @@ def add_permissions():
def add_print_formats():
frappe.reload_doc("regional", "print_format", "gst_tax_invoice")
frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
frappe.reload_doc("accounts", "print_format", "GST E-Invoice")
frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """)
name in('GST POS Invoice', 'GST Tax Invoice') """)
def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
@@ -370,30 +369,13 @@ def make_custom_fields(update=True):
'fieldname': 'ewaybill',
'label': 'e-Way Bill No.',
'fieldtype': 'Data',
'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)',
'depends_on': 'eval:(doc.docstatus === 1)',
'allow_on_submit': 1,
'insert_after': 'tax_id',
'translatable': 0
}
]
si_einvoice_fields = [
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
]
custom_fields = {
'Address': [
dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data',
@@ -406,7 +388,7 @@ def make_custom_fields(update=True):
'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields,
'Purchase Order': purchase_invoice_gst_fields,
'Purchase Receipt': purchase_invoice_gst_fields,
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields,
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields,
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields,
'Sales Order': sales_invoice_gst_fields,
'Tax Category': inter_state_gst_field,

View File

@@ -8,4 +8,3 @@ PyGithub==1.44.1
python-stdnum==1.12
Unidecode==1.1.1
WooCommerce==2.1.1
pycryptodome==3.9.8