fix(file-format): auto-detect columns on sample upload, quiet alerts

Two changes that drop the manual step from the column-mapping setup:

- Auto-detect now runs implicitly when the user attaches a Sample Excel
  File. The Auto-detect Columns Button field is removed (Preview Sample
  stays — it answers a different question). Auto-detect only fires when
  the Column Mappings table is empty; a user-edited table is never
  overwritten silently.
- Replaced the modal "Auto-detect Complete" / "Sample File Error" /
  "Empty Header Row" msgprints with small bottom-right show_alert
  toasts. The result is one line — "Auto-mapped 9 of 11 columns" —
  with green/orange/red indicator instead of a full-screen dialog.

The synonym dictionary used by detection lives in code at
jey_erp/bank_integration/excel_parser.py (_HEADER_SYNONYMS).
This commit is contained in:
Ali 2026-05-18 12:50:31 +00:00
parent 54ac6288fd
commit 381aebdbdb
2 changed files with 56 additions and 93 deletions

View File

@ -124,7 +124,7 @@ frappe.ui.form.on('Bank Statement Importer', {
}, },
sample_file(frm) { sample_file(frm) {
BIFileFormat.applySampleHeaders(frm); BIFileFormat.onSampleFileChange(frm);
}, },
header_row(frm) { header_row(frm) {
@ -134,10 +134,6 @@ frappe.ui.form.on('Bank Statement Importer', {
preview_sample_btn(frm) { preview_sample_btn(frm) {
BIFileFormat.showPreview(frm); BIFileFormat.showPreview(frm);
}, },
auto_detect_btn(frm) {
BIFileFormat.autoDetect(frm);
},
}); });
// ─── File Format helpers ───────────────────────────────────────────────────── // ─── File Format helpers ─────────────────────────────────────────────────────
@ -172,11 +168,10 @@ const BIFileFormat = {
}, },
callback: (r) => { callback: (r) => {
if (!r.message || !r.message.success) { if (!r.message || !r.message.success) {
frappe.msgprint({ frappe.show_alert({
title: __('Sample File Error'),
indicator: 'red',
message: (r.message && r.message.message) || __('Could not read sample file.'), message: (r.message && r.message.message) || __('Could not read sample file.'),
}); indicator: 'red',
}, 5);
return; return;
} }
const headers = r.message.headers || []; const headers = r.message.headers || [];
@ -189,16 +184,21 @@ const BIFileFormat = {
message: __("No headers found in row {0}. Adjust 'Header Row'.", [frm.doc.header_row || 1]), message: __("No headers found in row {0}. Adjust 'Header Row'.", [frm.doc.header_row || 1]),
indicator: 'orange', indicator: 'orange',
}, 5); }, 5);
} else {
frappe.show_alert({
message: __('{0} column header(s) loaded.', [headers.length]),
indicator: 'green',
}, 3);
} }
}, },
}); });
}, },
// Called when the user attaches / replaces the Sample Excel File. Refresh
// the Excel Column dropdown and, if the table is still empty, run
// auto-detect silently. We never overwrite a user-filled table.
onSampleFileChange(frm) {
this.applySampleHeaders(frm, /*force*/ true);
const hasRows = (frm.doc.column_mappings || []).some(r => r.excel_column || r.standard_field);
if (!frm.doc.sample_file || hasRows) return;
this.autoDetect(frm, /*silent*/ true);
},
setColumnOptions(frm, headers) { setColumnOptions(frm, headers) {
const grid = frm.fields_dict.column_mappings && frm.fields_dict.column_mappings.grid; const grid = frm.fields_dict.column_mappings && frm.fields_dict.column_mappings.grid;
if (!grid) return; if (!grid) return;
@ -286,72 +286,48 @@ const BIFileFormat = {
}); });
}, },
autoDetect(frm) { autoDetect(frm, silent) {
if (!frm.doc.sample_file) { if (!frm.doc.sample_file) return;
frappe.msgprint(__('Upload a Sample Excel File first.')); frappe.call({
return; method: 'jey_erp.bank_integration.excel_parser.auto_detect_column_mappings',
} args: {
const existing = (frm.doc.column_mappings || []).filter(r => r.excel_column || r.standard_field); file_url: frm.doc.sample_file,
const proceed = () => { header_row: frm.doc.header_row || 1,
frappe.call({ },
method: 'jey_erp.bank_integration.excel_parser.auto_detect_column_mappings', callback: (r) => {
args: { if (!r.message || !r.message.success) {
file_url: frm.doc.sample_file, frappe.show_alert({
header_row: frm.doc.header_row || 1, message: (r.message && r.message.message) || __('Could not read sample file.'),
}, indicator: 'red',
callback: (r) => { }, 5);
if (!r.message || !r.message.success) { return;
frappe.msgprint({ }
title: __('Auto-detect Error'), const mappings = r.message.mappings || [];
indicator: 'red', if (!mappings.length) {
message: (r.message && r.message.message) || __('Could not read sample file.'), frappe.show_alert({
}); message: __("No headers found in row {0}. Adjust 'Header Row'.", [frm.doc.header_row || 1]),
return; indicator: 'orange',
} }, 5);
const mappings = r.message.mappings || []; return;
if (!mappings.length) { }
frappe.msgprint({ frm.clear_table('column_mappings');
title: __('No Headers Found'), mappings.forEach(m => {
indicator: 'orange', const row = frm.add_child('column_mappings');
message: __("No headers found in row {0}. Adjust 'Header Row'.", [frm.doc.header_row || 1]), row.excel_column = m.excel_column;
}); row.standard_field = m.standard_field || '';
return; });
} frm.refresh_field('column_mappings');
// Replace the table contents in-place. frm.dirty();
frm.clear_table('column_mappings');
mappings.forEach(m => {
const row = frm.add_child('column_mappings');
row.excel_column = m.excel_column;
row.standard_field = m.standard_field || '';
});
frm.refresh_field('column_mappings');
frm.dirty();
const matched = r.message.matched_count || 0; const matched = r.message.matched_count || 0;
const total = r.message.total_headers || mappings.length; const total = r.message.total_headers || mappings.length;
const unmatched = r.message.unmatched || []; const indicator = matched === total ? 'green' : (matched > 0 ? 'orange' : 'red');
let msg = __('Mapped {0} of {1} column(s).', [matched, total]); frappe.show_alert({
if (unmatched.length) { message: __('Auto-mapped {0} of {1} columns.', [matched, total]),
msg += '<br><br>' + __('Unmapped columns (set Standard Field manually if needed):') + indicator,
'<br><code>' + unmatched.map(frappe.utils.escape_html).join(', ') + '</code>'; }, silent ? 4 : 6);
} },
msg += '<br><br>' + __('Review the table, then <b>Save</b> the form to confirm.'); });
frappe.msgprint({
title: __('Auto-detect Complete'),
indicator: matched === total ? 'green' : (matched > 0 ? 'orange' : 'red'),
message: msg,
});
},
});
};
if (existing.length) {
frappe.confirm(
__('This will replace the {0} existing Column Mapping row(s). Continue?', [existing.length]),
proceed,
);
} else {
proceed();
}
}, },
}; };

View File

@ -25,8 +25,6 @@
"sample_file", "sample_file",
"ff_sample_actions_section", "ff_sample_actions_section",
"preview_sample_btn", "preview_sample_btn",
"ff_sample_actions_col",
"auto_detect_btn",
"ff_format_section", "ff_format_section",
"header_row", "header_row",
"amount_mode", "amount_mode",
@ -180,22 +178,11 @@
}, },
{ {
"depends_on": "eval:doc.sample_file", "depends_on": "eval:doc.sample_file",
"description": "Show the first 20 data rows from the sample file with mapping status per column.", "description": "Show the first 20 data rows from the sample file with mapping status per column. Column mappings are auto-detected when you attach a file — re-attach to re-run detection.",
"fieldname": "preview_sample_btn", "fieldname": "preview_sample_btn",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Preview Sample" "label": "Preview Sample"
}, },
{
"fieldname": "ff_sample_actions_col",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.sample_file",
"description": "Replace the Column Mappings table with a best-effort guess based on the sample file's headers.",
"fieldname": "auto_detect_btn",
"fieldtype": "Button",
"label": "Auto-detect Columns"
},
{ {
"fieldname": "ff_format_section", "fieldname": "ff_format_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",