refactor(bank-integration): merge Excel Preset into File Format tab
File format settings (header row, amount mode, column mappings, sample file, direction values, custom date format, currency-from-file) now live on the Bank Integration record itself under a new "File Format" tab, removing the extra hop through Bank Integration Excel Preset. - Bank Integration JSON gains the File Format tab and column_mappings child - Excel parser and import API read format from Bank Integration directly, drop the preset_name argument from public endpoints - Load Data dialog (on Bank Integration) and Load From Excel dialog (on Bank Transaction list) drop the preset Link field; the chosen Bank Integration provides everything needed - after_migrate patch copies fields and column mappings from an existing preset into each Bank Integration whose File Format is still empty - Bank Integration Excel Preset doctype kept around for rollback but marked deprecated (System Manager only, in-form warning), removed from the fixtures list so fresh installs don't ship the Generic preset
This commit is contained in:
parent
4f0c26208b
commit
04b16a6eea
|
|
@ -1,4 +1,4 @@
|
|||
"""Parse Excel bank statements using Bank Integration Excel Preset configuration."""
|
||||
"""Parse Excel bank statements using a Bank Integration's File Format configuration."""
|
||||
|
||||
import re
|
||||
from datetime import datetime, date
|
||||
|
|
@ -26,7 +26,7 @@ STANDARD_FIELD_TO_CODE = {
|
|||
|
||||
|
||||
# Internal codes that must be present in the headers of any imported file, by
|
||||
# amount_mode. Kept in sync with bank_integration_excel_preset.py:_REQUIRED_BY_MODE.
|
||||
# amount_mode. Kept in sync with bank_integration.py:_REQUIRED_BY_MODE.
|
||||
_REQUIRED_CODES_BY_MODE = {
|
||||
"Single column with sign": ("date", "reference_number", "amount"),
|
||||
"Separate debit/credit columns": ("date", "reference_number", "debit", "credit"),
|
||||
|
|
@ -51,8 +51,8 @@ _COMMON_DATE_FORMATS = (
|
|||
)
|
||||
|
||||
|
||||
def parse_excel(file_url, preset_name):
|
||||
"""Parse an Excel bank statement using a saved preset.
|
||||
def parse_excel(file_url, bank_integration):
|
||||
"""Parse an Excel bank statement using a Bank Integration's File Format.
|
||||
|
||||
Returns a dict:
|
||||
{
|
||||
|
|
@ -64,11 +64,11 @@ def parse_excel(file_url, preset_name):
|
|||
"""
|
||||
from openpyxl import load_workbook
|
||||
|
||||
preset = frappe.get_doc("Bank Integration Excel Preset", preset_name)
|
||||
bi = frappe.get_doc("Bank Integration", bank_integration)
|
||||
|
||||
col_to_standard = {}
|
||||
unknown_labels = []
|
||||
for row in preset.column_mappings:
|
||||
for row in bi.column_mappings:
|
||||
if not row.excel_column or not row.standard_field:
|
||||
continue
|
||||
code = STANDARD_FIELD_TO_CODE.get(row.standard_field)
|
||||
|
|
@ -79,12 +79,15 @@ def parse_excel(file_url, preset_name):
|
|||
|
||||
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)))
|
||||
"Bank Integration '{}' has unknown standard field(s): {}. Open the File Format tab and pick a value from the dropdown.".format(
|
||||
bank_integration, ", ".join(sorted(set(unknown_labels)))
|
||||
)
|
||||
)
|
||||
if not col_to_standard:
|
||||
frappe.throw(f"Preset '{preset_name}' has no column mappings")
|
||||
frappe.throw(
|
||||
f"Bank Integration '{bank_integration}' has no column mappings. "
|
||||
"Open the File Format tab and map Excel columns to Standard Fields."
|
||||
)
|
||||
|
||||
file_doc = frappe.get_doc("File", {"file_url": file_url})
|
||||
file_path = file_doc.get_full_path()
|
||||
|
|
@ -93,7 +96,7 @@ def parse_excel(file_url, preset_name):
|
|||
try:
|
||||
ws = wb.active
|
||||
|
||||
header_row = preset.header_row or 1
|
||||
header_row = bi.header_row or 1
|
||||
|
||||
all_rows = list(ws.iter_rows(values_only=True))
|
||||
header_idx = header_row - 1
|
||||
|
|
@ -114,7 +117,7 @@ def parse_excel(file_url, preset_name):
|
|||
f"found in Excel headers ({[h for h in headers if h]})"
|
||||
)
|
||||
|
||||
amount_mode = preset.amount_mode or "Separate debit/credit columns"
|
||||
amount_mode = bi.amount_mode or "Separate debit/credit columns"
|
||||
found_codes = set(idx_to_standard.values())
|
||||
required = _REQUIRED_CODES_BY_MODE.get(amount_mode, ())
|
||||
missing_codes = [c for c in required if c not in found_codes]
|
||||
|
|
@ -124,21 +127,21 @@ def parse_excel(file_url, preset_name):
|
|||
missing_labels = [code_to_label.get(c, c) for c in missing_codes]
|
||||
frappe.throw(
|
||||
"Required column(s) for Amount Mode '{}' not found in the file headers: {}. "
|
||||
"Either the Header Row on the preset points at the wrong line, or the Excel "
|
||||
"column names in the preset don't match the file. Headers seen: {}".format(
|
||||
"Either the Header Row points at the wrong line, or the Excel column "
|
||||
"names in the File Format tab don't match the file. Headers seen: {}".format(
|
||||
amount_mode, ", ".join(missing_labels), [h for h in headers if h]
|
||||
)
|
||||
)
|
||||
|
||||
use_custom = bool(preset.get("use_custom_date_format"))
|
||||
custom_format = (preset.date_format or "").strip() if use_custom else ""
|
||||
debit_aliases = _split_aliases(preset.get("direction_debit_values"))
|
||||
credit_aliases = _split_aliases(preset.get("direction_credit_values"))
|
||||
use_custom = bool(bi.get("use_custom_date_format"))
|
||||
custom_format = (bi.date_format or "").strip() if use_custom else ""
|
||||
debit_aliases = _split_aliases(bi.get("direction_debit_values"))
|
||||
credit_aliases = _split_aliases(bi.get("direction_credit_values"))
|
||||
|
||||
if amount_mode == "Single column + direction column" and not (debit_aliases or credit_aliases):
|
||||
frappe.throw(
|
||||
"Preset '{}' uses 'Single column + direction column' but has no Debit/Credit values configured. "
|
||||
"Open the preset and fill in at least one of 'Debit Values' / 'Credit Values'.".format(preset_name)
|
||||
"Bank Integration '{}' uses 'Single column + direction column' but has no Debit/Credit values configured. "
|
||||
"Open the File Format tab and fill in at least one of 'Debit Values' / 'Credit Values'.".format(bank_integration)
|
||||
)
|
||||
|
||||
data_start = header_idx + 1
|
||||
|
|
@ -383,7 +386,7 @@ def _parse_date(value, custom_format):
|
|||
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
|
||||
Used by the Bank Integration File Format tab to populate the Excel Column
|
||||
dropdown without parsing the whole file.
|
||||
"""
|
||||
from openpyxl import load_workbook
|
||||
|
|
|
|||
|
|
@ -10,9 +10,9 @@ 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):
|
||||
def parse_excel_for_preview(file_url, bank_integration, bank_account):
|
||||
"""Parse Excel and return preview transactions, deduped against existing BTs."""
|
||||
result = parse_excel(file_url, preset_name)
|
||||
result = parse_excel(file_url, bank_integration)
|
||||
transactions = result["transactions"]
|
||||
dropped_date = result.get("dropped_date", 0)
|
||||
dropped_amount = result.get("dropped_amount", 0)
|
||||
|
|
@ -73,7 +73,7 @@ def parse_excel_for_preview(file_url, preset_name, bank_integration, bank_accoun
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def import_bulk_bt(txn_list, bank_integration, bank_account, preset_name=None):
|
||||
def import_bulk_bt(txn_list, bank_integration, bank_account):
|
||||
"""Enqueue bulk Bank Transaction creation as a background job.
|
||||
|
||||
Creates Bank Transactions only — registries (Bank Integration Customer /
|
||||
|
|
@ -91,7 +91,6 @@ def import_bulk_bt(txn_list, bank_integration, bank_account, preset_name=None):
|
|||
txn_list=txn_list,
|
||||
bank_integration=bank_integration,
|
||||
bank_account=bank_account,
|
||||
preset_name=preset_name,
|
||||
user=user,
|
||||
queue="default",
|
||||
timeout=600,
|
||||
|
|
@ -100,7 +99,7 @@ def import_bulk_bt(txn_list, bank_integration, bank_account, preset_name=None):
|
|||
return {"success": True, "enqueued": True, "total": len(txn_list)}
|
||||
|
||||
|
||||
def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user, preset_name=None):
|
||||
def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user):
|
||||
"""Background job: create one Bank Transaction per row."""
|
||||
frappe.set_user(user)
|
||||
|
||||
|
|
@ -113,11 +112,7 @@ def _process_bulk_bt_import(txn_list, bank_integration, bank_account, user, pres
|
|||
frappe.db.get_value("Account", ba_account, "account_currency") if ba_account else None
|
||||
) or "AZN"
|
||||
|
||||
use_currency_from_file = False
|
||||
if preset_name:
|
||||
use_currency_from_file = bool(
|
||||
frappe.db.get_value("Bank Integration Excel Preset", preset_name, "use_currency_from_file")
|
||||
)
|
||||
use_currency_from_file = bool(bi.get("use_currency_from_file"))
|
||||
|
||||
total = len(txn_list)
|
||||
imported_count = 0
|
||||
|
|
@ -335,7 +330,7 @@ def update_bank_account_bi_default(bank_account, bi_type, bi_name):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def load_registries_from_excel(file_url, preset_name, bank_integration,
|
||||
def load_registries_from_excel(file_url, bank_integration,
|
||||
load_customers=1, load_suppliers=1, load_purposes=1):
|
||||
"""Parse an Excel statement and upsert ONLY the selected reference registries
|
||||
(Bank Integration Customer / Supplier / Purpose) — no Bank Transactions.
|
||||
|
|
@ -347,7 +342,7 @@ 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.")}
|
||||
|
||||
result = parse_excel(file_url, preset_name)
|
||||
result = parse_excel(file_url, bank_integration)
|
||||
transactions = result["transactions"]
|
||||
dropped_date = result.get("dropped_date", 0)
|
||||
dropped_amount = result.get("dropped_amount", 0)
|
||||
|
|
|
|||
|
|
@ -101,6 +101,83 @@ _STANDARD_FIELD_CODE_TO_LABEL = {
|
|||
}
|
||||
|
||||
|
||||
_PRESET_FIELDS_TO_COPY = (
|
||||
"header_row",
|
||||
"amount_mode",
|
||||
"direction_debit_values",
|
||||
"direction_credit_values",
|
||||
"use_currency_from_file",
|
||||
"use_custom_date_format",
|
||||
"date_format",
|
||||
"sample_file",
|
||||
)
|
||||
|
||||
|
||||
def copy_excel_preset_into_bank_integration():
|
||||
"""Iteration: merging Bank Integration Excel Preset into Bank Integration.
|
||||
|
||||
For each Bank Integration whose File Format is still empty (no column
|
||||
mappings, no format chosen), copy the format and column mappings from
|
||||
a Bank Integration Excel Preset. Strategy: pick the only user-created
|
||||
preset if there's exactly one (excluding the 'Generic Bank Statement'
|
||||
fixture); otherwise pick the fixture as a starting template.
|
||||
|
||||
Idempotent: once a Bank Integration has any column mappings or an
|
||||
explicit amount_mode, it's left alone.
|
||||
"""
|
||||
if not frappe.db.exists("DocType", "Bank Integration"):
|
||||
return
|
||||
if not frappe.db.exists("DocType", "Bank Integration Excel Preset"):
|
||||
return
|
||||
|
||||
bi_names = frappe.get_all("Bank Integration", pluck="name")
|
||||
if not bi_names:
|
||||
return
|
||||
|
||||
preset_names = frappe.get_all(
|
||||
"Bank Integration Excel Preset",
|
||||
fields=["name"],
|
||||
order_by="modified desc",
|
||||
pluck="name",
|
||||
)
|
||||
if not preset_names:
|
||||
return
|
||||
|
||||
non_fixture = [p for p in preset_names if p != "Generic Bank Statement"]
|
||||
chosen_preset_name = non_fixture[0] if len(non_fixture) == 1 else (
|
||||
non_fixture[0] if non_fixture else preset_names[0]
|
||||
)
|
||||
preset = frappe.get_doc("Bank Integration Excel Preset", chosen_preset_name)
|
||||
|
||||
copied = 0
|
||||
for bi_name in bi_names:
|
||||
bi = frappe.get_doc("Bank Integration", bi_name)
|
||||
# Skip if File Format already configured.
|
||||
if bi.column_mappings or (bi.amount_mode and bi.amount_mode != "Separate debit/credit columns"):
|
||||
continue
|
||||
if bi.column_mappings:
|
||||
continue
|
||||
for f in _PRESET_FIELDS_TO_COPY:
|
||||
value = preset.get(f)
|
||||
if value is None:
|
||||
continue
|
||||
bi.set(f, value)
|
||||
for row in preset.column_mappings or []:
|
||||
bi.append("column_mappings", {
|
||||
"excel_column": row.excel_column,
|
||||
"standard_field": row.standard_field,
|
||||
"notes": row.notes,
|
||||
})
|
||||
bi.flags.ignore_validate = True
|
||||
bi.flags.ignore_mandatory = True
|
||||
bi.save(ignore_permissions=True)
|
||||
copied += 1
|
||||
|
||||
if copied:
|
||||
frappe.db.commit()
|
||||
print(f"BI: copied File Format from preset '{chosen_preset_name}' into {copied} Bank Integration(s)")
|
||||
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -143,10 +143,12 @@ def after_migrate_combined():
|
|||
link_kb_bank_accounts_to_bi,
|
||||
rename_bip_purposes,
|
||||
migrate_standard_field_codes_to_labels,
|
||||
copy_excel_preset_into_bank_integration,
|
||||
)
|
||||
link_kb_bank_accounts_to_bi()
|
||||
rename_bip_purposes()
|
||||
migrate_standard_field_codes_to_labels()
|
||||
copy_excel_preset_into_bank_integration()
|
||||
except Exception as e:
|
||||
print(f"BI: post-migrate sync failed: {e}")
|
||||
# REMOVED: Asset categories creation moved to setup_wizard_complete hook
|
||||
|
|
@ -160,12 +162,6 @@ fixtures = [
|
|||
{
|
||||
"doctype": "Print Format"
|
||||
},
|
||||
{
|
||||
"doctype": "Bank Integration Excel Preset"
|
||||
},
|
||||
{
|
||||
"doctype": "Bank Integration Excel Column Mapping"
|
||||
},
|
||||
]
|
||||
|
||||
# Includes in <head>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ frappe.ui.form.on('Bank Integration', {
|
|||
},
|
||||
|
||||
refresh(frm) {
|
||||
BIFileFormat.applySampleHeaders(frm);
|
||||
|
||||
if (frm.is_new()) return;
|
||||
|
||||
// === Load Data (file-based, registry-only) ===
|
||||
|
|
@ -120,8 +122,83 @@ frappe.ui.form.on('Bank Integration', {
|
|||
// === Data tab rendering ===
|
||||
_bi_load_data_tabs(frm);
|
||||
},
|
||||
|
||||
sample_file(frm) {
|
||||
BIFileFormat.applySampleHeaders(frm);
|
||||
},
|
||||
|
||||
header_row(frm) {
|
||||
if (frm.doc.sample_file) BIFileFormat.applySampleHeaders(frm, /*force*/ true);
|
||||
},
|
||||
});
|
||||
|
||||
// ─── File Format helpers ─────────────────────────────────────────────────────
|
||||
// Mirrors what bank_integration_excel_preset.js used to do, but on the merged
|
||||
// File Format tab of the Bank Integration form: a Sample File attachment turns
|
||||
// the Excel Column field in the column_mappings grid into a dropdown of real
|
||||
// header strings, so users don't have to type them.
|
||||
|
||||
const BIFileFormat = {
|
||||
_cachedHeaders: [],
|
||||
_cachedFor: null,
|
||||
|
||||
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;
|
||||
grid.update_docfield_property('excel_column', 'options', (headers || []).join('\n'));
|
||||
grid.refresh();
|
||||
},
|
||||
};
|
||||
|
||||
function _safe(frm, action) {
|
||||
if (frm.is_dirty()) {
|
||||
frappe.confirm(
|
||||
|
|
@ -156,21 +233,23 @@ function _call(frm, method, args, onSuccess) {
|
|||
}
|
||||
|
||||
function _show_load_data_dialog(frm) {
|
||||
if (!frm.doc.column_mappings || !frm.doc.column_mappings.length) {
|
||||
frappe.msgprint({
|
||||
title: __('No Column Mappings'),
|
||||
indicator: 'orange',
|
||||
message: __('Open the <b>File Format</b> tab first and map Excel columns to Standard Fields.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Load Data from Excel'),
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'preset',
|
||||
fieldtype: 'Link',
|
||||
options: 'Bank Integration Excel Preset',
|
||||
label: __('Excel Preset'),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'file_url',
|
||||
fieldtype: 'Attach',
|
||||
label: __('Excel File'),
|
||||
reqd: 1,
|
||||
description: __('Format is read from the File Format tab on this Bank Integration.'),
|
||||
},
|
||||
{ fieldname: 'sec_what', fieldtype: 'Section Break', label: __('What to load') },
|
||||
{
|
||||
|
|
@ -207,7 +286,6 @@ function _show_load_data_dialog(frm) {
|
|||
method: 'jey_erp.bank_integration.import_api.load_registries_from_excel',
|
||||
args: {
|
||||
file_url: values.file_url,
|
||||
preset_name: values.preset,
|
||||
bank_integration: frm.doc.name,
|
||||
load_customers: values.load_customers ? 1 : 0,
|
||||
load_suppliers: values.load_suppliers ? 1 : 0,
|
||||
|
|
@ -233,14 +311,14 @@ function _show_load_data_dialog(frm) {
|
|||
[res.total_rows, res.new_customers, res.new_suppliers, res.new_purposes]);
|
||||
if (droppedDate > 0) {
|
||||
msg += '<br><br><span class="text-warning">' +
|
||||
__("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.",
|
||||
__("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]) +
|
||||
'</span>';
|
||||
}
|
||||
if (droppedDirection > 0) {
|
||||
const sample = unknownDirections.map(frappe.utils.escape_html).join(', ') || '?';
|
||||
msg += '<br><br><span class="text-warning">' +
|
||||
__('Also skipped {0} row(s) with unknown direction values: <code>{1}</code>. Add them to Debit/Credit Values on the preset.',
|
||||
__('Also skipped {0} row(s) with unknown direction values: <code>{1}</code>. Add them to Debit/Credit Values in the File Format tab.',
|
||||
[droppedDirection, sample]) +
|
||||
'</span>';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,19 @@
|
|||
"default_payment_terms",
|
||||
"col_break_party",
|
||||
"default_territory",
|
||||
"file_format_tab",
|
||||
"sample_file",
|
||||
"ff_format_section",
|
||||
"header_row",
|
||||
"amount_mode",
|
||||
"direction_debit_values",
|
||||
"direction_credit_values",
|
||||
"use_currency_from_file",
|
||||
"ff_column_break",
|
||||
"use_custom_date_format",
|
||||
"date_format",
|
||||
"ff_columns_section",
|
||||
"column_mappings",
|
||||
"data_tab",
|
||||
"data_tab_html",
|
||||
"accounts_tab",
|
||||
|
|
@ -145,6 +158,87 @@
|
|||
"label": "Default Territory",
|
||||
"options": "Territory"
|
||||
},
|
||||
{
|
||||
"fieldname": "file_format_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "File Format"
|
||||
},
|
||||
{
|
||||
"description": "Upload a sample bank statement 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": "ff_format_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Format"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "1-indexed row number containing column headers",
|
||||
"fieldname": "header_row",
|
||||
"fieldtype": "Int",
|
||||
"label": "Header Row"
|
||||
},
|
||||
{
|
||||
"default": "Separate debit/credit columns",
|
||||
"fieldname": "amount_mode",
|
||||
"fieldtype": "Select",
|
||||
"label": "Amount Mode",
|
||||
"options": "Single column with sign\nSeparate debit/credit columns\nSingle column + direction column"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.amount_mode == 'Single column + direction column'",
|
||||
"description": "Comma-separated values from the Direction column that mean DEBIT (outflow). Case-insensitive. Example: DR, Debit, Məxaric, -",
|
||||
"fieldname": "direction_debit_values",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Debit Values"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.amount_mode == 'Single column + direction column'",
|
||||
"description": "Comma-separated values from the Direction column that mean CREDIT (inflow). Case-insensitive. Example: CR, Credit, Mədaxil, +",
|
||||
"fieldname": "direction_credit_values",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Credit Values"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Off: every imported Bank Transaction gets its Bank Account's currency.\nOn: the parsed Currency column on each row is used, falling back to the Bank Account's currency when empty. Enable only for multi-currency Bank Accounts where the statement actually carries per-row currency.",
|
||||
"fieldname": "use_currency_from_file",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Currency From File"
|
||||
},
|
||||
{
|
||||
"fieldname": "ff_column_break",
|
||||
"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": "ff_columns_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Column Mappings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_mappings",
|
||||
"fieldtype": "Table",
|
||||
"label": "Column Mappings",
|
||||
"options": "Bank Integration Excel Column Mapping"
|
||||
},
|
||||
{
|
||||
"fieldname": "data_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
|
|
@ -285,7 +379,7 @@
|
|||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-08 00:00:00.000000",
|
||||
"modified": "2026-05-18 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Jey Erp",
|
||||
"name": "Bank Integration",
|
||||
|
|
|
|||
|
|
@ -2,10 +2,23 @@
|
|||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
# Standard fields the parser needs depending on the chosen amount_mode.
|
||||
# Date + Reference Number are always required.
|
||||
_REQUIRED_BY_MODE = {
|
||||
"Single column with sign": ("Date", "Reference Number", "Amount"),
|
||||
"Separate debit/credit columns": ("Date", "Reference Number", "Debit", "Credit"),
|
||||
"Single column + direction column": ("Date", "Reference Number", "Amount", "Direction"),
|
||||
}
|
||||
|
||||
|
||||
class BankIntegration(Document):
|
||||
def validate(self):
|
||||
self._validate_file_format()
|
||||
|
||||
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
|
||||
|
|
@ -13,6 +26,33 @@ class BankIntegration(Document):
|
|||
self._sync_customer_statuses()
|
||||
self._sync_supplier_statuses()
|
||||
|
||||
def _validate_file_format(self):
|
||||
# Only validate when the user has started filling in the File Format tab.
|
||||
# An empty Bank Integration is allowed (e.g. just created, before any setup).
|
||||
if not (self.column_mappings or self.amount_mode or self.header_row):
|
||||
return
|
||||
|
||||
mode = self.amount_mode or "Separate debit/credit columns"
|
||||
mapped = {
|
||||
(row.standard_field or "").strip()
|
||||
for row in (self.column_mappings or [])
|
||||
if row.excel_column and row.standard_field
|
||||
}
|
||||
required = _REQUIRED_BY_MODE.get(mode, ())
|
||||
missing = [f for f in required if f not in mapped]
|
||||
if mapped and missing:
|
||||
frappe.throw(_(
|
||||
"Amount Mode '{0}' requires these Standard Fields in the Column Mappings table: {1}. "
|
||||
"Add a row for each missing field."
|
||||
).format(mode, ", ".join(missing)))
|
||||
|
||||
if mode == "Single column + direction column":
|
||||
if not (self.direction_debit_values or "").strip() and not (self.direction_credit_values or "").strip():
|
||||
frappe.throw(_(
|
||||
"Mode 'Single column + direction column' requires at least one of "
|
||||
"'Debit Values' or 'Credit Values' to be filled (comma-separated)."
|
||||
))
|
||||
|
||||
def _sync_customer_statuses(self):
|
||||
mapped_names = {
|
||||
row.bi_customer_name
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@
|
|||
|
||||
frappe.ui.form.on("Bank Integration Excel Preset", {
|
||||
refresh(frm) {
|
||||
frm.dashboard.add_comment(
|
||||
__("This doctype is deprecated. File format configuration now lives directly on the Bank Integration record, under the <b>File Format</b> tab."),
|
||||
"orange",
|
||||
true
|
||||
);
|
||||
|
||||
BIPreset.applySampleHeaders(frm);
|
||||
|
||||
if (frm.doc.sample_file) {
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@
|
|||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-13 00:00:00.000000",
|
||||
"modified": "2026-05-18 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Jey Erp",
|
||||
"name": "Bank Integration Excel Preset",
|
||||
|
|
@ -135,25 +135,12 @@
|
|||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1
|
||||
"role": "System Manager"
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
|
|
|||
|
|
@ -20,24 +20,10 @@
|
|||
|
||||
const BIExcelImport = {
|
||||
showDialog(listview) {
|
||||
Promise.all([
|
||||
frappe.db.get_list('Bank Integration Excel Preset', {
|
||||
fields: ['name', 'preset_name', 'description'],
|
||||
limit: 200,
|
||||
}),
|
||||
frappe.db.get_list('Bank Integration', {
|
||||
fields: ['name', 'bank_name'],
|
||||
limit: 100,
|
||||
}),
|
||||
]).then(([presets, integrations]) => {
|
||||
if (!presets || !presets.length) {
|
||||
frappe.msgprint({
|
||||
title: __('No Presets'),
|
||||
indicator: 'orange',
|
||||
message: __('Create at least one Bank Integration Excel Preset first.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
frappe.db.get_list('Bank Integration', {
|
||||
fields: ['name', 'bank_name'],
|
||||
limit: 100,
|
||||
}).then((integrations) => {
|
||||
if (!integrations || !integrations.length) {
|
||||
frappe.msgprint({
|
||||
title: __('No Bank Integration'),
|
||||
|
|
@ -46,29 +32,21 @@ const BIExcelImport = {
|
|||
});
|
||||
return;
|
||||
}
|
||||
BIExcelImport._showSetupDialog(presets, integrations, listview);
|
||||
BIExcelImport._showSetupDialog(integrations, listview);
|
||||
});
|
||||
},
|
||||
|
||||
_showSetupDialog(presets, integrations, listview) {
|
||||
const presetOptions = ['', ...presets.map(p => p.name)].join('\n');
|
||||
|
||||
_showSetupDialog(integrations, listview) {
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Load Bank Transactions from Excel'),
|
||||
fields: [
|
||||
{
|
||||
fieldname: 'preset',
|
||||
fieldtype: 'Select',
|
||||
label: __('Excel Preset'),
|
||||
options: presetOptions,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: 'bank_integration',
|
||||
fieldtype: 'Link',
|
||||
label: __('Bank Integration'),
|
||||
options: 'Bank Integration',
|
||||
reqd: 1,
|
||||
description: __('Format and column mappings come from the chosen Bank Integration.'),
|
||||
},
|
||||
{
|
||||
fieldname: 'bank_account',
|
||||
|
|
@ -100,7 +78,6 @@ const BIExcelImport = {
|
|||
method: 'jey_erp.bank_integration.import_api.parse_excel_for_preview',
|
||||
args: {
|
||||
file_url: values.file_url,
|
||||
preset_name: values.preset,
|
||||
bank_integration: values.bank_integration,
|
||||
bank_account: values.bank_account,
|
||||
},
|
||||
|
|
@ -122,15 +99,15 @@ const BIExcelImport = {
|
|||
const emptyRefCount = r.message.empty_ref_count || 0;
|
||||
|
||||
if (droppedDate > 0 && !txns.length) {
|
||||
BIExcelImport._showDateParseHelp(droppedDate, values.preset);
|
||||
BIExcelImport._showDateParseHelp(droppedDate, values.bank_integration);
|
||||
return;
|
||||
}
|
||||
if (droppedDirection > 0 && !txns.length) {
|
||||
BIExcelImport._showDirectionHelp(droppedDirection, unknownDirections, values.preset);
|
||||
BIExcelImport._showDirectionHelp(droppedDirection, unknownDirections, values.bank_integration);
|
||||
return;
|
||||
}
|
||||
if (droppedAmount > 0 && !txns.length) {
|
||||
BIExcelImport._showAmountHelp(droppedAmount, values.preset);
|
||||
BIExcelImport._showAmountHelp(droppedAmount, values.bank_integration);
|
||||
return;
|
||||
}
|
||||
if (!txns.length) {
|
||||
|
|
@ -139,7 +116,7 @@ const BIExcelImport = {
|
|||
indicator: 'blue',
|
||||
message: skipped > 0
|
||||
? __('All {0} parsed rows are duplicates.', [skipped])
|
||||
: __('No transactions found in the file. Check that the Header Row on the preset points at the actual header line.'),
|
||||
: __('No transactions found in the file. Check that the Header Row in the File Format tab points at the actual header line.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -159,7 +136,7 @@ const BIExcelImport = {
|
|||
if (droppedDate > 0) {
|
||||
table += '<div class="alert alert-warning">' +
|
||||
__('Dropped {0} row(s) — the date could not be parsed.', [droppedDate]) + ' ' +
|
||||
__("Open the Bank Integration Excel Preset, enable 'Use Custom Date Format', and set the exact format your bank uses.") +
|
||||
__("Open the Bank Integration's File Format tab, enable 'Use Custom Date Format', and set the exact format your bank uses.") +
|
||||
'</div>';
|
||||
}
|
||||
if (droppedDirection > 0) {
|
||||
|
|
@ -167,7 +144,7 @@ const BIExcelImport = {
|
|||
table += '<div class="alert alert-warning">' +
|
||||
__('Dropped {0} row(s) — unknown direction value(s):', [droppedDirection]) +
|
||||
' <code>' + (sample || '?') + '</code>. ' +
|
||||
__('Open the preset and add these to Debit Values or Credit Values.') +
|
||||
__('Open the File Format tab and add these to Debit Values or Credit Values.') +
|
||||
'</div>';
|
||||
}
|
||||
if (droppedAmount > 0) {
|
||||
|
|
@ -292,7 +269,6 @@ const BIExcelImport = {
|
|||
txn_list: JSON.stringify(selected),
|
||||
bank_integration: values.bank_integration,
|
||||
bank_account: values.bank_account,
|
||||
preset_name: values.preset,
|
||||
},
|
||||
error() {
|
||||
frappe.realtime.off('bi_bt_import_progress');
|
||||
|
|
@ -306,9 +282,9 @@ const BIExcelImport = {
|
|||
});
|
||||
},
|
||||
|
||||
_showDateParseHelp(droppedCount, presetName) {
|
||||
const openPreset = function () {
|
||||
frappe.set_route('Form', 'Bank Integration Excel Preset', presetName);
|
||||
_showDateParseHelp(droppedCount, bankIntegrationName) {
|
||||
const openForm = function () {
|
||||
frappe.set_route('Form', 'Bank Integration', bankIntegrationName);
|
||||
};
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Date Format Not Recognized'),
|
||||
|
|
@ -319,19 +295,19 @@ const BIExcelImport = {
|
|||
__('All {0} row(s) were dropped because the date column could not be parsed.', [droppedCount]) +
|
||||
'</div>' +
|
||||
'<p>' + __("The parser tries common formats like <code>2026-01-31</code>, <code>31.01.2026</code>, <code>01/31/2026</code> automatically.") + '</p>' +
|
||||
'<p>' + __("If your bank uses a different format, open the preset and enable <b>Use Custom Date Format</b>, then enter the exact Python strftime format (e.g. <code>%d-%b-%Y</code> for <code>31-Jan-2026</code>).") + '</p>',
|
||||
'<p>' + __("If your bank uses a different format, open the Bank Integration's File Format tab and enable <b>Use Custom Date Format</b>, then enter the exact Python strftime format (e.g. <code>%d-%b-%Y</code> for <code>31-Jan-2026</code>).") + '</p>',
|
||||
}],
|
||||
primary_action_label: __('Open Preset'),
|
||||
primary_action() { d.hide(); openPreset(); },
|
||||
primary_action_label: __('Open Bank Integration'),
|
||||
primary_action() { d.hide(); openForm(); },
|
||||
secondary_action_label: __('Close'),
|
||||
secondary_action() { d.hide(); },
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
|
||||
_showAmountHelp(droppedCount, presetName) {
|
||||
const openPreset = function () {
|
||||
frappe.set_route('Form', 'Bank Integration Excel Preset', presetName);
|
||||
_showAmountHelp(droppedCount, bankIntegrationName) {
|
||||
const openForm = function () {
|
||||
frappe.set_route('Form', 'Bank Integration', bankIntegrationName);
|
||||
};
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __('Amount Column Missing or Empty'),
|
||||
|
|
@ -341,20 +317,20 @@ const BIExcelImport = {
|
|||
'<div class="alert alert-danger">' +
|
||||
__('All {0} row(s) were dropped because the amount could not be read.', [droppedCount]) +
|
||||
'</div>' +
|
||||
'<p>' + __('Most likely the <b>Amount</b> column (or <b>Debit</b>/<b>Credit</b>, depending on Amount Mode) is not mapped to an Excel header in the preset.') + '</p>' +
|
||||
'<p>' + __('Open the preset and check the Column Mappings table — Standard Field values must include all columns required by the chosen Amount Mode.') + '</p>',
|
||||
'<p>' + __('Most likely the <b>Amount</b> column (or <b>Debit</b>/<b>Credit</b>, depending on Amount Mode) is not mapped to an Excel header in the File Format tab.') + '</p>' +
|
||||
'<p>' + __('Open the Bank Integration and check the Column Mappings table — Standard Field values must include all columns required by the chosen Amount Mode.') + '</p>',
|
||||
}],
|
||||
primary_action_label: __('Open Preset'),
|
||||
primary_action() { d.hide(); openPreset(); },
|
||||
primary_action_label: __('Open Bank Integration'),
|
||||
primary_action() { d.hide(); openForm(); },
|
||||
secondary_action_label: __('Close'),
|
||||
secondary_action() { d.hide(); },
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
|
||||
_showDirectionHelp(droppedCount, unknownDirections, presetName) {
|
||||
const openPreset = function () {
|
||||
frappe.set_route('Form', 'Bank Integration Excel Preset', presetName);
|
||||
_showDirectionHelp(droppedCount, unknownDirections, bankIntegrationName) {
|
||||
const openForm = function () {
|
||||
frappe.set_route('Form', 'Bank Integration', bankIntegrationName);
|
||||
};
|
||||
const sample = (unknownDirections || []).map(frappe.utils.escape_html).join(', ') || '?';
|
||||
const d = new frappe.ui.Dialog({
|
||||
|
|
@ -363,13 +339,13 @@ const BIExcelImport = {
|
|||
fieldtype: 'HTML',
|
||||
options:
|
||||
'<div class="alert alert-danger">' +
|
||||
__('All {0} row(s) were dropped because the Direction column contained values not configured on the preset.', [droppedCount]) +
|
||||
__('All {0} row(s) were dropped because the Direction column contained values not configured in the File Format tab.', [droppedCount]) +
|
||||
'</div>' +
|
||||
'<p>' + __('Unrecognised values:') + ' <code>' + sample + '</code></p>' +
|
||||
'<p>' + __('Open the preset and add these to <b>Debit Values</b> (for outflows) or <b>Credit Values</b> (for inflows), comma-separated.') + '</p>',
|
||||
'<p>' + __('Open the Bank Integration and add these to <b>Debit Values</b> (for outflows) or <b>Credit Values</b> (for inflows), comma-separated.') + '</p>',
|
||||
}],
|
||||
primary_action_label: __('Open Preset'),
|
||||
primary_action() { d.hide(); openPreset(); },
|
||||
primary_action_label: __('Open Bank Integration'),
|
||||
primary_action() { d.hide(); openForm(); },
|
||||
secondary_action_label: __('Close'),
|
||||
secondary_action() { d.hide(); },
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue