`;
+
+ _bi_inject_wizard_styles();
+
+ // Mount at the top of the form body; re-render replaces any prior instance.
+ const $host = frm.$wrapper.find('.form-layout').first();
+ frm.$wrapper.find('.bi-wizard-form').remove();
+ const $w = $(html);
+ $host.prepend($w);
+
+ $w.find('.bi-wizard-bar').on('click', () => $w.toggleClass('open'));
+ $w.find('.bi-wizard-step-main').on('click', function () {
+ steps[$(this).data('bi-step')].action();
+ });
+ $w.find('.bi-wizard-skip').on('click', function (e) {
+ e.stopPropagation();
+ const key = $(this).data('bi-skip');
+ const set = _bi_get_skipped(frm);
+ const idx = set.indexOf(key);
+ if (idx === -1) set.push(key); else set.splice(idx, 1);
+ _bi_set_skipped(frm, set);
+ _bi_render_form_wizard(frm, /* keepOpen */ true);
+ });
+ },
+ });
+}
+
+// ─── Load Data from the attached File Format sample file ─────────────────────
+// The File Format sample file is normally a design-time sample (used only to
+// detect columns). But when it actually holds real statement rows, this lets the
+// user load its counterparties / purposes into the registries straight from the
+// File Format tab — no separate dialog, no re-attaching the file.
+
+function _bi_load_from_sample(frm) {
+ if (!frm.doc.sample_file) {
+ frappe.msgprint({
+ title: __('No File'),
+ indicator: 'orange',
+ message: __('Attach a Sample Excel File first.'),
+ });
+ return;
+ }
+ if (!frm.doc.column_mappings || !frm.doc.column_mappings.length) {
+ frappe.msgprint({
+ title: __('No Column Mappings'),
+ indicator: 'orange',
+ message: __('Map the Excel columns to standard fields first.'),
+ });
+ return;
+ }
+ frappe.confirm(
+ __('Load counterparties and purposes from the attached file into the registries? Use this only if the file contains real statement data, not just a column sample.'),
+ () => {
+ frappe.call({
+ method: 'jey_erp.bank_integration.import_api.load_registries_from_excel',
+ args: {
+ file_url: frm.doc.sample_file,
+ bank_integration: frm.doc.name,
+ load_customers: 1,
+ load_suppliers: 1,
+ load_purposes: 1,
+ },
+ freeze: true,
+ freeze_message: __('Loading data from file…'),
+ callback(r) {
+ if (!r.message || r.message.success === false) {
+ frappe.msgprint({
+ title: __('Error'),
+ indicator: 'red',
+ message: (r.message && r.message.message) || __('Unknown error'),
+ });
+ return;
+ }
+ _bi_show_load_result(r.message);
+ frm.reload_doc();
+ },
+ });
+ }
+ );
+}
+
+function _bi_show_load_result(res) {
+ const droppedDate = res.dropped_date || 0;
+ const droppedAmount = res.dropped_amount || 0;
+ const droppedDirection = res.dropped_direction || 0;
+ const unknownDirections = res.unknown_directions || [];
+ let msg = __('From {0} rows — new customers: {1}, suppliers: {2}, purposes: {3}',
+ [res.total_rows, res.new_customers, res.new_suppliers, res.new_purposes]);
+ if (droppedDate > 0) {
+ msg += '
' +
+ __("Note: {0} row(s) had unparseable dates and were skipped. Enable 'Use Custom Date Format' in the File Format tab if your bank uses an unusual date format.",
+ [droppedDate]) +
+ '';
+ }
+ if (droppedDirection > 0) {
+ const sample = unknownDirections.map(frappe.utils.escape_html).join(', ') || '?';
+ msg += '
' +
+ __('Also skipped {0} row(s) with unknown direction values: {1}. Add them to Debit/Credit Values in the File Format tab.',
+ [droppedDirection, sample]) +
+ '';
+ }
+ if (droppedAmount > 0) {
+ msg += ' ' +
+ __('Also skipped {0} row(s) with missing/zero amount.', [droppedAmount]) +
+ '';
+ }
+ frappe.msgprint({
+ title: __('Data Loaded'),
+ indicator: (droppedDate > 0 || droppedDirection > 0) ? 'orange' : 'green',
+ message: msg,
+ });
+}
diff --git a/jey_erp/jey_erp/doctype/bank_statement_importer/bank_statement_importer.json b/jey_erp/jey_erp/doctype/bank_integration_profile/bank_integration_profile.json
similarity index 94%
rename from jey_erp/jey_erp/doctype/bank_statement_importer/bank_statement_importer.json
rename to jey_erp/jey_erp/doctype/bank_integration_profile/bank_integration_profile.json
index 584d42f..abda6bc 100644
--- a/jey_erp/jey_erp/doctype/bank_statement_importer/bank_statement_importer.json
+++ b/jey_erp/jey_erp/doctype/bank_integration_profile/bank_integration_profile.json
@@ -24,6 +24,7 @@
"sample_file",
"ff_sample_actions_section",
"preview_sample_btn",
+ "load_data_from_file_btn",
"ff_format_section",
"header_row",
"amount_mode",
@@ -171,6 +172,13 @@
"fieldtype": "Button",
"label": "Preview Sample"
},
+ {
+ "depends_on": "eval:doc.sample_file",
+ "description": "Requires the Excel columns to be mapped to standard fields first. If this attached file contains REAL statement data (not just a sample used to detect columns), you can load its counterparties and purposes into the registries right now, ready for mapping. If it is only a design-time sample, you can ignore this and use the file just for detecting the columns.",
+ "fieldname": "load_data_from_file_btn",
+ "fieldtype": "Button",
+ "label": "Load Data from File"
+ },
{
"fieldname": "ff_format_section",
"fieldtype": "Section Break",
@@ -355,7 +363,7 @@
"modified": "2026-05-18 00:00:00.000000",
"modified_by": "Administrator",
"module": "Jey Erp",
- "name": "Bank Statement Importer",
+ "name": "Bank Integration Profile",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
diff --git a/jey_erp/jey_erp/doctype/bank_integration_profile/bank_integration_profile.py b/jey_erp/jey_erp/doctype/bank_integration_profile/bank_integration_profile.py
new file mode 100644
index 0000000..854162d
--- /dev/null
+++ b/jey_erp/jey_erp/doctype/bank_integration_profile/bank_integration_profile.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2026, JeyERP and contributors
+# For license information, please see license.txt
+
+import frappe
+from frappe.model.document import Document
+
+
+class BankIntegrationProfile(Document):
+ def on_update(self):
+ # Keep the registry records in sync with the mapping child tables, both ways:
+ # * a row with a linked ERP party -> registry record becomes Mapped
+ # (and inherits its group / territory / payment terms);
+ # * removing a row OR clearing its erp_customer / erp_supplier flips the
+ # corresponding registry record back to New.
+ self._sync_customer_statuses()
+ self._sync_supplier_statuses()
+
+ def _sync_customer_statuses(self):
+ # PROMOTE: every mapping row with a linked ERP customer becomes Mapped.
+ mapped_names = set()
+ for row in self.customer_mappings:
+ if not (row.bi_customer_name and row.erp_customer):
+ continue
+ mapped_names.add(row.bi_customer_name)
+ current = frappe.db.get_value(
+ "Bank Integration Customer", row.bi_customer_name,
+ ["status", "mapped_customer", "customer_group", "territory", "payment_terms"],
+ as_dict=True,
+ )
+ if not current:
+ continue
+ desired = {
+ "status": "Mapped",
+ "mapped_customer": row.erp_customer,
+ "customer_group": row.customer_group or None,
+ "territory": row.territory or None,
+ "payment_terms": row.payment_terms or None,
+ }
+ if any(current.get(k) != v for k, v in desired.items()):
+ frappe.db.set_value(
+ "Bank Integration Customer", row.bi_customer_name, desired, update_modified=False
+ )
+
+ # DEMOTE: Mapped records no longer present in the table revert to New.
+ orphaned = frappe.get_all(
+ "Bank Integration Customer",
+ filters={"status": "Mapped", "parent_bank_integration": self.name},
+ fields=["name"],
+ )
+ for c in orphaned:
+ if c.name not in mapped_names:
+ frappe.db.set_value("Bank Integration Customer", c.name, {
+ "status": "New",
+ "mapped_customer": None,
+ "customer_group": None,
+ "territory": None,
+ "payment_terms": None,
+ }, update_modified=False)
+
+ def _sync_supplier_statuses(self):
+ # PROMOTE: every mapping row with a linked ERP supplier becomes Mapped.
+ mapped_names = set()
+ for row in self.supplier_mappings:
+ if not (row.bi_supplier_name and row.erp_supplier):
+ continue
+ mapped_names.add(row.bi_supplier_name)
+ current = frappe.db.get_value(
+ "Bank Integration Supplier", row.bi_supplier_name,
+ ["status", "mapped_supplier", "supplier_group", "payment_terms"],
+ as_dict=True,
+ )
+ if not current:
+ continue
+ desired = {
+ "status": "Mapped",
+ "mapped_supplier": row.erp_supplier,
+ "supplier_group": row.supplier_group or None,
+ "payment_terms": row.payment_terms or None,
+ }
+ if any(current.get(k) != v for k, v in desired.items()):
+ frappe.db.set_value(
+ "Bank Integration Supplier", row.bi_supplier_name, desired, update_modified=False
+ )
+
+ # DEMOTE: Mapped records no longer present in the table revert to New.
+ orphaned = frappe.get_all(
+ "Bank Integration Supplier",
+ filters={"status": "Mapped", "parent_bank_integration": self.name},
+ fields=["name"],
+ )
+ for s in orphaned:
+ if s.name not in mapped_names:
+ frappe.db.set_value("Bank Integration Supplier", s.name, {
+ "status": "New",
+ "mapped_supplier": None,
+ "supplier_group": None,
+ "payment_terms": None,
+ }, update_modified=False)
diff --git a/jey_erp/jey_erp/doctype/bank_integration_purpose/bank_integration_purpose.json b/jey_erp/jey_erp/doctype/bank_integration_purpose/bank_integration_purpose.json
index a68fc81..026124f 100644
--- a/jey_erp/jey_erp/doctype/bank_integration_purpose/bank_integration_purpose.json
+++ b/jey_erp/jey_erp/doctype/bank_integration_purpose/bank_integration_purpose.json
@@ -23,8 +23,8 @@
"fieldname": "parent_bank_integration",
"fieldtype": "Link",
"in_list_view": 1,
- "label": "Bank Statement Importer",
- "options": "Bank Statement Importer",
+ "label": "Bank Integration Profile",
+ "options": "Bank Integration Profile",
"reqd": 1
},
{
diff --git a/jey_erp/jey_erp/doctype/bank_integration_supplier/bank_integration_supplier.json b/jey_erp/jey_erp/doctype/bank_integration_supplier/bank_integration_supplier.json
index 40f3144..fc837d4 100644
--- a/jey_erp/jey_erp/doctype/bank_integration_supplier/bank_integration_supplier.json
+++ b/jey_erp/jey_erp/doctype/bank_integration_supplier/bank_integration_supplier.json
@@ -45,8 +45,8 @@
"fieldname": "parent_bank_integration",
"fieldtype": "Link",
"in_list_view": 1,
- "label": "Bank Statement Importer",
- "options": "Bank Statement Importer",
+ "label": "Bank Integration Profile",
+ "options": "Bank Integration Profile",
"reqd": 1
},
{
diff --git a/jey_erp/jey_erp/doctype/bank_integration_supplier_mapping/bank_integration_supplier_mapping.json b/jey_erp/jey_erp/doctype/bank_integration_supplier_mapping/bank_integration_supplier_mapping.json
index 664165b..e23193c 100644
--- a/jey_erp/jey_erp/doctype/bank_integration_supplier_mapping/bank_integration_supplier_mapping.json
+++ b/jey_erp/jey_erp/doctype/bank_integration_supplier_mapping/bank_integration_supplier_mapping.json
@@ -23,10 +23,12 @@
"reqd": 1
},
{
+ "fetch_from": "bi_supplier_name.tax_id",
"fieldname": "tax_id",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Tax ID"
+ "label": "Tax ID",
+ "read_only": 1
},
{
"fieldname": "erp_supplier",
diff --git a/jey_erp/jey_erp/doctype/bank_statement_importer/bank_statement_importer.py b/jey_erp/jey_erp/doctype/bank_statement_importer/bank_statement_importer.py
deleted file mode 100644
index f6f2f30..0000000
--- a/jey_erp/jey_erp/doctype/bank_statement_importer/bank_statement_importer.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# Copyright (c) 2026, JeyERP and contributors
-# For license information, please see license.txt
-
-import frappe
-from frappe.model.document import Document
-
-
-class BankStatementImporter(Document):
- def on_update(self):
- # Keep the registry records in sync with the mapping child tables:
- # removing a row OR clearing its erp_customer / erp_supplier flips the
- # corresponding registry record back to status=New.
- self._sync_customer_statuses()
- self._sync_supplier_statuses()
-
- def _sync_customer_statuses(self):
- mapped_names = {
- row.bi_customer_name
- for row in self.customer_mappings
- if row.bi_customer_name and row.erp_customer
- }
- orphaned = frappe.get_all(
- "Bank Integration Customer",
- filters={"status": "Mapped", "parent_bank_integration": self.name},
- fields=["name"],
- )
- for c in orphaned:
- if c.name not in mapped_names:
- frappe.db.set_value("Bank Integration Customer", c.name, {
- "status": "New",
- "mapped_customer": None,
- "customer_group": None,
- "territory": None,
- "payment_terms": None,
- }, update_modified=False)
-
- def _sync_supplier_statuses(self):
- mapped_names = {
- row.bi_supplier_name
- for row in self.supplier_mappings
- if row.bi_supplier_name and row.erp_supplier
- }
- orphaned = frappe.get_all(
- "Bank Integration Supplier",
- filters={"status": "Mapped", "parent_bank_integration": self.name},
- fields=["name"],
- )
- for s in orphaned:
- if s.name not in mapped_names:
- frappe.db.set_value("Bank Integration Supplier", s.name, {
- "status": "New",
- "mapped_supplier": None,
- "supplier_group": None,
- "payment_terms": None,
- }, update_modified=False)
diff --git a/jey_erp/patches.txt b/jey_erp/patches.txt
index c27500a..e14350f 100644
--- a/jey_erp/patches.txt
+++ b/jey_erp/patches.txt
@@ -4,6 +4,7 @@
jey_erp.patches.fix_employee_row_format
jey_erp.patches.fix_employee_columns_as_text
jey_erp.patches.rename_bank_integration_to_importer
+jey_erp.patches.rename_importer_to_profile
[post_model_sync]
# Patches added in this section will be executed after doctypes are migrated
@@ -11,7 +12,7 @@ jey_erp.patches.create_asset_categories_post_migration
jey_erp.patches.remove_old_won_lost_fields
jey_erp.patches.remove_won_lost_breaks
jey_erp.patches.remove_won_lost_checkboxes
-jey_erp.patches.drop_bank_integration_account_mapping
+jey_erp.patches.drop_orphan_bank_doctypes
jey_erp.patches.backfill_si_tax_article_row_keys
jey_erp.patches.backfill_sales_invoice_tax_articles_history
jey_erp.patches.add_tax_articles_to_si_grid_views
\ No newline at end of file
diff --git a/jey_erp/patches/drop_bank_integration_account_mapping.py b/jey_erp/patches/drop_bank_integration_account_mapping.py
deleted file mode 100644
index c82c327..0000000
--- a/jey_erp/patches/drop_bank_integration_account_mapping.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""Drop the Bank Integration Account Mapping DocType.
-
-The child table was unused in the Bank Statement Importer flow: Bank Account
-is picked explicitly in the Import dialog, so there was no need for an
-IBAN→Bank Account lookup table. The kapital_bank app keeps its own
-equivalent because that flow pulls transactions through an API without a
-per-import Bank Account choice.
-
-Idempotent — runs only when the DocType is still around.
-"""
-
-import frappe
-
-
-def execute():
- dt = "Bank Integration Account Mapping"
- if not frappe.db.exists("DocType", dt):
- return
- # Drops both the metadata row and the underlying `tab` table.
- frappe.delete_doc("DocType", dt, ignore_permissions=True, force=True)
- frappe.db.commit()
- print(f"BI: dropped DocType '{dt}'")
diff --git a/jey_erp/patches/drop_orphan_bank_doctypes.py b/jey_erp/patches/drop_orphan_bank_doctypes.py
new file mode 100644
index 0000000..5199e6f
--- /dev/null
+++ b/jey_erp/patches/drop_orphan_bank_doctypes.py
@@ -0,0 +1,42 @@
+"""Drop orphaned Bank Integration DocTypes left over from earlier iterations.
+
+Two DocTypes are no longer part of the Bank Integration Profile flow and have
+no references anywhere in the codebase:
+
+ * Bank Integration Account Mapping — the IBAN→Bank Account lookup child table.
+ Bank Account is now picked explicitly in the Import dialog, so the table is
+ unused. (Its metadata was already removed on some sites, but the underlying
+ `tab` table can linger; the old `drop_bank_integration_account_mapping`
+ patch only called delete_doc, which is a no-op once the meta is gone.)
+
+ * Bank Integration Standard Field — a seed/config table holding the 12 standard
+ field labels (Date, Amount, Counterparty, …). These are now hard-coded in
+ `jey_erp.bank_integration.excel_parser.STANDARD_FIELD_TO_CODE`, so the table
+ is dead config data.
+
+This patch removes any leftover DocType metadata AND drops the lingering tables
+explicitly, so the database is fully cleaned regardless of how far the previous
+(incomplete) cleanup got on a given site. Idempotent.
+"""
+
+import frappe
+
+ORPHANS = [
+ "Bank Integration Account Mapping",
+ "Bank Integration Standard Field",
+]
+
+
+def execute():
+ for dt in ORPHANS:
+ # Remove DocType metadata if it is still registered.
+ if frappe.db.exists("DocType", dt):
+ frappe.delete_doc("DocType", dt, ignore_permissions=True, force=True)
+
+ # Drop the underlying table if it survived the meta removal.
+ table = f"tab{dt}"
+ if frappe.db.sql("SHOW TABLES LIKE %s", (table,)):
+ frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `{table}`")
+ print(f"BI cleanup: dropped table `{table}`")
+
+ frappe.db.commit()
diff --git a/jey_erp/patches/rename_importer_to_profile.py b/jey_erp/patches/rename_importer_to_profile.py
new file mode 100644
index 0000000..f7189e6
--- /dev/null
+++ b/jey_erp/patches/rename_importer_to_profile.py
@@ -0,0 +1,56 @@
+"""Rename the Bank Statement Importer DocType to Bank Integration Profile.
+
+"Bank Statement Importer" collides with ERPNext's stock "Bank Statement Import"
+DocType, which is confusing in the UI and in searches. "Bank Integration Profile"
+also brings the main record name in line with the rest of the family (all the
+registry / mapping doctypes are already "Bank Integration *").
+
+Runs in [pre_model_sync], AFTER `rename_bank_integration_to_importer`, so the
+chain is: Bank Integration -> Bank Statement Importer -> Bank Integration Profile.
+Idempotent: only runs while the old name still exists and the new one doesn't.
+"""
+
+import frappe
+
+
+def execute():
+ old_name = "Bank Statement Importer"
+ new_name = "Bank Integration Profile"
+
+ if frappe.db.exists("DocType", new_name):
+ # Already renamed (or freshly created under the new name).
+ return
+
+ if not frappe.db.exists("DocType", old_name):
+ # Fresh install / nothing to rename yet.
+ return
+
+ # frappe.rename_doc on a DocType:
+ # - renames the underlying `tab` table
+ # - updates Link/Dynamic Link references stored in DocField.options
+ # - updates `parenttype` rows in linked child tables
+ frappe.rename_doc("DocType", old_name, new_name, force=True, show_alert=False)
+
+ # Dynamic Link VALUES are user data, not metadata — rename_doc leaves them be.
+ # Re-point the Bank Account.bank_integration_type column.
+ frappe.db.sql(
+ """UPDATE `tabBank Account`
+ SET bank_integration_type = %s
+ WHERE bank_integration_type = %s""",
+ (new_name, old_name),
+ )
+
+ # Keep the Select options on the bank_integration_type Custom Field consistent
+ # within this same migrate run (custom_fields.py also rewrites it afterwards).
+ custom_field = frappe.db.exists("Custom Field", {
+ "dt": "Bank Account", "fieldname": "bank_integration_type"
+ })
+ if custom_field:
+ frappe.db.set_value(
+ "Custom Field", custom_field, "options",
+ "\n" + new_name + "\nKapital Bank Settings",
+ update_modified=False,
+ )
+
+ frappe.db.commit()
+ print(f"BI: renamed DocType '{old_name}' to '{new_name}'")
diff --git a/jey_erp/public/js/bank_excel_import.bundle.js b/jey_erp/public/js/bank_excel_import.bundle.js
new file mode 100644
index 0000000..1c2af6a
--- /dev/null
+++ b/jey_erp/public/js/bank_excel_import.bundle.js
@@ -0,0 +1,551 @@
+// Shared Excel bank-statement importer dialog.
+//
+// Loaded globally (app_include_js) so it can be launched from BOTH the Bank
+// Transaction list view AND the Bank Reconciliation Tool form.
+//
+// Usage:
+// BIExcelImport.showDialog({
+// bankIntegration: "", // optional pre-fill
+// bankAccount: "", // optional pre-fill
+// onComplete: function (res) { ... }, // optional; res = { imported, errors, transactions }
+// });
+//
+// `res.transactions` is the list of rows that were sent to import (each with a
+// `date`), so the caller can e.g. derive the imported statement's date range.
+
+const BIExcelImport = {
+ showDialog(opts) {
+ opts = opts || {};
+ frappe.db.get_list('Bank Integration Profile', {
+ fields: ['name', 'bank_name'],
+ limit: 100,
+ }).then((integrations) => {
+ if (!integrations || !integrations.length) {
+ frappe.msgprint({
+ title: __('No Bank Integration Profile'),
+ indicator: 'orange',
+ message: __('Create at least one Bank Integration Profile record first.'),
+ });
+ return;
+ }
+ BIExcelImport._showSetupDialog(integrations, opts);
+ });
+ },
+
+ _showSetupDialog(integrations, opts) {
+ const d = new frappe.ui.Dialog({
+ title: __('Load Bank Transactions from Excel'),
+ fields: [
+ {
+ fieldname: 'bank_integration',
+ fieldtype: 'Link',
+ label: __('Bank Integration Profile'),
+ options: 'Bank Integration Profile',
+ reqd: 1,
+ default: opts.bankIntegration || undefined,
+ description: __('Format and column mappings come from the chosen Bank Integration Profile.'),
+ },
+ {
+ fieldname: 'bank_account',
+ fieldtype: 'Link',
+ label: __('Bank Account'),
+ options: 'Bank Account',
+ reqd: 1,
+ default: opts.bankAccount || undefined,
+ description: __('The Bank Account these transactions will be assigned to.'),
+ },
+ {
+ fieldname: 'file_url',
+ fieldtype: 'Attach',
+ label: __('Excel File'),
+ reqd: 1,
+ },
+ ],
+ primary_action_label: __('Parse'),
+ primary_action(values) {
+ d.hide();
+ BIExcelImport._parseAndPreview(values, opts);
+ },
+ });
+ d.show();
+ },
+
+ _parseAndPreview(values, opts) {
+ frappe.show_alert({ message: __('Parsing file...'), indicator: 'blue' });
+ frappe.call({
+ method: 'jey_erp.bank_integration.import_api.parse_excel_for_preview',
+ args: {
+ file_url: values.file_url,
+ bank_integration: values.bank_integration,
+ bank_account: values.bank_account,
+ },
+ callback(r) {
+ if (!r.message || !r.message.success) {
+ frappe.msgprint({
+ title: __('Parse Error'),
+ indicator: 'red',
+ message: (r.message && r.message.message) || __('Unknown error'),
+ });
+ return;
+ }
+ const txns = r.message.transactions || [];
+ const skipped = r.message.skipped_duplicates || 0;
+ const droppedDate = r.message.dropped_date || 0;
+ const droppedAmount = r.message.dropped_amount || 0;
+ const droppedDirection = r.message.dropped_direction || 0;
+ const unknownDirections = r.message.unknown_directions || [];
+ const emptyRefCount = r.message.empty_ref_count || 0;
+
+ if (droppedDate > 0 && !txns.length) {
+ BIExcelImport._showDateParseHelp(droppedDate, values.bank_integration);
+ return;
+ }
+ if (droppedDirection > 0 && !txns.length) {
+ BIExcelImport._showDirectionHelp(droppedDirection, unknownDirections, values.bank_integration);
+ return;
+ }
+ if (droppedAmount > 0 && !txns.length) {
+ BIExcelImport._showAmountHelp(droppedAmount, values.bank_integration);
+ return;
+ }
+ if (!txns.length) {
+ frappe.msgprint({
+ title: __('Nothing to Import'),
+ indicator: 'blue',
+ message: skipped > 0
+ ? __('All {0} parsed rows are duplicates.', [skipped])
+ : __('No transactions found in the file. Check that the Header Row in the File Format tab points at the actual header line.'),
+ });
+ return;
+ }
+ BIExcelImport._showPreview(txns, skipped, values, opts,
+ droppedDate, droppedAmount, droppedDirection, unknownDirections, emptyRefCount);
+ },
+ });
+ },
+
+ _showPreview(txns, skipped, values, opts, droppedDate, droppedAmount, droppedDirection, unknownDirections, emptyRefCount) {
+ let table = '
' +
+ __('Dropped {0} row(s) — the date could not be parsed.', [droppedDate]) + ' ' +
+ __("Open the Bank Integration Profile's File Format tab, enable 'Use Custom Date Format', and set the exact format your bank uses.") +
+ '
' +
+ __('Dropped {0} row(s) — unknown direction value(s):', [droppedDirection]) +
+ ' ' + (sample || '?') + '. ' +
+ __('Open the File Format tab and add these to Debit Values or Credit Values.') +
+ '
';
+ }
+ if (droppedAmount > 0) {
+ table += '
' +
+ __('Dropped {0} row(s) — missing or zero amount.', [droppedAmount]) +
+ '
';
+ }
+ if (emptyRefCount > 0) {
+ table += '
' +
+ __('{0} of {1} row(s) have no Reference Number — re-importing the same file will create duplicates because deduplication uses the Reference Number.',
+ [emptyRefCount, txns.length]) +
+ '
' +
+ __('All {0} row(s) were dropped because the date column could not be parsed.', [droppedCount]) +
+ '
' +
+ '
' + __("The parser tries common formats like 2026-01-31, 31.01.2026, 01/31/2026 automatically.") + '
' +
+ '
' + __("If your bank uses a different format, open the Bank Integration Profile's File Format tab and enable Use Custom Date Format, then enter the exact Python strftime format (e.g. %d-%b-%Y for 31-Jan-2026).") + '
' +
+ __('All {0} row(s) were dropped because the amount could not be read.', [droppedCount]) +
+ '
' +
+ '
' + __('Most likely the Amount column (or Debit/Credit, depending on Amount Mode) is not mapped to an Excel header in the File Format tab.') + '
' +
+ '
' + __('Open the Bank Integration Profile and check the Column Mappings table — Standard Field values must include all columns required by the chosen Amount Mode.') + '
' +
- __('Dropped {0} row(s) — the date could not be parsed.', [droppedDate]) + ' ' +
- __("Open the Bank Statement Importer's File Format tab, enable 'Use Custom Date Format', and set the exact format your bank uses.") +
- '
' +
- __('Dropped {0} row(s) — unknown direction value(s):', [droppedDirection]) +
- ' ' + (sample || '?') + '. ' +
- __('Open the File Format tab and add these to Debit Values or Credit Values.') +
- '
';
- }
- if (droppedAmount > 0) {
- table += '
' +
- __('Dropped {0} row(s) — missing or zero amount.', [droppedAmount]) +
- '
';
- }
- if (emptyRefCount > 0) {
- table += '
' +
- __('{0} of {1} row(s) have no Reference Number — re-importing the same file will create duplicates because deduplication uses the Reference Number.',
- [emptyRefCount, txns.length]) +
- '
' +
- __('All {0} row(s) were dropped because the date column could not be parsed.', [droppedCount]) +
- '
' +
- '
' + __("The parser tries common formats like 2026-01-31, 31.01.2026, 01/31/2026 automatically.") + '
' +
- '
' + __("If your bank uses a different format, open the Bank Statement Importer's File Format tab and enable Use Custom Date Format, then enter the exact Python strftime format (e.g. %d-%b-%Y for 31-Jan-2026).") + '
' +
- __('All {0} row(s) were dropped because the amount could not be read.', [droppedCount]) +
- '
' +
- '
' + __('Most likely the Amount column (or Debit/Credit, depending on Amount Mode) is not mapped to an Excel header in the File Format tab.') + '
' +
- '
' + __('Open the Bank Statement Importer and check the Column Mappings table — Standard Field values must include all columns required by the chosen Amount Mode.') + '