Merge pull request #48013 from aerele/mt940-bank-statement-import

feat(bank-statement-import): add support for uploading MT940 format b…
This commit is contained in:
ruthra kumar
2025-06-23 15:45:27 +05:30
committed by GitHub
4 changed files with 156 additions and 20 deletions

View File

@@ -70,7 +70,7 @@ frappe.ui.form.on("Bank Statement Import", {
frm.get_field("import_file").df.options = {
restrictions: {
allowed_file_types: [".csv", ".xls", ".xlsx"],
allowed_file_types: [".csv", ".xls", ".xlsx", ".TXT", ".txt"],
},
};
@@ -81,6 +81,7 @@ frappe.ui.form.on("Bank Statement Import", {
refresh(frm) {
frm.page.hide_icon_group();
frm.trigger("toggle_mt940_note");
frm.trigger("update_indicators");
frm.trigger("import_file");
frm.trigger("show_import_log");
@@ -192,6 +193,24 @@ frappe.ui.form.on("Bank Statement Import", {
});
},
import_mt940_fromat(frm) {
frm.trigger("toggle_mt940_note");
frm.save();
},
toggle_mt940_note(frm) {
if (!frm.doc.import_mt940_fromat) {
frm.set_df_property("custom_delimiters", "hidden", 0);
frm.set_df_property("google_sheets_url", "hidden", 0);
frm.set_df_property("html_5", "hidden", 0);
} else {
frm.set_df_property("custom_delimiters", "hidden", 1);
frm.set_df_property("google_sheets_url", "hidden", 1);
frm.set_df_property("html_5", "hidden", 1);
}
frm.set_value("import_mt940_fromat", frm.doc.import_mt940_fromat);
},
show_report_error_button(frm) {
if (frm.doc.status === "Error") {
frappe.db
@@ -290,23 +309,45 @@ frappe.ui.form.on("Bank Statement Import", {
.html(__("Loading import file..."))
.appendTo(frm.get_field("import_preview").$wrapper);
frm.call({
method: "get_preview_from_template",
args: {
data_import: frm.doc.name,
import_file: frm.doc.import_file,
google_sheets_url: frm.doc.google_sheets_url,
frappe.run_serially([
// Convert MT940 to CSV if .txt file
() => {
if (frm.doc.import_file && frm.doc.import_file.toLowerCase().endsWith(".txt")) {
return frm
.call({
method: "convert_mt940_to_csv",
args: {
data_import: frm.doc.name,
mt940_file_path: frm.doc.import_file,
},
})
.then((r) => {
const file_url = r.message;
frm.set_value("import_file", file_url);
frm.save();
});
}
},
error_handlers: {
TimestampMismatchError() {
// ignore this error
},
() => {
frm.call({
method: "get_preview_from_template",
args: {
data_import: frm.doc.name,
import_file: frm.doc.import_file,
google_sheets_url: frm.doc.google_sheets_url,
},
error_handlers: {
TimestampMismatchError() {
// ignore this error
},
},
}).then((r) => {
let preview_data = r.message;
frm.events.show_import_preview(frm, preview_data);
frm.events.show_import_warnings(frm, preview_data);
});
},
}).then((r) => {
let preview_data = r.message;
frm.events.show_import_preview(frm, preview_data);
frm.events.show_import_warnings(frm, preview_data);
});
]);
},
// method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',

View File

@@ -11,6 +11,7 @@
"bank_account",
"bank",
"column_break_4",
"import_mt940_fromat",
"custom_delimiters",
"delimiter_options",
"google_sheets_url",
@@ -20,6 +21,7 @@
"download_template",
"status",
"template_options",
"use_csv_sniffer",
"import_warnings_section",
"template_warnings",
"import_warnings",
@@ -207,14 +209,28 @@
"fieldname": "delimiter_options",
"fieldtype": "Data",
"label": "Delimiter options"
},
{
"default": "0",
"fieldname": "use_csv_sniffer",
"fieldtype": "Check",
"hidden": 1,
"label": "Use CSV Sniffer"
},
{
"default": "0",
"fieldname": "import_mt940_fromat",
"fieldtype": "Check",
"label": "Import MT940 Fromat"
}
],
"hide_toolbar": 1,
"links": [],
"modified": "2024-06-25 17:32:07.658250",
"modified": "2025-06-11 02:23:22.159961",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Statement Import",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
@@ -230,8 +246,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -3,15 +3,19 @@
import csv
import io
import json
import re
from datetime import date, datetime
import frappe
import mt940
import openpyxl
from frappe import _
from frappe.core.doctype.data_import.data_import import DataImport
from frappe.core.doctype.data_import.importer import Importer, ImportFile
from frappe.utils.background_jobs import enqueue
from frappe.utils.file_manager import get_file, save_file
from frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
@@ -35,6 +39,7 @@ class BankStatementImport(DataImport):
delimiter_options: DF.Data | None
google_sheets_url: DF.Data | None
import_file: DF.Attach | None
import_mt940_fromat: DF.Check
import_type: DF.Literal["", "Insert New Records", "Update Existing Records"]
mute_emails: DF.Check
reference_doctype: DF.Link
@@ -43,6 +48,7 @@ class BankStatementImport(DataImport):
submit_after_import: DF.Check
template_options: DF.Code | None
template_warnings: DF.Code | None
use_csv_sniffer: DF.Check
# end: auto-generated types
def __init__(self, *args, **kwargs):
@@ -65,8 +71,9 @@ class BankStatementImport(DataImport):
self.template_warnings = ""
self.validate_import_file()
self.validate_google_sheets_url()
if self.import_file and not self.import_file.lower().endswith(".txt"):
self.validate_import_file()
self.validate_google_sheets_url()
def start_import(self):
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
@@ -104,6 +111,68 @@ class BankStatementImport(DataImport):
return None
@frappe.whitelist()
def convert_mt940_to_csv(data_import, mt940_file_path):
doc = frappe.get_doc("Bank Statement Import", data_import)
file_doc, content = get_file(mt940_file_path)
if not is_mt940_format(content):
frappe.throw(_("The uploaded file does not appear to be in valid MT940 format."))
if is_mt940_format(content) and not doc.import_mt940_fromat:
frappe.throw(_("MT940 file detected. Please enable 'Import MT940 Format' to proceed."))
try:
transactions = mt940.parse(content)
except Exception as e:
frappe.throw(_("Failed to parse MT940 format. Error: {0}").format(str(e)))
if not transactions:
frappe.throw(_("Parsed file is not in valid MT940 format or contains no transactions."))
# Use in-memory file buffer instead of writing to temp file
csv_buffer = io.StringIO()
writer = csv.writer(csv_buffer)
headers = ["Date", "Deposit", "Withdrawal", "Description", "Reference Number", "Bank Account", "Currency"]
writer.writerow(headers)
for txn in transactions:
txn_date = getattr(txn, "date", None)
raw_date = txn.data.get("date", "")
if txn_date:
date_str = txn_date.strftime("%Y-%m-%d")
elif isinstance(raw_date, date | datetime):
date_str = raw_date.strftime("%Y-%m-%d")
else:
date_str = str(raw_date)
raw_amount = str(txn.data.get("amount", ""))
parts = raw_amount.strip().split()
amount_value = float(parts[0]) if parts else 0.0
deposit = amount_value if amount_value > 0 else ""
withdrawal = abs(amount_value) if amount_value < 0 else ""
description = txn.data.get("extra_details") or ""
reference = txn.data.get("transaction_reference") or ""
currency = txn.data.get("currency", "")
writer.writerow([date_str, deposit, withdrawal, description, reference, doc.bank_account, currency])
# Prepare in-memory CSV for upload
csv_content = csv_buffer.getvalue().encode("utf-8")
csv_buffer.close()
filename = f"{frappe.utils.now_datetime().strftime('%Y%m%d%H%M%S')}_converted_mt940.csv"
# Save to File Manager
saved_file = save_file(filename, csv_content, doc.doctype, doc.name, is_private=True, df="import_file")
return saved_file.file_url
@frappe.whitelist()
def get_preview_from_template(data_import, import_file=None, google_sheets_url=None):
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
@@ -128,6 +197,12 @@ def download_import_log(data_import_name):
return frappe.get_doc("Bank Statement Import", data_import_name).download_import_log()
def is_mt940_format(content: str) -> bool:
"""Check if the content has key MT940 tags"""
required_tags = [":20:", ":25:", ":28C:", ":61:"]
return all(tag in content for tag in required_tags)
def parse_data_from_template(raw_data):
data = []

View File

@@ -22,6 +22,9 @@ dependencies = [
# Not used directly - required by PyQRCode for PNG generation
"pypng~=0.20220715.0",
# MT940 parser for bank statements
"mt-940>=4.26.0"
]
[build-system]