Co-authored-by: David <dgx.arnold@gmail.com> Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
This commit is contained in:
0
erpnext/edi/__init__.py
Normal file
0
erpnext/edi/__init__.py
Normal file
0
erpnext/edi/doctype/__init__.py
Normal file
0
erpnext/edi/doctype/__init__.py
Normal file
0
erpnext/edi/doctype/code_list/__init__.py
Normal file
0
erpnext/edi/doctype/code_list/__init__.py
Normal file
51
erpnext/edi/doctype/code_list/code_list.js
Normal file
51
erpnext/edi/doctype/code_list/code_list.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on("Code List", {
|
||||||
|
refresh: (frm) => {
|
||||||
|
if (!frm.doc.__islocal) {
|
||||||
|
frm.add_custom_button(__("Import Genericode File"), function () {
|
||||||
|
erpnext.edi.import_genericode(frm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup: (frm) => {
|
||||||
|
frm.savetrash = () => {
|
||||||
|
frm.validate_form_action("Delete");
|
||||||
|
frappe.confirm(
|
||||||
|
__(
|
||||||
|
"Are you sure you want to delete {0}?<p>This action will also delete all associated Common Code documents.</p>",
|
||||||
|
[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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
frm.set_query("default_common_code", function (doc) {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
code_list: doc.name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
112
erpnext/edi/doctype/code_list/code_list.json
Normal file
112
erpnext/edi/doctype/code_list/code_list.json
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_copy": 1,
|
||||||
|
"allow_rename": 1,
|
||||||
|
"autoname": "prompt",
|
||||||
|
"creation": "2024-09-29 06:55:03.920375",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"title",
|
||||||
|
"canonical_uri",
|
||||||
|
"url",
|
||||||
|
"default_common_code",
|
||||||
|
"column_break_nkls",
|
||||||
|
"version",
|
||||||
|
"publisher",
|
||||||
|
"publisher_id",
|
||||||
|
"section_break_npxp",
|
||||||
|
"description"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "publisher",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Publisher"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 1,
|
||||||
|
"fieldname": "version",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Version"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "publisher_id",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Publisher ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "url",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "URL",
|
||||||
|
"options": "URL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "This value shall be used when no matching Common Code for a record is found.",
|
||||||
|
"fieldname": "default_common_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Default Common Code",
|
||||||
|
"options": "Common Code"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"link_doctype": "Common Code",
|
||||||
|
"link_fieldname": "code_list"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"modified": "2024-11-16 17:01:40.260293",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "EDI",
|
||||||
|
"name": "Code List",
|
||||||
|
"naming_rule": "Set by user",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"search_fields": "description",
|
||||||
|
"show_title_field_in_link": 1,
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "title"
|
||||||
|
}
|
||||||
125
erpnext/edi/doctype/code_list/code_list.py
Normal file
125
erpnext/edi/doctype/code_list/code_list.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from lxml.etree import Element
|
||||||
|
|
||||||
|
|
||||||
|
class CodeList(Document):
|
||||||
|
# begin: auto-generated types
|
||||||
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from frappe.types import DF
|
||||||
|
|
||||||
|
canonical_uri: DF.Data | None
|
||||||
|
default_common_code: DF.Link | None
|
||||||
|
description: DF.SmallText | None
|
||||||
|
publisher: DF.Data | None
|
||||||
|
publisher_id: DF.Data | None
|
||||||
|
title: DF.Data | None
|
||||||
|
url: 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):
|
||||||
|
self.db_set("default_common_code", None)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def get_codes_for(self, doctype: str, name: str) -> tuple[str]:
|
||||||
|
"""Get the applicable codes for a doctype and name"""
|
||||||
|
return get_codes_for(self.name, doctype, name)
|
||||||
|
|
||||||
|
def get_docnames_for(self, doctype: str, code: str) -> tuple[str]:
|
||||||
|
"""Get the mapped docnames for a doctype and code"""
|
||||||
|
return get_docnames_for(self.name, doctype, code)
|
||||||
|
|
||||||
|
def get_default_code(self) -> str | None:
|
||||||
|
"""Get the default common code for this code list"""
|
||||||
|
return (
|
||||||
|
frappe.db.get_value("Common Code", self.default_common_code, "common_code")
|
||||||
|
if self.default_common_code
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
def from_genericode(self, root: "Element"):
|
||||||
|
"""Extract Code List details from genericode XML"""
|
||||||
|
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)
|
||||||
|
if not self.publisher:
|
||||||
|
self.publisher = getattr(root.find(".//Identification/Agency/LongName"), "text", None)
|
||||||
|
self.publisher_id = getattr(root.find(".//Identification/Agency/Identifier"), "text", None)
|
||||||
|
self.url = getattr(root.find(".//Identification/LocationUri"), "text", None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]:
|
||||||
|
"""Return the common code for a given record"""
|
||||||
|
CommonCode = frappe.qb.DocType("Common Code")
|
||||||
|
DynamicLink = frappe.qb.DocType("Dynamic Link")
|
||||||
|
|
||||||
|
codes = (
|
||||||
|
frappe.qb.from_(CommonCode)
|
||||||
|
.join(DynamicLink)
|
||||||
|
.on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code"))
|
||||||
|
.select(CommonCode.common_code)
|
||||||
|
.where(
|
||||||
|
(DynamicLink.link_doctype == doctype)
|
||||||
|
& (DynamicLink.link_name == name)
|
||||||
|
& (CommonCode.code_list == code_list)
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
.orderby(CommonCode.common_code)
|
||||||
|
).run()
|
||||||
|
|
||||||
|
return tuple(c[0] for c in codes) if codes else ()
|
||||||
|
|
||||||
|
|
||||||
|
def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]:
|
||||||
|
"""Return the record name for a given common code"""
|
||||||
|
CommonCode = frappe.qb.DocType("Common Code")
|
||||||
|
DynamicLink = frappe.qb.DocType("Dynamic Link")
|
||||||
|
|
||||||
|
docnames = (
|
||||||
|
frappe.qb.from_(CommonCode)
|
||||||
|
.join(DynamicLink)
|
||||||
|
.on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code"))
|
||||||
|
.select(DynamicLink.link_name)
|
||||||
|
.where(
|
||||||
|
(DynamicLink.link_doctype == doctype)
|
||||||
|
& (CommonCode.common_code == code)
|
||||||
|
& (CommonCode.code_list == code_list)
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
.orderby(DynamicLink.idx)
|
||||||
|
).run()
|
||||||
|
|
||||||
|
return tuple(d[0] for d in docnames) if docnames else ()
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_code(code_list: str) -> str | None:
|
||||||
|
"""Return the default common code for a given code list"""
|
||||||
|
code_id = frappe.db.get_value("Code List", code_list, "default_common_code")
|
||||||
|
return frappe.db.get_value("Common Code", code_id, "common_code") if code_id else None
|
||||||
218
erpnext/edi/doctype/code_list/code_list_import.js
Normal file
218
erpnext/edi/doctype/code_list/code_list_import.js
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
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,
|
||||||
|
allow_take_photo: false,
|
||||||
|
on_success: function (_file_doc, r) {
|
||||||
|
listview_or_form.refresh();
|
||||||
|
show_column_selection_dialog(r.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function show_column_selection_dialog(context) {
|
||||||
|
let title_description = __("If there is no title column, use the code column for the title.");
|
||||||
|
let default_title = get_default(context.columns, ["name", "Name", "code-name", "scheme-name"]);
|
||||||
|
let fields = [
|
||||||
|
{
|
||||||
|
fieldtype: "HTML",
|
||||||
|
fieldname: "code_list_info",
|
||||||
|
options: `<div class="text-muted">${__(
|
||||||
|
"You are importing data for the code list:"
|
||||||
|
)} ${frappe.utils.get_form_link(
|
||||||
|
"Code List",
|
||||||
|
context.code_list,
|
||||||
|
true,
|
||||||
|
context.code_list_title
|
||||||
|
)}</div>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Section Break",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "import_column",
|
||||||
|
label: __("Import"),
|
||||||
|
fieldtype: "Column Break",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "title_column",
|
||||||
|
label: __("as Title"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
reqd: 1,
|
||||||
|
options: context.columns,
|
||||||
|
default: default_title,
|
||||||
|
description: default_title ? null : title_description,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "code_column",
|
||||||
|
label: __("as Code"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: context.columns,
|
||||||
|
reqd: 1,
|
||||||
|
default: get_default(context.columns, ["code", "Code", "value"]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldname: "filters_column",
|
||||||
|
label: __("Filter"),
|
||||||
|
fieldtype: "Column Break",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (context.columns.length > 2) {
|
||||||
|
fields.splice(5, 0, {
|
||||||
|
fieldname: "description_column",
|
||||||
|
label: __("as Description"),
|
||||||
|
fieldtype: "Select",
|
||||||
|
options: [null].concat(context.columns),
|
||||||
|
default: get_default(context.columns, [
|
||||||
|
"description",
|
||||||
|
"Description",
|
||||||
|
"remark",
|
||||||
|
__("description"),
|
||||||
|
__("Description"),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
.xcall("erpnext.edi.doctype.code_list.code_list_import.process_genericode_import", {
|
||||||
|
code_list_name: context.code_list,
|
||||||
|
file_name: context.file,
|
||||||
|
code_column: values.code_column,
|
||||||
|
title_column: values.title_column,
|
||||||
|
description_column: values.description_column,
|
||||||
|
filters: filters,
|
||||||
|
})
|
||||||
|
.then((count) => {
|
||||||
|
frappe.msgprint(__("Import completed. {0} common codes created.", [count]));
|
||||||
|
});
|
||||||
|
d.hide();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
d.fields_dict.code_column.df.onchange = () => update_preview(d, context);
|
||||||
|
d.fields_dict.title_column.df.onchange = (e) => {
|
||||||
|
let field = d.fields_dict.title_column;
|
||||||
|
if (!e.target.value) {
|
||||||
|
field.df.description = title_description;
|
||||||
|
field.refresh();
|
||||||
|
} else {
|
||||||
|
field.df.description = null;
|
||||||
|
field.refresh();
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the first key from the keys array that is found in the columns array.
|
||||||
|
*/
|
||||||
|
function get_default(columns, keys) {
|
||||||
|
return keys.find((key) => columns.includes(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_preview(dialog, context) {
|
||||||
|
let code_column = dialog.get_value("code_column");
|
||||||
|
let title_column = dialog.get_value("title_column");
|
||||||
|
let description_column = dialog.get_value("description_column");
|
||||||
|
|
||||||
|
let html = '<table class="table table-bordered"><thead><tr>';
|
||||||
|
if (title_column) html += `<th>${__("Title")}</th>`;
|
||||||
|
if (code_column) html += `<th>${__("Code")}</th>`;
|
||||||
|
if (description_column) html += `<th>${__("Description")}</th>`;
|
||||||
|
|
||||||
|
// Add headers for filterable columns
|
||||||
|
for (let column in context.filterable_columns) {
|
||||||
|
if (dialog.get_value(`filter_${column}`)) {
|
||||||
|
html += `<th>${__(column)}</th>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += "</tr></thead><tbody>";
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
html += "<tr>";
|
||||||
|
if (title_column) {
|
||||||
|
let title = context.example_values[title_column][i] || "";
|
||||||
|
html += `<td title="${title}">${truncate(title)}</td>`;
|
||||||
|
}
|
||||||
|
if (code_column) {
|
||||||
|
let code = context.example_values[code_column][i] || "";
|
||||||
|
html += `<td title="${code}">${truncate(code)}</td>`;
|
||||||
|
}
|
||||||
|
if (description_column) {
|
||||||
|
let description = context.example_values[description_column][i] || "";
|
||||||
|
html += `<td title="${description}">${truncate(description)}</td>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 += `<td title="${value}">${truncate(value)}</td>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += "</tr>";
|
||||||
|
}
|
||||||
|
|
||||||
|
html += "</tbody></table>";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
140
erpnext/edi/doctype/code_list/code_list_import.py
Normal file
140
erpnext/edi/doctype/code_list/code_list_import.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import requests
|
||||||
|
from frappe import _
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
URL_PREFIXES = ("http://", "https://")
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def import_genericode():
|
||||||
|
doctype = "Code List"
|
||||||
|
docname = frappe.form_dict.docname
|
||||||
|
content = frappe.local.uploaded_file
|
||||||
|
|
||||||
|
# recover the content, if it's a link
|
||||||
|
if (file_url := frappe.local.uploaded_file_url) and file_url.startswith(URL_PREFIXES):
|
||||||
|
try:
|
||||||
|
# If it's a URL, fetch the content and make it a local file (for durable audit)
|
||||||
|
response = requests.get(frappe.local.uploaded_file_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
frappe.local.uploaded_file = content = response.content
|
||||||
|
frappe.local.uploaded_filename = frappe.local.uploaded_file_url.split("/")[-1]
|
||||||
|
frappe.local.uploaded_file_url = None
|
||||||
|
except Exception as e:
|
||||||
|
frappe.throw(f"<pre>{e!s}</pre>", title=_("Fetching Error"))
|
||||||
|
|
||||||
|
if file_url := frappe.local.uploaded_file_url:
|
||||||
|
file_path = frappe.utils.file_manager.get_file_path(file_url)
|
||||||
|
with open(file_path.encode(), mode="rb") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Parse the xml content
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
try:
|
||||||
|
root = etree.fromstring(content, parser=parser)
|
||||||
|
except Exception as e:
|
||||||
|
frappe.throw(f"<pre>{e!s}</pre>", title=_("Parsing Error"))
|
||||||
|
|
||||||
|
# Extract the name (CanonicalVersionUri) from the parsed XML
|
||||||
|
name = root.find(".//CanonicalVersionUri").text
|
||||||
|
docname = docname or name
|
||||||
|
|
||||||
|
if frappe.db.exists(doctype, 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.from_genericode(root)
|
||||||
|
code_list.save()
|
||||||
|
|
||||||
|
# Attach the file and provide a recoverable identifier
|
||||||
|
file_doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "File",
|
||||||
|
"attached_to_doctype": "Code List",
|
||||||
|
"attached_to_name": code_list.name,
|
||||||
|
"folder": "Home/Attachments",
|
||||||
|
"file_name": frappe.local.uploaded_filename,
|
||||||
|
"file_url": frappe.local.uploaded_file_url,
|
||||||
|
"is_private": 1,
|
||||||
|
"content": content,
|
||||||
|
}
|
||||||
|
).save()
|
||||||
|
|
||||||
|
# Get available columns and example values
|
||||||
|
columns, example_values, filterable_columns = get_genericode_columns_and_examples(root)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"code_list": code_list.name,
|
||||||
|
"code_list_title": code_list.title,
|
||||||
|
"file": file_doc.name,
|
||||||
|
"columns": columns,
|
||||||
|
"example_values": example_values,
|
||||||
|
"filterable_columns": filterable_columns,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def process_genericode_import(
|
||||||
|
code_list_name: str,
|
||||||
|
file_name: str,
|
||||||
|
code_column: str,
|
||||||
|
title_column: str | None = None,
|
||||||
|
description_column: str | None = None,
|
||||||
|
filters: str | None = None,
|
||||||
|
):
|
||||||
|
from erpnext.edi.doctype.common_code.common_code import import_genericode
|
||||||
|
|
||||||
|
column_map = {"code": code_column, "title": title_column, "description": description_column}
|
||||||
|
|
||||||
|
return import_genericode(code_list_name, file_name, column_map, json.loads(filters) if filters else None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_genericode_columns_and_examples(root):
|
||||||
|
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")
|
||||||
|
if column_id not in columns:
|
||||||
|
# Handle undeclared column
|
||||||
|
columns.append(column_id)
|
||||||
|
example_values[column_id] = []
|
||||||
|
filterable_columns[column_id] = set()
|
||||||
|
|
||||||
|
simple_value = value.find("./SimpleValue")
|
||||||
|
if simple_value is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
filterable_columns[column_id].add(simple_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")
|
||||||
|
simple_value = value.find("./SimpleValue")
|
||||||
|
if simple_value is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
example_values[column_id].append(simple_value.text)
|
||||||
|
|
||||||
|
filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5}
|
||||||
|
|
||||||
|
return columns, example_values, filterable_columns
|
||||||
8
erpnext/edi/doctype/code_list/code_list_list.js
Normal file
8
erpnext/edi/doctype/code_list/code_list_list.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
frappe.listview_settings["Code List"] = {
|
||||||
|
onload: function (listview) {
|
||||||
|
listview.page.add_inner_button(__("Import Genericode File"), function () {
|
||||||
|
erpnext.edi.import_genericode(listview);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
hide_name_column: true,
|
||||||
|
};
|
||||||
9
erpnext/edi/doctype/code_list/test_code_list.py
Normal file
9
erpnext/edi/doctype/code_list/test_code_list.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestCodeList(FrappeTestCase):
|
||||||
|
pass
|
||||||
0
erpnext/edi/doctype/common_code/__init__.py
Normal file
0
erpnext/edi/doctype/common_code/__init__.py
Normal file
8
erpnext/edi/doctype/common_code/common_code.js
Normal file
8
erpnext/edi/doctype/common_code/common_code.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
// frappe.ui.form.on("Common Code", {
|
||||||
|
// refresh(frm) {
|
||||||
|
|
||||||
|
// },
|
||||||
|
// });
|
||||||
103
erpnext/edi/doctype/common_code/common_code.json
Normal file
103
erpnext/edi/doctype/common_code/common_code.json
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "hash",
|
||||||
|
"creation": "2024-09-29 07:01:18.133067",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"code_list",
|
||||||
|
"title",
|
||||||
|
"common_code",
|
||||||
|
"description",
|
||||||
|
"column_break_wxsw",
|
||||||
|
"additional_data",
|
||||||
|
"section_break_rhgh",
|
||||||
|
"applies_to"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "code_list",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Code List",
|
||||||
|
"options": "Code List",
|
||||||
|
"reqd": 1,
|
||||||
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Title",
|
||||||
|
"length": 300,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_wxsw",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_rhgh",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "applies_to",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Applies To",
|
||||||
|
"options": "Dynamic Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "common_code",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Common Code",
|
||||||
|
"length": 300,
|
||||||
|
"reqd": 1,
|
||||||
|
"search_index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "additional_data",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"label": "Additional Data",
|
||||||
|
"max_height": "190px",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Description",
|
||||||
|
"max_height": "60px"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [],
|
||||||
|
"modified": "2024-11-06 07:46:17.175687",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "EDI",
|
||||||
|
"name": "Common Code",
|
||||||
|
"naming_rule": "Random",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"search_fields": "common_code,description",
|
||||||
|
"show_title_field_in_link": 1,
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "title"
|
||||||
|
}
|
||||||
114
erpnext/edi/doctype/common_code/common_code.py
Normal file
114
erpnext/edi/doctype/common_code/common_code.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# 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.data import get_link_to_form
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
|
||||||
|
class CommonCode(Document):
|
||||||
|
# begin: auto-generated types
|
||||||
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
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
|
||||||
|
# end: auto-generated types
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
self.validate_distinct_references()
|
||||||
|
|
||||||
|
def validate_distinct_references(self):
|
||||||
|
"""Ensure no two Common Codes of the same Code List are linked to the same document."""
|
||||||
|
for link in self.applies_to:
|
||||||
|
existing_links = frappe.get_all(
|
||||||
|
"Common Code",
|
||||||
|
filters=[
|
||||||
|
["name", "!=", self.name],
|
||||||
|
["code_list", "=", self.code_list],
|
||||||
|
["Dynamic Link", "link_doctype", "=", link.link_doctype],
|
||||||
|
["Dynamic Link", "link_name", "=", link.link_name],
|
||||||
|
],
|
||||||
|
fields=["name", "common_code"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_links:
|
||||||
|
existing_link = existing_links[0]
|
||||||
|
frappe.throw(
|
||||||
|
_("{0} {1} is already linked to Common Code {2}.").format(
|
||||||
|
link.link_doctype,
|
||||||
|
link.link_name,
|
||||||
|
get_link_to_form("Common Code", existing_link["name"], existing_link["common_code"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def from_genericode(self, column_map: dict, xml_element: "etree.Element"):
|
||||||
|
"""Populate the Common Code document from a genericode XML element
|
||||||
|
|
||||||
|
Args:
|
||||||
|
column_map (dict): A mapping of column names to XML column references. Keys: code, title, description
|
||||||
|
code (etree.Element): The XML element representing a code in the genericode file
|
||||||
|
"""
|
||||||
|
title_column = column_map.get("title")
|
||||||
|
code_column = column_map["code"]
|
||||||
|
description_column = column_map.get("description")
|
||||||
|
|
||||||
|
self.common_code = xml_element.find(f"./Value[@ColumnRef='{code_column}']/SimpleValue").text
|
||||||
|
|
||||||
|
if title_column:
|
||||||
|
simple_value_title = xml_element.find(f"./Value[@ColumnRef='{title_column}']/SimpleValue")
|
||||||
|
self.title = simple_value_title.text if simple_value_title is not None else self.common_code
|
||||||
|
|
||||||
|
if description_column:
|
||||||
|
simple_value_descr = xml_element.find(f"./Value[@ColumnRef='{description_column}']/SimpleValue")
|
||||||
|
self.description = simple_value_descr.text if simple_value_descr is not None else None
|
||||||
|
|
||||||
|
self.additional_data = etree.tostring(xml_element, encoding="unicode", pretty_print=True)
|
||||||
|
|
||||||
|
|
||||||
|
def simple_hash(input_string, length=6):
|
||||||
|
return hashlib.blake2b(input_string.encode(), digest_size=length // 2).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def import_genericode(code_list: str, file_name: str, column_map: dict, filters: dict | None = None):
|
||||||
|
"""Import genericode file and create Common Code entries"""
|
||||||
|
file_path = frappe.utils.file_manager.get_file_path(file_name)
|
||||||
|
parser = etree.XMLParser(remove_blank_text=True)
|
||||||
|
tree = etree.parse(file_path, parser=parser)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# Construct the XPath expression
|
||||||
|
xpath_expr = ".//SimpleCodeList/Row"
|
||||||
|
filter_conditions = [
|
||||||
|
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items()
|
||||||
|
]
|
||||||
|
if filter_conditions:
|
||||||
|
xpath_expr += "[" + " and ".join(filter_conditions) + "]"
|
||||||
|
|
||||||
|
elements = root.xpath(xpath_expr)
|
||||||
|
total_elements = len(elements)
|
||||||
|
for i, xml_element in enumerate(elements, start=1):
|
||||||
|
common_code: "CommonCode" = frappe.new_doc("Common Code")
|
||||||
|
common_code.code_list = code_list
|
||||||
|
common_code.from_genericode(column_map, xml_element)
|
||||||
|
common_code.save()
|
||||||
|
frappe.publish_progress(i / total_elements * 100, title=_("Importing Common Codes"))
|
||||||
|
|
||||||
|
return total_elements
|
||||||
|
|
||||||
|
|
||||||
|
def on_doctype_update():
|
||||||
|
frappe.db.add_index("Common Code", ["code_list", "common_code"])
|
||||||
8
erpnext/edi/doctype/common_code/common_code_list.js
Normal file
8
erpnext/edi/doctype/common_code/common_code_list.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
frappe.listview_settings["Common Code"] = {
|
||||||
|
onload: function (listview) {
|
||||||
|
listview.page.add_inner_button(__("Import Genericode File"), function () {
|
||||||
|
erpnext.edi.import_genericode(listview);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
hide_name_column: true,
|
||||||
|
};
|
||||||
9
erpnext/edi/doctype/common_code/test_common_code.py
Normal file
9
erpnext/edi/doctype/common_code/test_common_code.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestCommonCode(FrappeTestCase):
|
||||||
|
pass
|
||||||
@@ -35,6 +35,14 @@ doctype_js = {
|
|||||||
"Newsletter": "public/js/newsletter.js",
|
"Newsletter": "public/js/newsletter.js",
|
||||||
"Contact": "public/js/contact.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"}
|
override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
|
||||||
|
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ Communication
|
|||||||
Telephony
|
Telephony
|
||||||
Bulk Transaction
|
Bulk Transaction
|
||||||
Subcontracting
|
Subcontracting
|
||||||
|
EDI
|
||||||
Reference in New Issue
Block a user