|
|
|
|
@@ -5,6 +5,7 @@
|
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
import six
|
|
|
|
|
import jwt
|
|
|
|
|
import sys
|
|
|
|
|
import json
|
|
|
|
|
@@ -16,16 +17,38 @@ 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, get_link_to_form
|
|
|
|
|
from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, getdate, get_link_to_form
|
|
|
|
|
|
|
|
|
|
def validate_einvoice_fields(doc):
|
|
|
|
|
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
|
|
|
|
|
invalid_doctype = doc.doctype != 'Sales Invoice'
|
|
|
|
|
@frappe.whitelist()
|
|
|
|
|
def validate_eligibility(doc):
|
|
|
|
|
if isinstance(doc, six.string_types):
|
|
|
|
|
doc = json.loads(doc)
|
|
|
|
|
|
|
|
|
|
invalid_doctype = doc.get('doctype') != 'Sales Invoice'
|
|
|
|
|
if invalid_doctype:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
einvoicing_enabled = cint(frappe.db.get_single_value('E Invoice Settings', 'enable'))
|
|
|
|
|
if not einvoicing_enabled:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
if getdate(doc.get('posting_date')) < getdate('2021-04-01'):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') })
|
|
|
|
|
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
|
|
|
|
|
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
|
|
|
|
|
no_taxes_applied = not doc.get('taxes')
|
|
|
|
|
|
|
|
|
|
if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied:
|
|
|
|
|
|
|
|
|
|
if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
def validate_einvoice_fields(doc):
|
|
|
|
|
invoice_eligible = validate_eligibility(doc)
|
|
|
|
|
|
|
|
|
|
if not invoice_eligible:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if doc.docstatus == 0 and doc._action == 'save':
|
|
|
|
|
@@ -86,36 +109,36 @@ def get_doc_details(invoice):
|
|
|
|
|
invoice_date=invoice_date
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
def get_party_details(address_name, company_address=None, billing_address=None, shipping_address=None):
|
|
|
|
|
d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
|
|
|
|
|
|
|
|
|
|
if ((not d.gstin and not shipping_address)
|
|
|
|
|
or not d.city
|
|
|
|
|
or not d.pincode
|
|
|
|
|
or not d.address_title
|
|
|
|
|
or not d.address_line1
|
|
|
|
|
or not d.gst_state_number):
|
|
|
|
|
def validate_address_fields(address, is_shipping_address):
|
|
|
|
|
if ((not address.gstin and not is_shipping_address)
|
|
|
|
|
or not address.city
|
|
|
|
|
or not address.pincode
|
|
|
|
|
or not address.address_title
|
|
|
|
|
or not address.address_line1
|
|
|
|
|
or not address.gst_state_number):
|
|
|
|
|
|
|
|
|
|
frappe.throw(
|
|
|
|
|
msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format(
|
|
|
|
|
get_link_to_form('Address', address_name)
|
|
|
|
|
),
|
|
|
|
|
msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name),
|
|
|
|
|
title=_('Missing Address Fields')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if d.gst_state_number == 97:
|
|
|
|
|
def get_party_details(address_name, is_shipping_address=False):
|
|
|
|
|
addr = frappe.get_doc('Address', address_name)
|
|
|
|
|
|
|
|
|
|
validate_address_fields(addr, is_shipping_address)
|
|
|
|
|
|
|
|
|
|
if addr.gst_state_number == 97:
|
|
|
|
|
# according to einvoice standard
|
|
|
|
|
pincode = 999999
|
|
|
|
|
addr.pincode = 999999
|
|
|
|
|
|
|
|
|
|
party_address_details = frappe._dict(dict(
|
|
|
|
|
legal_name=d.address_title,
|
|
|
|
|
location=d.city, pincode=d.pincode,
|
|
|
|
|
state_code=d.gst_state_number,
|
|
|
|
|
address_line1=d.address_line1,
|
|
|
|
|
address_line2=d.address_line2
|
|
|
|
|
legal_name=sanitize_for_json(addr.address_title),
|
|
|
|
|
location=sanitize_for_json(addr.city),
|
|
|
|
|
pincode=addr.pincode, gstin=addr.gstin,
|
|
|
|
|
state_code=addr.gst_state_number,
|
|
|
|
|
address_line1=sanitize_for_json(addr.address_line1),
|
|
|
|
|
address_line2=sanitize_for_json(addr.address_line2)
|
|
|
|
|
))
|
|
|
|
|
if d.gstin:
|
|
|
|
|
party_address_details.gstin = d.gstin
|
|
|
|
|
|
|
|
|
|
return party_address_details
|
|
|
|
|
|
|
|
|
|
@@ -167,10 +190,15 @@ def get_item_list(invoice):
|
|
|
|
|
item.description = json.dumps(d.item_name)[1:-1]
|
|
|
|
|
|
|
|
|
|
item.qty = abs(item.qty)
|
|
|
|
|
item.discount_amount = 0
|
|
|
|
|
item.unit_rate = abs(item.base_net_amount / item.qty)
|
|
|
|
|
item.gross_amount = abs(item.base_net_amount)
|
|
|
|
|
item.taxable_value = abs(item.base_net_amount)
|
|
|
|
|
|
|
|
|
|
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
|
|
|
|
|
item.discount_amount = abs(item.base_amount - item.base_net_amount)
|
|
|
|
|
else:
|
|
|
|
|
item.discount_amount = 0
|
|
|
|
|
|
|
|
|
|
item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty)
|
|
|
|
|
item.gross_amount = abs(item.taxable_value) + item.discount_amount
|
|
|
|
|
item.taxable_value = abs(item.taxable_value)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
@@ -200,12 +228,14 @@ def update_item_taxes(invoice, item):
|
|
|
|
|
item[attr] = 0
|
|
|
|
|
|
|
|
|
|
for t in invoice.taxes:
|
|
|
|
|
# this contains item wise tax rate & tax amount (incl. discount)
|
|
|
|
|
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
|
|
|
|
|
if t.account_head in gst_accounts_list:
|
|
|
|
|
is_applicable = t.tax_amount and t.account_head in gst_accounts_list
|
|
|
|
|
if is_applicable:
|
|
|
|
|
# this contains item wise tax rate & tax amount (incl. discount)
|
|
|
|
|
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code or item.item_name)
|
|
|
|
|
|
|
|
|
|
item_tax_rate = item_tax_detail[0]
|
|
|
|
|
# item tax amount excluding discount amount
|
|
|
|
|
item_tax_amount = (item_tax_rate / 100) * item.base_net_amount
|
|
|
|
|
item_tax_amount = (item_tax_rate / 100) * item.taxable_value
|
|
|
|
|
|
|
|
|
|
if t.account_head in gst_accounts.cess_account:
|
|
|
|
|
item_tax_amount_after_discount = item_tax_detail[1]
|
|
|
|
|
@@ -226,10 +256,14 @@ def get_invoice_value_details(invoice):
|
|
|
|
|
invoice_value_details = frappe._dict(dict())
|
|
|
|
|
|
|
|
|
|
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
|
|
|
|
|
invoice_value_details.base_total = abs(invoice.base_total)
|
|
|
|
|
# Discount already applied on net total which means on items
|
|
|
|
|
invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
|
|
|
|
|
invoice_value_details.invoice_discount_amt = 0
|
|
|
|
|
elif invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount:
|
|
|
|
|
invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
|
|
|
|
|
invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
|
|
|
|
|
else:
|
|
|
|
|
invoice_value_details.base_total = abs(invoice.base_net_total)
|
|
|
|
|
invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')]))
|
|
|
|
|
# since tax already considers discount amount
|
|
|
|
|
invoice_value_details.invoice_discount_amt = 0
|
|
|
|
|
|
|
|
|
|
@@ -250,7 +284,11 @@ def update_invoice_taxes(invoice, invoice_value_details):
|
|
|
|
|
invoice_value_details.total_igst_amt = 0
|
|
|
|
|
invoice_value_details.total_cess_amt = 0
|
|
|
|
|
invoice_value_details.total_other_charges = 0
|
|
|
|
|
considered_rows = []
|
|
|
|
|
|
|
|
|
|
for t in invoice.taxes:
|
|
|
|
|
tax_amount = t.base_tax_amount if (invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount) \
|
|
|
|
|
else t.base_tax_amount_after_discount_amount
|
|
|
|
|
if t.account_head in gst_accounts_list:
|
|
|
|
|
if t.account_head in gst_accounts.cess_account:
|
|
|
|
|
# using after discount amt since item also uses after discount amt for cess calc
|
|
|
|
|
@@ -258,12 +296,26 @@ def update_invoice_taxes(invoice, invoice_value_details):
|
|
|
|
|
|
|
|
|
|
for tax_type in ['igst', 'cgst', 'sgst']:
|
|
|
|
|
if t.account_head in gst_accounts[f'{tax_type}_account']:
|
|
|
|
|
invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount_after_discount_amount)
|
|
|
|
|
invoice_value_details[f'total_{tax_type}_amt'] += abs(tax_amount)
|
|
|
|
|
|
|
|
|
|
update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows)
|
|
|
|
|
else:
|
|
|
|
|
invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount)
|
|
|
|
|
invoice_value_details.total_other_charges += abs(tax_amount)
|
|
|
|
|
|
|
|
|
|
return invoice_value_details
|
|
|
|
|
|
|
|
|
|
def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows):
|
|
|
|
|
prev_row_id = cint(tax_row.row_id) - 1
|
|
|
|
|
if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows:
|
|
|
|
|
if tax_row.charge_type == 'On Previous Row Amount':
|
|
|
|
|
amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount
|
|
|
|
|
invoice_value_details.total_other_charges -= abs(amount)
|
|
|
|
|
considered_rows.append(prev_row_id)
|
|
|
|
|
if tax_row.charge_type == 'On Previous Row Total':
|
|
|
|
|
amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total
|
|
|
|
|
invoice_value_details.total_other_charges -= abs(amount)
|
|
|
|
|
considered_rows.append(prev_row_id)
|
|
|
|
|
|
|
|
|
|
def get_payment_details(invoice):
|
|
|
|
|
payee_name = invoice.company
|
|
|
|
|
mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
|
|
|
|
|
@@ -276,6 +328,10 @@ def get_payment_details(invoice):
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
def get_return_doc_reference(invoice):
|
|
|
|
|
if not invoice.return_against:
|
|
|
|
|
frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.')
|
|
|
|
|
.format(frappe.bold('Return Against')), title=_('Missing Field'))
|
|
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
@@ -283,7 +339,11 @@ def get_return_doc_reference(invoice):
|
|
|
|
|
|
|
|
|
|
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'))
|
|
|
|
|
frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'),
|
|
|
|
|
title=_('Invalid Fields'))
|
|
|
|
|
|
|
|
|
|
if not invoice.distance:
|
|
|
|
|
frappe.throw(_('Distance is mandatory for generating e-way bill for an e-invoice.'), title=_('Missing Field'))
|
|
|
|
|
|
|
|
|
|
mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
|
|
|
|
|
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
|
|
|
|
|
@@ -301,9 +361,15 @@ def get_eway_bill_details(invoice):
|
|
|
|
|
|
|
|
|
|
def validate_mandatory_fields(invoice):
|
|
|
|
|
if not invoice.company_address:
|
|
|
|
|
frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields'))
|
|
|
|
|
frappe.throw(
|
|
|
|
|
_('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'),
|
|
|
|
|
title=_('Missing Fields')
|
|
|
|
|
)
|
|
|
|
|
if not invoice.customer_address:
|
|
|
|
|
frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields'))
|
|
|
|
|
frappe.throw(
|
|
|
|
|
_('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'),
|
|
|
|
|
title=_('Missing Fields')
|
|
|
|
|
)
|
|
|
|
|
if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
|
|
|
|
|
frappe.throw(
|
|
|
|
|
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
|
|
|
|
|
@@ -315,6 +381,39 @@ def validate_mandatory_fields(invoice):
|
|
|
|
|
title=_('Missing Fields')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def validate_totals(einvoice):
|
|
|
|
|
item_list = einvoice['ItemList']
|
|
|
|
|
value_details = einvoice['ValDtls']
|
|
|
|
|
|
|
|
|
|
total_item_ass_value = 0
|
|
|
|
|
total_item_cgst_value = 0
|
|
|
|
|
total_item_sgst_value = 0
|
|
|
|
|
total_item_igst_value = 0
|
|
|
|
|
total_item_value = 0
|
|
|
|
|
for item in item_list:
|
|
|
|
|
total_item_ass_value += flt(item['AssAmt'])
|
|
|
|
|
total_item_cgst_value += flt(item['CgstAmt'])
|
|
|
|
|
total_item_sgst_value += flt(item['SgstAmt'])
|
|
|
|
|
total_item_igst_value += flt(item['IgstAmt'])
|
|
|
|
|
total_item_value += flt(item['TotItemVal'])
|
|
|
|
|
|
|
|
|
|
if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1:
|
|
|
|
|
frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx))
|
|
|
|
|
|
|
|
|
|
if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1:
|
|
|
|
|
frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.'))
|
|
|
|
|
|
|
|
|
|
if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - flt(value_details['OthChrg']) - total_item_value) > 1:
|
|
|
|
|
frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.'))
|
|
|
|
|
|
|
|
|
|
calculated_invoice_value = \
|
|
|
|
|
flt(value_details['AssVal']) + flt(value_details['CgstVal']) \
|
|
|
|
|
+ flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \
|
|
|
|
|
+ flt(value_details['OthChrg']) - flt(value_details['Discount'])
|
|
|
|
|
|
|
|
|
|
if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1:
|
|
|
|
|
frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.'))
|
|
|
|
|
|
|
|
|
|
def make_einvoice(invoice):
|
|
|
|
|
validate_mandatory_fields(invoice)
|
|
|
|
|
|
|
|
|
|
@@ -324,12 +423,12 @@ def make_einvoice(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, company_address=1)
|
|
|
|
|
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, billing_address=1)
|
|
|
|
|
buyer_details = get_party_details(invoice.customer_address)
|
|
|
|
|
place_of_supply = get_place_of_supply(invoice, invoice.doctype)
|
|
|
|
|
if place_of_supply:
|
|
|
|
|
place_of_supply = place_of_supply.split('-')[0]
|
|
|
|
|
@@ -338,12 +437,15 @@ def make_einvoice(invoice):
|
|
|
|
|
|
|
|
|
|
buyer_details.update(dict(place_of_supply=place_of_supply))
|
|
|
|
|
|
|
|
|
|
seller_details.update(dict(legal_name=invoice.company))
|
|
|
|
|
buyer_details.update(dict(legal_name=invoice.customer_name or invoice.customer))
|
|
|
|
|
|
|
|
|
|
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
|
|
|
|
|
if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
|
|
|
|
|
if invoice.gst_category == 'Overseas':
|
|
|
|
|
shipping_details = get_overseas_address_details(invoice.shipping_address_name)
|
|
|
|
|
else:
|
|
|
|
|
shipping_details = get_party_details(invoice.shipping_address_name, shipping_address=1)
|
|
|
|
|
shipping_details = get_party_details(invoice.shipping_address_name, is_shipping_address=True)
|
|
|
|
|
|
|
|
|
|
if invoice.is_pos and invoice.base_paid_amount:
|
|
|
|
|
payment_details = get_payment_details(invoice)
|
|
|
|
|
@@ -351,7 +453,7 @@ def make_einvoice(invoice):
|
|
|
|
|
if invoice.is_return and invoice.return_against:
|
|
|
|
|
prev_doc_details = get_return_doc_reference(invoice)
|
|
|
|
|
|
|
|
|
|
if invoice.transporter and cint(invoice.distance):
|
|
|
|
|
if invoice.transporter and flt(invoice.distance) and not invoice.is_return:
|
|
|
|
|
eway_bill_details = get_eway_bill_details(invoice)
|
|
|
|
|
|
|
|
|
|
# not yet implemented
|
|
|
|
|
@@ -364,74 +466,86 @@ def make_einvoice(invoice):
|
|
|
|
|
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)
|
|
|
|
|
try:
|
|
|
|
|
einvoice = safe_json_load(einvoice)
|
|
|
|
|
einvoice = santize_einvoice_fields(einvoice)
|
|
|
|
|
except Exception:
|
|
|
|
|
show_link_to_error_log(invoice, einvoice)
|
|
|
|
|
|
|
|
|
|
validate_totals(einvoice)
|
|
|
|
|
|
|
|
|
|
return einvoice
|
|
|
|
|
|
|
|
|
|
def validate_einvoice(validations, einvoice, errors=None):
|
|
|
|
|
if errors is None:
|
|
|
|
|
errors = []
|
|
|
|
|
def show_link_to_error_log(invoice, einvoice):
|
|
|
|
|
err_log = log_error(einvoice)
|
|
|
|
|
link_to_error_log = get_link_to_form('Error Log', err_log.name, 'Error Log')
|
|
|
|
|
frappe.throw(
|
|
|
|
|
_('An error occurred while creating e-invoice for {}. Please check {} for more information.').format(
|
|
|
|
|
invoice.name, link_to_error_log),
|
|
|
|
|
title=_('E Invoice Creation Failed')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
def log_error(data=None):
|
|
|
|
|
if isinstance(data, six.string_types):
|
|
|
|
|
data = json.loads(data)
|
|
|
|
|
|
|
|
|
|
value_type = field_validation.get("type").lower()
|
|
|
|
|
if value_type in ['object', 'array']:
|
|
|
|
|
child_validations = field_validation.get('properties')
|
|
|
|
|
seperator = "--" * 50
|
|
|
|
|
err_tb = traceback.format_exc()
|
|
|
|
|
err_msg = str(sys.exc_info()[1])
|
|
|
|
|
data = json.dumps(data, indent=4)
|
|
|
|
|
|
|
|
|
|
if isinstance(value, list):
|
|
|
|
|
for d in value:
|
|
|
|
|
validate_einvoice(child_validations, d, errors)
|
|
|
|
|
if not d:
|
|
|
|
|
# remove empty dicts
|
|
|
|
|
einvoice.pop(fieldname, None)
|
|
|
|
|
message = "\n".join([
|
|
|
|
|
"Error", err_msg, seperator,
|
|
|
|
|
"Data:", data, seperator,
|
|
|
|
|
"Exception:", err_tb
|
|
|
|
|
])
|
|
|
|
|
frappe.log_error(title=_('E Invoice Request Failed'), message=message)
|
|
|
|
|
|
|
|
|
|
def santize_einvoice_fields(einvoice):
|
|
|
|
|
int_fields = ["Pin","Distance","CrDay"]
|
|
|
|
|
float_fields = ["Qty","FreeQty","UnitPrice","TotAmt","Discount","PreTaxVal","AssAmt","GstRt","IgstAmt","CgstAmt","SgstAmt","CesRt","CesAmt","CesNonAdvlAmt","StateCesRt","StateCesAmt","StateCesNonAdvlAmt","OthChrg","TotItemVal","AssVal","CgstVal","SgstVal","IgstVal","CesVal","StCesVal","Discount","OthChrg","RndOffAmt","TotInvVal","TotInvValFc","PaidAmt","PaymtDue","ExpDuty",]
|
|
|
|
|
copy = einvoice.copy()
|
|
|
|
|
for key, value in copy.items():
|
|
|
|
|
if isinstance(value, list):
|
|
|
|
|
for idx, d in enumerate(value):
|
|
|
|
|
santized_dict = santize_einvoice_fields(d)
|
|
|
|
|
if santized_dict:
|
|
|
|
|
einvoice[key][idx] = santized_dict
|
|
|
|
|
else:
|
|
|
|
|
einvoice[key].pop(idx)
|
|
|
|
|
|
|
|
|
|
if not einvoice[key]:
|
|
|
|
|
einvoice.pop(key, None)
|
|
|
|
|
|
|
|
|
|
elif isinstance(value, dict):
|
|
|
|
|
santized_dict = santize_einvoice_fields(value)
|
|
|
|
|
if santized_dict:
|
|
|
|
|
einvoice[key] = santized_dict
|
|
|
|
|
else:
|
|
|
|
|
validate_einvoice(child_validations, value, errors)
|
|
|
|
|
if not value:
|
|
|
|
|
# remove empty dicts
|
|
|
|
|
einvoice.pop(fieldname, None)
|
|
|
|
|
continue
|
|
|
|
|
einvoice.pop(key, None)
|
|
|
|
|
|
|
|
|
|
# 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'))
|
|
|
|
|
precision = 3 if '.999' in str(field_validation.get('maximum')) else 2
|
|
|
|
|
einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value)
|
|
|
|
|
value = einvoice[fieldname]
|
|
|
|
|
elif not value or value == "None":
|
|
|
|
|
einvoice.pop(key, None)
|
|
|
|
|
|
|
|
|
|
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 '')
|
|
|
|
|
elif key in float_fields:
|
|
|
|
|
einvoice[key] = flt(value, 2)
|
|
|
|
|
|
|
|
|
|
label = field_validation.get('description') or fieldname
|
|
|
|
|
elif key in int_fields:
|
|
|
|
|
einvoice[key] = cint(value)
|
|
|
|
|
|
|
|
|
|
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) and field_validation.get('validationMsg'):
|
|
|
|
|
errors.append(field_validation.get('validationMsg'))
|
|
|
|
|
return einvoice
|
|
|
|
|
|
|
|
|
|
return errors
|
|
|
|
|
def safe_json_load(json_string):
|
|
|
|
|
JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError
|
|
|
|
|
try:
|
|
|
|
|
return json.loads(json_string)
|
|
|
|
|
except JSONDecodeError as e:
|
|
|
|
|
# print a snippet of 40 characters around the location where error occured
|
|
|
|
|
pos = e.pos
|
|
|
|
|
start, end = max(0, pos-20), min(len(json_string)-1, pos+20)
|
|
|
|
|
snippet = json_string[start:end]
|
|
|
|
|
frappe.throw(_("Error in input data. Please check for any special characters near following input: <br> {}").format(snippet))
|
|
|
|
|
|
|
|
|
|
class RequestFailed(Exception): pass
|
|
|
|
|
|
|
|
|
|
@@ -457,14 +571,11 @@ class GSPConnector():
|
|
|
|
|
def get_credentials(self):
|
|
|
|
|
if self.invoice:
|
|
|
|
|
gstin = self.get_seller_gstin()
|
|
|
|
|
if not self.e_invoice_settings.enable:
|
|
|
|
|
frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings")))
|
|
|
|
|
|
|
|
|
|
credentials_for_gstin = [d for d in self.e_invoice_settings.credentials if d.gstin == gstin]
|
|
|
|
|
if credentials_for_gstin:
|
|
|
|
|
credentials = credentials_for_gstin[0]
|
|
|
|
|
else:
|
|
|
|
|
frappe.throw(_('Cannot find e-invoicing credentials for GSTIN {}. Please check E-Invoice Settings').format(gstin))
|
|
|
|
|
frappe.throw(_('Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings'))
|
|
|
|
|
else:
|
|
|
|
|
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
|
|
|
|
|
return credentials
|
|
|
|
|
@@ -518,7 +629,7 @@ class GSPConnector():
|
|
|
|
|
self.e_invoice_settings.reload()
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
self.log_error(res)
|
|
|
|
|
log_error(res)
|
|
|
|
|
self.raise_error(True)
|
|
|
|
|
|
|
|
|
|
def get_headers(self):
|
|
|
|
|
@@ -540,14 +651,14 @@ class GSPConnector():
|
|
|
|
|
if res.get('success'):
|
|
|
|
|
return res.get('result')
|
|
|
|
|
else:
|
|
|
|
|
self.log_error(res)
|
|
|
|
|
log_error(res)
|
|
|
|
|
raise RequestFailed
|
|
|
|
|
|
|
|
|
|
except RequestFailed:
|
|
|
|
|
self.raise_error()
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
self.log_error()
|
|
|
|
|
log_error()
|
|
|
|
|
self.raise_error(True)
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_gstin_details(gstin):
|
|
|
|
|
@@ -592,7 +703,7 @@ class GSPConnector():
|
|
|
|
|
self.raise_error(errors=errors)
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
self.log_error(data)
|
|
|
|
|
log_error(data)
|
|
|
|
|
self.raise_error(True)
|
|
|
|
|
|
|
|
|
|
def get_irn_details(self, irn):
|
|
|
|
|
@@ -611,7 +722,7 @@ class GSPConnector():
|
|
|
|
|
self.raise_error(errors=errors)
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
self.log_error()
|
|
|
|
|
log_error()
|
|
|
|
|
self.raise_error(True)
|
|
|
|
|
|
|
|
|
|
def cancel_irn(self, irn, reason, remark):
|
|
|
|
|
@@ -624,7 +735,7 @@ class GSPConnector():
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
res = self.make_request('post', self.cancel_irn_url, headers, data)
|
|
|
|
|
if res.get('success'):
|
|
|
|
|
if res.get('success') or '9999' in res.get('message'):
|
|
|
|
|
self.invoice.irn_cancelled = 1
|
|
|
|
|
self.invoice.flags.updater_reference = {
|
|
|
|
|
'doctype': self.invoice.doctype,
|
|
|
|
|
@@ -641,7 +752,7 @@ class GSPConnector():
|
|
|
|
|
self.raise_error(errors=errors)
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
self.log_error(data)
|
|
|
|
|
log_error(data)
|
|
|
|
|
self.raise_error(True)
|
|
|
|
|
def generate_eway_bill(self, **kwargs):
|
|
|
|
|
args = frappe._dict(kwargs)
|
|
|
|
|
@@ -681,7 +792,7 @@ class GSPConnector():
|
|
|
|
|
self.raise_error(errors=errors)
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
self.log_error(data)
|
|
|
|
|
log_error(data)
|
|
|
|
|
self.raise_error(True)
|
|
|
|
|
|
|
|
|
|
def cancel_eway_bill(self, eway_bill, reason, remark):
|
|
|
|
|
@@ -713,7 +824,7 @@ class GSPConnector():
|
|
|
|
|
self.raise_error(errors=errors)
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
self.log_error(data)
|
|
|
|
|
log_error(data)
|
|
|
|
|
self.raise_error(True)
|
|
|
|
|
|
|
|
|
|
def sanitize_error_message(self, message):
|
|
|
|
|
@@ -739,22 +850,6 @@ class GSPConnector():
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
@@ -774,6 +869,8 @@ class GSPConnector():
|
|
|
|
|
|
|
|
|
|
self.invoice.irn = res.get('Irn')
|
|
|
|
|
self.invoice.ewaybill = res.get('EwbNo')
|
|
|
|
|
self.invoice.ack_no = res.get('AckNo')
|
|
|
|
|
self.invoice.ack_date = res.get('AckDt')
|
|
|
|
|
self.invoice.signed_einvoice = dec_signed_invoice
|
|
|
|
|
self.invoice.signed_qr_code = res.get('SignedQRCode')
|
|
|
|
|
|
|
|
|
|
@@ -811,6 +908,11 @@ class GSPConnector():
|
|
|
|
|
self.invoice.flags.ignore_validate = True
|
|
|
|
|
self.invoice.save()
|
|
|
|
|
|
|
|
|
|
def sanitize_for_json(string):
|
|
|
|
|
"""Escape JSON specific characters from a string."""
|
|
|
|
|
# json.dumps adds double-quotes to the string. Indexing to remove them.
|
|
|
|
|
return json.dumps(string)[1:-1]
|
|
|
|
|
|
|
|
|
|
@frappe.whitelist()
|
|
|
|
|
def get_einvoice(doctype, docname):
|
|
|
|
|
invoice = frappe.get_doc(doctype, docname)
|
|
|
|
|
@@ -833,5 +935,9 @@ def generate_eway_bill(doctype, docname, **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)
|
|
|
|
|
# TODO: uncomment when eway_bill api from Adequare is enabled
|
|
|
|
|
# gsp_connector = GSPConnector(doctype, docname)
|
|
|
|
|
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
|
|
|
|
|
|
|
|
|
# update cancelled status only, to be able to cancel irn next
|
|
|
|
|
frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1)
|
|
|
|
|
|