From 9b29195a06bc018f3c2ba84ad3c4f90759b118ab Mon Sep 17 00:00:00 2001 From: Sanket322 Date: Thu, 26 Sep 2024 17:40:24 +0530 Subject: [PATCH] fix: add item mapping to e-invoice log --- .../client_scripts/purchase_invoice.js | 53 +++- .../doctype/e_invoice_log/e_invoice_log.json | 16 +- .../e_invoice_mapping/e_invoice_mapping.json | 50 +-- .../gst_india/overrides/purchase_invoice.py | 295 +++++++++++------- 4 files changed, 262 insertions(+), 152 deletions(-) diff --git a/india_compliance/gst_india/client_scripts/purchase_invoice.js b/india_compliance/gst_india/client_scripts/purchase_invoice.js index e23f1a182..f2e1332c7 100644 --- a/india_compliance/gst_india/client_scripts/purchase_invoice.js +++ b/india_compliance/gst_india/client_scripts/purchase_invoice.js @@ -18,13 +18,13 @@ frappe.ui.form.on(DOCTYPE, { }); }, - onload: function(frm) { + onload: function (frm) { toggle_reverse_charge(frm); if (frm.is_new()) { frm.add_custom_button( - __("Create Purchase Invoice"), - () => get_irn_dialog(frm), + __("Create Invoice from IRN"), + () => show_irn_dialog(frm), ); } }, @@ -113,7 +113,7 @@ frappe.ui.form.on("Purchase Invoice Item", { }); -function get_irn_dialog(frm) { +function show_irn_dialog(frm) { const dialog = new frappe.ui.Dialog({ title: __("Create Purchase Invoice"), fields: [ @@ -126,27 +126,62 @@ function get_irn_dialog(frm) { { label: "Company GSTIN", fieldname: "gstin", - fieldtype: "Data", + fieldtype: "Autocomplete", + get_query: function () { + return { + query: "india_compliance.gst_india.overrides.purchase_invoice.get_gstin_with_company_name", + }; + }, reqd: 1, } ], + primary_action_label: 'Create', primary_action(values) { taxpayer_api.call( - method ="india_compliance.gst_india.overrides.purchase_invoice.create_purchase_invoice_from_irn", - args= { + method = "india_compliance.gst_india.overrides.purchase_invoice.create_purchase_invoice_from_irn", + args = { company_gstin: values.gstin, irn: values.irn, }, - function (r){ + function (r) { + doc = r.message; dialog.hide(); - frappe.set_route("purchase-invoice", r.message); + frappe.set_route("purchase-invoice", doc.name); + set_party_details(doc, frm); }, ); }, }); dialog.show(); + + frappe.db.get_value("Company", frappe.defaults.get_default("company"), "gstin").then(r => { + dialog.fields_dict.gstin.set_input(r.message.gstin); + }) +} + +function set_party_details(doc, frm) { + erpnext.utils.get_party_details( + frm, + "erpnext.accounts.party.get_party_details", + { + posting_date: doc.posting_date, + bill_date: doc.bill_date, + party: doc.supplier, + party_type: "Supplier", + account: doc.credit_to, + price_list: doc.buying_price_list, + fetch_payment_terms_template: cint(!doc.ignore_default_payment_terms_template), + }, + function () { + frm.set_value("apply_tds", frm.supplier_tds ? 1 : 0); + frm.set_value("tax_withholding_category", frm.supplier_tds); + frm.set_df_property("apply_tds", "read_only", frm.supplier_tds ? 0 : 1); + frm.set_df_property("tax_withholding_category", "hidden", frm.supplier_tds ? 0 : 1); + } + ); } + 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_log/e_invoice_log.json b/india_compliance/gst_india/doctype/e_invoice_log/e_invoice_log.json index 0a1f3093e..816dd88ef 100644 --- a/india_compliance/gst_india/doctype/e_invoice_log/e_invoice_log.json +++ b/india_compliance/gst_india/doctype/e_invoice_log/e_invoice_log.json @@ -22,7 +22,9 @@ "cancelled_on", "column_break_3", "cancel_reason_code", - "cancel_remark" + "cancel_remark", + "section_break_clwh", + "item_mapping" ], "fields": [ { @@ -131,6 +133,16 @@ "label": "Reference Document Name", "options": "reference_doctype", "read_only": 1 + }, + { + "fieldname": "section_break_clwh", + "fieldtype": "Section Break" + }, + { + "fieldname": "item_mapping", + "fieldtype": "Table", + "label": "Item Mapping", + "options": "e-Invoice Mapping" } ], "in_create": 1, @@ -140,7 +152,7 @@ "link_fieldname": "irn" } ], - "modified": "2024-09-19 18:59:01.195753", + "modified": "2024-09-24 17:51:10.730928", "modified_by": "Administrator", "module": "GST India", "name": "e-Invoice Log", 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 index 10b2103c4..41e4ce036 100644 --- 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 @@ -9,7 +9,9 @@ "party", "erpnext_fieldname", "erpnext_value", - "log_value" + "e_invoice_value", + "item_row_name", + "rate" ], "fields": [ { @@ -17,20 +19,23 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Party Type", - "options": "DocType" + "options": "DocType", + "read_only": 1 }, { "fieldname": "party", "fieldtype": "Dynamic Link", "in_list_view": 1, "label": "Party", - "options": "party_type" + "options": "party_type", + "read_only": 1 }, { "fieldname": "erpnext_fieldname", "fieldtype": "Data", "in_list_view": 1, - "label": "Erpnext Fieldname" + "label": "Erpnext Fieldname", + "read_only": 1 }, { "fieldname": "erpnext_value", @@ -39,33 +44,36 @@ "label": "Erpnext Value" }, { - "fieldname": "log_value", + "fieldname": "e_invoice_value", "fieldtype": "Data", "in_list_view": 1, - "label": "Log Value" + "label": "e-Invoice Value", + "read_only": 1 + }, + { + "fieldname": "item_row_name", + "fieldtype": "Data", + "label": "Item Row Name", + "read_only": 1 + }, + { + "fieldname": "rate", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Rate", + "precision": "2", + "read_only": 1 } ], "index_web_pages_for_search": 1, + "istable": 1, "links": [], - "modified": "2024-09-20 15:09:27.296167", + "modified": "2024-09-26 00:12:00.088452", "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 - } - ], + "permissions": [], "sort_field": "creation", "sort_order": "DESC", "states": [] diff --git a/india_compliance/gst_india/overrides/purchase_invoice.py b/india_compliance/gst_india/overrides/purchase_invoice.py index 76104ab43..c956435dd 100644 --- a/india_compliance/gst_india/overrides/purchase_invoice.py +++ b/india_compliance/gst_india/overrides/purchase_invoice.py @@ -4,8 +4,8 @@ import frappe from frappe import _ -from frappe.utils import flt -from erpnext.accounts.doctype.sales_invoice.sales_invoice import update_address +from frappe.query_builder.functions import IfNull +from frappe.utils import cstr, flt from india_compliance.gst_india.api_classes.taxpayer_base import ( TaxpayerBaseAPI, @@ -67,17 +67,6 @@ def validate(doc, method=None): update_item_mapping(doc) -def update_item_mapping(doc): - pass - # if not frappe.db.exists( - # "e-Invoice Log", - # {"reference_name": doc.name, "reference_doctype": "Purchase Invoice"}, - # ): - # return - - # # TODO - - def on_cancel(doc, method=None): frappe.db.set_value( "GST Inward Supply", @@ -172,6 +161,37 @@ def validate_supplier_invoice_number(doc): ) +def update_item_mapping(doc): + if not frappe.db.exists( + "e-Invoice Log", + {"reference_name": doc.name, "reference_doctype": "Purchase Invoice"}, + ): + return + + log = frappe.get_doc( + "e-Invoice Log", + {"reference_name": doc.name, "reference_doctype": "Purchase Invoice"}, + ) + + mapping_dict = {} + for mapping in log.item_mapping: + key = (mapping.item_row_name, flt(mapping.rate, precision=2)) + mapping_dict.setdefault(key, []).append(mapping) + + for item in doc.items: + key = (item.name, flt(item.rate, precision=2)) + + if key in mapping_dict: + for map_doc in mapping_dict[key]: + if map_doc.erpnext_fieldname == "item_name": + item.item_code = map_doc.erpnext_value + + else: + item.uom = map_doc.erpnext_value + + log.save(ignore_permissions=True) + + def get_dashboard_data(data): transactions = data.setdefault("transactions", []) reference_section = next( @@ -319,152 +339,171 @@ def create_purchase_invoice_from_irn(company_gstin, irn): jwt.decode(response.SignedInvoice, options={"verify_signature": False})["data"] ) - supplier_name, supplier_address_name = get_party_details( - invoice_data.get("SellerDtls"), party_type="Company" # to change + supplier_name = get_party_name( + invoice_data.get("SellerDtls"), party_type="Supplier" ) - buyer_name, buyer_address_name = get_party_details( - invoice_data.get("BuyerDtls"), party_type="Company" + company_name = get_party_name(invoice_data.get("BuyerDtls"), party_type="Company") + + items, unmapped_items = get_mapped_and_unmapped_items( + invoice_data.get("ItemList"), supplier_name ) - 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 + supplier_name, company_name, invoice_data.get("AckDt"), items, unmapped_items ) + create_invoice_log(doc, invoice_data, irn, unmapped_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 - return doc.name +def get_party_name(party_details, party_type): + try: + address_doc = frappe.get_doc( + "Address", + { + "gstin": party_details.get("Gstin") or None, + "pincode": cstr(party_details.get("Pin")), + "gst_state_number": party_details.get("Stcd"), + }, + ) -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 - ], - } - ) + except frappe.DoesNotExistError: + frappe.clear_last_message() + frappe.throw( + _( + "Address with GSTIN {gstin}, Pincode {pincode}, and State code {state_code} not found" + ).format( + gstin=party_details.get("Gstin"), + pincode=party_details.get("Pin"), + state_code=party_details.get("Stcd"), + ) + ) - update_address(doc, "supplier_address", "address_display", supplier_address_name) - update_address( - doc, "billing_address", "billing_address_display", buyer_address_name - ) + for link in address_doc.links: + if link.link_doctype == party_type: + return link.link_name - doc.flags.ignore_validate = True - doc.insert(ignore_mandatory=True) - return doc + frappe.throw(f"{party_type.capitalize()} not found with this address") -def get_item_info(items, supplier): +def get_mapped_and_unmapped_items(items, supplier_name): unmapped_items = {"item_name": [], "uom": []} - mapped_item_names = frappe.get_all( + mappings = frappe.get_all( "e-Invoice Mapping", - filters={"party": supplier, "erpnext_fieldname": "item_name"}, - fields=["log_value", "erpnext_value"], + filters={"party": supplier_name}, + fields=["e_invoice_value", "erpnext_value", "erpnext_fieldname"], ) - item_name_map = { - item.get("log_value"): item.get("erpnext_value") for item in mapped_item_names + mapped_items = { + mapping.get("e_invoice_value"): mapping.get("erpnext_value") + for mapping in mappings + if mapping.get("erpnext_fieldname") == "item_name" } - 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 + mapped_uoms = { + mapping.get("e_invoice_value"): mapping.get("erpnext_value") + for mapping in mappings + if mapping.get("erpnext_fieldname") == "uom" } for item in items: - if item_desc := item_name_map.get(item.get("PrdDesc")): - item["PrdDesc"] = item_desc + if item_code := mapped_items.get(item.get("PrdDesc")): + item["item_code"] = item_code else: - unmapped_items["item_name"].append(item.get("PrdDesc")) + unmapped_items["item_name"].append(item) - if item_uom := item_uom_map.get(item.get("Unit")): + if item_uom := mapped_uoms.get(item.get("Unit")): item["Unit"] = item_uom else: - unmapped_items["uom"].append(item.get("Unit")) + unmapped_items["uom"].append(item) return items, unmapped_items -def create_item_mapping(unmapped_items, supplier): - def save_mapping(fieldname, value): - frappe.get_doc( +def create_purchase_invoice( + supplier_name, company_name, ack_date, items, unmapped_items +): + invoice_data = { + "doctype": "Purchase Invoice", + "supplier": supplier_name, + "company": company_name, + "posting_date": ack_date, + "due_date": frappe.utils.nowdate(), + "items": [], + } + + for item in items: + if item_code := item.get("item_code"): + invoice_data["items"].append( + { + "item_code": item_code, + "qty": item.get("Qty"), + "rate": item.get("UnitPrice"), + "uom": item.get("Unit"), + "amount": item.get("TotAmt"), + } + ) + + for item in unmapped_items["item_name"]: + invoice_data["items"].append( { - "doctype": "e-Invoice Mapping", - "party_type": "Company", # to change - "party": supplier, - "erpnext_fieldname": fieldname, - "log_value": value, + "item_name": item.get("PrdDesc"), + "qty": item.get("Qty"), + "rate": item.get("UnitPrice"), + "uom": item.get("Unit"), } - ).save() + ) - for item_name in unmapped_items.get("item_name", []): - save_mapping("item_name", item_name) + doc = frappe.get_doc(invoice_data) + doc.set_posting_time = 1 + doc.flags.ignore_validate = True + doc.flags.ignore_links = True + doc.insert(ignore_mandatory=True) - for uom_value in unmapped_items.get("uom", []): - save_mapping("uom", uom_value) + return doc -def get_party_details(party_details, party_type): - try: - address_doc = frappe.get_doc( - "Address", +def create_invoice_log(doc, invoice_data, irn, unmapped_items): + if not len(unmapped_items["item_name"]) and not len(unmapped_items["uom"]): + return + + e_invoice_log = ( + frappe.get_doc("e-Invoice Log", irn) + if frappe.db.exists("e-Invoice Log", irn) + else frappe.get_doc( { - "gstin": party_details.get("Gstin") or None, - "pincode": party_details.get("Pin"), - "state": party_details.get("Loc"), - }, + "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), + } ) + ) - 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"), - ) - ) + item_desc_table_map = {item.get("item_name"): item.name for item in doc.items} - for link in address_doc.links: - if link.link_doctype == party_type: - return link.link_name, address_doc.name + for field_type, items in unmapped_items.items(): + for item in items: + e_invoice_log.append( + "item_mapping", + { + "party_type": "Supplier", + "party": doc.supplier, + "erpnext_fieldname": field_type, + "item_row_name": item_desc_table_map.get(item.get("PrdDesc")), + "e_invoice_value": ( + item.get("PrdDesc") + if field_type == "item_name" + else item.get("Unit") + ), + "rate": item.get("UnitPrice"), + }, + ) - frappe.throw(f"{party_type.capitalize()} not found with this address") + e_invoice_log.save(ignore_permissions=True) @frappe.whitelist() @@ -483,7 +522,23 @@ def get_item_details(args, doc): data.uom = args.get("uom") data.price_list_rate = 0 data.discount_percentage = 0 - data.discount_amount = 20 + data.discount_amount = 0 data.margin_rate_or_amount = data.rate return data + + +@frappe.whitelist() +def get_gstin_with_company_name(): + address = frappe.qb.DocType("Address") + links = frappe.qb.DocType("Dynamic Link") + + return ( + frappe.qb.from_(address) + .join(links) + .on(address.name == links.parent) + .select(address.gstin.as_("value"), links.link_name.as_("description")) + .where(links.link_doctype == "Company") + .where(IfNull(address.gstin, "") != "") + .run(as_dict=True) + )