From 499273e64ac1d335d893de28d5e0073df90f2a72 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 6 Oct 2020 21:27:00 +0530 Subject: [PATCH 1/9] feat: manual download / upload json --- erpnext/regional/india/e_invoice_utils.py | 95 +++++++--- erpnext/regional/india/einv_validation.json | 21 ++- erpnext/regional/india/einvoice.js | 188 ++++++++++++++------ 3 files changed, 214 insertions(+), 90 deletions(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index dc38b25bed7..6109f800f7d 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -22,10 +22,12 @@ def validate_einvoice_fields(doc): e_invoice_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable") if not doc.doctype in ['Sales Invoice', 'Purchase Invoice'] or not e_invoice_enabled: return - if doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: + if doc.docstatus == 0 and doc._action == 'save' and doc.irn: + frappe.throw(_("You cannot edit the invoice after generating IRN"), title=_("Edit Not Allowed")) + elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: frappe.throw(_("You must generate IRN before submitting the document."), title=_("Missing IRN")) elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: - frappe.throw(_("You must cancel IRN before cancelling the document."), title=_("Not Allowed")) + frappe.throw(_("You must cancel IRN before cancelling the document."), title=_("Cancel Not Allowed")) def get_einv_credentials(): return frappe.get_doc("E Invoice Settings") @@ -266,20 +268,24 @@ def get_doc_details(invoice): )) def get_party_gstin_details(party_address): - gstin, address_line1, address_line2, phone, email_id = frappe.db.get_value( - "Address", party_address, ["gstin", "address_line1", "address_line2", "phone", "email_id"] - ) - gstin_details = get_gstin_details(gstin) - legal_name = gstin_details.get('LegalName') - trade_name = gstin_details.get('TradeName') - location = gstin_details.get('AddrLoc') - state_code = gstin_details.get('StateCode') - pincode = cint(gstin_details.get('AddrPncd')) + address = frappe.get_all("Address", filters={"name": party_address}, fields=["*"])[0] + + # gstin_details = get_gstin_details(gstin) + gstin = address.get('gstin') + legal_name = address.get('address_title') + # trade_name = gstin_details.get('TradeName') + location = address.get('city') + state_code = address.get('gst_state_number') + pincode = cint(address.get('pincode')) + address_line1 = address.get('address_line1') + address_line2 = address.get('address_line2') + email_id = address.get('email_id') + phone = address.get('phone') if state_code == 97: pincode = 999999 return frappe._dict(dict( - gstin=gstin, legal_name=legal_name, trade_name=trade_name, location=location, + gstin=gstin, legal_name=legal_name, location=location, pincode=pincode, state_code=state_code, address_line1=address_line1, address_line2=address_line2, email=email_id, phone=phone )) @@ -338,8 +344,6 @@ def get_item_list(invoice): e_inv_item = item_schema.format(item=item) item_list.append(e_inv_item) - print(e_inv_item) - return ", ".join(item_list) def get_value_details(invoice): @@ -404,10 +408,11 @@ def get_eway_bill_details(invoice): vehicle_type=vehicle_type[invoice.gst_vehicle_type] )) -def make_e_invoice(invoice): +@frappe.whitelist() +def make_e_invoice(doctype, name): + invoice = frappe.get_doc(doctype, name) schema = read_json("einv_template") - validations = read_json("einv_validation") - validations = json.loads(validations) + validations = json.loads(read_json("einv_validation")) trans_details = get_trans_details(invoice) doc_details = get_doc_details(invoice) @@ -451,13 +456,17 @@ def make_e_invoice(invoice): ) e_invoice = json.loads(e_invoice) - error_msgs = run_e_invoice_validations(validations, e_invoice, []) + error_msgs = validate_einvoice(validations, e_invoice, []) if error_msgs: - frappe.throw(_("{}").format("
".join(error_msgs)), title=_("E Invoice Validation")) + if len(error_msgs) > 1: + li = ["
  • "+ d +"
  • " for d in error_msgs] + frappe.throw(_("""""").format("".join(li)), title=_("E Invoice Validation Failed")) + else: + frappe.throw(_("{}").format(error_msgs[0]), title=_("E Invoice Validation Failed")) - return json.dumps(e_invoice) + return {'einvoice': json.dumps(e_invoice)} -def run_e_invoice_validations(validations, e_invoice, error_msgs=[]): +def validate_einvoice(validations, e_invoice, error_msgs=[]): type_map = { "string": cstr, "number": cint, @@ -478,9 +487,9 @@ def run_e_invoice_validations(validations, e_invoice, error_msgs=[]): if isinstance(invoice_value, list): for d in invoice_value: - run_e_invoice_validations(properties, d, error_msgs) + validate_einvoice(properties, d, error_msgs) else: - run_e_invoice_validations(properties, invoice_value, error_msgs) + validate_einvoice(properties, invoice_value, error_msgs) # remove keys with empty dicts if not invoice_value: e_invoice.pop(field, None) @@ -507,6 +516,42 @@ def run_e_invoice_validations(validations, e_invoice, error_msgs=[]): if value.get('type').lower() == 'number' and not (flt(invoice_value) <= should_be_less_than): error_msgs.append("{} should be less than {}".format(field_label, should_be_less_than)) if pattern_str and not pattern.match(invoice_value): - error_msgs.append("{} should match {}".format(field_label, pattern_str)) + error_msgs.append(value.get('validationMsg')) - return error_msgs \ No newline at end of file + return error_msgs + +@frappe.whitelist() +def download_einvoice(): + data = frappe._dict(frappe.local.form_dict) + einvoice = data['einvoice'] + name = data['name'] + + frappe.response['filename'] = "E-Invoice-" + name + ".json" + frappe.response['filecontent'] = einvoice + frappe.response['content_type'] = 'application/json' + frappe.response['type'] = 'download' + +@frappe.whitelist() +def upload_einvoice(): + signed_einvoice = json.loads(frappe.local.uploaded_file) + data = frappe._dict(frappe.local.form_dict) + doctype = data['doctype'] + name = data['docname'] + + frappe.db.set_value(doctype, name, "irn", signed_einvoice.get("Irn")) + frappe.db.set_value(doctype, name, "ewaybill", signed_einvoice.get("EwbNo")) + +@frappe.whitelist() +def download_cancel_einvoice(): + data = frappe._dict(frappe.local.form_dict) + name = data['name'] + irn = data['irn'] + reason = data['reason'] + remark = data['remark'] + + cancel_einvoice = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark)) + + frappe.response['filename'] = "Cancel E-Invoice " + name + ".json" + frappe.response['filecontent'] = cancel_einvoice + frappe.response['content_type'] = 'application/json' + frappe.response['type'] = 'download' \ No newline at end of file diff --git a/erpnext/regional/india/einv_validation.json b/erpnext/regional/india/einv_validation.json index 28bfc396e30..9f0ef5006c4 100644 --- a/erpnext/regional/india/einv_validation.json +++ b/erpnext/regional/india/einv_validation.json @@ -59,13 +59,15 @@ "minLength": 1, "maxLength": 16, "pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$", - "label": "Document Name" + "label": "Document Name", + "validationMsg": "Document name should not be starting with 0, / and -" }, "Dt": { "type": "string", "minLength": 10, "maxLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "validationMsg": "Document Date is invalid" } }, "required": ["Typ", "No", "Dt"] @@ -77,7 +79,8 @@ "type": "string", "minLength": 15, "maxLength": 15, - "pattern": "([0-9]{2}[0-9A-Z]{13})" + "pattern": "([0-9]{2}[0-9A-Z]{13})", + "validationMsg": "Seller GSTIN is invalid" }, "LglNm": { "type": "string", @@ -134,7 +137,8 @@ "type": "string", "minLength": 3, "maxLength": 15, - "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$" + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", + "validationMsg": "Buyer GSTIN is invalid" }, "LglNm": { "type": "string", @@ -232,7 +236,8 @@ "type": "string", "maxLength": 15, "minLength": 3, - "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$" + "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$", + "validationMsg": "Shipping Address GSTIN is invalid" }, "LglNm": { "type": "string", @@ -429,13 +434,15 @@ "type": "string", "maxLength": 10, "minLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "validationMsg": "Expiry Date is invalid" }, "WrDt": { "type": "string", "maxLength": 10, "minLength": 10, - "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]" + "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]", + "validationMsg": "Warranty Date is invalid" } }, "required": ["Nm"] diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index 20461ea317b..4f98ffc88ab 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -7,27 +7,131 @@ erpnext.setup_einvoice_actions = (doctype) => { || !['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type)) { return; } + // if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) { + // frm.add_custom_button( + // "Generate IRN", + // () => { + // frappe.call({ + // method: 'erpnext.regional.india.e_invoice_utils.generate_irn', + // args: { doctype: frm.doc.doctype, name: frm.doc.name }, + // freeze: true, + // callback: (res) => { + // console.log(res.message); + // frm.set_value('irn', res.message['Irn']); + // frm.set_value('signed_einvoice', JSON.stringify(res.message['DecryptedSignedInvoice'])); + // frm.set_value('signed_qr_code', JSON.stringify(res.message['DecryptedSignedQRCode'])); + + // if (res.message['EwbNo']) frm.set_value('ewaybill', res.message['EwbNo']); + // frm.save(); + // } + // }) + // } + // ) + // } + + // if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) { + // frm.add_custom_button( + // "Cancel IRN", + // () => { + // const d = new frappe.ui.Dialog({ + // title: __('Cancel IRN'), + // fields: [ + // { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", + // "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, + // { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 } + // ], + // primary_action: function() { + // const data = d.get_values(); + // frappe.call({ + // method: 'erpnext.regional.india.e_invoice_utils.cancel_irn', + // args: { irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark }, + // freeze: true, + // callback: () => { + // frm.set_value('irn_cancelled', 1); + // frm.save("Update"); + // d.hide() + // }, + // error: () => d.hide() + // }) + // }, + // primary_action_label: __('Submit') + // }); + // d.show(); + // } + // ) + // } + + // if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled && !frm.doc.eway_bill_cancelled) { + // frm.add_custom_button( + // "Cancel E-Way Bill", + // () => { + // const d = new frappe.ui.Dialog({ + // title: __('Cancel E-Way Bill'), + // fields: [ + // { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", + // "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, + // { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 } + // ], + // primary_action: function() { + // const data = d.get_values(); + // frappe.call({ + // method: 'erpnext.regional.india.e_invoice_utils.cancel_eway_bill', + // args: { eway_bill: frm.doc.ewaybill, reason: data.reason.split('-')[0], remark: data.remark }, + // freeze: true, + // callback: () => { + // frm.set_value('eway_bill_cancelled', 1); + // frm.save("Update"); + // d.hide() + // }, + // error: () => d.hide() + // }) + // }, + // primary_action_label: __('Submit') + // }); + // d.show(); + // } + // ) + // } if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) { frm.add_custom_button( - "Generate IRN", + "Download E-Invoice", () => { frappe.call({ - method: 'erpnext.regional.india.e_invoice_utils.generate_irn', + method: 'erpnext.regional.india.e_invoice_utils.make_e_invoice', args: { doctype: frm.doc.doctype, name: frm.doc.name }, freeze: true, callback: (res) => { - console.log(res.message); - frm.set_value('irn', res.message['Irn']); - frm.set_value('signed_einvoice', JSON.stringify(res.message['DecryptedSignedInvoice'])); - frm.set_value('signed_qr_code', JSON.stringify(res.message['DecryptedSignedQRCode'])); - - if (res.message['EwbNo']) frm.set_value('ewaybill', res.message['EwbNo']); - frm.save(); + if (!res.exc) { + const args = { + cmd: 'erpnext.regional.india.e_invoice_utils.download_einvoice', + einvoice: res.message.einvoice, + name: frm.doc.name + }; + open_url_post(frappe.request.url, args); + } } }) } - ) + ); + } + if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) { + frm.add_custom_button( + "Upload Signed E-Invoice", + () => { + new frappe.ui.FileUploader({ + method: 'erpnext.regional.india.e_invoice_utils.upload_einvoice', + allow_multiple: 0, + doctype: frm.doc.doctype, + docname: frm.doc.name, + on_success: (attachment, r) => { + if (!r.exc) { + frm.reload_doc(); + } + } + }); + } + ); } if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) { frm.add_custom_button( @@ -36,62 +140,30 @@ erpnext.setup_einvoice_actions = (doctype) => { const d = new frappe.ui.Dialog({ title: __('Cancel IRN'), fields: [ - { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", - "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, - { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 } + { + "label" : "Reason", "fieldname": "reason", + "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", + "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] + }, + { + "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 + } ], primary_action: function() { const data = d.get_values(); - frappe.call({ - method: 'erpnext.regional.india.e_invoice_utils.cancel_irn', - args: { irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark }, - freeze: true, - callback: () => { - frm.set_value('irn_cancelled', 1); - frm.save("Update"); - d.hide() - }, - error: () => d.hide() - }) + const args = { + cmd: 'erpnext.regional.india.e_invoice_utils.download_cancel_einvoice', + irn: frm.doc.irn, reason: data.reason.split('-')[0], remark: data.remark, name: frm.doc.name + }; + open_url_post(frappe.request.url, args); + d.hide(); }, - primary_action_label: __('Submit') + primary_action_label: __('Download JSON') }); d.show(); } - ) - } - if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled && !frm.doc.eway_bill_cancelled) { - frm.add_custom_button( - "Cancel E-Way Bill", - () => { - const d = new frappe.ui.Dialog({ - title: __('Cancel E-Way Bill'), - fields: [ - { "label" : "Reason", "fieldname": "reason", "fieldtype": "Select", "reqd": 1, "default": "1-Duplicate", - "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] }, - { "label": "Remark", "fieldname": "remark", "fieldtype": "Data", "reqd": 1 } - ], - primary_action: function() { - const data = d.get_values(); - frappe.call({ - method: 'erpnext.regional.india.e_invoice_utils.cancel_eway_bill', - args: { eway_bill: frm.doc.ewaybill, reason: data.reason.split('-')[0], remark: data.remark }, - freeze: true, - callback: () => { - frm.set_value('eway_bill_cancelled', 1); - frm.save("Update"); - d.hide() - }, - error: () => d.hide() - }) - }, - primary_action_label: __('Submit') - }); - d.show(); - } - ) + ); } } - }) } \ No newline at end of file From f6d23df826ceb69e3ab20800fd2a8501467db796 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 7 Oct 2020 11:07:11 +0530 Subject: [PATCH 2/9] chore: group e-invoicing actions --- erpnext/regional/india/e_invoice_utils.py | 15 ++++++++++--- erpnext/regional/india/einvoice.js | 26 ++++++++++++++++------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index 6109f800f7d..b33c15507c0 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -464,7 +464,7 @@ def make_e_invoice(doctype, name): else: frappe.throw(_("{}").format(error_msgs[0]), title=_("E Invoice Validation Failed")) - return {'einvoice': json.dumps(e_invoice)} + return {'einvoice': json.dumps([e_invoice])} def validate_einvoice(validations, e_invoice, error_msgs=[]): type_map = { @@ -549,9 +549,18 @@ def download_cancel_einvoice(): reason = data['reason'] remark = data['remark'] - cancel_einvoice = json.dumps(dict(Irn=irn, CnlRsn=reason, CnlRem=remark)) + cancel_einvoice = json.dumps([dict(Irn=irn, CnlRsn=reason, CnlRem=remark)]) frappe.response['filename'] = "Cancel E-Invoice " + name + ".json" frappe.response['filecontent'] = cancel_einvoice frappe.response['content_type'] = 'application/json' - frappe.response['type'] = 'download' \ No newline at end of file + frappe.response['type'] = 'download' + +@frappe.whitelist() +def download_cancel_ack(): + cancel_ack = json.loads(frappe.local.uploaded_file) + data = frappe._dict(frappe.local.form_dict) + doctype = data['doctype'] + name = data['docname'] + + frappe.db.set_value(doctype, name, "irn_cancelled", 1) \ No newline at end of file diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index 4f98ffc88ab..2255358b8da 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -112,10 +112,7 @@ erpnext.setup_einvoice_actions = (doctype) => { } } }) - } - ); - } - if (frm.doc.docstatus == 0 && !frm.doc.irn && !frm.doc.__unsaved) { + }, "E-Invoicing"); frm.add_custom_button( "Upload Signed E-Invoice", () => { @@ -130,8 +127,7 @@ erpnext.setup_einvoice_actions = (doctype) => { } } }); - } - ); + }, "E-Invoicing"); } if (frm.doc.docstatus == 1 && frm.doc.irn && !frm.doc.irn_cancelled) { frm.add_custom_button( @@ -161,8 +157,22 @@ erpnext.setup_einvoice_actions = (doctype) => { primary_action_label: __('Download JSON') }); d.show(); - } - ); + }, "E-Invoicing"); + frm.add_custom_button( + "Upload Cancel JSON", + () => { + new frappe.ui.FileUploader({ + method: 'erpnext.regional.india.e_invoice_utils.download_cancel_ack', + allow_multiple: 0, + doctype: frm.doc.doctype, + docname: frm.doc.name, + on_success: (attachment, r) => { + if (!r.exc) { + frm.reload_doc(); + } + } + }); + }, "E-Invoicing"); } } }) From 5e0817f57a2b20aac4c181eb0385d26119d515bc Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 7 Oct 2020 11:08:11 +0530 Subject: [PATCH 3/9] fix: fn name --- erpnext/regional/india/e_invoice_utils.py | 2 +- erpnext/regional/india/einvoice.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index b33c15507c0..f7ae758a601 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -557,7 +557,7 @@ def download_cancel_einvoice(): frappe.response['type'] = 'download' @frappe.whitelist() -def download_cancel_ack(): +def upload_cancel_ack(): cancel_ack = json.loads(frappe.local.uploaded_file) data = frappe._dict(frappe.local.form_dict) doctype = data['doctype'] diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index 2255358b8da..f1494b81716 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -162,7 +162,7 @@ erpnext.setup_einvoice_actions = (doctype) => { "Upload Cancel JSON", () => { new frappe.ui.FileUploader({ - method: 'erpnext.regional.india.e_invoice_utils.download_cancel_ack', + method: 'erpnext.regional.india.e_invoice_utils.upload_cancel_ack', allow_multiple: 0, doctype: frm.doc.doctype, docname: frm.doc.name, From fb33f7d3c3903ae933f540234c26fa7c594f3600 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 7 Oct 2020 11:21:47 +0530 Subject: [PATCH 4/9] chore: save signed invoice and qrcode after uplaoding irn --- erpnext/regional/india/e_invoice_utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index f7ae758a601..8d54f7e90ad 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -538,8 +538,13 @@ def upload_einvoice(): doctype = data['doctype'] name = data['docname'] - frappe.db.set_value(doctype, name, "irn", signed_einvoice.get("Irn")) - frappe.db.set_value(doctype, name, "ewaybill", signed_einvoice.get("EwbNo")) + enc_signed_invoice = signed_einvoice.get('SignedInvoice') + decrypted_signed_invoice = jwt_decrypt(enc_signed_invoice)['data'] + + frappe.db.set_value(doctype, name, 'irn', signed_einvoice.get('Irn')) + frappe.db.set_value(doctype, name, 'ewaybill', signed_einvoice.get('EwbNo')) + frappe.db.set_value(doctype, name, 'signed_qr_code', signed_einvoice.get('SignedQRCode')) + frappe.db.set_value(doctype, name, 'signed_einvoice', decrypted_signed_invoice) @frappe.whitelist() def download_cancel_einvoice(): From 96bd2f72d0902b89dd36cadf87382bded5fd2b48 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 7 Oct 2020 12:26:45 +0530 Subject: [PATCH 5/9] fix: fetch token if not valid --- erpnext/regional/india/e_invoice_utils.py | 23 +++++++++++++++-------- erpnext/regional/india/einvoice.js | 3 ++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/erpnext/regional/india/e_invoice_utils.py b/erpnext/regional/india/e_invoice_utils.py index 8d54f7e90ad..86f65285244 100644 --- a/erpnext/regional/india/e_invoice_utils.py +++ b/erpnext/regional/india/e_invoice_utils.py @@ -15,8 +15,8 @@ from Crypto.Util.Padding import pad, unpad from frappe.model.document import Document from frappe import _, get_module_path, scrub from erpnext.regional.india.utils import get_gst_accounts -from frappe.utils.data import get_datetime, cstr, cint, format_date, flt from frappe.integrations.utils import make_post_request, make_get_request +from frappe.utils.data import get_datetime, cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime def validate_einvoice_fields(doc): e_invoice_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable") @@ -29,8 +29,13 @@ def validate_einvoice_fields(doc): elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled: frappe.throw(_("You must cancel IRN before cancelling the document."), title=_("Cancel Not Allowed")) -def get_einv_credentials(): - return frappe.get_doc("E Invoice Settings") +def get_einv_credentials(for_token=False): + creds = frappe.get_doc("E Invoice Settings") + if not for_token and (not creds.token_expiry or time_diff_in_seconds(now_datetime(), creds.token_expiry) > 5.0): + fetch_token() + creds.load_from_db() + + return creds def rsa_encrypt(msg, key): if not (isinstance(msg, bytes) or isinstance(msg, bytearray)): @@ -78,7 +83,7 @@ def get_header(creds): @frappe.whitelist() def fetch_token(): - einv_creds = get_einv_credentials() + einv_creds = get_einv_credentials(for_token=True) endpoint = 'https://einv-apisandbox.nic.in/eivital/v1.03/auth' headers = { 'content-type': 'application/json' } @@ -270,11 +275,13 @@ def get_doc_details(invoice): def get_party_gstin_details(party_address): address = frappe.get_all("Address", filters={"name": party_address}, fields=["*"])[0] - # gstin_details = get_gstin_details(gstin) gstin = address.get('gstin') - legal_name = address.get('address_title') - # trade_name = gstin_details.get('TradeName') - location = address.get('city') + gstin_details = get_gstin_details(gstin) + # legal_name = address.get('address_title') + legal_name = gstin_details.get('LegalName') + trade_name = gstin_details.get('TradeName') + # location = address.get('city') + location = gstin_details.get('Loc') state_code = address.get('gst_state_number') pincode = cint(address.get('pincode')) address_line1 = address.get('address_line1') diff --git a/erpnext/regional/india/einvoice.js b/erpnext/regional/india/einvoice.js index f1494b81716..f30e81a500c 100644 --- a/erpnext/regional/india/einvoice.js +++ b/erpnext/regional/india/einvoice.js @@ -157,7 +157,8 @@ erpnext.setup_einvoice_actions = (doctype) => { primary_action_label: __('Download JSON') }); d.show(); - }, "E-Invoicing"); + }, "E-Invoicing"); + frm.add_custom_button( "Upload Cancel JSON", () => { From 0f30c527596e217ebbc2e4843f5a3c4d5c564fcf Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 4 Dec 2020 22:37:33 +0530 Subject: [PATCH 6/9] fix: Test Case --- .../sales_invoice/test_sales_invoice.py | 61 +++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 964566a17ef..f2de364ddf4 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1562,7 +1562,7 @@ class TestSalesInvoice(unittest.TestCase): for gle in gl_entries: self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) - + def test_sales_invoice_with_project_link(self): from erpnext.projects.doctype.project.test_project import make_project @@ -1596,9 +1596,9 @@ class TestSalesInvoice(unittest.TestCase): debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s order by account asc""", sales_invoice.name, as_dict=1) - + self.assertTrue(gl_entries) - + for gle in gl_entries: self.assertEqual(expected_values[gle.account]["project"], gle.project) @@ -2028,4 +2028,57 @@ def get_taxes_and_charges(): "parentfield": "taxes", "rate": 2, "row_id": 1 - }] \ No newline at end of file + }] + +def create_internal_customer(customer_name, represents_company, allowed_to_interact_with): + if not frappe.db.exists("Customer", customer_name): + customer = frappe.get_doc({ + "customer_group": "_Test Customer Group", + "customer_name": customer_name, + "customer_type": "Individual", + "doctype": "Customer", + "territory": "_Test Territory", + "is_internal_customer": 1, + "represents_company": represents_company + }) + + customer.append("companies", { + "company": allowed_to_interact_with + }) + + customer.insert() + customer_name = customer.name + else: + customer_name = frappe.db.get_value("Customer", customer_name) + + return customer_name + +def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with): + if not frappe.db.exists("Supplier", supplier_name): + supplier = frappe.get_doc({ + "supplier_group": "_Test Supplier Group", + "supplier_name": supplier_name, + "doctype": "Supplier", + "is_internal_supplier": 1, + "represents_company": represents_company + }) + + supplier.append("companies", { + "company": allowed_to_interact_with + }) + + supplier.insert() + supplier_name = supplier.name + else: + supplier_name = frappe.db.exists("Supplier", supplier_name) + + return supplier_name + +def add_taxes(doc): + doc.append('taxes', { + 'account_head': '_Test Account Excise Duty - TCP1', + "charge_type": "On Net Total", + "cost_center": "Main - TCP1", + "description": "Excise Duty", + "rate": 12 + }) From dd2f6aade16e70f274088df1b7e0f584e8ec1598 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 11 Dec 2020 14:10:09 +0530 Subject: [PATCH 7/9] fix: Add description for fields --- .../purchase_invoice/purchase_invoice.json | 19 ++++++++++++- .../doctype/sales_invoice/sales_invoice.json | 27 ++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 2e91c8ef19f..2efe2a59b04 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1328,13 +1328,30 @@ "fieldtype": "Link", "label": "Project", "options": "Project" + }, + { + "depends_on": "eval:doc.is_internal_supplier", + "description": "Unrealized Profit / Loss account for intra-company transfers", + "fieldname": "unrealized_profit_loss_account", + "fieldtype": "Link", + "label": "Unrealized Profit / Loss Account", + "options": "Account" + }, + { + "depends_on": "eval:doc.is_internal_supplier", + "description": "Company which internal supplier represents", + "fetch_from": "supplier.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-08-03 12:46:01.411074", + "modified": "2020-12-11 12:46:12.796378", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 4dc81e90875..90d539d18af 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1941,13 +1941,38 @@ "hide_seconds": 1, "label": "Is Internal Customer", "read_only": 1 + }, + { + "fetch_from": "company.tax_id", + "fieldname": "company_tax_id", + "fieldtype": "Data", + "label": "Company Tax ID", + "read_only": 1 + }, + { + "depends_on": "eval:doc.is_internal_customer", + "description": "Unrealized Profit / Loss account for intra-company transfers", + "fieldname": "unrealized_profit_loss_account", + "fieldtype": "Link", + "label": "Unrealized Profit / Loss Account", + "options": "Account" + }, + { + "depends_on": "eval:doc.is_internal_customer", + "description": "Company which internal customer represents", + "fetch_from": "customer.represents_company", + "fieldname": "represents_company", + "fieldtype": "Link", + "label": "Represents Company", + "options": "Company", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-07-18 05:07:16.725974", + "modified": "2020-12-11 12:48:31.769958", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From 9d806df589f752fbd7e4f06ba8f7ed958aa79356 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 11 Dec 2020 14:12:58 +0530 Subject: [PATCH 8/9] fix: Commonfied code --- erpnext/controllers/accounts_controller.py | 34 ++++++++++++++++++++ erpnext/controllers/buying_controller.py | 21 +++++++++--- erpnext/controllers/stock_controller.py | 10 ++++-- erpnext/selling/doctype/customer/customer.py | 7 ++-- 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 61cf53e6d84..bcdff37ae69 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -104,6 +104,8 @@ class AccountsController(TransactionBase): else: self.validate_deferred_start_and_end_date() + self.set_inter_company_account() + validate_regional(self) validate_einvoice_fields(self) @@ -877,6 +879,38 @@ class AccountsController(TransactionBase): else: return frappe.db.get_single_value("Global Defaults", "disable_rounded_total") + def set_inter_company_account(self): + """ + Set intercompany account for inter warehouse transactions + This account will be used in case billing company and internal customer's + representation company is same + """ + + if self.is_internal_transfer() and not self.unrealized_profit_loss_account: + unrealized_profit_loss_account = frappe.db.get_value('Company', self.company, 'unrealized_profit_loss_account') + + if not unrealized_profit_loss_account: + msg = _("Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}").format( + frappe.bold(self.company)) + frappe.throw(msg) + + self.unrealized_profit_loss_account = unrealized_profit_loss_account + + def is_internal_transfer(self): + """ + It will an internal transfer if its an internal customer and representation + company is same as billing company + """ + if self.doctype == 'Sales Invoice': + internal_party_field = 'is_internal_customer' + else: + internal_party_field = 'is_internal_supplier' + + if self.get(internal_party_field) and (self.represents_company == self.company): + return True + + return False + @frappe.whitelist() def get_tax_rate(account_head): return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 89b48f07ee8..1fc88a78153 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -42,6 +42,7 @@ class BuyingController(StockController): self.validate_items() self.set_qty_as_per_stock_uom() self.validate_stock_or_nonstock_items() + self.update_tax_category_for_internal_transfer() self.validate_warehouse() self.validate_from_warehouse() self.set_supplier_address() @@ -94,13 +95,23 @@ class BuyingController(StockController): def validate_stock_or_nonstock_items(self): if self.meta.get_field("taxes") and not self.get_stock_items() and not self.get_asset_items(): - tax_for_valuation = [d for d in self.get("taxes") + msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items') + self.update_tax_category(msg) + + def update_tax_category_for_internal_transfer(self): + if self.doctype == 'Purchase Invoice' and self.is_internal_transfer(): + msg = _('Tax Category has been changed to "Total" as its an internal purchase.') + self.update_tax_category(msg) + + def update_tax_category(self, msg): + tax_for_valuation = [d for d in self.get("taxes") if d.category in ["Valuation", "Valuation and Total"]] - if tax_for_valuation: - for d in tax_for_valuation: - d.category = 'Total' - msgprint(_('Tax Category has been changed to "Total" because all the Items are non-stock items')) + if tax_for_valuation: + for d in tax_for_valuation: + d.category = 'Total' + + msgprint(msg) def validate_asset_return(self): if self.doctype not in ['Purchase Receipt', 'Purchase Invoice'] or not self.is_return: diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 2017f16d080..deb83fa7371 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -92,9 +92,16 @@ class StockController(AccountsController): sle = self.update_stock_ledger_entries(sle) + # expense account/ target_warehouse / source_warehouse + if item_row.get('target_warehouse'): + warehouse = item_row.get('target_warehouse') + expense_account = warehouse_account[warehouse]["account"] + else: + expense_account = item_row.expense_account + gl_list.append(self.get_gl_dict({ "account": warehouse_account[sle.warehouse]["account"], - "against": item_row.expense_account, + "against": expense_account, "cost_center": item_row.cost_center, "project": item_row.project or self.get('project'), "remarks": self.get("remarks") or "Accounting Entry for Stock", @@ -102,7 +109,6 @@ class StockController(AccountsController): "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No", }, warehouse_account[sle.warehouse]["account_currency"], item=item_row)) - # expense account gl_list.append(self.get_gl_dict({ "account": item_row.expense_account, "against": warehouse_account[sle.warehouse]["account"], diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index ca62488a8c4..c8e3e6764aa 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -388,10 +388,9 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False, credit_controller_users = get_users_with_role(credit_controller_role or "Sales Master Manager") # form a list of emails and names to show to the user - credit_controller_users = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] - - if not credit_controller_users: - frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer))) + credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users] + if not credit_controller_users_formatted: + frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.").format(customer)) message = """Please contact any of the following users to extend the credit limits for {0}:

    • {1}
    """.format(customer, '
  • '.join(credit_controller_users)) From 7b458be170122610eda44b3387a0ebdf7731a494 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 11 Dec 2020 18:38:21 +0530 Subject: [PATCH 9/9] fix: Map warehouse and serial no --- .../doctype/sales_invoice/sales_invoice.py | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index aca13b02c38..7f35b134b9c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1548,6 +1548,26 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): if currency: target_doc.currency = currency + item_field_map = { + "doctype": target_doctype + " Item", + "field_no_map": [ + "income_account", + "expense_account", + "cost_center", + "warehouse" + ] + } + + if source_doc.get('update_stock'): + item_field_map.update({ + 'field_map': { + source_document_warehouse_field: target_document_warehouse_field, + 'batch_no': 'batch_no', + 'serial_no': 'serial_no' + } + }) + + doclist = get_mapped_doc(doctype, source_name, { doctype: { "doctype": target_doctype, @@ -1619,22 +1639,22 @@ def update_multi_mode_option(doc, pos_profile): for pos_payment_method in pos_profile.get('payments'): pos_payment_method = pos_payment_method.as_dict() - + payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company) payment_mode[0].default = pos_payment_method.default append_payment(payment_mode[0]) def get_all_mode_of_payments(doc): return frappe.db.sql(""" - select mpa.default_account, mpa.parent, mp.type as type - from `tabMode of Payment Account` mpa,`tabMode of Payment` mp + select mpa.default_account, mpa.parent, mp.type as type + from `tabMode of Payment Account` mpa,`tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""", {'company': doc.company}, as_dict=1) def get_mode_of_payment_info(mode_of_payment, company): return frappe.db.sql(""" - select mpa.default_account, mpa.parent, mp.type as type - from `tabMode of Payment Account` mpa,`tabMode of Payment` mp + select mpa.default_account, mpa.parent, mp.type as type + from `tabMode of Payment Account` mpa,`tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""", (company, mode_of_payment), as_dict=1)