diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py index dacdc86af53..cbad581ab2a 100644 --- a/erpnext/patches/v12_0/setup_einvoice_fields.py +++ b/erpnext/patches/v12_0/setup_einvoice_fields.py @@ -32,4 +32,5 @@ def execute(): add_print_formats() frappe.db.set_value('Custom Field', { 'fieldname': 'mode_of_transport' }, 'default', '') - frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'depends_on', 'eval:doc.mode_of_transport == "Road"') \ No newline at end of file + frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'depends_on', 'eval:doc.mode_of_transport == "Road"') + frappe.db.set_value('Custom Field', { 'fieldname': 'ewaybill' }, 'depends_on', 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)') \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js index 62a2d2d5202..d4c473363d0 100644 --- a/erpnext/regional/india/e_invoice/einvoice.js +++ b/erpnext/regional/india/e_invoice/einvoice.js @@ -7,7 +7,13 @@ erpnext.setup_einvoice_actions = (doctype) => { if (!einvoicing_enabled || !valid_supply_type) return; - const { docstatus, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; + const { doctype, docstatus, 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 (ewaybill && irn) { frm.set_df_property('ewaybill', 'read_only', 1); @@ -17,13 +23,13 @@ erpnext.setup_einvoice_actions = (doctype) => { const action = () => { frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.generate_irn', - args: { docname: name }, + args: { doctype, docname: name }, freeze: true, callback: () => frm.reload_doc() }) }; - frm.add_custom_button(__("Generate IRN"), action, __('E Invoicing')); + add_custom_button(__("Generate IRN"), action); } if (docstatus == 1 && irn && !irn_cancelled && !ewaybill) { @@ -52,6 +58,7 @@ erpnext.setup_einvoice_actions = (doctype) => { frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.cancel_irn', args: { + doctype, docname: name, irn: irn, reason: data.reason.split('-')[0], @@ -66,105 +73,23 @@ erpnext.setup_einvoice_actions = (doctype) => { }); d.show(); }; - frm.add_custom_button(__("Cancel IRN"), action, __("E Invoicing")); + add_custom_button(__("Cancel IRN"), action); } if (irn && !irn_cancelled && !ewaybill) { - const fields = [ - { - '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 action = () => { const d = new frappe.ui.Dialog({ title: __('Generate E-Way Bill'), wide: 1, - fields: fields, + 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: { - docname: name, irn, + doctype, + docname: name, + irn, ...data }, freeze: true, @@ -177,7 +102,7 @@ erpnext.setup_einvoice_actions = (doctype) => { d.show(); }; - frm.add_custom_button(__("Generate E-Way Bill"), action, __("E Invoicing")); + add_custom_button(__("Generate E-Way Bill"), action); } if (docstatus == 1 && irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { @@ -206,6 +131,7 @@ erpnext.setup_einvoice_actions = (doctype) => { frappe.call({ method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', args: { + doctype, docname: name, eway_bill: ewaybill, reason: data.reason.split('-')[0], @@ -220,8 +146,94 @@ erpnext.setup_einvoice_actions = (doctype) => { }); d.show(); }; - frm.add_custom_button(__("Cancel E-Way Bill"), action, __("E Invoicing")); + 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 + } + ]; } \ No newline at end of file diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 54f18d37f00..b38c5070cbd 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -240,9 +240,7 @@ def get_eway_bill_details(invoice): vehicle_type=vehicle_type[invoice.gst_vehicle_type] )) -@frappe.whitelist() -def make_einvoice(doctype, name): - invoice = frappe.get_doc(doctype, name) +def make_einvoice(invoice): schema = read_json('einv_template') trans_details = get_trans_details(invoice) @@ -255,7 +253,7 @@ def make_einvoice(doctype, name): 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, doctype) or invoice.billing_address_gstin + 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)) @@ -351,42 +349,10 @@ def validate_einvoice(validations, einvoice, errors=[]): return errors -def update_invoice(doctype, docname, res): - enc_signed_invoice = res.get('SignedInvoice') - dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data'] - - frappe.db.set_value(doctype, docname, 'irn', res.get('Irn')) - frappe.db.set_value(doctype, docname, 'ewaybill', res.get('EwbNo')) - frappe.db.set_value(doctype, docname, 'signed_einvoice', dec_signed_invoice) - - signed_qr_code = res.get('SignedQRCode') - frappe.db.set_value(doctype, docname, 'signed_qr_code', signed_qr_code) - - attach_qrcode_image(doctype, docname, signed_qr_code) - -def attach_qrcode_image(doctype, docname, qrcode): - if not qrcode: return - - _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) - abs_file_path = os.path.abspath(_file.get_full_path()) - url.png(abs_file_path, scale=2) - - frappe.db.set_value(doctype, docname, 'qrcode_image', _file.file_url) - class ResponseFailure(Exception): pass class GSPConnector(): - def __init__(self): + def __init__(self, doctype, docname): self.credentials = frappe.get_cached_doc('E Invoice Settings') self.base_url = 'https://gsp.adaequare.com/' @@ -397,6 +363,8 @@ class GSPConnector(): 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' + + self.invoice = frappe.get_cached_doc(doctype, docname) def get_auth_token(self): if time_diff_in_seconds(self.credentials.token_expiry, now_datetime()) < 150.0: @@ -473,23 +441,22 @@ class GSPConnector(): frappe.cache().hset('gstin_cache', key, details) return details - def generate_irn(self, docname): + def generate_irn(self): headers = self.get_headers() - doctype = 'Sales Invoice' - einvoice = make_einvoice(doctype, docname) + einvoice = make_einvoice(self.invoice) data = json.dumps(einvoice) try: res = make_post_request(self.generate_irn_url, headers=headers, data=data) if res.get('success'): - update_invoice(doctype, docname, res.get('result')) + self.set_einvoice_data(res.get('result')) elif '2150' in res.get('message'): # IRN already generated irn = res.get('result')[0].get('Desc').get('Irn') irn_details = self.get_irn_details(irn) if irn_details: - update_invoice(doctype, docname, irn_details) + self.set_einvoice_data(irn_details) else: self.log_error(res) @@ -521,9 +488,8 @@ class GSPConnector(): self.log_error() self.raise_error(True) - def cancel_irn(self, docname, irn, reason, remark): + def cancel_irn(self, irn, reason, remark): headers = self.get_headers() - doctype = 'Sales Invoice' data = json.dumps({ 'Irn': irn, 'Cnlrsn': reason, @@ -533,8 +499,14 @@ class GSPConnector(): try: res = make_post_request(self.cancel_irn_url, headers=headers, data=data) if res.get('success'): - frappe.db.set_value(doctype, docname, 'irn_cancelled', 1) - # frappe.db.set_value(doctype, docname, 'cancelled_on', res.get('CancelDate')) + 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: self.log_error(res) raise ResponseFailure @@ -550,8 +522,6 @@ class GSPConnector(): args = frappe._dict(kwargs) headers = self.get_headers() - doctype = 'Sales Invoice' - docname = args.docname eway_bill_details = get_eway_bill_details(args) data = json.dumps({ 'Irn': args.irn, @@ -568,12 +538,16 @@ class GSPConnector(): try: res = make_post_request(self.generate_ewaybill_url, headers=headers, data=data) if res.get('success'): - frappe.db.set_value(doctype, docname, 'ewaybill', res.get('result').get('EwbNo')) - frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 0) - for d in args: - if d in ['docname', 'cmd']: continue - # update eway bill details in sales invoice - frappe.db.set_value(doctype, docname, d, args[d]) + 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: self.log_error(res) raise ResponseFailure @@ -585,7 +559,7 @@ class GSPConnector(): self.log_error(data) self.raise_error(True) - def cancel_eway_bill(self, docname, eway_bill, reason, remark): + def cancel_eway_bill(self, eway_bill, reason, remark): headers = self.get_headers() doctype = 'Sales Invoice' data = json.dumps({ @@ -597,8 +571,15 @@ class GSPConnector(): try: res = make_post_request(self.cancel_ewaybill_url, headers=headers, data=data) if res.get('success'): - frappe.db.set_value(doctype, docname, 'ewaybill', '') - frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1) + 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: self.log_error(res) raise ResponseFailure @@ -625,23 +606,67 @@ class GSPConnector(): 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) + abs_file_path = os.path.abspath(_file.get_full_path()) + url.png(abs_file_path, scale=2) + + 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 generate_irn(docname): - gsp_connector = GSPConnector() - gsp_connector.generate_irn(docname) +def generate_irn(doctype, docname): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.generate_irn() @frappe.whitelist() -def cancel_irn(docname, irn, reason, remark): - gsp_connector = GSPConnector() - gsp_connector.cancel_irn(docname, irn, reason, remark) +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(**kwargs): - gsp_connector = GSPConnector() +def generate_eway_bill(doctype, docname, **kwargs): + gsp_connector = GSPConnector(doctype, docname) gsp_connector.generate_eway_bill(**kwargs) @frappe.whitelist() -def cancel_eway_bill(docname, eway_bill, reason, remark): - gsp_connector = GSPConnector() - gsp_connector.cancel_eway_bill(docname, eway_bill, reason, remark) \ No newline at end of file +def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): + gsp_connector = GSPConnector(doctype, docname) + gsp_connector.cancel_eway_bill(eway_bill, reason, remark) \ No newline at end of file