diff --git a/erpnext/edi/doctype/code_list/code_list.js b/erpnext/edi/doctype/code_list/code_list.js index af804c3e5254..16a8a9e9fac2 100644 --- a/erpnext/edi/doctype/code_list/code_list.js +++ b/erpnext/edi/doctype/code_list/code_list.js @@ -1,8 +1,45 @@ // Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -// frappe.ui.form.on("Code List", { -// refresh(frm) { - -// }, -// }); +frappe.ui.form.on("Code List", { + refresh: (frm) => { + frm.add_custom_button( + __("Genericode"), + function () { + erpnext.edi.import_genericode(frm); + }, + __("Import") + ); + }, + setup: (frm) => { + frm.savetrash = () => { + frm.validate_form_action("Delete"); + frappe.confirm( + __( + "Are you sure you want to delete {0}?

This action will also delete all associated Common Code documents.

", + [frm.docname.bold()] + ), + function () { + return frappe.call({ + method: "frappe.client.delete", + args: { + doctype: frm.doctype, + name: frm.docname, + }, + freeze: true, + freeze_message: __("Deleting {0} and all associated Common Code documents...", [ + frm.docname, + ]), + callback: function (r) { + if (!r.exc) { + frappe.utils.play_sound("delete"); + frappe.model.clear_doc(frm.doctype, frm.docname); + window.history.back(); + } + }, + }); + } + ); + }; + }, +}); diff --git a/erpnext/edi/doctype/code_list/code_list.json b/erpnext/edi/doctype/code_list/code_list.json index 6da9dbbcb23c..15ce92321c44 100644 --- a/erpnext/edi/doctype/code_list/code_list.json +++ b/erpnext/edi/doctype/code_list/code_list.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_copy": 1, "allow_rename": 1, "autoname": "prompt", "creation": "2024-09-29 06:55:03.920375", @@ -7,8 +8,11 @@ "engine": "InnoDB", "field_order": [ "title", - "publisher", + "canonical_uri", + "column_break_nkls", "version", + "publisher", + "section_break_npxp", "description" ], "fields": [ @@ -24,6 +28,7 @@ "label": "Publisher" }, { + "columns": 1, "fieldname": "version", "fieldtype": "Data", "in_list_view": 1, @@ -33,6 +38,20 @@ "fieldname": "description", "fieldtype": "Small Text", "label": "Description" + }, + { + "fieldname": "canonical_uri", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Canonical URI" + }, + { + "fieldname": "column_break_nkls", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_npxp", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, @@ -42,7 +61,7 @@ "link_fieldname": "code_list" } ], - "modified": "2024-09-29 07:24:21.123903", + "modified": "2024-09-29 22:46:37.878075", "modified_by": "Administrator", "module": "EDI", "name": "Code List", @@ -66,4 +85,4 @@ "sort_order": "DESC", "states": [], "title_field": "title" -} +} \ No newline at end of file diff --git a/erpnext/edi/doctype/code_list/code_list.py b/erpnext/edi/doctype/code_list/code_list.py index a8300a9114d5..d01fa7d69301 100644 --- a/erpnext/edi/doctype/code_list/code_list.py +++ b/erpnext/edi/doctype/code_list/code_list.py @@ -3,6 +3,9 @@ import frappe from frappe.model.document import Document +from lxml import etree + +from erpnext.edi.doctype.common_code.common_code import CommonCode class CodeList(Document): @@ -14,12 +17,27 @@ class CodeList(Document): if TYPE_CHECKING: from frappe.types import DF + canonical_uri: DF.Data | None description: DF.SmallText | None publisher: DF.Data | None title: DF.Data | None version: DF.Data | None # end: auto-generated types + def on_trash(self): + if not frappe.flags.in_bulk_delete: + self.__delete_linked_docs() + + def __delete_linked_docs(self): + linked_docs = frappe.get_all( + "Common Code", + filters={"code_list": self.name}, + fields=["name"], + ) + + for doc in linked_docs: + frappe.delete_doc("Common Code", doc.name, force=1) + def get_code_for(self, doctype: str, name: str): """Get code for a doctype and name""" CommonCode = frappe.qb.DocType("Common Code") @@ -38,3 +56,31 @@ def get_code_for(self, doctype: str, name: str): ).run() return code[0][0] if code else None + + def import_genericode(self, file_path, code_column, title_column=None, filters=None): + """Import genericode file and create Common Code entries""" + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.parse(file_path, parser=parser) + root = tree.getroot() + + # Extract Code List details + self.title = root.find(".//Identification/ShortName").text + self.version = root.find(".//Identification/Version").text + self.canonical_uri = root.find(".//CanonicalUri").text + # optionals + self.description = getattr(root.find(".//Identification/LongName"), "text", None) + self.publisher = getattr(root.find(".//Identification/Agency/ShortName"), "text", None) + + self.save() + + common_codes = CommonCode.import_genericode(file_path, self.name, code_column, title_column, filters) + + # Bulk insert common codes + if common_codes: + frappe.db.bulk_insert( + "Common Code", + fields=["name", "code_list", "common_code", "title"], + values=[(cc["name"], cc["code_list"], cc["common_code"], cc["title"]) for cc in common_codes], + ) + + return {"code_list": self, "common_codes_count": len(common_codes)} diff --git a/erpnext/edi/doctype/code_list/code_list_import.js b/erpnext/edi/doctype/code_list/code_list_import.js new file mode 100644 index 000000000000..73092758dafa --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list_import.js @@ -0,0 +1,160 @@ +frappe.provide("erpnext.edi"); + +erpnext.edi.import_genericode = function (listview_or_form) { + let doctype = "Code List"; + let docname = undefined; + if (listview_or_form.doc !== undefined) { + docname = listview_or_form.doc.name; + } + new frappe.ui.FileUploader({ + method: "erpnext.edi.doctype.code_list.code_list_import.import_genericode", + doctype: doctype, + docname: docname, + allow_toggle_private: false, + on_success: function (_file_doc, r) { + listview_or_form.refresh(); + show_column_selection_dialog(r.message); + }, + }); +}; + +function show_column_selection_dialog(context) { + let fields = [ + { + fieldname: "import_column", + label: __("Import"), + fieldtype: "Column Break", + }, + { + fieldname: "title_column", + label: __("as Title"), + fieldtype: "Select", + options: [null].concat(context.columns), + }, + { + fieldname: "code_column", + label: __("as Code"), + fieldtype: "Select", + options: context.columns, + reqd: 1, + }, + { + fieldname: "filters_column", + label: __("Filter"), + fieldtype: "Column Break", + }, + ]; + + // Add filterable columns + for (let column in context.filterable_columns) { + fields.push({ + fieldname: `filter_${column}`, + label: __(`by ${column}`), + fieldtype: "Select", + options: [null].concat(context.filterable_columns[column]), + }); + } + + fields.push( + { + fieldname: "preview_section", + label: __("Preview"), + fieldtype: "Section Break", + }, + { + fieldname: "preview_html", + fieldtype: "HTML", + } + ); + + let d = new frappe.ui.Dialog({ + title: __("Select Columns and Filters"), + fields: fields, + primary_action_label: __("Import"), + size: "large", // This will make the modal wider + primary_action(values) { + let filters = {}; + for (let field in values) { + if (field.startsWith("filter_") && values[field]) { + filters[field.replace("filter_", "")] = values[field]; + } + } + frappe.call({ + method: "erpnext.edi.doctype.code_list.code_list_import.process_genericode_import", + args: { + code_list_name: context.code_list, + file_path: context.file_path, + code_column: values.code_column, + title_column: values.title_column, + filters: filters, + }, + callback: function (r) { + frappe.msgprint( + __("Import completed. {0} common codes created.", [r.message.common_codes_count]) + ); + }, + }); + d.hide(); + }, + }); + + d.fields_dict.code_column.df.onchange = () => update_preview(d, context); + d.fields_dict.title_column.df.onchange = () => update_preview(d, context); + + // Add onchange events for filterable columns + for (let column in context.filterable_columns) { + d.fields_dict[`filter_${column}`].df.onchange = () => update_preview(d, context); + } + + d.show(); + update_preview(d, context); +} + +function update_preview(dialog, context) { + let code_column = dialog.get_value("code_column"); + let title_column = dialog.get_value("title_column"); + + let html = ''; + if (title_column) html += ``; + if (code_column) html += ``; + + // Add headers for filterable columns + for (let column in context.filterable_columns) { + if (dialog.get_value(`filter_${column}`)) { + html += ``; + } + } + + html += ""; + + for (let i = 0; i < 3; i++) { + html += ""; + if (title_column) { + let title = context.example_values[title_column][i] || ""; + html += ``; + } + if (code_column) { + let code = context.example_values[code_column][i] || ""; + html += ``; + } + + // Add values for filterable columns + for (let column in context.filterable_columns) { + if (dialog.get_value(`filter_${column}`)) { + let value = context.example_values[column][i] || ""; + html += ``; + } + } + + html += ""; + } + + html += "
${__("Title")}${__("Code")}${__(column)}
${truncate(title)}${truncate(code)}${truncate(value)}
"; + + dialog.fields_dict.preview_html.$wrapper.html(html); +} + +function truncate(value, maxLength = 40) { + if (typeof value !== "string") return ""; + return value.length > maxLength ? value.substring(0, maxLength - 3) + "..." : value; +} diff --git a/erpnext/edi/doctype/code_list/code_list_import.py b/erpnext/edi/doctype/code_list/code_list_import.py new file mode 100644 index 000000000000..f6a00b953096 --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list_import.py @@ -0,0 +1,91 @@ +import json + +import frappe +from frappe import _ +from frappe.utils.file_manager import save_file +from lxml import etree + + +@frappe.whitelist() +def import_genericode(): + doctype = frappe.form_dict.doctype + docname = frappe.form_dict.docname + + # Parse the uploaded file content + parser = etree.XMLParser(remove_blank_text=True) + root = etree.fromstring(frappe.local.uploaded_file, parser=parser) + + # Extract the name (CanonicalVersionUri) from the parsed XML + name = root.find(".//CanonicalVersionUri").text + + if docname: + code_list = frappe.get_doc(doctype, docname) + if code_list.name != name: + frappe.throw(_("The uploaded file does not match the selected Code List.")) + else: + # Create a new Code List document with the extracted name + code_list = frappe.new_doc(doctype) + code_list.name = name + code_list.insert(ignore_permissions=True) + + # Save the file using save_file utility + file_doc = save_file( + fname=frappe.local.uploaded_filename, + content=frappe.local.uploaded_file, + dt="Code List", + dn=code_list.name, + folder="Home/Attachments", + is_private=1, + ) + file_path = file_doc.get_full_path() + + # Get available columns and example values + columns, example_values, filterable_columns = get_columns_and_examples(file_path) + + return { + "code_list": code_list.name, + "file_path": file_path, + "columns": columns, + "example_values": example_values, + "filterable_columns": filterable_columns, + } + + +@frappe.whitelist() +def process_genericode_import(code_list_name, file_path, code_column, title_column=None, filters=None): + code_list = frappe.get_doc("Code List", code_list_name) + return code_list.import_genericode(file_path, code_column, title_column, filters and json.loads(filters)) + + +def get_columns_and_examples(file_path): + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.parse(file_path, parser=parser) + root = tree.getroot() + + columns = [] + example_values = {} + filterable_columns = {} + + # Get column names + for column in root.findall(".//Column"): + column_id = column.get("Id") + columns.append(column_id) + example_values[column_id] = [] + filterable_columns[column_id] = set() + + # Get all values and count unique occurrences + for row in root.findall(".//SimpleCodeList/Row"): + for value in row.findall("Value"): + column_id = value.get("ColumnRef") + value_text = value.find("./SimpleValue").text + filterable_columns[column_id].add(value_text) + + # Get example values (up to 3) and filter columns with cardinality <= 5 + for row in root.findall(".//SimpleCodeList/Row")[:3]: + for value in row.findall("Value"): + column_id = value.get("ColumnRef") + example_values[column_id].append(value.find("./SimpleValue").text) + + filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5} + + return columns, example_values, filterable_columns diff --git a/erpnext/edi/doctype/code_list/code_list_list.js b/erpnext/edi/doctype/code_list/code_list_list.js new file mode 100644 index 000000000000..55d6fc82aa5d --- /dev/null +++ b/erpnext/edi/doctype/code_list/code_list_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings["Code List"] = { + onload: function (listview) { + listview.page.add_inner_button( + __("Genericode"), + function () { + erpnext.edi.import_genericode(listview); + }, + __("Import") + ); + }, + hide_name_column: true, +}; diff --git a/erpnext/edi/doctype/common_code/common_code.json b/erpnext/edi/doctype/common_code/common_code.json index 7bb266587a91..d4e572d12f2d 100644 --- a/erpnext/edi/doctype/common_code/common_code.json +++ b/erpnext/edi/doctype/common_code/common_code.json @@ -1,6 +1,5 @@ { "actions": [], - "autoname": "hash", "creation": "2024-09-29 07:01:18.133067", "doctype": "DocType", "engine": "InnoDB", @@ -9,7 +8,7 @@ "common_code", "title", "column_break_wxsw", - "description", + "additional_data", "section_break_rhgh", "applies_to" ], @@ -30,11 +29,6 @@ "in_standard_filter": 1, "label": "Title" }, - { - "fieldname": "description", - "fieldtype": "Small Text", - "label": "Description" - }, { "fieldname": "column_break_wxsw", "fieldtype": "Column Break" @@ -55,15 +49,20 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Common Code", + "length": 300, "reqd": 1 + }, + { + "fieldname": "additional_data", + "fieldtype": "Code", + "label": "Additional Data" } ], "links": [], - "modified": "2024-09-29 07:27:58.292767", + "modified": "2024-09-29 21:13:43.054943", "modified_by": "Administrator", "module": "EDI", "name": "Common Code", - "naming_rule": "Random", "owner": "Administrator", "permissions": [ { @@ -83,4 +82,4 @@ "sort_order": "DESC", "states": [], "title_field": "title" -} +} \ No newline at end of file diff --git a/erpnext/edi/doctype/common_code/common_code.py b/erpnext/edi/doctype/common_code/common_code.py index 6d436df4db48..07f719dbdcc0 100644 --- a/erpnext/edi/doctype/common_code/common_code.py +++ b/erpnext/edi/doctype/common_code/common_code.py @@ -1,9 +1,13 @@ # Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import hashlib + import frappe from frappe import _ from frappe.model.document import Document +from frappe.utils.html_utils import escape_html +from lxml import etree class CommonCode(Document): @@ -16,13 +20,20 @@ class CommonCode(Document): from frappe.core.doctype.dynamic_link.dynamic_link import DynamicLink from frappe.types import DF + additional_data: DF.Code | None applies_to: DF.Table[DynamicLink] code_list: DF.Link common_code: DF.Data - description: DF.SmallText | None title: DF.Data | None # end: auto-generated types + @staticmethod + def simple_hash(input_string, length=6): + return hashlib.blake2b(input_string.encode(), digest_size=length // 2).hexdigest() + + def autoname(self): + self.name = self.simple_hash(self.code_list) + "|" + self.simple_hash(self.common_code, 10) + def validate(self): if frappe.db.exists( "Common Code", @@ -32,6 +43,53 @@ def validate(self): _("Common Code {0} already exists in Code List {1}").format(self.common_code, self.code_list) ) + @staticmethod + def import_genericode(file_path, list_name, code_column, title_column=None, filters=None): + parser = etree.XMLParser(remove_blank_text=True) + tree = etree.parse(file_path, parser=parser) + root = tree.getroot() + + codes = [] + seen_common_codes = set() + list_hash = CommonCode.simple_hash(list_name) + + # Construct the XPath expression + xpath_expr = ".//SimpleCodeList/Row" + filter_conditions = [] + for column_ref, value in filters.items(): + filter_conditions.append(f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'") + if filter_conditions: + xpath_expr += "[" + " and ".join(filter_conditions) + "]" + + for idx, code in enumerate(root.xpath(xpath_expr)): + content = etree.tostring(code, encoding="unicode", pretty_print=True) + + code_value = code.find(f"./Value[@ColumnRef='{code_column}']/SimpleValue").text + if code_value in seen_common_codes: + frappe.throw( + _( + "Duplicate value found for '{}':\n\n
{}
" + ).format(code_column, escape_html(content)) + ) + seen_common_codes.add(code_value) + code_hash = CommonCode.simple_hash(code_value, 10) + + title = None + if title_column: + title = code.find(f"./Value[@ColumnRef='{title_column}']/SimpleValue").text + + codes.append( + { + "name": f"{list_hash}|{code_hash}|{idx}", # according to autoname + row index + "code_list": list_name, + "common_code": code_value, + "title": title, + "additional_data": content, + } + ) + + return codes + def on_doctype_update(): frappe.db.add_unique( diff --git a/erpnext/edi/doctype/common_code/common_code_list.js b/erpnext/edi/doctype/common_code/common_code_list.js new file mode 100644 index 000000000000..71b036f1f162 --- /dev/null +++ b/erpnext/edi/doctype/common_code/common_code_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings["Common Code"] = { + onload: function (listview) { + listview.page.add_inner_button( + __("Genericode"), + function () { + erpnext.edi.import_genericode(listview); + }, + __("Import") + ); + }, + hide_name_column: true, +}; diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 059075fe321d..ca7f72809434 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -35,6 +35,14 @@ "Newsletter": "public/js/newsletter.js", "Contact": "public/js/contact.js", } +doctype_list_js = { + "Code List": [ + "edi/doctype/code_list/code_list_import.js", + ], + "Common Code": [ + "edi/doctype/code_list/code_list_import.js", + ], +} override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}