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.
+
+