From fcb2167953dced2d92e6050c172e0492cf9672a8 Mon Sep 17 00:00:00 2001 From: Duradundi Date: Thu, 10 Sep 2026 11:00:50 +0000 Subject: [PATCH] asset-lite latest fixtures and data import code --- asset_lite/api/custom_api.py | 6 + asset_lite/api/data_import_api.py | 121 ++++++++++++++++++ asset_lite/api/ppm_generator_api.py | 50 ++++++++ asset_lite/fixtures/custom_field.json | 60 +-------- asset_lite/fixtures/role.json | 42 ++++++ asset_lite/fixtures/server_script.json | 4 +- asset_lite/fixtures/web_form.json | 142 +++++++++++++++++++++ asset_lite/fixtures/workflow_state.json | 34 ++--- asset_lite/hooks.py | 6 +- asset_lite/public/js/custom_data_import.js | 70 ++++++++++ 10 files changed, 457 insertions(+), 78 deletions(-) create mode 100644 asset_lite/api/data_import_api.py create mode 100644 asset_lite/fixtures/web_form.json create mode 100644 asset_lite/public/js/custom_data_import.js diff --git a/asset_lite/api/custom_api.py b/asset_lite/api/custom_api.py index 360d94f..bb57dab 100644 --- a/asset_lite/api/custom_api.py +++ b/asset_lite/api/custom_api.py @@ -21,13 +21,19 @@ def get_user_details(user_id=None): response_data = { "user_id": user_id, "full_name": user.full_name, + "first_name": user.first_name or "", + "middle_name": user.middle_name or "", + "last_name": user.last_name or "", + "username": user.username or "", "email": user.email, + "mobile_no": user.mobile_no or "", "user_image": user.user_image, "roles": roles, "last_login": user.last_login, "enabled": user.enabled, "creation": user.creation, "modified": user.modified, + "custom_user_id": user.get("custom_user_id") or "", "custom_site_name": user.get("custom_site_name") or "", "custom_phcc_site_name": user.get("custom_phcc_site_name") or "", "role_profile_name": user.get("role_profile_name") or "", diff --git a/asset_lite/api/data_import_api.py b/asset_lite/api/data_import_api.py new file mode 100644 index 0000000..e0b37b6 --- /dev/null +++ b/asset_lite/api/data_import_api.py @@ -0,0 +1,121 @@ +import csv +import os +from io import StringIO + +import frappe +from frappe import _ +from frappe.utils.csvutils import read_csv_content +from frappe.utils.file_manager import save_file +from frappe.utils.xlsxutils import ( + make_xlsx, + read_xls_file_from_attached_file, + read_xlsx_file_from_attached_file, +) + + +def _trim_cell(value): + if isinstance(value, str): + trimmed = value.strip() + return trimmed if trimmed else None + return value + + +def _trim_rows(rows): + trimmed_cells = 0 + out = [] + for row in rows: + new_row = [] + for cell in row: + if isinstance(cell, str): + original = cell + trimmed = _trim_cell(cell) + if original != (trimmed or ""): + trimmed_cells += 1 + new_row.append(trimmed) + else: + new_row.append(cell) + out.append(new_row) + return out, trimmed_cells + + +def _read_rows_from_content(content, extension): + extension = (extension or "").lower() + if extension == "csv": + return read_csv_content(content) + if extension == "xlsx": + return read_xlsx_file_from_attached_file(fcontent=content) + if extension == "xls": + return read_xls_file_from_attached_file(content) + frappe.throw(_("Import file must be .csv, .xlsx, or .xls")) + + +def _write_trimmed_content(rows, extension): + extension = (extension or "").lower() + if extension == "csv": + buffer = StringIO() + writer = csv.writer(buffer) + for row in rows: + writer.writerow(["" if cell is None else cell for cell in row]) + return buffer.getvalue().encode("utf-8"), "csv" + + if extension == "xls": + extension = "xlsx" + + if extension == "xlsx": + return make_xlsx(rows, "Import").getvalue(), "xlsx" + + frappe.throw(_("Import file must be .csv, .xlsx, or .xls")) + + +def _trimmed_filename(original_name, output_extension): + base, _ = os.path.splitext(original_name or "import") + return f"{base}_trimmed.{output_extension}" + + +@frappe.whitelist() +def trim_and_upload_import_file(data_import): + doc = frappe.get_doc("Data Import", data_import) + doc.check_permission("write") + + if not doc.import_file: + frappe.throw(_("Attach an import file first.")) + + if doc.google_sheets_url: + frappe.throw(_("Trim and Upload is only available for attached files, not Google Sheets URLs.")) + + file_name = frappe.db.get_value("File", {"file_url": doc.import_file}, "name") + if not file_name: + frappe.throw(_("Import file not found in File attachments.")) + + source_file = frappe.get_doc("File", file_name) + content = source_file.get_content() + extension = os.path.splitext(source_file.file_name or doc.import_file)[1][1:].lower() + + rows = _read_rows_from_content(content, extension) + if not rows: + frappe.throw(_("Import file is empty.")) + + trimmed_rows, trimmed_cells = _trim_rows(rows) + output_content, output_extension = _write_trimmed_content(trimmed_rows, extension) + output_name = _trimmed_filename(source_file.file_name or os.path.basename(doc.import_file), output_extension) + + new_file = save_file( + output_name, + output_content, + dt=doc.doctype, + dn=doc.name, + is_private=source_file.is_private, + df="import_file", + ) + + doc.import_file = new_file.file_url + doc.template_options = "" + doc.template_warnings = "" + doc.save() + + return { + "import_file": new_file.file_url, + "file_name": new_file.file_name, + "trimmed_cells": trimmed_cells, + "summary": _("Trimmed {0} cell(s) and uploaded {1}.").format(trimmed_cells, new_file.file_name), + } diff --git a/asset_lite/api/ppm_generator_api.py b/asset_lite/api/ppm_generator_api.py index 49818d6..8109cd6 100644 --- a/asset_lite/api/ppm_generator_api.py +++ b/asset_lite/api/ppm_generator_api.py @@ -42,6 +42,56 @@ PM_ENTRY_LINE_FIELDS = [ 'model' ] +PPM_PLANNER_ASSET_FIELDS = [ + "name", + "asset_name", + "custom_modality", + "company", + "custom_manufacturer", + "custom_device_status", + "custom_model", + "department", +] + + +@frappe.whitelist() +def get_ppm_planner_assets( + company=None, + custom_modality=None, + custom_manufacturer=None, + custom_device_status=None, + custom_model=None, + department=None, +): + """Fetch submitted assets for PPM Planner bulk scheduling.""" + filters = {"docstatus": 1} + + if company: + filters["company"] = company + if custom_modality: + filters["custom_modality"] = custom_modality + if custom_manufacturer: + filters["custom_manufacturer"] = custom_manufacturer + if custom_device_status: + filters["custom_device_status"] = custom_device_status + if custom_model: + filters["custom_model"] = custom_model + if department: + filters["department"] = department + + assets = frappe.get_all("Asset", filters=filters, fields=PPM_PLANNER_ASSET_FIELDS) + + existing_maintenance_assets = set( + frappe.get_all( + "Asset Maintenance", + filters={"docstatus": ["<", 2]}, + pluck="name", + ) + ) + + return [asset for asset in assets if asset.name not in existing_maintenance_assets] + + @frappe.whitelist() def create_bulk_schedules(asset_names, start_date, end_date, maintenance_team=None, periodicity='Monthly', maintenance_type='Preventive'): """Create maintenance schedules for multiple assets""" diff --git a/asset_lite/fixtures/custom_field.json b/asset_lite/fixtures/custom_field.json index 0df3be8..2e36481 100644 --- a/asset_lite/fixtures/custom_field.json +++ b/asset_lite/fixtures/custom_field.json @@ -55,62 +55,6 @@ "unique": 0, "width": null }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "User", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "custom_department", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 1, - "insert_after": "Username", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Department", - "length": 0, - "link_filters": null, - "mandatory_depends_on": null, - "modified": "2025-08-26 15:24:16.357765", - "module": "Asset Lite", - "name": "User-custom_department", - "no_copy": 0, - "non_negative": 0, - "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "show_dashboard": 0, - "sort_options": 0, - "translatable": 0, - "unique": 0, - "width": null - }, { "allow_in_quick_entry": 0, "allow_on_submit": 0, @@ -12153,7 +12097,7 @@ }, { "allow_in_quick_entry": 0, - "allow_on_submit": 0, + "allow_on_submit": 1, "bold": 0, "collapsible": 0, "collapsible_depends_on": null, @@ -12185,7 +12129,7 @@ "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2025-04-22 12:52:15.979629", + "modified": "2026-08-31 09:21:30.053035", "module": "Asset Lite", "name": "Asset-custom_installation_date", "no_copy": 0, diff --git a/asset_lite/fixtures/role.json b/asset_lite/fixtures/role.json index 6199201..e0ea171 100644 --- a/asset_lite/fixtures/role.json +++ b/asset_lite/fixtures/role.json @@ -1468,5 +1468,47 @@ "timeline": 0, "two_factor_auth": 0, "view_switcher": 0 + }, + { + "bulk_actions": 1, + "dashboard": 1, + "desk_access": 1, + "disabled": 0, + "docstatus": 0, + "doctype": "Role", + "form_sidebar": 1, + "home_page": null, + "is_custom": 0, + "list_sidebar": 1, + "modified": "2026-08-28 12:49:21.664589", + "name": "BME Director", + "notifications": 1, + "restrict_to_domain": null, + "role_name": "BME Director", + "search_bar": 1, + "timeline": 1, + "two_factor_auth": 0, + "view_switcher": 1 + }, + { + "bulk_actions": 1, + "dashboard": 1, + "desk_access": 1, + "disabled": 0, + "docstatus": 0, + "doctype": "Role", + "form_sidebar": 1, + "home_page": null, + "is_custom": 0, + "list_sidebar": 1, + "modified": "2026-08-28 12:49:45.525864", + "name": "Engineer", + "notifications": 1, + "restrict_to_domain": null, + "role_name": "Engineer", + "search_bar": 1, + "timeline": 1, + "two_factor_auth": 0, + "view_switcher": 1 } ] \ No newline at end of file diff --git a/asset_lite/fixtures/server_script.json b/asset_lite/fixtures/server_script.json index 1b5abfc..30e8310 100644 --- a/asset_lite/fixtures/server_script.json +++ b/asset_lite/fixtures/server_script.json @@ -674,13 +674,13 @@ "doctype_event": "Before Insert", "enable_rate_limit": 0, "event_frequency": "All", - "modified": "2026-01-09 13:21:21.936865", + "modified": "2026-08-25 12:15:18.912036", "module": "Asset Lite", "name": "Update Item Defaults", "rate_limit_count": 5, "rate_limit_seconds": 86400, "reference_doctype": "Item", - "script": "row = doc.append(\"item_defaults\", {})\r\nrow.company = \"Iman General Hospital\"\r\nrow.default_warehouse = \"Iman Central Warehouse - IGH\"", + "script": "#row = doc.append(\"item_defaults\", {})\r\n#row.company = \"Iman General Hospital\"\r\n#row.default_warehouse = \"Iman Central Warehouse - IGH\"\r\n\r\n\r\nif doc.custom_hospital_name:\r\n company = frappe.get_doc(\"Company\", doc.custom_hospital_name)\r\n\r\n if company.abbr:\r\n warehouse = \"Stores - \" + company.abbr\r\n\r\n # If no Item Defaults rows exist, create one\r\n if not doc.item_defaults:\r\n row = doc.append(\"item_defaults\")\r\n row.company = doc.custom_hospital_name\r\n row.default_warehouse = warehouse\r\n\r\n else:\r\n # Update existing Item Defaults rows\r\n for row in doc.item_defaults:\r\n row.company = doc.custom_hospital_name\r\n row.default_warehouse = warehouse", "script_type": "DocType Event" }, { diff --git a/asset_lite/fixtures/web_form.json b/asset_lite/fixtures/web_form.json new file mode 100644 index 0000000..de84180 --- /dev/null +++ b/asset_lite/fixtures/web_form.json @@ -0,0 +1,142 @@ +[ + { + "accept_payment": 0, + "allow_comments": 0, + "allow_delete": 0, + "allow_edit": 0, + "allow_incomplete": 0, + "allow_multiple": 0, + "allow_print": 0, + "amount": 0.0, + "amount_based_on_field": 0, + "amount_field": null, + "anonymous": 0, + "apply_document_permissions": 0, + "banner_image": null, + "breadcrumbs": null, + "button_label": "Save", + "client_script": null, + "condition_json": "[]", + "currency": "SAR", + "custom_css": null, + "doc_type": "WO Feedback", + "docstatus": 0, + "doctype": "Web Form", + "introduction_text": "

Work Order is Closed Please Give Your Feedback Here

", + "is_standard": 0, + "list_columns": [], + "list_title": null, + "login_required": 0, + "max_attachment_size": 0, + "meta_description": null, + "meta_image": null, + "meta_title": null, + "modified": "2026-08-18 13:35:38.853748", + "module": "Asset Lite", + "name": "wo-feedback", + "payment_button_help": null, + "payment_button_label": "Buy Now", + "payment_gateway": null, + "print_format": null, + "published": 1, + "route": "wo-feedback", + "show_attachments": 0, + "show_list": 0, + "show_sidebar": 0, + "success_message": "Your Feedback Submitted successfully", + "success_title": "Feedback Submitted", + "success_url": null, + "title": "WO Feedback", + "web_form_fields": [ + { + "allow_read_on_all_link_options": 0, + "default": null, + "depends_on": null, + "description": null, + "fieldname": "work_order", + "fieldtype": "Link", + "hidden": 0, + "label": "Work Order", + "mandatory_depends_on": null, + "max_length": 0, + "max_value": 0, + "options": "Work_Order", + "parent": "wo-feedback", + "parentfield": "web_form_fields", + "parenttype": "Web Form", + "precision": "", + "read_only": 0, + "read_only_depends_on": null, + "reqd": 1, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "default": null, + "depends_on": null, + "description": null, + "fieldname": "requester_name", + "fieldtype": "Data", + "hidden": 0, + "label": "Requester Name", + "mandatory_depends_on": null, + "max_length": 0, + "max_value": 0, + "options": null, + "parent": "wo-feedback", + "parentfield": "web_form_fields", + "parenttype": "Web Form", + "precision": "", + "read_only": 0, + "read_only_depends_on": null, + "reqd": 1, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "default": null, + "depends_on": null, + "description": null, + "fieldname": "rating", + "fieldtype": "Select", + "hidden": 0, + "label": "Rating", + "mandatory_depends_on": null, + "max_length": 0, + "max_value": 0, + "options": "\nExcellent / ممتاز\nGood / جيد\nAverage / متوسط\nPoor / ضعيف", + "parent": "wo-feedback", + "parentfield": "web_form_fields", + "parenttype": "Web Form", + "precision": "", + "read_only": 0, + "read_only_depends_on": null, + "reqd": 1, + "show_in_filter": 0 + }, + { + "allow_read_on_all_link_options": 0, + "default": null, + "depends_on": null, + "description": null, + "fieldname": "comments", + "fieldtype": "Small Text", + "hidden": 0, + "label": "Comments", + "mandatory_depends_on": null, + "max_length": 0, + "max_value": 0, + "options": null, + "parent": "wo-feedback", + "parentfield": "web_form_fields", + "parenttype": "Web Form", + "precision": "", + "read_only": 0, + "read_only_depends_on": null, + "reqd": 0, + "show_in_filter": 0 + } + ], + "website_sidebar": null + } +] \ No newline at end of file diff --git a/asset_lite/fixtures/workflow_state.json b/asset_lite/fixtures/workflow_state.json index 3352213..2ee98c4 100644 --- a/asset_lite/fixtures/workflow_state.json +++ b/asset_lite/fixtures/workflow_state.json @@ -80,15 +80,6 @@ "style": "", "workflow_state_name": "Sent To Site Manager" }, - { - "docstatus": 0, - "doctype": "Workflow State", - "icon": "", - "modified": "2026-08-11 11:37:18.893891", - "name": "Sent To Project Manager", - "style": "", - "workflow_state_name": "Sent To Project Manager" - }, { "docstatus": 0, "doctype": "Workflow State", @@ -116,6 +107,15 @@ "style": "", "workflow_state_name": "Pending Purchase" }, + { + "docstatus": 0, + "doctype": "Workflow State", + "icon": "", + "modified": "2026-08-11 11:37:18.893891", + "name": "Sent To Project Manager", + "style": "", + "workflow_state_name": "Sent To Project Manager" + }, { "docstatus": 0, "doctype": "Workflow State", @@ -138,19 +138,19 @@ "docstatus": 0, "doctype": "Workflow State", "icon": "", - "modified": "2025-08-11 12:01:35.474110", - "name": "Closed", - "style": "", - "workflow_state_name": "Closed" + "modified": "2024-09-13 13:50:24.315353", + "name": "Pending Approval", + "style": "Primary", + "workflow_state_name": "Pending Approval" }, { "docstatus": 0, "doctype": "Workflow State", "icon": "", - "modified": "2024-09-13 13:50:24.315353", - "name": "Pending Approval", - "style": "Primary", - "workflow_state_name": "Pending Approval" + "modified": "2025-08-11 12:01:35.474110", + "name": "Closed", + "style": "", + "workflow_state_name": "Closed" }, { "docstatus": 0, diff --git a/asset_lite/hooks.py b/asset_lite/hooks.py index e87767d..6009d61 100644 --- a/asset_lite/hooks.py +++ b/asset_lite/hooks.py @@ -44,7 +44,10 @@ app_include_js = "/assets/asset_lite/js/dashboard_embed.js?v=1.0.28" # page_js = {"page" : "public/js/file.js"} # include js in doctype views -doctype_js = {"Asset Maintenance Log" : "public/js/custom_asset_maintenance_log.js"} +doctype_js = { + "Asset Maintenance Log" : "public/js/custom_asset_maintenance_log.js", + "Data Import" : "public/js/custom_data_import.js" +} # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} @@ -281,6 +284,7 @@ fixtures = [ {"doctype": "Server Script", "filters": [["module","=","Asset Lite"]]}, {"doctype": "Notification", "filters": [["is_standard","=",0]]}, {"doctype": "Report", "filters": [["module","=","Asset Lite"]]}, + {"doctype": "Web Form", "filters": [["module","=","Asset Lite"]]}, { "doctype": "Dashboard Chart", "filters": [ diff --git a/asset_lite/public/js/custom_data_import.js b/asset_lite/public/js/custom_data_import.js new file mode 100644 index 0000000..a857358 --- /dev/null +++ b/asset_lite/public/js/custom_data_import.js @@ -0,0 +1,70 @@ +function trim_and_upload_import_file(frm) { + frappe.confirm( + __( + 'Trim leading and trailing spaces from all cells, upload the cleaned file, and update Import File?' + ), + () => { + frappe.call({ + method: 'asset_lite.api.data_import_api.trim_and_upload_import_file', + args: { data_import: frm.doc.name }, + freeze: true, + freeze_message: __('Trimming and uploading file...'), + callback(r) { + if (!r.message) { + return; + } + + frappe.show_alert( + { + message: r.message.summary || __('File trimmed and uploaded.'), + indicator: 'green', + }, + 5 + ); + + frm.reload_doc().then(() => frm.trigger('import_file')); + }, + }); + } + ); +} + +function add_trim_and_upload_button(frm) { + if (frm.is_new() || !frm.doc.import_file || frm.doc.google_sheets_url) { + frm.fields_dict.import_file?.$wrapper?.find('.trim-upload-btn').remove(); + return; + } + + const label = __('Trim and Upload file'); + const action = () => trim_and_upload_import_file(frm); + + frm.add_custom_button(label, action, __('Actions')); + frm.page.add_action_item(label, action); + + const field = frm.get_field('import_file'); + if (!field?.$wrapper) { + return; + } + + field.$wrapper.find('.trim-upload-btn').remove(); + const $btn = $(``); + $btn.on('click', action); + + const $attachRow = field.$wrapper.find('.attached-file, .file-preview, .control-input-wrapper').last(); + if ($attachRow.length) { + $attachRow.append($btn); + } else { + field.$wrapper.append($btn); + } +} + +frappe.ui.form.on('Data Import', { + refresh(frm) { + add_trim_and_upload_button(frm); + }, + onload_post_render(frm) { + add_trim_and_upload_button(frm); + }, +});