Browse Source
* feat: init e-invoice settings * feat: read public key file * feat: rsa encryption with public key * feat: save token and sek from auth request * chore: handle error response * feat: AES decryption of SEK with appkey * feat: decrypt json data with SEK * feat: make e invoice from erpnext sales invoice * feat: generate IRN * feat: decode signed json and QR code * chore: validations * feat: cancel IRN * feat: complete e-invoice schema * chore: move e-invoice settings to regional * chore: split einvoice settings and operations * chore: rename schema to template & js cleanup * feat: make IRN field on regional setup * feat: Generate & Cancel IRN from Sales Invoice * chore: minor fixes * fix: item discount * chore: show irn cancelled check after cancellation * fix: hide cancel irn dialog on error * fix: public key is required on validate * fix: cannot find attached key file * fix: validation if e invoicing is disabled * fix: do not show generate irn for invalid supply type * fix: update irn_cancelled after cancelling irn * chore: show irn field for proper gst_category * feat: e-way bill details in e-invoice * fix: save e-way bill no on irn generation * chore: no copy on e invoice custom fields * feat: cancel e-way bill before cancelling IRN * feat: manual download / upload json * chore: group e-invoicing actions * fix: fn name * chore: save signed invoice and qrcode after uplaoding irn * fix: fetch token if not valid * chore: move einvoicing stuff to seperate folder * feat: QRCode Image and E-Invoice Print Format * fix: bug * fix: invalid syntax * chore: code cleanup * chore: clean up e invoice actions * fix: download & upload e-invoice * fix: print format * fix: validations * fix: add permissions on regional setup * feat: add patch * fix: validate document name * fix: return date * fix: credit note einvoice * fix: validations * fix: error logging * fix: e_invoice module not found * fix: add missing package * fix: rename e_invoice_utils.py * fix: einvoice field validation * fix: patch * fix: invoice totals calculation * fix: other charges calculation * chore: improve document name validation message * fix: qr code image string * feat: initialize GSP connector * chore: remove unwanted fields * fix: qr code generation * feat: fetch and cache GSTIN details * feat: generate & cancel IRN * feat: cancel eway bill * chore: remove unwanted fuctions * chore: clean up einvoice actions * fix: attach qrcode on irn generation * fix: generate & cancel IRN * fix: show/hide eway bill fields * fix: valiations * feat: generate eway bill from IRN * chore: remove unwanted imports * chore: error logging * feat: header & footer in GST E Invoice * chore: remove test pincode * fix: invalid syntax * feat: cess non advolem on einvoice item * chore: remove fetch token from e invocie settings * fix: imports * fix: error handling * feat: update timeline on einvoice actions * fix: qrcode image size * fix: exclude intra company transactions * fix: eway bill test * fix: ewaybill mandatory conditions * chore: add tests * fix: returning condition * feat: log e-invocing requests * chore: add ack date and ack no field for print formats * fix: sider issues * feat: show e-invoice preview before IRN generation * fix: use as_list for error message * fix: minor ux issues * fix: dialog is undefined * fix: error handling * feat: add docs link to e invoice settings * feat: multiple gstins for e invoicing * fix: uncomment test condition * fix: remove test pincode * fix: cannot cancel irn without submitting sales invoice * chore: code cleanup * fix: sider issues * fix: e invoice request log permissions Co-authored-by: Nabin Hait <nabinhait@gmail.com>develop
Saqib
4 years ago
committed by
GitHub
31 changed files with 2922 additions and 94 deletions
@ -0,0 +1,162 @@ |
|||
{%- 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 {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}> |
|||
{% if letter_head and not no_letterhead %} |
|||
<div class="letter-head">{{ letter_head }}</div> |
|||
{% endif %} |
|||
<div class="print-heading"> |
|||
<h2>E Invoice<br><small>{{ doc.name }}</small></h2> |
|||
</div> |
|||
</div> |
|||
{% if print_settings.repeat_header_footer %} |
|||
<div id="footer-html" class="visible-pdf"> |
|||
{% if not no_letterhead and footer %} |
|||
<div class="letter-head-footer"> |
|||
{{ footer }} |
|||
</div> |
|||
{% endif %} |
|||
<p class="text-center small page-number visible-pdf"> |
|||
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }} |
|||
</p> |
|||
</div> |
|||
{% endif %} |
|||
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;"> |
|||
<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> |
@ -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" |
|||
} |
@ -0,0 +1,55 @@ |
|||
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("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) |
@ -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) {
|
|||
|
|||
// }
|
|||
}); |
@ -0,0 +1,103 @@ |
|||
{ |
|||
"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": "Link", |
|||
"label": "Reference Invoice", |
|||
"options": "Sales 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": "2020-12-24 21:09:38.882866", |
|||
"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" |
|||
} |
@ -0,0 +1,10 @@ |
|||
# -*- 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.model.document import Document |
|||
|
|||
class EInvoiceRequestLog(Document): |
|||
pass |
@ -0,0 +1,10 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors |
|||
# See license.txt |
|||
from __future__ import unicode_literals |
|||
|
|||
# import frappe |
|||
import unittest |
|||
|
|||
class TestEInvoiceRequestLog(unittest.TestCase): |
|||
pass |
@ -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/user/manual/en/regional/india/setup-e-invoicing'; |
|||
frm.dashboard.set_headline( |
|||
__("Read {0} for more information on E Invoicing features.", [`<a href='${docs_link}'>documentation</a>`]) |
|||
); |
|||
} |
|||
}); |
@ -0,0 +1,58 @@ |
|||
{ |
|||
"actions": [], |
|||
"creation": "2020-09-24 16:23:16.235722", |
|||
"doctype": "DocType", |
|||
"editable_grid": 1, |
|||
"engine": "InnoDB", |
|||
"field_order": [ |
|||
"enable", |
|||
"section_break_2", |
|||
"credentials", |
|||
"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" |
|||
} |
|||
], |
|||
"index_web_pages_for_search": 1, |
|||
"issingle": 1, |
|||
"links": [], |
|||
"modified": "2020-12-22 15:34:57.280044", |
|||
"modified_by": "Administrator", |
|||
"module": "Regional", |
|||
"name": "E Invoice Settings", |
|||
"owner": "Administrator", |
|||
"permissions": [], |
|||
"sort_field": "modified", |
|||
"sort_order": "DESC", |
|||
"track_changes": 1 |
|||
} |
@ -0,0 +1,14 @@ |
|||
# -*- 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.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.')) |
|||
|
@ -0,0 +1,10 @@ |
|||
# -*- coding: utf-8 -*- |
|||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors |
|||
# See license.txt |
|||
from __future__ import unicode_literals |
|||
|
|||
# import frappe |
|||
import unittest |
|||
|
|||
class TestEInvoiceSettings(unittest.TestCase): |
|||
pass |
@ -0,0 +1,48 @@ |
|||
{ |
|||
"actions": [], |
|||
"creation": "2020-12-22 15:02:46.229474", |
|||
"doctype": "DocType", |
|||
"editable_grid": 1, |
|||
"engine": "InnoDB", |
|||
"field_order": [ |
|||
"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 |
|||
} |
|||
], |
|||
"index_web_pages_for_search": 1, |
|||
"istable": 1, |
|||
"links": [], |
|||
"modified": "2020-12-22 15:10:53.466205", |
|||
"modified_by": "Administrator", |
|||
"module": "Regional", |
|||
"name": "E Invoice User", |
|||
"owner": "Administrator", |
|||
"permissions": [], |
|||
"quick_entry": 1, |
|||
"sort_field": "modified", |
|||
"sort_order": "DESC", |
|||
"track_changes": 1 |
|||
} |
@ -0,0 +1,10 @@ |
|||
# -*- 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.model.document import Document |
|||
|
|||
class EInvoiceUser(Document): |
|||
pass |
@ -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}" |
|||
}} |
|||
}} |
@ -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.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": "{invoice_value_details.base_net_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}" |
|||
}} |
|||
}} |
@ -0,0 +1,956 @@ |
|||
{ |
|||
"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" |
|||
}, |
|||
"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" |
|||
] |
|||
} |
@ -0,0 +1,305 @@ |
|||
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); |
|||
const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin; |
|||
|
|||
if (!einvoicing_enabled || !valid_supply_type || company_transaction) 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 = () => { |
|||
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'), |
|||
wide: 1, |
|||
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 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 E-Way Bill'), |
|||
fields: fields, |
|||
primary_action: function() { |
|||
const data = d.get_values(); |
|||
frappe.call({ |
|||
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', |
|||
args: { |
|||
doctype, |
|||
docname: name, |
|||
eway_bill: ewaybill, |
|||
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 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', |
|||
'depends_on': 'eval:(doc.mode_of_transport === "Road")', |
|||
'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"), |
|||
wide: 1, |
|||
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( |
|||
`<div>
|
|||
<div class="print-preview"> |
|||
<div class="print-format"></div> |
|||
</div> |
|||
<div class="page-break-message text-muted text-center text-medium margin-top"></div> |
|||
</div>` |
|||
); |
|||
|
|||
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(); |
|||
} |
|||
} |
|||
}); |
|||
}; |
@ -0,0 +1,772 @@ |
|||
# -*- 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 sys |
|||
import json |
|||
import base64 |
|||
import frappe |
|||
import traceback |
|||
from frappe import _, bold |
|||
from pyqrcode import create as qrcreate |
|||
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 cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date |
|||
|
|||
def validate_einvoice_fields(doc): |
|||
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')) |
|||
invalid_doctype = doc.doctype not in ['Sales Invoice'] |
|||
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 not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction: 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() |
|||
|
|||
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 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 += _('document id {} exceed 16 letters. ').format(bold(_('should not'))) |
|||
msg += '<br><br>' |
|||
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): |
|||
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_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') |
|||
location = gstin_details.get('AddrLoc') or address.get('city') |
|||
state_code = gstin_details.get('StateCode') |
|||
pincode = 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') |
|||
# get last 10 digit |
|||
phone = phone.replace(" ", "")[-10:] if phone else '' |
|||
|
|||
if state_code == 97: |
|||
# according to einvoice standard |
|||
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_gstin_details(gstin): |
|||
if not hasattr(frappe.local, 'gstin_cache'): |
|||
frappe.local.gstin_cache = {} |
|||
|
|||
key = gstin |
|||
details = frappe.local.gstin_cache.get(key) |
|||
if details: |
|||
return details |
|||
|
|||
details = frappe.cache().hget('gstin_cache', key) |
|||
if details: |
|||
frappe.local.gstin_cache[key] = details |
|||
return details |
|||
|
|||
if not details: |
|||
return GSPConnector.get_gstin_details(gstin) |
|||
|
|||
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 = [] |
|||
|
|||
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.qty = abs(item.qty) |
|||
item.description = d.item_name |
|||
item.taxable_value = abs(item.base_net_amount) |
|||
item.discount_amount = abs(item.discount_amount * item.qty) |
|||
item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_net_rate) |
|||
item.gross_amount = abs(item.unit_rate * item.qty) |
|||
|
|||
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 = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y' |
|||
|
|||
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: |
|||
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) |
|||
if t.account_head in gst_accounts_list: |
|||
if t.account_head in gst_accounts.cess_account: |
|||
if t.charge_type == 'On Item Quantity': |
|||
item.cess_nadv_amount += abs(item_tax_detail[1]) |
|||
else: |
|||
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]) |
|||
|
|||
return item |
|||
|
|||
def get_invoice_value_details(invoice): |
|||
invoice_value_details = frappe._dict(dict()) |
|||
invoice_value_details.base_net_total = abs(invoice.base_net_total) |
|||
invoice_value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount and invoice.discount_amount > 0 else 0 |
|||
# discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off |
|||
invoice_value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0) |
|||
disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total') |
|||
invoice_value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else abs(invoice.base_rounded_total) |
|||
invoice_value_details.grand_total = abs(invoice.grand_total) if disable_rounded else abs(invoice.rounded_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 |
|||
for t in invoice.taxes: |
|||
if t.account_head in gst_accounts_list: |
|||
if t.account_head in gst_accounts.cess_account: |
|||
invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount) |
|||
elif t.account_head in gst_accounts.igst_account: |
|||
invoice_value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount) |
|||
elif t.account_head in gst_accounts.sgst_account: |
|||
invoice_value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount) |
|||
elif t.account_head in gst_accounts.cgst_account: |
|||
invoice_value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount) |
|||
else: |
|||
invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) |
|||
|
|||
return invoice_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 invoice.is_return: |
|||
frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), 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 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 make_einvoice(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) 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_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( |
|||
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 |
|||
) |
|||
einvoice = json.loads(einvoice) |
|||
|
|||
validations = json.loads(read_json('einv_validation')) |
|||
errors = validate_einvoice(validations, einvoice) |
|||
if errors: |
|||
message = "\n".join([ |
|||
"E Invoice: ", json.dumps(einvoice, indent=4), |
|||
"-" * 50, |
|||
"Errors: ", json.dumps(errors, indent=4) |
|||
]) |
|||
frappe.log_error(title="E Invoice Validation Failed", message=message) |
|||
frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1) |
|||
|
|||
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': |
|||
is_integer = '.' not in str(field_validation.get('maximum')) |
|||
einvoice[fieldname] = flt(value, 2) if not is_integer else cint(value) |
|||
value = einvoice[fieldname] |
|||
|
|||
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('description') 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 (value > maximum or value < minimum): |
|||
errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum)) |
|||
if pattern_str and not pattern.match(value): |
|||
errors.append(field_validation.get('validationMsg')) |
|||
|
|||
return errors |
|||
|
|||
class RequestFailed(Exception): pass |
|||
|
|||
class GSPConnector(): |
|||
def __init__(self, doctype=None, docname=None): |
|||
self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') |
|||
self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None |
|||
self.credentials = self.get_credentials() |
|||
|
|||
self.base_url = 'https://gsp.adaequare.com/' |
|||
self.authenticate_url = self.base_url + 'gsp/authenticate?grant_type=token' |
|||
self.gstin_details_url = self.base_url + 'test/enriched/ei/api/master/gstin' |
|||
self.generate_irn_url = self.base_url + 'test/enriched/ei/api/invoice' |
|||
self.irn_details_url = self.base_url + 'test/enriched/ei/api/invoice/irn' |
|||
self.cancel_irn_url = self.base_url + 'test/enriched/ei/api/invoice/cancel' |
|||
self.cancel_ewaybill_url = self.base_url + '/test/enriched/ei/api/ewayapi' |
|||
self.generate_ewaybill_url = self.base_url + 'test/enriched/ei/api/ewaybill' |
|||
|
|||
def get_credentials(self): |
|||
if self.invoice: |
|||
gstin = self.get_seller_gstin() |
|||
credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin) |
|||
else: |
|||
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None |
|||
return credentials |
|||
|
|||
def get_seller_gstin(self): |
|||
gstin = self.invoice.company_gstin or 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.insert(ignore_permissions=True) |
|||
frappe.db.commit() |
|||
|
|||
def fetch_auth_token(self): |
|||
headers = { |
|||
'gspappid': frappe.conf.einvoice_client_id, |
|||
'gspappsecret': frappe.conf.einvoice_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() |
|||
|
|||
except Exception: |
|||
self.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: |
|||
self.log_error(res) |
|||
raise RequestFailed |
|||
|
|||
except RequestFailed: |
|||
self.raise_error() |
|||
|
|||
except Exception: |
|||
self.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): |
|||
headers = self.get_headers() |
|||
einvoice = make_einvoice(self.invoice) |
|||
data = json.dumps(einvoice, indent=4) |
|||
|
|||
try: |
|||
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.raise_error(errors=errors) |
|||
|
|||
except Exception: |
|||
self.log_error(data) |
|||
self.raise_error(True) |
|||
|
|||
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: |
|||
self.log_error() |
|||
self.raise_error(True) |
|||
|
|||
def cancel_irn(self, irn, reason, remark): |
|||
headers = self.get_headers() |
|||
data = json.dumps({ |
|||
'Irn': irn, |
|||
'Cnlrsn': reason, |
|||
'Cnlrem': remark |
|||
}, indent=4) |
|||
|
|||
try: |
|||
res = self.make_request('post', self.cancel_irn_url, headers, data) |
|||
if res.get('success'): |
|||
self.invoice.irn_cancelled = 1 |
|||
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.raise_error(errors=errors) |
|||
|
|||
except Exception: |
|||
self.log_error(data) |
|||
self.raise_error(True) |
|||
|
|||
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_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: |
|||
self.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) |
|||
|
|||
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: |
|||
self.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 |
|||
''' |
|||
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 log_error(self, data={}): |
|||
if not isinstance(data, dict): |
|||
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 |
|||
]) |
|||
frappe.log_error(title=_('E Invoice Request Failed'), message=message) |
|||
|
|||
def raise_error(self, raise_exception=False, errors=[]): |
|||
title = _('E Invoice Request Failed') |
|||
if errors: |
|||
frappe.throw(errors, title=title, as_list=1) |
|||
else: |
|||
link_to_error_list = '<a href="desk#List/Error Log/List?method=E Invoice Request Failed">Error Log</a>' |
|||
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, verify=False)['data'] |
|||
|
|||
self.invoice.irn = res.get('Irn') |
|||
self.invoice.ewaybill = res.get('EwbNo') |
|||
self.invoice.signed_einvoice = dec_signed_invoice |
|||
self.invoice.signed_qr_code = res.get('SignedQRCode') |
|||
|
|||
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 |
|||
|
|||
_file = frappe.new_doc('File') |
|||
_file.update({ |
|||
'file_name': f'QRCode_{docname}.png', |
|||
'attached_to_doctype': doctype, |
|||
'attached_to_name': docname, |
|||
'content': 'qrcode', |
|||
'is_private': 1 |
|||
}) |
|||
_file.insert() |
|||
frappe.db.commit() |
|||
url = qrcreate(qrcode, error='L') |
|||
abs_file_path = os.path.abspath(_file.get_full_path()) |
|||
url.png(abs_file_path, scale=2, quiet_zone=1) |
|||
|
|||
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() |
|||
|
|||
@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, eway_bill, reason, remark): |
|||
gsp_connector = GSPConnector(doctype, docname) |
|||
gsp_connector.cancel_eway_bill(eway_bill, reason, remark) |
Loading…
Reference in new issue