diff --git a/india_compliance/gst_india/client_scripts/purchase_invoice.js b/india_compliance/gst_india/client_scripts/purchase_invoice.js index dae0d69e32..e23f1a1822 100644 --- a/india_compliance/gst_india/client_scripts/purchase_invoice.js +++ b/india_compliance/gst_india/client_scripts/purchase_invoice.js @@ -18,7 +18,16 @@ frappe.ui.form.on(DOCTYPE, { }); }, - onload: toggle_reverse_charge, + onload: function(frm) { + toggle_reverse_charge(frm); + + if (frm.is_new()) { + frm.add_custom_button( + __("Create Purchase Invoice"), + () => get_irn_dialog(frm), + ); + } + }, gst_category(frm) { validate_gst_hsn_code(frm); @@ -103,6 +112,41 @@ frappe.ui.form.on("Purchase Invoice Item", { gst_hsn_code: validate_gst_hsn_code, }); + +function get_irn_dialog(frm) { + const dialog = new frappe.ui.Dialog({ + title: __("Create Purchase Invoice"), + fields: [ + { + label: "IRN", + fieldname: "irn", + fieldtype: "Data", + reqd: 1, + }, + { + label: "Company GSTIN", + fieldname: "gstin", + fieldtype: "Data", + reqd: 1, + } + ], + primary_action(values) { + taxpayer_api.call( + method ="india_compliance.gst_india.overrides.purchase_invoice.create_purchase_invoice_from_irn", + args= { + company_gstin: values.gstin, + irn: values.irn, + }, + function (r){ + dialog.hide(); + frappe.set_route("purchase-invoice", r.message); + }, + ); + }, + }); + dialog.show(); +} + function toggle_reverse_charge(frm) { let is_read_only = 0; if (frm.doc.gst_category !== "Overseas") is_read_only = 0; diff --git a/india_compliance/gst_india/doctype/e_invoice_mapping/__init__.py b/india_compliance/gst_india/doctype/e_invoice_mapping/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/india_compliance/gst_india/doctype/e_invoice_mapping/e_invoice_mapping.js b/india_compliance/gst_india/doctype/e_invoice_mapping/e_invoice_mapping.js new file mode 100644 index 0000000000..436245db83 --- /dev/null +++ b/india_compliance/gst_india/doctype/e_invoice_mapping/e_invoice_mapping.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, Resilient Tech and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("e-Invoice Mapping", { +// refresh(frm) { + +// }, +// }); diff --git a/india_compliance/gst_india/doctype/e_invoice_mapping/e_invoice_mapping.json b/india_compliance/gst_india/doctype/e_invoice_mapping/e_invoice_mapping.json new file mode 100644 index 0000000000..10b2103c40 --- /dev/null +++ b/india_compliance/gst_india/doctype/e_invoice_mapping/e_invoice_mapping.json @@ -0,0 +1,72 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-09-20 15:05:52.401445", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "party_type", + "party", + "erpnext_fieldname", + "erpnext_value", + "log_value" + ], + "fields": [ + { + "fieldname": "party_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Party Type", + "options": "DocType" + }, + { + "fieldname": "party", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Party", + "options": "party_type" + }, + { + "fieldname": "erpnext_fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Erpnext Fieldname" + }, + { + "fieldname": "erpnext_value", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Erpnext Value" + }, + { + "fieldname": "log_value", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Log Value" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-09-20 15:09:27.296167", + "modified_by": "Administrator", + "module": "GST India", + "name": "e-Invoice Mapping", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/india_compliance/gst_india/doctype/e_invoice_mapping/e_invoice_mapping.py b/india_compliance/gst_india/doctype/e_invoice_mapping/e_invoice_mapping.py new file mode 100644 index 0000000000..682bdd5029 --- /dev/null +++ b/india_compliance/gst_india/doctype/e_invoice_mapping/e_invoice_mapping.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Resilient Tech and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class eInvoiceMapping(Document): + pass diff --git a/india_compliance/gst_india/doctype/e_invoice_mapping/test_e_invoice_mapping.py b/india_compliance/gst_india/doctype/e_invoice_mapping/test_e_invoice_mapping.py new file mode 100644 index 0000000000..d9d734ec16 --- /dev/null +++ b/india_compliance/gst_india/doctype/e_invoice_mapping/test_e_invoice_mapping.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Resilient Tech and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TesteInvoiceMapping(FrappeTestCase): + pass diff --git a/india_compliance/gst_india/overrides/purchase_invoice.py b/india_compliance/gst_india/overrides/purchase_invoice.py index 2572d54365..632e12dba2 100644 --- a/india_compliance/gst_india/overrides/purchase_invoice.py +++ b/india_compliance/gst_india/overrides/purchase_invoice.py @@ -1,7 +1,12 @@ import frappe from frappe import _ from frappe.utils import flt +from erpnext.accounts.doctype.sales_invoice.sales_invoice import update_address +from india_compliance.gst_india.api_classes.taxpayer_base import otp_handler +from india_compliance.gst_india.api_classes.taxpayer_e_invoice import ( + EInvoiceAPI as TaxpayerEInvoiceAPI, +) from india_compliance.gst_india.overrides.sales_invoice import ( update_dashboard_with_gst_logs, ) @@ -52,6 +57,17 @@ def validate(doc, method=None): validate_supplier_invoice_number(doc) validate_with_inward_supply(doc) set_reconciliation_status(doc) + update_item_mapping(doc) + + +def update_item_mapping(doc): + if not frappe.db.exists( + "e-Invoice Log", + {"reference_name": doc.name, "reference_doctype": "Purchase Invoice"}, + ): + return + + # TODO def on_cancel(doc, method=None): @@ -166,6 +182,7 @@ def get_dashboard_data(data): "e-Waybill Log", "Integration Request", "GST Inward Supply", + "e-Invoice Log", ) return data @@ -276,3 +293,272 @@ def validate_hsn_codes(doc): throw=True, message="GST HSN Code is mandatory for Overseas Purchase Invoice.
", ) + + +def fetch_irn_details(company_gstin, irn): + return TaxpayerEInvoiceAPI(company_gstin=company_gstin).get_irn_details(irn) + + +@frappe.whitelist() +@otp_handler +def create_purchase_invoice_from_irn(company_gstin, irn): + # TaxpayerBaseAPI(company_gstin).validate_auth_token() + + # response = fetch_irn_details(company_gstin, irn) + # response = frappe._dict(response.data) + + # invoice_data = json.loads( + # jwt.decode(response.SignedInvoice, options={"verify_signature": False})["data"] + # ) + + invoice_data = { + "AckNo": 122421980389849, + "AckDt": "2024-06-27 15:56:00", + "Irn": "270d60f68063f0d9018ec8d3a3191f1dcf3097f7b58e8458ede443e35c0006ee", + "Version": "1.1", + "TranDtls": { + "TaxSch": "GST", + "SupTyp": "B2B", + "RegRev": "N", + "IgstOnIntra": "N", + }, + "DocDtls": {"Typ": "INV", "No": "SMA/102/2024-25", "Dt": "27/06/2024"}, + "SellerDtls": { + "Gstin": "24AAUPV7468F1ZW", + "LglNm": "SAAKSHI METAL & ALLOYS", + "Addr1": "PRASAD CHAMBERS, OFFICE NO 614, 6TH FLOOR, TATA ROAD NO 02,, OPERA HOUSE, MUMBAI - 400004, MSME NO. ", + "Addr2": "-UDHAM-MH-19-0188703", + "Loc": "Gujarat", + "Pin": 383440, + "Stcd": "24", + }, + "BuyerDtls": { + "Gstin": "24AAUPV7468F1ZW", + "LglNm": "SHALIBHADRA METAL CORPORATION", + "Pos": "24", + "Addr1": "8/A, Saimee Society-2, Near Panchratna Apartment,, Subhanpura,, Vadodara, Gujarat, 390023", + "Loc": "Gujarat", + "Pin": 383440, + "Stcd": "24", + }, + "DispDtls": { + "Nm": "SAAKSHI METAL & ALLOYS", + "Addr1": "PLOT NO 86 , STEEL MARKET ,, KALAMBOLI , NAVI MUMBAI", + "Loc": "KALAMBOLI", + "Pin": 410218, + "Stcd": "27", + }, + "ItemList": [ + { + "ItemNo": 0, + "SlNo": "1", + "IsServc": "N", + "PrdDesc": "A335 GR. P-11 ALLOY STEEL SEAMLESS PIPE-73045930", + "HsnCd": "73045930", + "Qty": 6.56, + "FreeQty": 0, + "Unit": "MTR", + "UnitPrice": 14012.957, + "TotAmt": 91925, + "Discount": 0, + "AssAmt": 91925, + "GstRt": 18, + "IgstAmt": 16546.5, + "CgstAmt": 0, + "SgstAmt": 0, + "CesRt": 0, + "CesAmt": 0, + "CesNonAdvlAmt": 0, + "TotItemVal": 108471.5, + } + ], + "ValDtls": { + "AssVal": 91925, + "CgstVal": 0, + "SgstVal": 0, + "IgstVal": 16546.5, + "CesVal": 0, + "Discount": 0, + "OthChrg": 0, + "RndOffAmt": 0.5, + "TotInvVal": 108472, + }, + "EwbDtls": { + "TransId": "27AARFT4915C1Z0", + "TransName": "TIME TRANSPORT KALAMBOLI", + "TransMode": "1", + "Distance": 422, + "VehNo": "MH18AA0418", + "VehType": "R", + }, + } + + supplier_name, supplier_address_name = get_party_details( + invoice_data.get("SellerDtls"), party_type="Company" # to change + ) + buyer_name, buyer_address_name = get_party_details( + invoice_data.get("BuyerDtls"), party_type="Company" + ) + items, unmapped_items = get_item_info(invoice_data.get("ItemList"), supplier_name) + + doc = create_purchase_invoice( + supplier_name, supplier_address_name, buyer_address_name, invoice_data, items + ) + + create_item_mapping(unmapped_items, doc.supplier) + + e_invoice_log = frappe.get_doc( + { + "doctype": "e-Invoice Log", + "reference_doctype": "Purchase Invoice", + "reference_name": doc.name, + "irn": invoice_data.get("Irn"), + "is_generated_from_irn": 0, + "acknowledgement_number": invoice_data.get("AckNo"), + "acknowledged_on": invoice_data.get("AckDt"), + "invoice_data": frappe.as_json(invoice_data, indent=4), + } + ) + e_invoice_log.save(ignore_permissions=True) + + return doc.name + + +def create_purchase_invoice( + supplier_name, supplier_address_name, buyer_address_name, invoice_data, items +): + doc = frappe.get_doc( + { + "doctype": "Purchase Invoice", + "supplier": supplier_name, + "company": "Shalibhadra Metal Corporation", + "posting_date": invoice_data.get("AckDt"), + "due_date": frappe.utils.nowdate(), + "items": [ + { + "item_name": item.get("PrdDesc"), + "qty": item.get("Qty"), + "rate": item.get("UnitPrice"), + "uom": item.get("Unit"), + "amount": item.get("AssAmt"), + } + for item in items + ], + } + ) + + update_address(doc, "supplier_address", "address_display", supplier_address_name) + update_address( + doc, "billing_address", "billing_address_display", buyer_address_name + ) + + doc.flags.ignore_validate = True + doc.insert(ignore_mandatory=True) + return doc + + +def get_item_info(items, supplier): + unmapped_items = {"item_name": [], "uom": []} + + mapped_item_names = frappe.get_all( + "e-Invoice Mapping", + filters={"party": supplier, "erpnext_fieldname": "item_name"}, + fields=["log_value", "erpnext_value"], + ) + item_name_map = { + item.get("log_value"): item.get("erpnext_value") for item in mapped_item_names + } + mapped_item_uoms = frappe.get_all( + "e-Invoice Mapping", + filters={"party": supplier, "erpnext_fieldname": "uom"}, + fields=["log_value", "erpnext_value"], + ) + item_uom_map = { + item.get("log_value"): item.get("erpnext_value") for item in mapped_item_uoms + } + + for item in items: + if item_desc := item_name_map.get(item.get("PrdDesc")): + item["PrdDesc"] = item_desc + else: + unmapped_items["item_name"].append(item.get("PrdDesc")) + + if item_uom := item_uom_map.get(item.get("Unit")): + item["Unit"] = item_uom + else: + unmapped_items["uom"].append(item.get("Unit")) + + return items, unmapped_items + + +def create_item_mapping(unmapped_items, supplier): + def save_mapping(fieldname, value): + frappe.get_doc( + { + "doctype": "e-Invoice Mapping", + "party_type": "Company", # to change + "party": supplier, + "erpnext_fieldname": fieldname, + "log_value": value, + } + ).save() + + for item_name in unmapped_items.get("item_name", []): + save_mapping("item_name", item_name) + + for uom_value in unmapped_items.get("uom", []): + save_mapping("uom", uom_value) + + +def get_party_details(party_details, party_type): + try: + address_doc = frappe.get_doc( + "Address", + { + "gstin": party_details.get("Gstin") or None, + "pincode": party_details.get("Pin"), + "state": party_details.get("Loc"), + }, + ) + + except frappe.DoesNotExistError: + # not able to handle + frappe.clear_last_message() + frappe.throw( + _( + "Address with GSTIN {gstin}, Pincode {pincode}, and State {state} not found" + ).format( + gstin=party_details.get("Gstin"), + pincode=party_details.get("Pin"), + state=party_details.get("Loc"), + ) + ) + + for link in address_doc.links: + if link.link_doctype == party_type: + return link.link_name, address_doc.name + + frappe.throw(f"{party_type.capitalize()} not found with this address") + + +@frappe.whitelist() +def get_item_details(args, doc): + import json + + from erpnext.stock.get_item_details import get_item_details + + data = get_item_details(args, doc) + + if not frappe.db.exists("e-Invoice Log", {"reference_name": doc.name}): + return data + + args = json.loads(args) + data.rate = args.get("net_rate") + data.qty = args.get("qty") + data.uom = args.get("uom") + data.price_list_rate = 0 + data.discount_percentage = 0 + data.discount_amount = 20 + data.margin_rate_or_amount = data.rate + + return data diff --git a/india_compliance/gst_india/overrides/sales_invoice.py b/india_compliance/gst_india/overrides/sales_invoice.py index d414982454..1c02e333ea 100644 --- a/india_compliance/gst_india/overrides/sales_invoice.py +++ b/india_compliance/gst_india/overrides/sales_invoice.py @@ -243,6 +243,7 @@ def update_dashboard_with_gst_logs(doctype, data, *log_doctypes): "e-Waybill Log": "reference_name", "Integration Request": "reference_docname", "GST Inward Supply": "link_name", + "e-Invoice Log": "reference_name", } ) diff --git a/india_compliance/hooks.py b/india_compliance/hooks.py index 78485ce809..9ec7daed8a 100644 --- a/india_compliance/hooks.py +++ b/india_compliance/hooks.py @@ -595,7 +595,10 @@ override_whitelisted_methods = { "erpnext.accounts.doctype.payment_entry.payment_entry.get_outstanding_reference_documents": ( "india_compliance.gst_india.overrides.payment_entry.get_outstanding_reference_documents" - ) + ), + "erpnext.stock.get_item_details.get_item_details": ( + "india_compliance.gst_india.overrides.purchase_invoice.get_item_details" + ), } # # each overriding function accepts a `data` argument;