diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b512f6e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "python.defaultInterpreterPath": "../../env/bin/python", + "python.analysis.extraPaths": [ + "../../apps/frappe", + "../../apps/erpnext", + "../../apps/crm", + "../../apps/waflo", + "../../apps/talal", + "../../apps/frappe_whatsapp", + "../../apps/frappe_notifier" + ] +} diff --git a/lightning_import/lightning_import/doctype/lightning_field_mapping/__init__.py b/lightning_import/lightning_import/doctype/lightning_field_mapping/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lightning_import/lightning_import/doctype/lightning_field_mapping/lightning_field_mapping.js b/lightning_import/lightning_import/doctype/lightning_field_mapping/lightning_field_mapping.js new file mode 100644 index 0000000..2879671 --- /dev/null +++ b/lightning_import/lightning_import/doctype/lightning_field_mapping/lightning_field_mapping.js @@ -0,0 +1,71 @@ +// Copyright (c) 2025, Tridz Technologies Pvt Ltd and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Lightning Field Mapping', { + refresh: function(frm) { + if (frm.doc.reference_doctype) { + frm.trigger('set_field_options'); + } + }, + + reference_doctype: function(frm) { + // Clear existing rows when changing doctype to avoid invalid mappings + if (frm.doc.mappings && frm.doc.mappings.length > 0) { + frm.clear_table('mappings'); + frm.refresh_field('mappings'); + } + frm.trigger('set_field_options'); + }, + + set_field_options: function(frm) { + if (frm.doc.reference_doctype) { + frappe.call({ + method: 'lightning_import.lightning_import.api.get_fields.get_doctype_fields', + args: { doctype: frm.doc.reference_doctype }, + callback: function(r) { + if (r.message && r.message.fields) { + const options = [''].concat(r.message.fields.map(f => f.fieldname)).join('\n'); + + // 1. Update base metadata + let base_df = frappe.meta.get_docfield('Lightning Fields', 'field_name'); + if (base_df) { + base_df.options = options; + } + + // 2. Update form-specific metadata + let form_df = frappe.meta.get_docfield('Lightning Fields', 'field_name', frm.doc.name); + if (form_df) { + form_df.options = options; + } + + // 3. Update grid column properties directly + if (frm.fields_dict.mappings && frm.fields_dict.mappings.grid) { + frm.fields_dict.mappings.grid.update_docfield_property('field_name', 'options', options); + } + + frm.refresh_field('mappings'); + } + } + }); + } else { + // Clear options if no doctype selected + let base_df = frappe.meta.get_docfield('Lightning Fields', 'field_name'); + if (base_df) base_df.options = ""; + + let form_df = frappe.meta.get_docfield('Lightning Fields', 'field_name', frm.doc.name); + if (form_df) form_df.options = ""; + + if (frm.fields_dict.mappings && frm.fields_dict.mappings.grid) { + frm.fields_dict.mappings.grid.update_docfield_property('field_name', 'options', ""); + } + frm.refresh_field('mappings'); + } + } +}); + +frappe.ui.form.on('Lightning Fields', { + mappings_add: function(frm, cdt, cdn) { + // Ensure options are set when a new row is added + frm.trigger('set_field_options'); + } +}); diff --git a/lightning_import/lightning_import/doctype/lightning_field_mapping/lightning_field_mapping.json b/lightning_import/lightning_import/doctype/lightning_field_mapping/lightning_field_mapping.json new file mode 100644 index 0000000..4d4384d --- /dev/null +++ b/lightning_import/lightning_import/doctype/lightning_field_mapping/lightning_field_mapping.json @@ -0,0 +1,54 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "LFM-.#####", + "creation": "2026-05-14 11:00:52.970656", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "reference_doctype", + "mappings" + ], + "fields": [ + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "label": "Reference DocType", + "options": "DocType" + }, + { + "fieldname": "mappings", + "fieldtype": "Table", + "label": "Mappings", + "options": "Lightning Fields" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-05-14 12:47:06.768504", + "modified_by": "Administrator", + "module": "Lightning Import", + "name": "Lightning Field Mapping", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/lightning_import/lightning_import/doctype/lightning_field_mapping/lightning_field_mapping.py b/lightning_import/lightning_import/doctype/lightning_field_mapping/lightning_field_mapping.py new file mode 100644 index 0000000..4e0c5c5 --- /dev/null +++ b/lightning_import/lightning_import/doctype/lightning_field_mapping/lightning_field_mapping.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Tridz Technologies Pvt Ltd and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LightningFieldMapping(Document): + pass diff --git a/lightning_import/lightning_import/doctype/lightning_field_mapping/test_lightning_field_mapping.py b/lightning_import/lightning_import/doctype/lightning_field_mapping/test_lightning_field_mapping.py new file mode 100644 index 0000000..f393ff8 --- /dev/null +++ b/lightning_import/lightning_import/doctype/lightning_field_mapping/test_lightning_field_mapping.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Tridz Technologies Pvt Ltd and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLightningFieldMapping(FrappeTestCase): + pass diff --git a/lightning_import/lightning_import/doctype/lightning_fields/__init__.py b/lightning_import/lightning_import/doctype/lightning_fields/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lightning_import/lightning_import/doctype/lightning_fields/lightning_fields.json b/lightning_import/lightning_import/doctype/lightning_fields/lightning_fields.json new file mode 100644 index 0000000..8da67f5 --- /dev/null +++ b/lightning_import/lightning_import/doctype/lightning_fields/lightning_fields.json @@ -0,0 +1,41 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-05-13 12:51:05.099561", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "field_name", + "alternate_name" + ], + "fields": [ + { + "fieldname": "field_name", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Field Name" + }, + { + "fieldname": "alternate_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Alternate Name" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-05-25 14:12:23.875906", + "modified_by": "Administrator", + "module": "Lightning Import", + "name": "Lightning Fields", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/lightning_import/lightning_import/doctype/lightning_fields/lightning_fields.py b/lightning_import/lightning_import/doctype/lightning_fields/lightning_fields.py new file mode 100644 index 0000000..46c6d51 --- /dev/null +++ b/lightning_import/lightning_import/doctype/lightning_fields/lightning_fields.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Tridz Technologies Pvt Ltd and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LightningFields(Document): + pass diff --git a/lightning_import/lightning_import/doctype/lightning_multi_import_target/__init__.py b/lightning_import/lightning_import/doctype/lightning_multi_import_target/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lightning_import/lightning_import/doctype/lightning_multi_import_target/lightning_multi_import_target.js b/lightning_import/lightning_import/doctype/lightning_multi_import_target/lightning_multi_import_target.js new file mode 100644 index 0000000..57557bb --- /dev/null +++ b/lightning_import/lightning_import/doctype/lightning_multi_import_target/lightning_multi_import_target.js @@ -0,0 +1,6 @@ +// Copyright (c) 2026, Tridz Technologies Pvt Ltd and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Lightning Multi Import Target", { + // custom logic for child table if needed +}); diff --git a/lightning_import/lightning_import/doctype/lightning_multi_import_target/lightning_multi_import_target.json b/lightning_import/lightning_import/doctype/lightning_multi_import_target/lightning_multi_import_target.json new file mode 100644 index 0000000..e5c1c04 --- /dev/null +++ b/lightning_import/lightning_import/doctype/lightning_multi_import_target/lightning_multi_import_target.json @@ -0,0 +1,114 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-05-20 05:00:00", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "enabled", + "execution_order", + "target_doctype", + "import_type", + "update_on_field", + "duplicate_check_field", + "status", + "total_records", + "successful_records", + "failed_records", + "last_processed_row", + "field_mapping" + ], + "fields": [ + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "execution_order", + "fieldtype": "Int", + "label": "Execution Order" + }, + { + "fieldname": "target_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Target DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "import_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Import Type", + "options": "Insert New Records\nUpdate Existing Records\nInsert and Update Records", + "reqd": 1 + }, + { + "fieldname": "update_on_field", + "fieldtype": "Select", + "label": "Validate On CSV Column" + }, + { + "fieldname": "duplicate_check_field", + "fieldtype": "Select", + "label": "Check Duplicates in Column" + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "options": "Draft\nQueued\nIn Progress\nCompleted\nFailed\nPartial Success", + "read_only": 1 + }, + { + "fieldname": "total_records", + "fieldtype": "Int", + "label": "Total Records", + "read_only": 1 + }, + { + "fieldname": "successful_records", + "fieldtype": "Int", + "label": "Successful Records", + "read_only": 1 + }, + { + "fieldname": "failed_records", + "fieldtype": "Int", + "label": "Failed Records", + "read_only": 1 + }, + { + "fieldname": "last_processed_row", + "fieldtype": "Int", + "label": "Last Processed Row", + "read_only": 1 + }, + { + "fieldname": "field_mapping", + "fieldtype": "Code", + "hidden": 1, + "label": "Field Mapping", + "options": "JSON", + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2026-05-22 18:13:11.967628", + "modified_by": "Administrator", + "module": "Lightning Import", + "name": "Lightning Multi Import Target", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "execution_order", + "sort_order": "ASC", + "states": [] +} \ No newline at end of file diff --git a/lightning_import/lightning_import/doctype/lightning_multi_import_target/lightning_multi_import_target.py b/lightning_import/lightning_import/doctype/lightning_multi_import_target/lightning_multi_import_target.py new file mode 100644 index 0000000..00cb67f --- /dev/null +++ b/lightning_import/lightning_import/doctype/lightning_multi_import_target/lightning_multi_import_target.py @@ -0,0 +1,8 @@ +# Copyright (c) 2026, Tridz Technologies Pvt Ltd and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + +class LightningMultiImportTarget(Document): + pass diff --git a/lightning_import/lightning_import/doctype/lightning_upload/lightning_upload.js b/lightning_import/lightning_import/doctype/lightning_upload/lightning_upload.js index f3ba7c0..011a2b0 100644 --- a/lightning_import/lightning_import/doctype/lightning_upload/lightning_upload.js +++ b/lightning_import/lightning_import/doctype/lightning_upload/lightning_upload.js @@ -1,4 +1,5 @@ // Copyright (c) 2025, Tridz Technologies Pvt Ltd and contributors +console.log("lightning_upload.js loaded"); // For license information, please see license.txt // Store progress state globally @@ -19,294 +20,418 @@ frappe.realtime.on('socket_disconnected', () => { console.log('[Lightning Import] Socket disconnected'); }); -frappe.ui.form.on('Lightning Upload', { - refresh: function(frm) { - // Store reference to current form - frappe.progress_state.current_form = frm; - - // Show Start Import button only when status is Draft and document is saved - if (!frm.is_new() && frm.doc.status === "Draft") { - frm.page.set_primary_action(__('Start Import'), () => { - start_import(frm); - }); - // Add Map Fields button if CSV file is attached - if (frm.doc.csv_file) { - frm.add_custom_button(__('Map Fields'), () => { - open_field_mapping_dialog(frm); - }); - } +async function ensure_doc_saved(frm) { + if (frm.is_new() || frm.is_dirty() || frm.doc.__unsaved) { + await frm.save(); + } +} + +function setup_buttons(frm) { + if (!frm.page) return; + + frm.page.clear_primary_action(); + frm.clear_custom_buttons(); + + if (frm.doc.status === "Draft") { + if (frm.is_new()) { + // Before Drafting / Before Save: Show only Save button + frm.page.set_primary_action(__('Save'), () => frm.save()); } else { - // Remove the primary action button if not in Draft status - frm.page.clear_primary_action(); + // After Drafting / After Save: Keep existing behavior + if (frm.doc.single_import) { + // Keep standard Save action as primary if form is dirty, otherwise set Start Import as primary + if (frm.is_dirty() || frm.doc.__unsaved) { + frm.page.set_primary_action(__('Save'), () => frm.save()); + frm.add_custom_button(__('Start Import'), async () => { + await ensure_doc_saved(frm); + start_import(frm); + }); + } else { + frm.page.set_primary_action(__('Start Import'), async () => { + await ensure_doc_saved(frm); + start_import(frm); + }); + } + if (frm.doc.csv_file) { + frm.add_custom_button(__('Map Fields'), async () => { + open_field_mapping_dialog(frm); + }); + } + } else if (frm.doc.multiple_import) { + // Keep standard Save action as primary if form is dirty, otherwise set Start Multi Import as primary + if (frm.is_dirty() || frm.doc.__unsaved) { + frm.page.set_primary_action(__('Save'), () => frm.save()); + frm.add_custom_button(__('Start Multi Import'), async () => { + await ensure_doc_saved(frm); + start_multi_import(frm); + }); + } else { + frm.page.set_primary_action(__('Start Multi Import'), async () => { + await ensure_doc_saved(frm); + start_multi_import(frm); + }); + } + if (frm.doc.csv_file) { + frm.add_custom_button(__('Map Fields'), async () => { + frappe.call({ + method: 'lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.auto_map_multi_import', + args: { docname: frm.doc.name }, + callback: function (r) { + if (r.message && r.message.status === 'success') { + frm.reload_doc().then(() => { + open_combined_multi_mapping_dialog(frm); + }); + } else { + frappe.msgprint(__('Error during auto-mapping. Please map fields manually.')); + } + } + }); + }); + } + } } + } - // Show Export Error Rows button only for Failed or Partial Success - if (frm.doc.status === 'Failed' || frm.doc.status === 'Partial Success') { - frm.add_custom_button(__('Export Error Rows'), () => { - export_error_rows(frm); - }); - } + // Show Export Error Rows button only for Failed or Partial Success + if (frm.doc.status === 'Failed' || frm.doc.status === 'Partial Success') { + frm.add_custom_button(__('Export Error Rows'), () => { + export_error_rows(frm); + }); + } +} - // Set up progress tracking if import is in progress - if (frm.doc.status === 'Queued' || frm.doc.status === 'In Progress') { - setup_progress_tracking(frm); +// Centralized UI state handler that handles all visibilities, field options, and buttons +function update_import_mode_ui(frm) { + if (!frm || !frm.doc) return; + + // 1. Toggle field visibilities and requirements + if (frm.doc.single_import) { + frm.set_df_property('import_doctype', 'reqd', 1); + frm.set_df_property('import_doctype', 'hidden', 0); + frm.set_df_property('import_type', 'reqd', 1); + frm.set_df_property('import_type', 'hidden', 0); + frm.set_df_property('update_on_field', 'hidden', 0); + frm.set_df_property('field_mapping', 'hidden', 0); + + frm.set_df_property('multi_import_targets', 'reqd', 0); + frm.set_df_property('multi_import_targets', 'hidden', 1); + } else if (frm.doc.multiple_import) { + frm.set_df_property('import_doctype', 'reqd', 0); + frm.set_df_property('import_doctype', 'hidden', 1); + frm.set_df_property('import_type', 'reqd', 0); + frm.set_df_property('import_type', 'hidden', 1); + frm.set_df_property('update_on_field', 'hidden', 1); + frm.set_df_property('duplicate_check_field', 'hidden', 1); + frm.set_df_property('field_mapping', 'hidden', 1); + + frm.set_df_property('multi_import_targets', 'reqd', 1); + frm.set_df_property('multi_import_targets', 'hidden', 0); + } + + frappe.db.get_single_value('Lightning Upload Settings', 'enable_file_duplicate_check').then(enabled => { + if (frm.doc.single_import) { + frm.set_df_property('duplicate_check_field', 'hidden', enabled ? 0 : 1); + } else if (frm.doc.multiple_import) { + if (frm.fields_dict.multi_import_targets && frm.fields_dict.multi_import_targets.grid) { + frm.fields_dict.multi_import_targets.grid.update_docfield_property( + 'duplicate_check_field', + 'hidden', + enabled ? 0 : 1 + ); + } } + }); - // Also trigger the import_type logic on refresh - // to populate the dropdown if the form loads in the right state - if (frm.doc.import_type === 'Insert and Update Records' && frm.doc.csv_file) { + // 2. Populate CSV column dropdowns dynamically + if (frm.doc.csv_file) { + if (frm.doc.single_import) { frm.events.populate_update_on_field(frm); + frm.events.populate_duplicate_check_field(frm); + } else if (frm.doc.multiple_import) { + frm.events.populate_multi_import_selects(frm); } + } - // Manage Duplicate Check field visibility and options - frappe.db.get_single_value('Lightning Upload Settings', 'enable_file_duplicate_check').then(enabled => { - if (enabled) { - frm.set_df_property('duplicate_check_field', 'hidden', 0); - if (frm.doc.csv_file) { - frm.events.populate_duplicate_check_field(frm); - } - } else { - frm.set_df_property('duplicate_check_field', 'hidden', 1); - } - }); + // 3. Stably debounce rendering of buttons to ensure they do not get cleared by framework/workflow + if (frm._button_timeout) { + clearTimeout(frm._button_timeout); + } + frm._button_timeout = setTimeout(() => { + setup_buttons(frm); + }, 150); +} + +function apply_multi_import_grid_properties(frm, options=[]) { + if (!frm.fields_dict.multi_import_targets || !frm.fields_dict.multi_import_targets.grid) return; + const grid = frm.fields_dict.multi_import_targets.grid; + + grid.update_docfield_property( + 'update_on_field', + 'hidden', + 0 + ); + + grid.update_docfield_property( + 'update_on_field', + 'options', + options?.length ? options.join('\n') : [] + ); + + grid.update_docfield_property( + 'duplicate_check_field', + 'options', + options?.length ? options.join('\n') : [] + ); + + frappe.db.get_single_value('Lightning Upload Settings', 'enable_file_duplicate_check').then(enabled => { + grid.update_docfield_property( + 'duplicate_check_field', + 'hidden', + enabled ? 0 : 1 + ); + grid.refresh(); + }); +} + + + +frappe.ui.form.on('Lightning Upload', { + refresh: function (frm) { + frappe.progress_state.current_form = frm; + + // Perform centralized UI updates + update_import_mode_ui(frm); + // Set up progress tracking if import is in progress + if (frm.doc.status === 'Queued' || frm.doc.status === 'In Progress') { + setup_progress_tracking(frm); + } }, - onload: function(frm) { - // Store reference to current form + onload: function (frm) { frappe.progress_state.current_form = frm; - - // Set up progress tracking when form loads if import is in progress + if (frm.doc.status === 'Queued' || frm.doc.status === 'In Progress') { setup_progress_tracking(frm); } + + update_import_mode_ui(frm); }, - csv_file: function(frm) { - if (frm.doc.import_type === 'Insert and Update Records') { - if (frm.doc.csv_file) { - frm.events.populate_update_on_field(frm); - } + csv_file: function (frm) { + update_import_mode_ui(frm); + }, + + import_type: function (frm) { + update_import_mode_ui(frm); + }, + + import_doctype: function (frm) { + update_import_mode_ui(frm); + }, + + single_import: function (frm) { + if (frm.doc.single_import) { + frm.set_value('multiple_import', 0); + update_import_mode_ui(frm); + } else if (!frm.doc.multiple_import) { + frm.set_value('single_import', 1); + frappe.msgprint(__('Please select either Single Import or Multiple Import.')); } - - frappe.db.get_single_value('Lightning Upload Settings', 'enable_file_duplicate_check').then(enabled => { - if (enabled && frm.doc.csv_file) { - frm.events.populate_duplicate_check_field(frm); - } - }); }, - import_type: function(frm) { - if (frm.doc.import_type === 'Insert and Update Records') { - if (frm.doc.csv_file) { - frm.events.populate_update_on_field(frm); - } else { - frappe.msgprint(__('Please attach a CSV file first to select the update column.')); - } + multiple_import: function (frm) { + if (frm.doc.multiple_import) { + frm.set_value('single_import', 0); + update_import_mode_ui(frm); + } else if (!frm.doc.single_import) { + frm.set_value('multiple_import', 1); + frappe.msgprint(__('Please select either Single Import or Multiple Import.')); } }, - populate_duplicate_check_field: function(frm) { + populate_duplicate_check_field: function (frm) { + if (!frm.doc.csv_file) { + frm.set_df_property('duplicate_check_field', 'options', []); + frm.refresh_field('duplicate_check_field'); + return; + } + frappe.call({ method: 'lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.get_csv_headers_for_upload', args: { file_url: frm.doc.csv_file }, - callback: function(r) { + callback: function (r) { if (r.message && r.message.status === 'success') { const headers = r.message.headers; - const options = [''].concat(headers); - frm.set_df_property('duplicate_check_field', 'options', options); + if (headers?.length) { + frm.set_df_property('duplicate_check_field', 'options', headers.join('\n')); + } else { + frm.set_df_property('duplicate_check_field', 'options', []); + } frm.refresh_field('duplicate_check_field'); } } }); }, - populate_update_on_field: function(frm) { - // Fetch CSV headers from the backend + populate_update_on_field: function (frm) { + if (!frm.doc.csv_file) { + frm.set_df_property('update_on_field', 'options', []); + frm.refresh_field('update_on_field'); + return; + } + frappe.call({ method: 'lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.get_csv_headers_for_upload', args: { file_url: frm.doc.csv_file }, - callback: function(r) { + callback: function (r) { if (r.message && r.message.status === 'success') { const headers = r.message.headers; - // Prepend a blank option - const options = [''].concat(headers); - // Set the options for the dropdown and refresh the field - frm.set_df_property('update_on_field', 'options', options); + if (headers?.length) { + frm.set_df_property('update_on_field', 'options', headers.join('\n')); + } else { + frm.set_df_property('update_on_field', 'options', []); + } frm.refresh_field('update_on_field'); } } }); + }, + + populate_multi_import_selects: function (frm) { + if (!frm.doc.csv_file) return; + + frappe.call({ + method: 'lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.get_csv_headers_for_upload', + args: { file_url: frm.doc.csv_file }, + callback: function (r) { + if (r.message && r.message.status === 'success') { + const headers = r.message.headers; + apply_multi_import_grid_properties(frm, headers); + } else { + apply_multi_import_grid_properties(frm, []); + } + } + }); + } +}); + +// Grid row trigger mapping +frappe.ui.form.on('Lightning Multi Import Target', { + multi_import_targets_add: function (frm, cdt, cdn) { + if (frm.doc.csv_file) { + frm.events.populate_multi_import_selects(frm); + } } }); // Global event handler for import progress -frappe.realtime.on('import_progress', function(data) { +frappe.realtime.on('import_progress', function (data) { console.log('[Lightning Import] Received import_progress event:', data); - + const frm = frappe.progress_state.current_form; - if (!frm) { - console.log('[Lightning Import] No form found in progress_state'); - return; - } + if (!frm) return; - // If we have a progress key in the event, verify it matches if (data.progress_key) { const formProgressKey = `lightning_import_${frm.doc.name}`; - console.log('[Lightning Import] Progress key check:', { - received: data.progress_key, - expected: formProgressKey, - matches: data.progress_key === formProgressKey - }); - if (data.progress_key !== formProgressKey) { - console.log('[Lightning Import] Progress key mismatch, ignoring event'); - return; - } + if (data.progress_key !== formProgressKey) return; } - console.log('[Lightning Import] Updating progress with data:', data); update_progress(frm, data); }); function setup_progress_tracking(frm) { - console.log('[Lightning Import] Setting up progress tracking for form:', frm.doc.name); - - // Clear any existing progress bar if (frm.progress_bar) { - console.log('[Lightning Import] Removing existing progress bar'); frm.progress_bar.remove(); } - // Create progress bar container - console.log('[Lightning Import] Creating new progress bar'); frm.progress_bar = $(`
-
-
+
`).insertAfter(frm.page.main); } function update_progress(frm, data) { - console.log('[Lightning Import] update_progress called with:', { - form: frm.doc.name, - data: data, - hasProgressBar: !!frm.progress_bar - }); - - if (!data || !frm.progress_bar) { - console.log('[Lightning Import] Missing data or progress bar, skipping update'); - return; - } - - // Update progress bar - console.log('[Lightning Import] Updating progress bar to:', data.progress + '%'); + if (!data || !frm.progress_bar) return; + frm.progress_bar.find('.progress-bar') .css('width', `${data.progress}%`) .attr('aria-valuenow', data.progress); - frm.progress_bar.find('.progress-status').html(data.title); - // Update status in form without marking as dirty + let titleHtml = data.title; + if (data.multiple_import && data.current_target_doctype) { + titleHtml = `Overall Progress: ${data.progress}%
` + + `Importing Target DocType: ${data.current_target_doctype} (${data.current_target_index}/${data.total_targets})
` + + `Processed: ${data.target_successful_records} Succeeded, ${data.target_failed_records} Failed (Total: ${data.target_total_records})`; + } + frm.progress_bar.find('.progress-status').html(titleHtml); + if (data.status) { - console.log('[Lightning Import] Updating status to:', data.status); frm.doc.status = data.status; frm.refresh_field('status'); - // pill is updating only after update of workflow_state and header frm.refresh_field('workflow_state'); frm.refresh_header(); } - // Update other fields if available if (data.successful_records !== undefined) { - console.log('[Lightning Import] Updating successful_records to:', data.successful_records); frm.doc.successful_records = data.successful_records; frm.refresh_field('successful_records'); } if (data.failed_records !== undefined) { - console.log('[Lightning Import] Updating failed_records to:', data.failed_records); frm.doc.failed_records = data.failed_records; frm.refresh_field('failed_records'); } if (data.import_time) { - console.log('[Lightning Import] Updating import_time to:', data.import_time); frm.doc.import_time = data.import_time; frm.refresh_field('import_time'); } if (data.total_records !== undefined) { - console.log('[Lightning Import] Updating total_records to:', data.total_records); frm.doc.total_records = data.total_records; frm.refresh_field('total_records'); } - // Handle completion + // Refresh child table rows state in real-time + if (data.multiple_import && frm.fields_dict['multi_import_targets']) { + frm.reload_doc(); + } + if (data.status === 'Completed' || data.status === 'Failed' || data.status === 'Partial Success') { - console.log('[Lightning Import] Import completed with status:', data.status); let message = ''; if (data.status === 'Completed') { message = __(`Successfully imported ${data.successful_records} records`); - if (data.time_taken) { - message += __(`, time taken: ${data.time_taken}`); - } + if (data.time_taken) message += __(`, time taken: ${data.time_taken}`); } else if (data.status === 'Partial Success') { message = __(`Import partially completed. ${data.successful_records} records imported, ${data.failed_records} failed`); - if (data.time_taken) { - message += __(`, time taken: ${data.time_taken}`); - } + if (data.time_taken) message += __(`, time taken: ${data.time_taken}`); } else { message = __('Import failed'); - if (data.error) { - message += `: ${data.error}`; - } + if (data.error) message += `: ${data.error}`; } - console.log('[Lightning Import] Showing completion alert:', message); frappe.show_alert({ message: message, indicator: data.status === 'Completed' ? 'green' : (data.status === 'Partial Success' ? 'orange' : 'red'), timeout: 10 }); - // Remove progress bar if (frm.progress_bar) { - console.log('[Lightning Import] Removing progress bar after completion'); frm.progress_bar.remove(); frm.progress_bar = null; } - - // Update buttons based on status - if (data.status === 'Failed' || data.status === 'Partial Success') { - // Check if the Export Error Rows button already exists - const hasExportButton = frm.page.custom_buttons && - frm.page.custom_buttons.some(btn => btn.label === __('Export Error Rows')); - - if (!hasExportButton) { - frm.add_custom_button(__('Export Error Rows'), () => { - export_error_rows(frm); - }); - } - } - - // Refresh primary action button - if (data.status === 'Draft') { - frm.page.set_primary_action(__('Start Import'), () => { - start_import(frm); - }); - } else { - frm.page.clear_primary_action(); - } - // Force a full form refresh after completion setTimeout(() => { frm.reload_doc(); }, 1000); } } +// Start Single Import function start_import(frm) { - // Helper function to make the final call to start the import const call_start_import_py = (mapping_json = null) => { if (!frm.progress_bar) { frm.progress_bar = $(` @@ -325,7 +450,7 @@ function start_import(frm) { docname: frm.doc.name, mapping: mapping_json }, - callback: function(r) { + callback: function (r) { if (r.message && r.message.status === 'success') { frappe.show_alert({ message: r.message.message, @@ -345,7 +470,6 @@ function start_import(frm) { }); }; - // Show duplicate warning dialog then proceed or cancel const check_and_proceed = (mapping_json = null) => { frappe.call({ method: 'lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.check_file_duplicates', @@ -355,22 +479,18 @@ function start_import(frm) { }, freeze: true, freeze_message: __('Checking for duplicates in file...'), - callback: function(r) { + callback: function (r) { if (!r.message || r.message.status === 'error') { - // If duplicate check itself fails, still allow continuing - console.warn('[Lightning Import] Duplicate check failed:', r.message && r.message.message); call_start_import_py(mapping_json); return; } const result = r.message; if (!result.has_duplicates) { - // No duplicates — proceed directly call_start_import_py(mapping_json); return; } - // Build an HTML summary table for duplicates let html = `
@@ -398,7 +518,6 @@ function start_import(frm) { `; col.duplicate_values.forEach(entry => { - // Show at most 20 row numbers to keep the dialog compact const rowDisplay = entry.rows.length > 20 ? entry.rows.slice(0, 20).join(', ') + `... (+${entry.rows.length - 20} more)` : entry.rows.join(', '); @@ -431,22 +550,18 @@ function start_import(frm) { } }); d.show(); - // Make the dialog wider for readability d.$wrapper.find('.modal-dialog').css('max-width', '750px'); } }); }; - // Main logic starts here if (frm.doc.field_mapping) { - // If a mapping is already saved, run duplicate check then proceed. check_and_proceed(); } else { - // If no mapping exists, perform auto-mapping and validation first. frappe.call({ method: 'lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.auto_map_and_validate', args: { docname: frm.doc.name }, - callback: function(r) { + callback: function (r) { if (!r.message) { frappe.msgprint(__('Error during auto-mapping. Please map fields manually.')); return; @@ -459,15 +574,12 @@ function start_import(frm) { }; if (data.unmapped_required.length > 0) { - // If required fields are missing, ask the user what to do first. frappe.confirm( __('The following required fields could not be auto-mapped:
{0}.

Rows without these fields will fail to import. Do you want to continue anyway?', [data.unmapped_required.join(', ')]), () => { - // User chose to "Continue Anyway" — still run dup check proceed_with_dup_check(); }, () => { - // User chose to "Cancel and Map Fields" open_field_mapping_dialog(frm); }, __('Missing Required Fields'), @@ -475,7 +587,6 @@ function start_import(frm) { __('Cancel and Map Fields') ); } else { - // All required fields mapped — run dup check before import frappe.show_alert({ message: __('All required fields were auto-mapped. Checking for duplicates...'), indicator: 'green' @@ -487,13 +598,126 @@ function start_import(frm) { } } +// Start Multiple Import +function start_multi_import(frm) { + const call_start_multi_import_py = () => { + setup_progress_tracking(frm); + + frappe.call({ + method: 'lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.start_multi_import', + args: { + docname: frm.doc.name + }, + callback: function (r) { + if (r.message && r.message.status === 'success') { + frappe.show_alert({ + message: r.message.message, + indicator: 'green' + }); + } else { + if (frm.progress_bar) { + frm.progress_bar.remove(); + frm.progress_bar = null; + } + frappe.show_alert({ + message: r.message.message || __('Failed to start Multiple Import'), + indicator: 'red' + }); + } + } + }); + }; + + // Check duplicates across all targets + frappe.call({ + method: 'lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.check_multi_file_duplicates', + args: { + docname: frm.doc.name + }, + freeze: true, + freeze_message: __('Checking for duplicates across target DocTypes...'), + callback: function (r) { + if (!r.message || r.message.status === 'error') { + call_start_multi_import_py(); + return; + } + + const result = r.message; + if (!result.has_duplicates) { + call_start_multi_import_py(); + return; + } + + // Build duplicate warning layout + let html = ` +
+ + ⚠ Duplicate values detected in the CSV file for one or more targets. + +
+ `; + + result.targets.forEach(target => { + html += ` +
+
+ DocType: ${frappe.utils.escape_html(target.target_doctype)} | + Column: ${frappe.utils.escape_html(target.csv_column)} + (maps to: ${frappe.utils.escape_html(target.field)}) +
+ + + + + + + + `; + target.duplicate_values.forEach(entry => { + const rowDisplay = entry.rows.length > 20 + ? entry.rows.slice(0, 20).join(', ') + `... (+${entry.rows.length - 20} more)` + : entry.rows.join(', '); + html += ` + + + + + + `; + }); + html += `
Duplicate ValueOccurrencesRow Numbers
${frappe.utils.escape_html(String(entry.value))}${entry.count}${rowDisplay}
`; + }); + + html += `
+ You can still continue with the import. Duplicate rows will be processed normally — no rows are skipped automatically. +
`; + + const d = new frappe.ui.Dialog({ + title: __('Duplicate Values Detected'), + fields: [{ fieldtype: 'HTML', fieldname: 'dup_summary', options: html }], + primary_action_label: __('Continue Import'), + primary_action() { + d.hide(); + call_start_multi_import_py(); + }, + secondary_action_label: __('Cancel'), + secondary_action() { + d.hide(); + } + }); + d.show(); + d.$wrapper.find('.modal-dialog').css('max-width', '750px'); + } + }); +} + function export_error_rows(frm) { frappe.call({ method: 'lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.export_error_rows', args: { docname: frm.doc.name }, - callback: function(r) { + callback: function (r) { if (r.message && r.message.status === 'success') { window.open(r.message.file_url, '_blank'); } else { @@ -506,139 +730,470 @@ function export_error_rows(frm) { }); } -// --- Field Mapping Dialog --- +// Single Field Mapping Dialog function open_field_mapping_dialog(frm) { frappe.call({ method: 'lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.get_csv_headers_for_upload', args: { file_url: frm.doc.csv_file }, - callback: function(csvRes) { + callback: function (csvRes) { if (!csvRes.message || csvRes.message.status !== 'success') { frappe.show_alert({ message: csvRes.message ? csvRes.message.message : __('Failed to fetch CSV headers'), indicator: 'red' }); return; } const csvHeaders = csvRes.message.headers; - frappe.call({ - method: 'lightning_import.lightning_import.api.get_fields.get_doctype_fields', - args: { doctype: frm.doc.import_doctype }, - callback: function (dtRes) { - if (!dtRes.message || !dtRes.message.fields) { - frappe.show_alert({ message: __('Failed to fetch DocType fields'), indicator: 'red' }); - return; + + frappe.model.with_doctype(frm.doc.import_doctype, async () => { + const meta = frappe.get_meta(frm.doc.import_doctype); + const requiredFields = meta.fields.filter(f => f.reqd).map(f => f.fieldname); + const fields = meta.fields.filter(f => !['Section Break', 'Column Break', 'Tab Break', 'Fold'].includes(f.fieldtype)).map(f => ({ + fieldname: f.fieldname, + label: f.label || f.fieldname, + reqd: f.reqd + })); + const system_fields = [ + { fieldname: 'name', label: 'ID' }, + { fieldname: 'owner', label: 'Owner' }, + { fieldname: 'creation', label: 'Created On' }, + { fieldname: 'modified', label: 'Last Modified' }, + { fieldname: 'modified_by', label: 'Modified By' } + ]; + const fieldOptions = fields.concat(system_fields); + + let existingMapping = null; + try { + if (frm.doc.field_mapping) { + existingMapping = JSON.parse(frm.doc.field_mapping); } - - const fieldOptions = dtRes.message.fields || []; - - const normalize = str => (typeof str === 'string' ? str.toLowerCase().replace(/[\s_]+/g, '') : ''); - - const normalizedFieldMap = {}; - fieldOptions.forEach(f => { - if (f.fieldname) { - normalizedFieldMap[normalize(f.fieldname)] = f.fieldname; - if (f.label) { - normalizedFieldMap[normalize(f.label)] = f.fieldname; - } - } + } catch (e) { + console.log('Error parsing existing mapping:', e); + } + + let backend_mapping = {}; + if (!existingMapping || Object.keys(existingMapping).length === 0) { + const auto_mapping_res = await frappe.xcall('lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.auto_map_and_validate', { docname: frm.doc.name }); + backend_mapping = auto_mapping_res ? auto_mapping_res.mapping : {}; + } + + const mapping = {}; + csvHeaders.forEach(header => { + if (existingMapping && existingMapping.hasOwnProperty(header)) { + mapping[header] = existingMapping[header]; + } else { + mapping[header] = backend_mapping[header] || ''; + } + }); + + let tableHtml = `
Map columns from ${frappe.utils.escape_html(frm.doc.csv_file.split('/').pop())} to fields in ${frappe.utils.escape_html(frm.doc.import_doctype)}
`; + tableHtml += ``; + + csvHeaders.forEach(header => { + tableHtml += ``; + tableHtml += ``; + }); + + tableHtml += `
CSV ColumnDocType Field
`; + + const d = new frappe.ui.Dialog({ + title: __('Map Columns'), + fields: [ + { fieldtype: 'HTML', fieldname: 'mapping_table', options: tableHtml } + ], + primary_action_label: __('Save Mapping'), + primary_action() { + const values = {}; + d.$wrapper.find('.field-mapping-select').each(function () { + const header = $(this).data('header'); + const value = $(this).val(); + values[header] = value; }); - - let tableHtml = `
Map columns from ${frappe.utils.escape_html(frm.doc.csv_file.split('/').pop())} to fields in ${frappe.utils.escape_html(frm.doc.import_doctype)}
`; - tableHtml += ``; - - csvHeaders.forEach(header => { - tableHtml += ``; - tableHtml += ``; - }); - - tableHtml += `
CSV ColumnDocType Field
`; - - const d = new frappe.ui.Dialog({ - title: __('Map Columns'), - fields: [ - { fieldtype: 'HTML', fieldname: 'mapping_table', options: tableHtml } - ], - primary_action_label: __('Save Mapping'), - primary_action() { - const values = {}; - d.$wrapper.find('.field-mapping-select').each(function () { - const header = $(this).data('header'); - const value = $(this).val(); - - values[header] = value; - }); - - const mappedFields = Object.values(values).filter(Boolean); - const unmappedRequired = requiredFields.filter(f => !mappedFields.includes(f)); - if (unmappedRequired.length) { - frappe.msgprint(__('Please map all required fields: {0}', [unmappedRequired.join(', ')])); - return; - } - - const duplicates = mappedFields.filter((item, idx) => mappedFields.indexOf(item) !== idx); - if (duplicates.length) { - frappe.msgprint(__('Duplicate mapping for: {0}', [duplicates.join(', ')])); - return; + } + + const duplicates = mappedFields.filter((item, idx) => mappedFields.indexOf(item) !== idx); + if (duplicates.length) { + frappe.msgprint(__('Duplicate mapping for: {0}', [duplicates.join(', ')])); + return; + } + + frappe.call({ + method: 'lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.save_field_mapping', + args: { + docname: frm.doc.name, + mapping: JSON.stringify(values) + }, + callback: function (res) { + if (res.message && res.message.status === 'success') { + d.hide(); + frm.reload_doc(); + frappe.show_alert({ message: __('Field mapping saved.'), indicator: 'green' }); + } else { + frappe.show_alert({ message: res.message?.message || __('Failed to save mapping'), indicator: 'red' }); } - - frappe.call({ - method: 'lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.save_field_mapping', - args: { - docname: frm.doc.name, - mapping: JSON.stringify(values) - }, - callback: function (res) { - if (res.message && res.message.status === 'success') { - d.hide(); - frm.reload_doc(); - frappe.show_alert({ message: __('Field mapping saved.'), indicator: 'green' }); - } else { - frappe.show_alert({ message: res.message?.message || __('Failed to save mapping'), indicator: 'red' }); - } - } - }); } }); - - d.show(); + } + }); + + d.show(); + d.$wrapper.find('.modal-dialog').css('max-width', '750px'); + + // Immediate persist on change + d.$wrapper.on('change', '.field-mapping-select', function () { + const values = {}; + d.$wrapper.find('.field-mapping-select').each(function () { + const header = $(this).data('header'); + const value = $(this).val(); + values[header] = value; + }); + frm.set_value('field_mapping', JSON.stringify(values)); + frm.dirty(); + }); + }); + } + }); +} + +// Combined Multi-Field Mapping Dialog +async function open_combined_multi_mapping_dialog(frm) { + const enabled_targets = frm.doc.multi_import_targets.filter(t => t.enabled && t.target_doctype); + if (!enabled_targets.length) { + frappe.msgprint(__('Please add, enable, and select at least one Target DocType first.')); + return; + } + if (!frm.doc.csv_file) { + frappe.msgprint(__('Please attach a CSV file first.')); + return; + } + + let csvRes; + try { + // 1. Fetch CSV headers safely + csvRes = await frappe.xcall('lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.get_csv_headers_for_upload', { + file_url: frm.doc.csv_file + }); + } catch (err) { + console.error('Error fetching CSV headers:', err); + frappe.msgprint(__('Could not retrieve CSV headers. Please ensure the attached file is valid and the document is saved.')); + return; + } + + if (!csvRes || csvRes.status !== 'success') { + frappe.show_alert({ message: csvRes ? csvRes.message : __('Failed to fetch CSV headers'), indicator: 'red' }); + return; + } + + const csvHeaders = csvRes.headers; + + // 2. Fetch metadata for all target DocTypes and auto-mappings safely + const doctypes_metadata = {}; + for (const target of enabled_targets) { + try { + // Load metadata + await new Promise((resolve, reject) => { + frappe.model.with_doctype(target.target_doctype, resolve, reject); + }); + + const meta = frappe.get_meta(target.target_doctype); + if (!meta) { + throw new Error(`Meta not found for ${target.target_doctype}`); + } + + const requiredFields = meta.fields.filter(f => f.reqd).map(f => f.fieldname); + const fields = meta.fields.filter(f => !['Section Break', 'Column Break', 'Tab Break', 'Fold'].includes(f.fieldtype)).map(f => ({ + fieldname: f.fieldname, + label: f.label || f.fieldname, + reqd: f.reqd + })); + const system_fields = [ + { fieldname: 'name', label: 'ID' }, + { fieldname: 'owner', label: 'Owner' }, + { fieldname: 'creation', label: 'Created On' }, + { fieldname: 'modified', label: 'Last Modified' }, + { fieldname: 'modified_by', label: 'Modified By' } + ]; + const fieldOptions = fields.concat(system_fields); + + // Fetch backend auto-mapping safely + let existingMapping = null; + try { + if (target.field_mapping) { + existingMapping = JSON.parse(target.field_mapping); + } + } catch (e) { + console.log('Error parsing existing mapping:', e); + } + + let backend_mapping = {}; + if (!existingMapping || Object.keys(existingMapping).length === 0) { + try { + const auto_mapping_res = await frappe.xcall('lightning_import.lightning_import.doctype.lightning_upload.lightning_upload.get_auto_mapping_for_doctype', { + docname: frm.doc.name, + doctype: target.target_doctype + }); + backend_mapping = auto_mapping_res ? auto_mapping_res.mapping : {}; + } catch (err) { + console.error(`Error getting auto mapping for ${target.target_doctype}:`, err); + } + } + + doctypes_metadata[target.target_doctype] = { + fields: fieldOptions, + required: requiredFields, + backend_mapping: backend_mapping, + existingMapping: existingMapping || {}, + targetName: target.name + }; + } catch (targetErr) { + console.error(`Error preparing target DocType ${target.target_doctype}:`, targetErr); + frappe.msgprint(__('Failed to prepare metadata for Target DocType "{0}". Please check its configuration.', [target.target_doctype])); + return; + } + } + + // 3. Build combined HTML table with C * T rows to allow multi-doctype mapping + let tableHtml = `
Combined Map Columns from ${frappe.utils.escape_html(frm.doc.csv_file.split('/').pop())} to targets
`; + tableHtml += `
`; + tableHtml += ``; + + csvHeaders.forEach(header => { + Object.keys(doctypes_metadata).forEach(doctype => { + const meta = doctypes_metadata[doctype]; + + // Determine initial Field for this specific target + let initialField = ''; + const uniqueKey = `${header}::${doctype}`; + if (meta.existingMapping.hasOwnProperty(uniqueKey)) { + initialField = meta.existingMapping[uniqueKey]; + } else if (meta.existingMapping.hasOwnProperty(header)) { + initialField = meta.existingMapping[header]; + } else if (meta.backend_mapping[uniqueKey]) { + initialField = meta.backend_mapping[uniqueKey]; + } else if (meta.backend_mapping[header]) { + initialField = meta.backend_mapping[header]; + } + + // Show all rows to allow user to manually map unmatched fields + // Removed the if (!initialField) return; check as per requirement. + + tableHtml += ``; + tableHtml += ``; + + // DocType Dropdown Selector + tableHtml += ``; + + // Target Field Dropdown Selector + tableHtml += ``; + }); + }); + + tableHtml += `
CSV ColumnDocTypeDocType Field
`; + + const d = new frappe.ui.Dialog({ + title: __('Combined Field Mapping'), + fields: [ + { fieldtype: 'HTML', fieldname: 'mapping_table', options: tableHtml } + ], + primary_action_label: __('Save Mapping'), + async primary_action() { + // Collect mapped values for each target row + const targetMappings = {}; + enabled_targets.forEach(t => { + targetMappings[t.name] = {}; + }); + + // Gather inputs from table + let hasHeaderDoctypeDuplicate = false; + d.$wrapper.find('.mapping-dialog-row').each(function () { + const headerDoctype = $(this).attr('data-header-doctype') || $(this).data('header-doctype'); + if (!headerDoctype) return; + + const parts = headerDoctype.split("::"); + const header = parts[0]; + const docType = $(this).find('.combined-doctype-select').val(); + const field = $(this).find('.combined-field-select').val(); + + if (docType) { + const meta = doctypes_metadata[docType]; + if (meta && targetMappings[meta.targetName]) { + const uniqueKey = `${header}::${docType}`; + if (targetMappings[meta.targetName][uniqueKey] && field) { + frappe.msgprint(__('Invalid Mapping: CSV Header "{0}" is mapped to multiple fields in DocType "{1}".', [header, docType])); + hasHeaderDoctypeDuplicate = true; + return false; // break jquery each loop + } + targetMappings[meta.targetName][uniqueKey] = field || ""; + } + } + }); + + if (hasHeaderDoctypeDuplicate) return; + + // Save values back to the child rows and check duplicates inside each target independently + let hasDuplicates = false; + for (const doctype of Object.keys(doctypes_metadata)) { + const meta = doctypes_metadata[doctype]; + const targetName = meta.targetName; + const mapping = targetMappings[targetName]; + const mappedFields = Object.values(mapping).filter(Boolean); + + // Duplicate check within a single doctype mappings + const duplicates = mappedFields.filter((item, idx) => mappedFields.indexOf(item) !== idx); + if (duplicates.length) { + frappe.msgprint(__('Duplicate mapping for {0} fields: {1}', [doctype, duplicates.join(', ')])); + hasDuplicates = true; + break; + } + + // Warn for missing required fields + const unmappedRequired = meta.required.filter(f => !mappedFields.includes(f)); + if (unmappedRequired.length) { + frappe.show_alert({ + message: __('Note: {0} is missing required fields: {1}', [doctype, unmappedRequired.join(', ')]), + indicator: 'orange' }); } + } + + if (hasDuplicates) return; + + // Commit mappings to the child rows in the form doc + enabled_targets.forEach(t => { + const mappingString = JSON.stringify(targetMappings[t.name]); + frappe.model.set_value('Lightning Multi Import Target', t.name, 'field_mapping', mappingString); + }); + + frm.refresh_field("multi_import_targets"); + frm.dirty(); + + console.log("Before save targetMappings:", targetMappings); + + // Save document to database + try { + await frm.save(); + d.hide(); + console.log("After save, multi_import_targets field_mappings:", frm.doc.multi_import_targets.map(t => ({ name: t.name, field_mapping: t.field_mapping }))); + frappe.show_alert({ message: __('All field mappings successfully saved in database.'), indicator: 'green' }); + } catch (err) { + console.error("Error saving document after mapping fields:", err); + frappe.show_alert({ message: __('Failed to save field mapping.'), indicator: 'red' }); + } + } + }); + + // Dynamic field list switching on DocType change + d.$wrapper.on('change', '.combined-doctype-select', function () { + const headerDoctype = $(this).data('header-doctype'); + if (!headerDoctype) return; + + const parts = headerDoctype.split("::"); + const header = parts[0]; + const selectedDocType = $(this).val(); + const row = $(this).closest('tr'); + + // Update dataset of row and child select elements + const newHeaderDoctype = `${header}::${selectedDocType}`; + row.attr('data-header-doctype', newHeaderDoctype); + row.data('header-doctype', newHeaderDoctype); + $(this).attr('data-header-doctype', newHeaderDoctype); + $(this).data('header-doctype', newHeaderDoctype); + + const fieldSelect = row.find('.combined-field-select'); + fieldSelect.attr('data-header-doctype', newHeaderDoctype); + fieldSelect.data('header-doctype', newHeaderDoctype); + + // Clear existing options + fieldSelect.empty(); + fieldSelect.append($('