diff --git a/jey_erp/bank_integration/excel_parser.py b/jey_erp/bank_integration/excel_parser.py index 98fd8ff..65b0d99 100644 --- a/jey_erp/bank_integration/excel_parser.py +++ b/jey_erp/bank_integration/excel_parser.py @@ -107,40 +107,73 @@ def parse_excel(file_url, preset_name): 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" + debit_aliases = _split_aliases(preset.get("direction_debit_values")) + credit_aliases = _split_aliases(preset.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) + ) + data_start = header_idx + 1 transactions = [] dropped_date = 0 dropped_amount = 0 + dropped_direction = 0 + unknown_directions = set() 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, reason = _parse_row(raw_row, idx_to_standard, custom_format, amount_mode) + parsed, reason, ctx = _parse_row( + raw_row, idx_to_standard, custom_format, amount_mode, + debit_aliases, credit_aliases, + ) if parsed: transactions.append(parsed) elif reason == "date": dropped_date += 1 elif reason == "amount": dropped_amount += 1 + elif reason == "direction": + dropped_direction += 1 + if ctx: + unknown_directions.add(ctx) return { "transactions": transactions, "dropped_date": dropped_date, "dropped_amount": dropped_amount, + "dropped_direction": dropped_direction, + "unknown_directions": sorted(unknown_directions)[:20], } finally: wb.close() +def _split_aliases(raw): + """Parse a comma-separated alias list from a preset field into an uppercase set.""" + if not raw: + return set() + return {part.strip().upper() for part in str(raw).split(",") if part.strip()} + + 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"}.""" +def _parse_row(raw_row, idx_to_standard, custom_format, amount_mode, + debit_aliases=None, credit_aliases=None): + """Returns (record_dict_or_None, reason, ctx). + + reason in {"", "date", "amount", "direction"}; ctx is the offending value + when applicable (used to show the user which Direction strings were not + recognised so they can add them to the preset). + """ record = { "ref_no": "", "date": None, @@ -201,33 +234,33 @@ def _parse_row(raw_row, idx_to_standard, custom_format, amount_mode): record["amount"] = abs(flt(credit_val)) record["drcr"] = "C" else: - return None, "amount" + return None, "amount", None elif amount_mode == "Single column with sign": if amount_val is None: - return None, "amount" + return None, "amount", None 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, "amount" + return None, "amount", None record["amount"] = abs(flt(amount_val)) - if direction_val in ("D", "DR", "DEBIT", "DT", "OUT", "PAY", "WITHDRAWAL", "-", - "MƏXARIC", "MEXARIC", "ÇIXIB", "CIXIB", "XƏRC", "XERC"): + if debit_aliases and direction_val in debit_aliases: record["drcr"] = "D" - elif direction_val in ("C", "CR", "CREDIT", "CT", "IN", "RECEIVE", "DEPOSIT", "+", - "MƏDAXIL", "MEDAXIL", "DAXIL", "DAXIL OLUB", "GƏLIR", "GELIR"): + elif credit_aliases and direction_val in credit_aliases: record["drcr"] = "C" else: - record["drcr"] = "D" + # Unknown direction value — surface to the user so they can add it + # to the preset, instead of silently guessing. + return None, "direction", direction_val 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" + return None, "date" if saw_date_input else "amount", None if record["amount"] == 0: - return None, "amount" + return None, "amount", None - return record, "" + return record, "", None def _parse_date(value, custom_format): diff --git a/jey_erp/bank_integration/import_api.py b/jey_erp/bank_integration/import_api.py index 91cf8fa..e4a3743 100644 --- a/jey_erp/bank_integration/import_api.py +++ b/jey_erp/bank_integration/import_api.py @@ -16,6 +16,8 @@ def parse_excel_for_preview(file_url, preset_name, bank_integration, bank_accoun transactions = result["transactions"] dropped_date = result.get("dropped_date", 0) dropped_amount = result.get("dropped_amount", 0) + dropped_direction = result.get("dropped_direction", 0) + unknown_directions = result.get("unknown_directions", []) if not transactions: return { @@ -25,6 +27,8 @@ def parse_excel_for_preview(file_url, preset_name, bank_integration, bank_accoun "total_parsed": 0, "dropped_date": dropped_date, "dropped_amount": dropped_amount, + "dropped_direction": dropped_direction, + "unknown_directions": unknown_directions, } ref_nos = [t["ref_no"] for t in transactions if t.get("ref_no")] @@ -56,6 +60,8 @@ def parse_excel_for_preview(file_url, preset_name, bank_integration, bank_accoun "total_parsed": len(transactions), "dropped_date": dropped_date, "dropped_amount": dropped_amount, + "dropped_direction": dropped_direction, + "unknown_directions": unknown_directions, } @@ -329,6 +335,8 @@ def load_registries_from_excel(file_url, preset_name, bank_integration, transactions = result["transactions"] dropped_date = result.get("dropped_date", 0) dropped_amount = result.get("dropped_amount", 0) + dropped_direction = result.get("dropped_direction", 0) + unknown_directions = result.get("unknown_directions", []) new_c = new_s = new_p = 0 for txn in transactions: @@ -359,4 +367,6 @@ def load_registries_from_excel(file_url, preset_name, bank_integration, "new_purposes": new_p, "dropped_date": dropped_date, "dropped_amount": dropped_amount, + "dropped_direction": dropped_direction, + "unknown_directions": unknown_directions, } 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 cd08bb5..584a678 100644 --- a/jey_erp/jey_erp/doctype/bank_integration/bank_integration.js +++ b/jey_erp/jey_erp/doctype/bank_integration/bank_integration.js @@ -227,6 +227,8 @@ function _show_load_data_dialog(frm) { const res = r.message; 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) { @@ -235,6 +237,13 @@ function _show_load_data_dialog(frm) { [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 on the preset.', + [droppedDirection, sample]) + + ''; + } if (droppedAmount > 0) { msg += '
' + __('Also skipped {0} row(s) with missing/zero amount.', [droppedAmount]) + @@ -242,7 +251,7 @@ function _show_load_data_dialog(frm) { } frappe.msgprint({ title: __('Data Loaded'), - indicator: droppedDate > 0 ? 'orange' : 'green', + indicator: (droppedDate > 0 || droppedDirection > 0) ? 'orange' : 'green', message: msg, }); frm.reload_doc(); 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 b4d1723..61551a8 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,6 +10,8 @@ "format_section", "header_row", "amount_mode", + "direction_debit_values", + "direction_credit_values", "column_break_1", "use_custom_date_format", "date_format", @@ -54,6 +56,20 @@ "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" + }, { "fieldname": "column_break_1", "fieldtype": "Column Break" diff --git a/jey_erp/jey_erp/doctype/bank_integration_excel_preset/bank_integration_excel_preset.py b/jey_erp/jey_erp/doctype/bank_integration_excel_preset/bank_integration_excel_preset.py index 7d53514..cb58532 100644 --- a/jey_erp/jey_erp/doctype/bank_integration_excel_preset/bank_integration_excel_preset.py +++ b/jey_erp/jey_erp/doctype/bank_integration_excel_preset/bank_integration_excel_preset.py @@ -1,8 +1,16 @@ # Copyright (c) 2026, JeyERP and contributors # For license information, please see license.txt +import frappe +from frappe import _ from frappe.model.document import Document class BankIntegrationExcelPreset(Document): - pass + def validate(self): + if self.amount_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)." + )) diff --git a/jey_erp/public/js/bank_transaction_list.js b/jey_erp/public/js/bank_transaction_list.js index 5e161f4..fa5f93d 100644 --- a/jey_erp/public/js/bank_transaction_list.js +++ b/jey_erp/public/js/bank_transaction_list.js @@ -117,11 +117,17 @@ const BIExcelImport = { 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 || []; if (droppedDate > 0 && !txns.length) { BIExcelImport._showDateParseHelp(droppedDate, values.preset); return; } + if (droppedDirection > 0 && !txns.length) { + BIExcelImport._showDirectionHelp(droppedDirection, unknownDirections, values.preset); + return; + } if (!txns.length) { frappe.msgprint({ title: __('Nothing to Import'), @@ -132,12 +138,13 @@ const BIExcelImport = { }); return; } - BIExcelImport._showPreview(txns, skipped, values, listview, droppedDate, droppedAmount); + BIExcelImport._showPreview(txns, skipped, values, listview, + droppedDate, droppedAmount, droppedDirection, unknownDirections); }, }); }, - _showPreview(txns, skipped, values, listview, droppedDate, droppedAmount) { + _showPreview(txns, skipped, values, listview, droppedDate, droppedAmount, droppedDirection, unknownDirections) { let table = '
'; if (skipped > 0) { table += '
' + @@ -150,6 +157,14 @@ const BIExcelImport = { __("Open the Bank Integration Excel Preset, enable 'Use Custom Date Format', and set the exact format your bank uses.") + '
'; } + if (droppedDirection > 0) { + const sample = (unknownDirections || []).map(frappe.utils.escape_html).join(', '); + table += '
' + + __('Dropped {0} row(s) — unknown direction value(s):', [droppedDirection]) + + ' ' + (sample || '?') + '. ' + + __('Open the preset and add these to Debit Values or Credit Values.') + + '
'; + } if (droppedAmount > 0) { table += '
' + __('Dropped {0} row(s) — missing or zero amount.', [droppedAmount]) + @@ -301,6 +316,30 @@ const BIExcelImport = { d.show(); }, + _showDirectionHelp(droppedCount, unknownDirections, presetName) { + const openPreset = function () { + frappe.set_route('Form', 'Bank Integration Excel Preset', presetName); + }; + const sample = (unknownDirections || []).map(frappe.utils.escape_html).join(', ') || '?'; + const d = new frappe.ui.Dialog({ + title: __('Unknown Direction Values'), + fields: [{ + fieldtype: 'HTML', + options: + '
' + + __('All {0} row(s) were dropped because the Direction column contained values not configured on the preset.', [droppedCount]) + + '
' + + '

' + __('Unrecognised values:') + ' ' + sample + '

' + + '

' + __('Open the preset and add these to Debit Values (for outflows) or Credit Values (for inflows), comma-separated.') + '

', + }], + primary_action_label: __('Open Preset'), + primary_action() { d.hide(); openPreset(); }, + secondary_action_label: __('Close'), + secondary_action() { d.hide(); }, + }); + d.show(); + }, + _closeProgress() { try { if (frappe.cur_progress) {