2020-12-25 04:56:43 +00:00
# -*- 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
2021-02-23 11:56:37 +00:00
import six
2020-12-25 04:56:43 +00:00
import traceback
2021-01-24 05:29:37 +00:00
import io
2020-12-25 04:56:43 +00:00
from frappe import _ , bold
from pyqrcode import create as qrcreate
2021-04-12 12:25:46 +00:00
from frappe . utils . background_jobs import enqueue
from frappe . utils . scheduler import is_scheduler_inactive
from frappe . core . page . background_jobs . background_jobs import get_info
2020-12-25 04:56:43 +00:00
from frappe . integrations . utils import make_post_request , make_get_request
from erpnext . regional . india . utils import get_gst_accounts , get_place_of_supply
2021-04-12 12:25:46 +00:00
from frappe . utils . data import cstr , cint , format_date , flt , time_diff_in_seconds , now_datetime , add_to_date , get_link_to_form , getdate , time_diff_in_hours
@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
einvoicing_eligible_from = frappe . db . get_single_value ( ' E Invoice Settings ' , ' applicable_from ' ) or ' 2021-04-01 '
if getdate ( doc . get ( ' posting_date ' ) ) < getdate ( einvoicing_eligible_from ) :
return False
2020-12-25 04:56:43 +00:00
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 ' )
2021-02-12 06:54:41 +00:00
no_taxes_applied = not doc . get ( ' taxes ' )
2020-12-25 04:56:43 +00:00
2021-04-12 12:25:46 +00:00
if 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 :
2021-02-08 06:10:56 +00:00
return
2020-12-25 04:56:43 +00:00
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 ( )
2021-04-12 12:25:46 +00:00
doc . einvoice_status = ' Pending '
2020-12-25 04:56:43 +00:00
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 ' ) )
2021-02-11 12:20:57 +00:00
elif doc . irn and doc . docstatus == 2 and doc . _action == ' cancel ' and not doc . irn_cancelled :
2020-12-25 04:56:43 +00:00
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 '
2021-02-12 06:54:41 +00:00
if not supply_type :
2020-12-25 04:56:43 +00:00
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 ) :
2021-04-12 12:25:46 +00:00
if getdate ( invoice . posting_date ) < getdate ( ' 2021-01-01 ' ) :
frappe . throw ( _ ( ' IRN generation is not allowed for invoices dated before 1st Jan 2021 ' ) , title = _ ( ' Not Allowed ' ) )
2020-12-25 04:56:43 +00:00
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
) )
2021-04-12 12:25:46 +00:00
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 ) :
2021-01-05 10:29:17 +00:00
frappe . throw (
2021-04-12 12:25:46 +00:00
msg = _ ( ' Address Lines, City, Pincode, GSTIN are mandatory for address {} . Please set them and try again. ' ) . format ( address . name ) ,
2021-01-05 10:29:17 +00:00
title = _ ( ' Missing Address Fields ' )
)
2021-04-12 12:25:46 +00:00
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 :
2020-12-25 04:56:43 +00:00
# according to einvoice standard
2021-04-12 12:25:46 +00:00
addr . pincode = 999999
2020-12-25 04:56:43 +00:00
2021-04-02 09:34:48 +00:00
party_address_details = frappe . _dict ( dict (
2021-04-12 12:25:46 +00:00
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 )
2020-12-25 04:56:43 +00:00
) )
2021-04-12 12:25:46 +00:00
return party_address_details
2020-12-25 04:56:43 +00:00
def get_overseas_address_details ( address_name ) :
2021-01-05 10:29:17 +00:00
address_title , address_line1 , address_line2 , city = frappe . db . get_value (
' Address ' , address_name , [ ' address_title ' , ' address_line1 ' , ' address_line2 ' , ' city ' ]
2020-12-25 04:56:43 +00:00
)
2021-01-05 10:29:17 +00:00
if not address_title or not address_line1 or not city :
frappe . throw (
msg = _ ( ' Address lines and city is mandatory for address {} . Please set them and try again. ' ) . format (
get_link_to_form ( ' Address ' , address_name )
) ,
title = _ ( ' Missing Address Fields ' )
)
2020-12-25 04:56:43 +00:00
return frappe . _dict ( dict (
2021-02-23 11:56:37 +00:00
gstin = ' URP ' ,
legal_name = sanitize_for_json ( address_title ) ,
location = city ,
address_line1 = sanitize_for_json ( address_line1 ) ,
address_line2 = sanitize_for_json ( address_line2 ) ,
2021-01-05 10:29:17 +00:00
pincode = 999999 , state_code = 96 , place_of_supply = 96
2020-12-25 04:56:43 +00:00
) )
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
2021-02-23 11:56:37 +00:00
item . description = sanitize_for_json ( d . item_name )
2021-01-05 08:23:51 +00:00
2020-12-30 09:13:42 +00:00
item . qty = abs ( item . qty )
2021-04-12 05:25:43 +00:00
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 )
2020-12-25 04:56:43 +00:00
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 '
2021-01-05 08:23:51 +00:00
item . serial_no = " "
2020-12-25 04:56:43 +00:00
item = update_item_taxes ( invoice , item )
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
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 :
2021-02-28 15:16:23 +00:00
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)
2021-04-12 12:25:46 +00:00
item_tax_detail = json . loads ( t . item_wise_tax_detail ) . get ( item . item_code or item . item_name )
2021-02-28 15:16:23 +00:00
2020-12-30 09:13:42 +00:00
item_tax_rate = item_tax_detail [ 0 ]
# item tax amount excluding discount amount
2021-04-12 05:25:43 +00:00
item_tax_amount = ( item_tax_rate / 100 ) * item . taxable_value
2020-12-30 09:13:42 +00:00
2020-12-25 04:56:43 +00:00
if t . account_head in gst_accounts . cess_account :
2020-12-30 09:13:42 +00:00
item_tax_amount_after_discount = item_tax_detail [ 1 ]
2020-12-25 04:56:43 +00:00
if t . charge_type == ' On Item Quantity ' :
2020-12-30 09:13:42 +00:00
item . cess_nadv_amount + = abs ( item_tax_amount_after_discount )
2020-12-25 04:56:43 +00:00
else :
2020-12-30 09:13:42 +00:00
item . cess_rate + = item_tax_rate
item . cess_amount + = abs ( item_tax_amount_after_discount )
for tax_type in [ ' igst ' , ' cgst ' , ' sgst ' ] :
if t . account_head in gst_accounts [ f ' { tax_type } _account ' ] :
item . tax_rate + = item_tax_rate
item [ f ' { tax_type } _amount ' ] + = abs ( item_tax_amount )
2021-04-12 12:25:46 +00:00
else :
# TODO: other charges per item
pass
2020-12-30 09:13:42 +00:00
2020-12-25 04:56:43 +00:00
return item
def get_invoice_value_details ( invoice ) :
invoice_value_details = frappe . _dict ( dict ( ) )
2021-01-21 13:13:55 +00:00
2021-01-20 07:47:42 +00:00
if invoice . apply_discount_on == ' Net Total ' and invoice . discount_amount :
2021-04-12 05:25:43 +00:00
# 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 ' ) ] ) )
2021-01-20 07:47:42 +00:00
else :
2021-04-12 05:25:43 +00:00
invoice_value_details . base_total = abs ( sum ( [ i . taxable_value for i in invoice . get ( ' items ' ) ] ) )
2021-01-29 08:54:08 +00:00
# since tax already considers discount amount
invoice_value_details . invoice_discount_amt = 0
2021-01-21 13:13:55 +00:00
2021-01-13 15:30:44 +00:00
invoice_value_details . round_off = invoice . base_rounding_adjustment
2020-12-30 09:13:42 +00:00
invoice_value_details . base_grand_total = abs ( invoice . base_rounded_total ) or abs ( invoice . base_grand_total )
invoice_value_details . grand_total = abs ( invoice . rounded_total ) or abs ( invoice . grand_total )
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
invoice_value_details = update_invoice_taxes ( invoice , invoice_value_details )
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
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
2021-04-12 05:25:43 +00:00
considered_rows = [ ]
2020-12-25 04:56:43 +00:00
for t in invoice . taxes :
2021-04-12 05:25:43 +00:00
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
2020-12-25 04:56:43 +00:00
if t . account_head in gst_accounts_list :
if t . account_head in gst_accounts . cess_account :
2020-12-30 09:13:42 +00:00
# using after discount amt since item also uses after discount amt for cess calc
2020-12-25 04:56:43 +00:00
invoice_value_details . total_cess_amt + = abs ( t . base_tax_amount_after_discount_amount )
2021-02-12 06:54:41 +00:00
2020-12-30 09:13:42 +00:00
for tax_type in [ ' igst ' , ' cgst ' , ' sgst ' ] :
if t . account_head in gst_accounts [ f ' { tax_type } _account ' ] :
2021-04-12 05:25:43 +00:00
invoice_value_details [ f ' total_ { tax_type } _amt ' ] + = abs ( tax_amount )
update_other_charges ( t , invoice_value_details , gst_accounts_list , invoice , considered_rows )
2020-12-25 04:56:43 +00:00
else :
2021-04-12 05:25:43 +00:00
invoice_value_details . total_other_charges + = abs ( tax_amount )
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
return invoice_value_details
2021-04-12 05:25:43 +00:00
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 )
2020-12-25 04:56:43 +00:00
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 ) :
2021-04-12 12:25:46 +00:00
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 ' ) )
2020-12-25 04:56:43 +00:00
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 :
2021-04-12 12:25:46 +00:00
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 ' ) )
2020-12-25 04:56:43 +00:00
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 ]
) )
2021-01-05 08:23:51 +00:00
def validate_mandatory_fields ( invoice ) :
if not invoice . company_address :
2021-04-12 12:25:46 +00:00
frappe . throw (
_ ( ' Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again. ' ) ,
title = _ ( ' Missing Fields ' )
)
2021-01-05 08:23:51 +00:00
if not invoice . customer_address :
2021-04-12 12:25:46 +00:00
frappe . throw (
_ ( ' Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again. ' ) ,
title = _ ( ' Missing Fields ' )
)
2021-01-05 08:23:51 +00:00
if not frappe . db . get_value ( ' Address ' , invoice . company_address , ' gstin ' ) :
frappe . throw (
_ ( ' GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address. ' ) ,
title = _ ( ' Missing Fields ' )
)
2021-02-06 12:25:20 +00:00
if invoice . gst_category != ' Overseas ' and not frappe . db . get_value ( ' Address ' , invoice . customer_address , ' gstin ' ) :
2021-01-05 08:23:51 +00:00
frappe . throw (
_ ( ' GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address. ' ) ,
title = _ ( ' Missing Fields ' )
)
2021-04-12 12:25:46 +00:00
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 ' ] ) - 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. ' ) )
2020-12-25 04:56:43 +00:00
def make_einvoice ( invoice ) :
2021-01-05 08:23:51 +00:00
validate_mandatory_fields ( invoice )
2020-12-25 04:56:43 +00:00
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 )
2021-04-12 12:25:46 +00:00
seller_details = get_party_details ( invoice . company_address )
2020-12-25 04:56:43 +00:00
if invoice . gst_category == ' Overseas ' :
buyer_details = get_overseas_address_details ( invoice . customer_address )
else :
2021-04-12 12:25:46 +00:00
buyer_details = get_party_details ( invoice . customer_address )
2021-04-02 06:46:19 +00:00
place_of_supply = get_place_of_supply ( invoice , invoice . doctype )
if place_of_supply :
place_of_supply = place_of_supply . split ( ' - ' ) [ 0 ]
else :
place_of_supply = sanitize_for_json ( invoice . billing_address_gstin ) [ : 2 ]
2020-12-25 04:56:43 +00:00
buyer_details . update ( dict ( place_of_supply = place_of_supply ) )
2021-02-12 06:54:41 +00:00
2021-04-12 12:25:46 +00:00
seller_details . update ( dict ( legal_name = invoice . company ) )
buyer_details . update ( dict ( legal_name = invoice . customer_name or invoice . customer ) )
2020-12-25 04:56:43 +00:00
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 :
2021-01-13 15:30:44 +00:00
if invoice . gst_category == ' Overseas ' :
shipping_details = get_overseas_address_details ( invoice . shipping_address_name )
else :
2021-04-12 12:25:46 +00:00
shipping_details = get_party_details ( invoice . shipping_address_name , is_shipping_address = True )
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
if invoice . is_pos and invoice . base_paid_amount :
payment_details = get_payment_details ( invoice )
2021-02-12 06:54:41 +00:00
2021-04-12 12:25:46 +00:00
if invoice . is_return :
2020-12-25 04:56:43 +00:00
prev_doc_details = get_return_doc_reference ( invoice )
2021-02-12 06:54:41 +00:00
2021-04-12 12:25:46 +00:00
if invoice . transporter and flt ( invoice . distance ) and not invoice . is_return :
2020-12-25 04:56:43 +00:00
eway_bill_details = get_eway_bill_details ( invoice )
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
# 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
)
2021-04-12 12:25:46 +00:00
try :
einvoice = safe_json_load ( einvoice )
einvoice = santize_einvoice_fields ( einvoice )
except Exception :
2021-04-13 13:19:03 +00:00
show_link_to_error_log ( invoice , einvoice )
validate_totals ( einvoice )
2021-04-12 12:25:46 +00:00
return einvoice
2021-04-13 13:19:03 +00:00
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 ' )
)
2021-04-12 12:25:46 +00:00
def log_error ( data = None ) :
2021-04-13 13:19:03 +00:00
if isinstance ( data , six . string_types ) :
2021-04-12 12:25:46 +00:00
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 santize_einvoice_fields ( einvoice ) :
int_fields = [ " Pin " , " Distance " , " CrDay " ]
float_fields = [ " Qty " , " FreeQty " , " UnitPrice " , " TotAmt " , " Discount " , " PreTaxVal " , " AssAmt " , " GstRt " , " IgstAmt " , " CgstAmt " , " SgstAmt " , " CesRt " , " CesAmt " , " CesNonAdvlAmt " , " StateCesRt " , " StateCesAmt " , " StateCesNonAdvlAmt " , " OthChrg " , " TotItemVal " , " AssVal " , " CgstVal " , " SgstVal " , " IgstVal " , " CesVal " , " StCesVal " , " Discount " , " OthChrg " , " RndOffAmt " , " TotInvVal " , " TotInvValFc " , " PaidAmt " , " PaymtDue " , " ExpDuty " , ]
copy = einvoice . copy ( )
for key , value in copy . items ( ) :
if isinstance ( value , list ) :
for idx , d in enumerate ( value ) :
santized_dict = santize_einvoice_fields ( d )
if santized_dict :
einvoice [ key ] [ idx ] = santized_dict
else :
einvoice [ key ] . pop ( idx )
if not einvoice [ key ] :
einvoice . pop ( key , None )
elif isinstance ( value , dict ) :
santized_dict = santize_einvoice_fields ( value )
if santized_dict :
einvoice [ key ] = santized_dict
else :
einvoice . pop ( key , None )
elif not value or value == " None " :
einvoice . pop ( key , None )
elif key in float_fields :
einvoice [ key ] = flt ( value , 2 )
elif key in int_fields :
einvoice [ key ] = cint ( value )
2020-12-25 04:56:43 +00:00
return einvoice
2021-02-23 11:56:37 +00:00
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 ) )
2021-04-12 12:25:46 +00:00
class RequestFailed ( Exception ) :
pass
class CancellationNotAllowed ( Exception ) :
pass
2020-12-25 04:56:43 +00:00
class GSPConnector ( ) :
def __init__ ( self , doctype = None , docname = None ) :
2021-04-12 12:25:46 +00:00
self . doctype = doctype
self . docname = docname
2021-01-13 15:30:44 +00:00
2021-04-12 12:25:46 +00:00
self . set_invoice ( )
self . set_credentials ( )
2020-12-25 04:56:43 +00:00
2021-01-13 15:30:44 +00:00
# authenticate url is same for sandbox & live
self . authenticate_url = ' https://gsp.adaequare.com/gsp/authenticate?grant_type=token '
2021-04-12 12:25:46 +00:00
self . base_url = ' https://gsp.adaequare.com ' if not self . e_invoice_settings . sandbox_mode else ' https://gsp.adaequare.com/test '
2021-01-13 15:30:44 +00:00
2020-12-30 13:52:37 +00:00
self . cancel_irn_url = self . base_url + ' /enriched/ei/api/invoice/cancel '
2021-01-13 15:30:44 +00:00
self . irn_details_url = self . base_url + ' /enriched/ei/api/invoice/irn '
self . generate_irn_url = self . base_url + ' /enriched/ei/api/invoice '
self . gstin_details_url = self . base_url + ' /enriched/ei/api/master/gstin '
2021-01-24 05:29:37 +00:00
self . cancel_ewaybill_url = self . base_url + ' /enriched/ewb/ewayapi?action=CANEWB '
2020-12-30 13:52:37 +00:00
self . generate_ewaybill_url = self . base_url + ' /enriched/ei/api/ewaybill '
2021-04-12 12:25:46 +00:00
def set_invoice ( self ) :
self . invoice = None
if self . doctype and self . docname :
self . invoice = frappe . get_cached_doc ( self . doctype , self . docname )
def set_credentials ( self ) :
self . e_invoice_settings = frappe . get_cached_doc ( ' E Invoice Settings ' )
if not self . e_invoice_settings . enable :
frappe . throw ( _ ( " E-Invoicing is disabled. Please enable it from {} to generate e-invoices. " ) . format ( get_link_to_form ( " E Invoice Settings " , " E Invoice Settings " ) ) )
2020-12-25 04:56:43 +00:00
if self . invoice :
gstin = self . get_seller_gstin ( )
2021-04-12 12:25:46 +00:00
credentials_for_gstin = [ d for d in self . e_invoice_settings . credentials if d . gstin == gstin ]
if credentials_for_gstin :
self . credentials = credentials_for_gstin [ 0 ]
else :
frappe . throw ( _ ( ' Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings ' ) )
2020-12-25 04:56:43 +00:00
else :
2021-04-12 12:25:46 +00:00
self . credentials = self . e_invoice_settings . credentials [ 0 ] if self . e_invoice_settings . credentials else None
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
def get_seller_gstin ( self ) :
2021-04-13 13:19:03 +00:00
gstin = frappe . db . get_value ( ' Address ' , self . invoice . company_address , ' gstin ' )
2020-12-25 04:56:43 +00:00
if not gstin :
frappe . throw ( _ ( ' Cannot retrieve Company GSTIN. Please select company address with valid GSTIN. ' ) )
return gstin
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
def get_auth_token ( self ) :
if time_diff_in_seconds ( self . e_invoice_settings . token_expiry , now_datetime ( ) ) < 150.0 :
self . fetch_auth_token ( )
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
return self . e_invoice_settings . auth_token
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
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
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
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
} )
2021-01-20 07:47:42 +00:00
request_log . save ( ignore_permissions = True )
2020-12-25 04:56:43 +00:00
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 ' ) )
2021-01-20 07:47:42 +00:00
self . e_invoice_settings . save ( ignore_permissions = True )
self . e_invoice_settings . reload ( )
2020-12-25 04:56:43 +00:00
except Exception :
2021-04-12 12:25:46 +00:00
log_error ( res )
2020-12-25 04:56:43 +00:00
self . raise_error ( True )
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
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 :
2021-04-12 12:25:46 +00:00
log_error ( res )
2020-12-25 04:56:43 +00:00
raise RequestFailed
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
except RequestFailed :
self . raise_error ( )
except Exception :
2021-04-12 12:25:46 +00:00
log_error ( )
2020-12-25 04:56:43 +00:00
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 ) :
2021-04-12 12:25:46 +00:00
data = { }
2020-12-25 04:56:43 +00:00
try :
2021-04-12 12:25:46 +00:00
headers = self . get_headers ( )
einvoice = make_einvoice ( self . invoice )
data = json . dumps ( einvoice , indent = 4 )
2020-12-25 04:56:43 +00:00
res = self . make_request ( ' post ' , self . generate_irn_url , headers , data )
2021-04-12 12:25:46 +00:00
2020-12-25 04:56:43 +00:00
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
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
except RequestFailed :
errors = self . sanitize_error_message ( res . get ( ' message ' ) )
2021-04-12 12:25:46 +00:00
self . set_failed_status ( errors = errors )
2020-12-25 04:56:43 +00:00
self . raise_error ( errors = errors )
2021-04-12 12:25:46 +00:00
except Exception as e :
self . set_failed_status ( errors = str ( e ) )
log_error ( data )
2020-12-25 04:56:43 +00:00
self . raise_error ( True )
2021-02-12 06:54:41 +00:00
2021-04-12 12:25:46 +00:00
@staticmethod
def bulk_generate_irn ( invoices ) :
gsp_connector = GSPConnector ( )
gsp_connector . doctype = ' Sales Invoice '
failed = [ ]
for invoice in invoices :
try :
gsp_connector . docname = invoice
gsp_connector . set_invoice ( )
gsp_connector . set_credentials ( )
gsp_connector . generate_irn ( )
except Exception as e :
failed . append ( {
' docname ' : invoice ,
' message ' : str ( e )
} )
return failed
2020-12-25 04:56:43 +00:00
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
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
except RequestFailed :
errors = self . sanitize_error_message ( res . get ( ' message ' ) )
self . raise_error ( errors = errors )
except Exception :
2021-04-12 12:25:46 +00:00
log_error ( )
2020-12-25 04:56:43 +00:00
self . raise_error ( True )
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
def cancel_irn ( self , irn , reason , remark ) :
2021-04-12 12:25:46 +00:00
data , res = { } , { }
2020-12-25 04:56:43 +00:00
try :
2021-04-12 12:25:46 +00:00
# validate cancellation
if time_diff_in_hours ( now_datetime ( ) , self . invoice . ack_date ) > 24 :
frappe . throw ( _ ( ' E-Invoice cannot be cancelled after 24 hours of IRN generation. ' ) , title = _ ( ' Not Allowed ' ) , exc = CancellationNotAllowed )
if not irn :
frappe . throw ( _ ( ' IRN not found. You must generate IRN before cancelling. ' ) , title = _ ( ' Not Allowed ' ) , exc = CancellationNotAllowed )
headers = self . get_headers ( )
data = json . dumps ( {
' Irn ' : irn ,
' Cnlrsn ' : reason ,
' Cnlrem ' : remark
} , indent = 4 )
2020-12-25 04:56:43 +00:00
res = self . make_request ( ' post ' , self . cancel_irn_url , headers , data )
2021-04-12 12:25:46 +00:00
if res . get ( ' success ' ) or ' 9999 ' in res . get ( ' message ' ) :
2020-12-25 04:56:43 +00:00
self . invoice . irn_cancelled = 1
2021-04-12 12:25:46 +00:00
self . invoice . irn_cancel_date = res . get ( ' result ' ) [ ' CancelDate ' ] if res . get ( ' result ' ) else " "
self . invoice . einvoice_status = ' Cancelled '
2020-12-25 04:56:43 +00:00
self . invoice . flags . updater_reference = {
' doctype ' : self . invoice . doctype ,
' docname ' : self . invoice . name ,
' label ' : _ ( ' IRN Cancelled - {} ' ) . format ( remark )
}
self . update_invoice ( )
else :
raise RequestFailed
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
except RequestFailed :
errors = self . sanitize_error_message ( res . get ( ' message ' ) )
2021-04-12 12:25:46 +00:00
self . set_failed_status ( errors = errors )
2020-12-25 04:56:43 +00:00
self . raise_error ( errors = errors )
2021-04-12 12:25:46 +00:00
except CancellationNotAllowed as e :
self . set_failed_status ( errors = str ( e ) )
self . raise_error ( errors = str ( e ) )
except Exception as e :
self . set_failed_status ( errors = str ( e ) )
log_error ( data )
2020-12-25 04:56:43 +00:00
self . raise_error ( True )
2021-01-24 05:29:37 +00:00
2021-04-12 12:25:46 +00:00
@staticmethod
def bulk_cancel_irn ( invoices , reason , remark ) :
gsp_connector = GSPConnector ( )
gsp_connector . doctype = ' Sales Invoice '
failed = [ ]
for invoice in invoices :
try :
gsp_connector . docname = invoice
gsp_connector . set_invoice ( )
gsp_connector . set_credentials ( )
irn = gsp_connector . invoice . irn
gsp_connector . cancel_irn ( irn , reason , remark )
except Exception as e :
failed . append ( {
' docname ' : invoice ,
' message ' : str ( e )
} )
return failed
2020-12-25 04:56:43 +00:00
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 :
2021-04-12 12:25:46 +00:00
log_error ( data )
2020-12-25 04:56:43 +00:00
self . raise_error ( True )
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
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 )
2021-01-24 05:29:37 +00:00
headers [ " username " ] = headers [ " user_name " ]
del headers [ " user_name " ]
2020-12-25 04:56:43 +00:00
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 :
2021-04-12 12:25:46 +00:00
log_error ( data )
2020-12-25 04:56:43 +00:00
self . raise_error ( True )
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
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
'''
2021-04-12 12:25:46 +00:00
if not message :
return [ ]
2020-12-25 04:56:43 +00:00
errors = re . findall ( ' : [^:]+ ' , message )
for idx , e in enumerate ( errors ) :
# remove colons
errors [ idx ] = errors [ idx ] . replace ( ' : ' , ' ' ) . strip ( )
# if not last
if idx != len ( errors ) - 1 :
# remove last 7 chars eg: ', 3095 '
errors [ idx ] = errors [ idx ] [ : - 6 ]
return errors
def raise_error ( self , raise_exception = False , errors = [ ] ) :
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 '
)
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
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 ' )
2021-04-08 13:36:44 +00:00
self . invoice . ack_no = res . get ( ' AckNo ' )
self . invoice . ack_date = res . get ( ' AckDt ' )
2020-12-25 04:56:43 +00:00
self . invoice . signed_einvoice = dec_signed_invoice
2021-04-12 12:25:46 +00:00
self . invoice . ack_no = res . get ( ' AckNo ' )
self . invoice . ack_date = res . get ( ' AckDt ' )
2020-12-25 04:56:43 +00:00
self . invoice . signed_qr_code = res . get ( ' SignedQRCode ' )
2021-04-12 12:25:46 +00:00
self . invoice . einvoice_status = ' Generated '
2020-12-25 04:56:43 +00:00
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
2021-01-24 05:29:37 +00:00
filename = ' QRCode_ {} .png ' . format ( docname ) . replace ( os . path . sep , " __ " )
2020-12-25 04:56:43 +00:00
2021-01-24 05:29:37 +00:00
qr_image = io . BytesIO ( )
2020-12-25 04:56:43 +00:00
url = qrcreate ( qrcode , error = ' L ' )
2021-01-24 05:29:37 +00:00
url . png ( qr_image , scale = 2 , quiet_zone = 1 )
_file = frappe . get_doc ( {
" doctype " : " File " ,
" file_name " : filename ,
" attached_to_doctype " : doctype ,
" attached_to_name " : docname ,
" attached_to_field " : " qrcode_image " ,
" is_private " : 1 ,
" content " : qr_image . getvalue ( ) } )
_file . save ( )
frappe . db . commit ( )
2020-12-25 04:56:43 +00:00
self . invoice . qrcode_image = _file . file_url
2021-02-12 06:54:41 +00:00
2020-12-25 04:56:43 +00:00
def update_invoice ( self ) :
self . invoice . flags . ignore_validate_update_after_submit = True
self . invoice . flags . ignore_validate = True
self . invoice . save ( )
2021-04-12 12:25:46 +00:00
def set_failed_status ( self , errors = None ) :
frappe . db . rollback ( )
self . invoice . einvoice_status = ' Failed '
self . invoice . failure_description = self . get_failure_message ( errors ) if errors else " "
self . update_invoice ( )
frappe . db . commit ( )
def get_failure_message ( self , errors ) :
if isinstance ( errors , list ) :
errors = ' , ' . join ( errors )
return errors
2021-02-23 11:56:37 +00:00
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 ]
2020-12-25 04:56:43 +00:00
@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 ) :
2021-04-12 12:25:46 +00:00
# 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 )
@frappe.whitelist ( )
def generate_einvoices ( docnames ) :
docnames = json . loads ( docnames ) or [ ]
if len ( docnames ) < 10 :
failures = GSPConnector . bulk_generate_irn ( docnames )
frappe . local . message_log = [ ]
if failures :
show_bulk_action_failure_message ( failures )
success = len ( docnames ) - len ( failures )
frappe . msgprint (
_ ( ' {} e-invoices generated successfully ' ) . format ( success ) ,
title = _ ( ' Bulk E-Invoice Generation Complete ' )
)
else :
enqueue_bulk_action ( schedule_bulk_generate_irn , docnames = docnames )
def schedule_bulk_generate_irn ( docnames ) :
failures = GSPConnector . bulk_generate_irn ( docnames )
frappe . local . message_log = [ ]
frappe . publish_realtime ( " bulk_einvoice_generation_complete " , {
" user " : frappe . session . user ,
" failures " : failures ,
" invoices " : docnames
} )
def show_bulk_action_failure_message ( failures ) :
for doc in failures :
docname = ' <a href= " sales-invoice/ {0} " > {0} </a> ' . format ( doc . get ( ' docname ' ) )
message = doc . get ( ' message ' ) . replace ( " ' " , ' " ' )
if message [ 0 ] == ' [ ' :
errors = json . loads ( message )
error_list = ' ' . join ( [ ' <li> {} </li> ' . format ( err ) for err in errors ] )
message = ''' {} has following errors:<br>
< ul style = " padding-left: 20px; padding-top: 5px " > { } < / ul > ''' .format(docname, error_list)
else :
message = ' {} - {} ' . format ( docname , message )
frappe . msgprint (
message ,
title = _ ( ' Bulk E-Invoice Generation Complete ' ) ,
indicator = ' red '
)
@frappe.whitelist ( )
def cancel_irns ( docnames , reason , remark ) :
docnames = json . loads ( docnames ) or [ ]
if len ( docnames ) < 10 :
failures = GSPConnector . bulk_cancel_irn ( docnames , reason , remark )
frappe . local . message_log = [ ]
if failures :
show_bulk_action_failure_message ( failures )
success = len ( docnames ) - len ( failures )
frappe . msgprint (
_ ( ' {} e-invoices cancelled successfully ' ) . format ( success ) ,
title = _ ( ' Bulk E-Invoice Cancellation Complete ' )
)
else :
enqueue_bulk_action ( schedule_bulk_cancel_irn , docnames = docnames , reason = reason , remark = remark )
def schedule_bulk_cancel_irn ( docnames , reason , remark ) :
failures = GSPConnector . bulk_cancel_irn ( docnames , reason , remark )
frappe . local . message_log = [ ]
frappe . publish_realtime ( " bulk_einvoice_cancellation_complete " , {
" user " : frappe . session . user ,
" failures " : failures ,
" invoices " : docnames
} )
def enqueue_bulk_action ( job , * * kwargs ) :
check_scheduler_status ( )
enqueue (
job ,
* * kwargs ,
queue = " long " ,
timeout = 10000 ,
event = " processing_bulk_einvoice_action " ,
now = frappe . conf . developer_mode or frappe . flags . in_test ,
)
if job == schedule_bulk_generate_irn :
msg = _ ( ' E-Invoices will be generated in a background process. ' )
else :
msg = _ ( ' E-Invoices will be cancelled in a background process. ' )
frappe . msgprint ( msg , alert = 1 )
def check_scheduler_status ( ) :
if is_scheduler_inactive ( ) and not frappe . flags . in_test :
frappe . throw ( _ ( " Scheduler is inactive. Cannot enqueue job. " ) , title = _ ( " Scheduler Inactive " ) )
def job_already_enqueued ( job_name ) :
enqueued_jobs = [ d . get ( " job_name " ) for d in get_info ( ) ]
if job_name in enqueued_jobs :
return True