diff --git a/jey_erp/bank_integration/excel_parser.py b/jey_erp/bank_integration/excel_parser.py
index 2cc764f..3753e38 100644
--- a/jey_erp/bank_integration/excel_parser.py
+++ b/jey_erp/bank_integration/excel_parser.py
@@ -6,22 +6,73 @@ import frappe
from frappe.utils import flt
+# Display labels (stored in DB on Bank Integration Excel Column Mapping.standard_field)
+# mapped to the short internal codes the parser logic uses.
+STANDARD_FIELD_TO_CODE = {
+ "Date": "date",
+ "Reference Number": "reference_number",
+ "Counterparty": "counterparty",
+ "Counterparty Tax ID (VOEN)": "counterparty_tax_id",
+ "Counterparty IBAN": "counterparty_iban",
+ "Amount": "amount",
+ "Debit": "debit",
+ "Credit": "credit",
+ "Direction": "direction",
+ "Currency": "currency",
+ "Purpose": "purpose",
+ "Description": "description",
+}
+
+
+# Tried in order until one parses the value. Covers ISO-style, day-first (EU/AZ),
+# and month-first (US) on the four common separators, with and without time.
+# Day-first variants are listed BEFORE month-first to bias towards the AZ region;
+# users with US-style ambiguous dates can enable "Use Custom Date Format".
+_COMMON_DATE_FORMATS = (
+ "%Y-%m-%d", "%Y.%m.%d", "%Y/%m/%d",
+ "%d-%m-%Y", "%d.%m.%Y", "%d/%m/%Y", "%d\\%m\\%Y",
+ "%m-%d-%Y", "%m.%d.%Y", "%m/%d/%Y",
+ "%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M",
+ "%d.%m.%Y %H:%M:%S", "%d.%m.%Y %H:%M",
+ "%d/%m/%Y %H:%M:%S", "%d/%m/%Y %H:%M",
+ "%m/%d/%Y %H:%M:%S", "%m/%d/%Y %H:%M",
+ "%d-%m-%y", "%d.%m.%y", "%d/%m/%y",
+ "%m-%d-%y", "%m.%d.%y", "%m/%d/%y",
+)
+
+
def parse_excel(file_url, preset_name):
"""Parse an Excel bank statement using a saved preset.
- Returns: list of dicts with keys:
- ref_no, date (ISO string), counterparty, contr_voen, contr_iban,
- amount (positive float), currency, drcr (D/C), purpose, description
+ Returns a dict:
+ {
+ "transactions": [ {ref_no, date, counterparty, contr_voen, contr_iban,
+ amount, currency, drcr, purpose, description}, ... ],
+ "dropped_date": int, # rows skipped because date couldn't be parsed
+ "dropped_amount": int, # rows skipped because amount was zero/missing
+ }
"""
from openpyxl import load_workbook
preset = frappe.get_doc("Bank Integration Excel Preset", preset_name)
col_to_standard = {}
+ unknown_labels = []
for row in preset.column_mappings:
- if row.excel_column and row.standard_field:
- col_to_standard[row.excel_column.strip()] = row.standard_field
+ if not row.excel_column or not row.standard_field:
+ continue
+ code = STANDARD_FIELD_TO_CODE.get(row.standard_field)
+ if not code:
+ unknown_labels.append(row.standard_field)
+ continue
+ col_to_standard[row.excel_column.strip()] = code
+ if unknown_labels:
+ frappe.throw(
+ "Preset '{}' has unknown standard field(s): {}. Edit the preset and pick a value from the dropdown.".format(
+ preset_name, ", ".join(sorted(set(unknown_labels)))
+ )
+ )
if not col_to_standard:
frappe.throw(f"Preset '{preset_name}' has no column mappings")
@@ -40,38 +91,57 @@ def parse_excel(file_url, preset_name):
if header_idx >= len(all_rows):
frappe.throw(f"Header row {header_row} is out of range (file has {len(all_rows)} rows)")
- headers = [str(c).strip() if c is not None else "" for c in all_rows[header_idx]]
+ headers = [_normalize_header(c) for c in all_rows[header_idx]]
+ normalized_lookup = {_normalize_header(k): v for k, v in col_to_standard.items()}
idx_to_standard = {}
for i, h in enumerate(headers):
- if h in col_to_standard:
- idx_to_standard[i] = col_to_standard[h]
+ if h in normalized_lookup:
+ idx_to_standard[i] = normalized_lookup[h]
if not idx_to_standard:
frappe.throw(
f"None of the configured columns ({sorted(col_to_standard.keys())}) "
- f"found in Excel headers ({headers})"
+ f"found in Excel headers ({[h for h in headers if h]})"
)
- date_format = preset.date_format or "%Y-%m-%d"
+ use_custom = bool(preset.get("use_custom_date_format"))
+ custom_format = (preset.date_format or "").strip() if use_custom else ""
amount_mode = preset.amount_mode or "Separate debit/credit columns"
data_start = header_idx + 1 + skip_after
transactions = []
+ dropped_date = 0
+ dropped_amount = 0
for raw_row in all_rows[data_start:]:
if not raw_row or all(c is None or str(c).strip() == "" for c in raw_row):
continue
- parsed = _parse_row(raw_row, idx_to_standard, date_format, amount_mode)
+ parsed, reason = _parse_row(raw_row, idx_to_standard, custom_format, amount_mode)
if parsed:
transactions.append(parsed)
+ elif reason == "date":
+ dropped_date += 1
+ elif reason == "amount":
+ dropped_amount += 1
- return transactions
+ return {
+ "transactions": transactions,
+ "dropped_date": dropped_date,
+ "dropped_amount": dropped_amount,
+ }
finally:
wb.close()
-def _parse_row(raw_row, idx_to_standard, date_format, amount_mode):
+def _normalize_header(value):
+ if value is None:
+ return ""
+ return " ".join(str(value).strip().lower().split())
+
+
+def _parse_row(raw_row, idx_to_standard, custom_format, amount_mode):
+ """Returns (record_dict_or_None, reason). reason in {"", "date", "amount"}."""
record = {
"ref_no": "",
"date": None,
@@ -89,6 +159,7 @@ def _parse_row(raw_row, idx_to_standard, date_format, amount_mode):
credit_val = None
amount_val = None
direction_val = None
+ saw_date_input = False
for i, std in idx_to_standard.items():
if i >= len(raw_row):
@@ -98,7 +169,8 @@ def _parse_row(raw_row, idx_to_standard, date_format, amount_mode):
continue
if std == "date":
- record["date"] = _parse_date(cell, date_format)
+ saw_date_input = saw_date_input or (str(cell).strip() != "")
+ record["date"] = _parse_date(cell, custom_format)
elif std == "reference_number":
record["ref_no"] = str(cell).strip()
elif std == "counterparty":
@@ -130,30 +202,36 @@ def _parse_row(raw_row, idx_to_standard, date_format, amount_mode):
record["amount"] = abs(flt(credit_val))
record["drcr"] = "C"
else:
- return None
+ return None, "amount"
elif amount_mode == "Single column with sign":
if amount_val is None:
- return None
+ return None, "amount"
record["amount"] = abs(flt(amount_val))
record["drcr"] = "D" if amount_val < 0 else "C"
elif amount_mode == "Single column + direction column":
if amount_val is None or direction_val is None:
- return None
+ return None, "amount"
record["amount"] = abs(flt(amount_val))
- if direction_val in ("D", "DR", "DEBIT", "DT", "OUT", "PAY", "WITHDRAWAL", "-"):
+ if direction_val in ("D", "DR", "DEBIT", "DT", "OUT", "PAY", "WITHDRAWAL", "-",
+ "MƏXARIC", "MEXARIC", "ÇIXIB", "CIXIB", "XƏRC", "XERC"):
record["drcr"] = "D"
- elif direction_val in ("C", "CR", "CREDIT", "CT", "IN", "RECEIVE", "DEPOSIT", "+"):
+ elif direction_val in ("C", "CR", "CREDIT", "CT", "IN", "RECEIVE", "DEPOSIT", "+",
+ "MƏDAXIL", "MEDAXIL", "DAXIL", "DAXIL OLUB", "GƏLIR", "GELIR"):
record["drcr"] = "C"
else:
record["drcr"] = "D"
- if not record["date"] or record["amount"] == 0:
- return None
+ if not record["date"]:
+ # Distinguish "row had a date input we couldn't parse" from "row had no date
+ # input at all" — only the former is a user-actionable error.
+ return None, "date" if saw_date_input else "amount"
+ if record["amount"] == 0:
+ return None, "amount"
- return record
+ return record, ""
-def _parse_date(value, date_format):
+def _parse_date(value, custom_format):
if isinstance(value, datetime):
return value.date().isoformat()
if isinstance(value, date):
@@ -167,9 +245,62 @@ def _parse_date(value, date_format):
s = str(value).strip()
if not s:
return None
- for fmt in (date_format, "%Y-%m-%d", "%d.%m.%Y", "%d/%m/%Y", "%m/%d/%Y", "%Y/%m/%d"):
+
+ if custom_format:
+ try:
+ return datetime.strptime(s, custom_format).date().isoformat()
+ except ValueError:
+ return None
+
+ for fmt in _COMMON_DATE_FORMATS:
try:
return datetime.strptime(s, fmt).date().isoformat()
except ValueError:
continue
return None
+
+
+@frappe.whitelist()
+def parse_sample_headers(file_url, header_row=1):
+ """Read just the header row of a sample file and return cleaned header strings.
+
+ Used by the Bank Integration Excel Preset form to populate the Excel Column
+ dropdown without parsing the whole file.
+ """
+ from openpyxl import load_workbook
+ from frappe.utils import cint
+
+ header_row = max(cint(header_row) or 1, 1)
+
+ if not file_url:
+ frappe.throw("file_url is required")
+
+ file_doc = frappe.get_doc("File", {"file_url": file_url})
+ # Restrict to private files owned by the caller or files they otherwise have
+ # read access to — public attachments are always allowed.
+ if file_doc.is_private and file_doc.owner != frappe.session.user:
+ if not frappe.has_permission("File", "read", file_doc.name):
+ frappe.throw("Not permitted to read this file")
+
+ file_path = file_doc.get_full_path()
+ wb = load_workbook(file_path, data_only=True, read_only=True)
+ try:
+ ws = wb.active
+ all_rows = list(ws.iter_rows(min_row=header_row, max_row=header_row, values_only=True))
+ if not all_rows:
+ return {"success": True, "headers": []}
+ headers = []
+ seen = set()
+ for cell in all_rows[0]:
+ if cell is None:
+ continue
+ h = str(cell).strip()
+ if not h:
+ continue
+ if h in seen:
+ continue
+ seen.add(h)
+ headers.append(h)
+ return {"success": True, "headers": headers}
+ finally:
+ wb.close()
diff --git a/jey_erp/bank_integration/import_api.py b/jey_erp/bank_integration/import_api.py
index a37d5fc..91cf8fa 100644
--- a/jey_erp/bank_integration/import_api.py
+++ b/jey_erp/bank_integration/import_api.py
@@ -12,10 +12,20 @@ from jey_erp.bank_integration.excel_parser import parse_excel
@frappe.whitelist()
def parse_excel_for_preview(file_url, preset_name, bank_integration, bank_account):
"""Parse Excel and return preview transactions, deduped against existing BTs."""
- transactions = parse_excel(file_url, preset_name)
+ result = parse_excel(file_url, preset_name)
+ transactions = result["transactions"]
+ dropped_date = result.get("dropped_date", 0)
+ dropped_amount = result.get("dropped_amount", 0)
if not transactions:
- return {"success": True, "transactions": [], "skipped_duplicates": 0, "total_parsed": 0}
+ return {
+ "success": True,
+ "transactions": [],
+ "skipped_duplicates": 0,
+ "total_parsed": 0,
+ "dropped_date": dropped_date,
+ "dropped_amount": dropped_amount,
+ }
ref_nos = [t["ref_no"] for t in transactions if t.get("ref_no")]
existing = set()
@@ -44,6 +54,8 @@ def parse_excel_for_preview(file_url, preset_name, bank_integration, bank_accoun
"transactions": fresh,
"skipped_duplicates": skipped,
"total_parsed": len(transactions),
+ "dropped_date": dropped_date,
+ "dropped_amount": dropped_amount,
}
@@ -313,7 +325,10 @@ def load_registries_from_excel(file_url, preset_name, bank_integration,
if not (load_customers or load_suppliers or load_purposes):
return {"success": False, "message": _("Select at least one type to load.")}
- transactions = parse_excel(file_url, preset_name)
+ result = parse_excel(file_url, preset_name)
+ transactions = result["transactions"]
+ dropped_date = result.get("dropped_date", 0)
+ dropped_amount = result.get("dropped_amount", 0)
new_c = new_s = new_p = 0
for txn in transactions:
@@ -342,4 +357,6 @@ def load_registries_from_excel(file_url, preset_name, bank_integration,
"new_customers": new_c,
"new_suppliers": new_s,
"new_purposes": new_p,
+ "dropped_date": dropped_date,
+ "dropped_amount": dropped_amount,
}
diff --git a/jey_erp/bank_integration/migrations.py b/jey_erp/bank_integration/migrations.py
index 095d44a..cb96acd 100644
--- a/jey_erp/bank_integration/migrations.py
+++ b/jey_erp/bank_integration/migrations.py
@@ -83,3 +83,45 @@ def rename_bip_purposes():
_rename_to_readable("Bank Integration Purpose", "BIP", "purpose_keyword")
_rename_to_readable("Bank Integration Customer", "BIC", "customer_name")
_rename_to_readable("Bank Integration Supplier", "BIS", "supplier_name")
+
+
+_STANDARD_FIELD_CODE_TO_LABEL = {
+ "date": "Date",
+ "reference_number": "Reference Number",
+ "counterparty": "Counterparty",
+ "counterparty_tax_id": "Counterparty Tax ID (VOEN)",
+ "counterparty_iban": "Counterparty IBAN",
+ "amount": "Amount",
+ "debit": "Debit",
+ "credit": "Credit",
+ "direction": "Direction",
+ "currency": "Currency",
+ "purpose": "Purpose",
+ "description": "Description",
+}
+
+
+def migrate_standard_field_codes_to_labels():
+ """Rewrite Bank Integration Excel Column Mapping.standard_field from the old
+ machine codes ('date', 'reference_number', ...) to the new human-readable
+ labels stored by the doctype select options. Idempotent: rows already on a
+ label value are left alone."""
+ if not frappe.db.exists("DocType", "Bank Integration Excel Column Mapping"):
+ return
+ rows = frappe.get_all(
+ "Bank Integration Excel Column Mapping",
+ filters={"standard_field": ["in", list(_STANDARD_FIELD_CODE_TO_LABEL.keys())]},
+ fields=["name", "standard_field"],
+ )
+ if not rows:
+ return
+ for r in rows:
+ label = _STANDARD_FIELD_CODE_TO_LABEL.get(r.standard_field)
+ if not label:
+ continue
+ frappe.db.set_value(
+ "Bank Integration Excel Column Mapping", r.name,
+ "standard_field", label, update_modified=False,
+ )
+ frappe.db.commit()
+ print(f"BI: migrated {len(rows)} Excel column mapping(s) to human-readable labels")
diff --git a/jey_erp/fixtures/bank_integration_excel_preset.json b/jey_erp/fixtures/bank_integration_excel_preset.json
index 604050a..e01437e 100644
--- a/jey_erp/fixtures/bank_integration_excel_preset.json
+++ b/jey_erp/fixtures/bank_integration_excel_preset.json
@@ -6,58 +6,59 @@
"description": "Generic preset showing the supported standard fields. Copy this preset and adjust the Excel column names to match your bank's export format.",
"header_row": 1,
"skip_rows_after_header": 0,
- "date_format": "%Y-%m-%d",
+ "use_custom_date_format": 0,
+ "date_format": "",
"amount_mode": "Separate debit/credit columns",
"column_mappings": [
{
"doctype": "Bank Integration Excel Column Mapping",
"excel_column": "Date",
- "standard_field": "date"
+ "standard_field": "Date"
},
{
"doctype": "Bank Integration Excel Column Mapping",
"excel_column": "Reference",
- "standard_field": "reference_number"
+ "standard_field": "Reference Number"
},
{
"doctype": "Bank Integration Excel Column Mapping",
"excel_column": "Counterparty",
- "standard_field": "counterparty"
+ "standard_field": "Counterparty"
},
{
"doctype": "Bank Integration Excel Column Mapping",
"excel_column": "Tax ID",
- "standard_field": "counterparty_tax_id"
+ "standard_field": "Counterparty Tax ID (VOEN)"
},
{
"doctype": "Bank Integration Excel Column Mapping",
"excel_column": "IBAN",
- "standard_field": "counterparty_iban"
+ "standard_field": "Counterparty IBAN"
},
{
"doctype": "Bank Integration Excel Column Mapping",
"excel_column": "Debit",
- "standard_field": "debit"
+ "standard_field": "Debit"
},
{
"doctype": "Bank Integration Excel Column Mapping",
"excel_column": "Credit",
- "standard_field": "credit"
+ "standard_field": "Credit"
},
{
"doctype": "Bank Integration Excel Column Mapping",
"excel_column": "Currency",
- "standard_field": "currency"
+ "standard_field": "Currency"
},
{
"doctype": "Bank Integration Excel Column Mapping",
"excel_column": "Purpose",
- "standard_field": "purpose"
+ "standard_field": "Purpose"
},
{
"doctype": "Bank Integration Excel Column Mapping",
"excel_column": "Description",
- "standard_field": "description"
+ "standard_field": "Description"
}
]
}
diff --git a/jey_erp/hooks.py b/jey_erp/hooks.py
index cd58aff..1aea2f6 100644
--- a/jey_erp/hooks.py
+++ b/jey_erp/hooks.py
@@ -143,9 +143,14 @@ def after_migrate_combined():
# Link kb-mapped Bank Accounts to Kapital Bank Settings via the
# Dynamic Link fields used by the universal Create & Reconcile resolver.
try:
- from jey_erp.bank_integration.migrations import link_kb_bank_accounts_to_bi, rename_bip_purposes
+ from jey_erp.bank_integration.migrations import (
+ link_kb_bank_accounts_to_bi,
+ rename_bip_purposes,
+ migrate_standard_field_codes_to_labels,
+ )
link_kb_bank_accounts_to_bi()
rename_bip_purposes()
+ migrate_standard_field_codes_to_labels()
except Exception as e:
print(f"BI: post-migrate sync failed: {e}")
# REMOVED: Asset categories creation moved to setup_wizard_complete hook
diff --git a/jey_erp/jey_erp/doctype/bank_integration/bank_integration.js b/jey_erp/jey_erp/doctype/bank_integration/bank_integration.js
index 2601382..cd08bb5 100644
--- a/jey_erp/jey_erp/doctype/bank_integration/bank_integration.js
+++ b/jey_erp/jey_erp/doctype/bank_integration/bank_integration.js
@@ -12,6 +12,7 @@ frappe.ui.form.on('Bank Integration', {
frm.set_query('customer_group', 'customer_mappings', leaf);
frm.set_query('territory', 'customer_mappings', leaf);
frm.set_query('supplier_group', 'supplier_mappings', leaf);
+ frm.set_query('cost_center', 'transaction_mappings', leaf);
},
refresh(frm) {
@@ -224,11 +225,25 @@ function _show_load_data_dialog(frm) {
}
d.hide();
const res = r.message;
+ const droppedDate = res.dropped_date || 0;
+ const droppedAmount = res.dropped_amount || 0;
+ 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' on the preset if your bank uses an unusual date format.",
+ [droppedDate]) +
+ '';
+ }
+ if (droppedAmount > 0) {
+ msg += '
' +
+ __('Also skipped {0} row(s) with missing/zero amount.', [droppedAmount]) +
+ '';
+ }
frappe.msgprint({
title: __('Data Loaded'),
- indicator: 'green',
- message: __('From {0} rows — new customers: {1}, suppliers: {2}, purposes: {3}',
- [res.total_rows, res.new_customers, res.new_suppliers, res.new_purposes]),
+ indicator: droppedDate > 0 ? 'orange' : 'green',
+ message: msg,
});
frm.reload_doc();
},
diff --git a/jey_erp/jey_erp/doctype/bank_integration_excel_column_mapping/bank_integration_excel_column_mapping.json b/jey_erp/jey_erp/doctype/bank_integration_excel_column_mapping/bank_integration_excel_column_mapping.json
index 398145f..29812e5 100644
--- a/jey_erp/jey_erp/doctype/bank_integration_excel_column_mapping/bank_integration_excel_column_mapping.json
+++ b/jey_erp/jey_erp/doctype/bank_integration_excel_column_mapping/bank_integration_excel_column_mapping.json
@@ -12,17 +12,18 @@
"fields": [
{
"fieldname": "excel_column",
- "fieldtype": "Data",
+ "fieldtype": "Autocomplete",
"in_list_view": 1,
"label": "Excel Column",
- "reqd": 1
+ "reqd": 1,
+ "description": "Column header text in the Excel file. Upload a Sample File above to pick from a list."
},
{
"fieldname": "standard_field",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Standard Field",
- "options": "\ndate\nreference_number\ncounterparty\ncounterparty_tax_id\ncounterparty_iban\namount\ndebit\ncredit\ndirection\ncurrency\npurpose\ndescription",
+ "options": "\nDate\nReference Number\nCounterparty\nCounterparty Tax ID (VOEN)\nCounterparty IBAN\nAmount\nDebit\nCredit\nDirection\nCurrency\nPurpose\nDescription",
"reqd": 1
},
{
diff --git a/jey_erp/jey_erp/doctype/bank_integration_excel_preset/bank_integration_excel_preset.js b/jey_erp/jey_erp/doctype/bank_integration_excel_preset/bank_integration_excel_preset.js
new file mode 100644
index 0000000..9d6bf3f
--- /dev/null
+++ b/jey_erp/jey_erp/doctype/bank_integration_excel_preset/bank_integration_excel_preset.js
@@ -0,0 +1,99 @@
+// Bank Integration Excel Preset form:
+// - When a Sample File is attached, fetch its header row and use those as
+// options for the Excel Column dropdown in the child table.
+// - "Clear Sample" button removes the sample and reverts the column to free
+// text (Autocomplete with no options).
+//
+// The Custom Date Format field is hidden by depends_on; the parser tries a
+// list of common formats automatically when the checkbox is off.
+
+frappe.ui.form.on("Bank Integration Excel Preset", {
+ refresh(frm) {
+ BIPreset.applySampleHeaders(frm);
+
+ if (frm.doc.sample_file) {
+ frm.add_custom_button(__("Clear Sample"), function () {
+ frappe.confirm(
+ __("Remove the sample file? The Excel Column dropdown will go back to free text."),
+ function () {
+ frm.set_value("sample_file", null);
+ BIPreset._cachedHeaders = [];
+ BIPreset.setColumnOptions(frm, []);
+ frm.save();
+ }
+ );
+ });
+ }
+ },
+
+ sample_file(frm) {
+ BIPreset.applySampleHeaders(frm);
+ },
+
+ header_row(frm) {
+ // Header row changed — if there's a sample, re-fetch headers.
+ if (frm.doc.sample_file) BIPreset.applySampleHeaders(frm, /*force*/ true);
+ },
+});
+
+const BIPreset = {
+ _cachedHeaders: [],
+ _cachedFor: null, // "file_url::header_row" so we know when to refetch
+
+ applySampleHeaders(frm, force) {
+ if (!frm.doc.sample_file) {
+ this._cachedHeaders = [];
+ this._cachedFor = null;
+ this.setColumnOptions(frm, []);
+ return;
+ }
+
+ const key = frm.doc.sample_file + "::" + (frm.doc.header_row || 1);
+ if (!force && this._cachedFor === key && this._cachedHeaders.length) {
+ this.setColumnOptions(frm, this._cachedHeaders);
+ return;
+ }
+
+ frappe.call({
+ method: "jey_erp.bank_integration.excel_parser.parse_sample_headers",
+ args: {
+ file_url: frm.doc.sample_file,
+ header_row: frm.doc.header_row || 1,
+ },
+ callback: (r) => {
+ if (!r.message || !r.message.success) {
+ frappe.msgprint({
+ title: __("Sample File Error"),
+ indicator: "red",
+ message: (r.message && r.message.message) || __("Could not read sample file."),
+ });
+ return;
+ }
+ const headers = r.message.headers || [];
+ this._cachedHeaders = headers;
+ this._cachedFor = key;
+ this.setColumnOptions(frm, headers);
+
+ if (!headers.length) {
+ frappe.show_alert({
+ message: __("No headers found in row {0}. Adjust 'Header Row'.", [frm.doc.header_row || 1]),
+ indicator: "orange",
+ }, 5);
+ } else {
+ frappe.show_alert({
+ message: __("{0} column header(s) loaded.", [headers.length]),
+ indicator: "green",
+ }, 3);
+ }
+ },
+ });
+ },
+
+ setColumnOptions(frm, headers) {
+ const grid = frm.fields_dict.column_mappings && frm.fields_dict.column_mappings.grid;
+ if (!grid) return;
+ // Autocomplete field accepts options as a newline-separated string.
+ grid.update_docfield_property("excel_column", "options", (headers || []).join("\n"));
+ grid.refresh();
+ },
+};
diff --git a/jey_erp/jey_erp/doctype/bank_integration_excel_preset/bank_integration_excel_preset.json b/jey_erp/jey_erp/doctype/bank_integration_excel_preset/bank_integration_excel_preset.json
index 6a4585d..2b646ca 100644
--- a/jey_erp/jey_erp/doctype/bank_integration_excel_preset/bank_integration_excel_preset.json
+++ b/jey_erp/jey_erp/doctype/bank_integration_excel_preset/bank_integration_excel_preset.json
@@ -10,9 +10,13 @@
"format_section",
"header_row",
"skip_rows_after_header",
- "date_format",
- "column_break_1",
"amount_mode",
+ "column_break_1",
+ "use_custom_date_format",
+ "date_format",
+ "sample_section",
+ "sample_file",
+ "sample_section_help",
"columns_section",
"column_mappings"
],
@@ -50,18 +54,6 @@
"fieldtype": "Int",
"label": "Skip Rows After Header"
},
- {
- "default": "%Y-%m-%d",
- "description": "Python strftime format. Examples: %Y-%m-%d, %d.%m.%Y, %d/%m/%Y",
- "fieldname": "date_format",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Date Format"
- },
- {
- "fieldname": "column_break_1",
- "fieldtype": "Column Break"
- },
{
"default": "Separate debit/credit columns",
"fieldname": "amount_mode",
@@ -70,6 +62,41 @@
"label": "Amount Mode",
"options": "Single column with sign\nSeparate debit/credit columns\nSingle column + direction column"
},
+ {
+ "fieldname": "column_break_1",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "description": "Off: the parser auto-detects common date formats (2026-01-31, 31.01.2026, 01/31/2026, etc.).\nOn: only the format below is used — set this if your bank uses an unusual format the auto-detection misses.",
+ "fieldname": "use_custom_date_format",
+ "fieldtype": "Check",
+ "label": "Use Custom Date Format"
+ },
+ {
+ "depends_on": "eval:doc.use_custom_date_format",
+ "mandatory_depends_on": "eval:doc.use_custom_date_format",
+ "description": "Python strftime format. Examples: %Y-%m-%d, %d.%m.%Y, %d/%m/%Y %H:%M:%S",
+ "fieldname": "date_format",
+ "fieldtype": "Data",
+ "label": "Custom Date Format"
+ },
+ {
+ "fieldname": "sample_section",
+ "fieldtype": "Section Break",
+ "label": "Sample File"
+ },
+ {
+ "description": "Upload a sample bank statement here so the column names below can be picked from a dropdown. Only used at design time.",
+ "fieldname": "sample_file",
+ "fieldtype": "Attach",
+ "label": "Sample Excel File"
+ },
+ {
+ "fieldname": "sample_section_help",
+ "fieldtype": "HTML",
+ "options": "
| '; @@ -260,6 +278,29 @@ const BIExcelImport = { }); }, + _showDateParseHelp(droppedCount, presetName) { + const openPreset = function () { + frappe.set_route('Form', 'Bank Integration Excel Preset', presetName); + }; + const d = new frappe.ui.Dialog({ + title: __('Date Format Not Recognized'), + fields: [{ + fieldtype: 'HTML', + options: + ' |
|---|