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 = { frm.get_field("import_file").df.options = {
restrictions: { 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) { refresh(frm) {
frm.page.hide_icon_group(); frm.page.hide_icon_group();
frm.trigger("toggle_mt940_note");
frm.trigger("update_indicators"); frm.trigger("update_indicators");
frm.trigger("import_file"); frm.trigger("import_file");
frm.trigger("show_import_log"); 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) { show_report_error_button(frm) {
if (frm.doc.status === "Error") { if (frm.doc.status === "Error") {
frappe.db frappe.db
@@ -290,6 +309,26 @@ frappe.ui.form.on("Bank Statement Import", {
.html(__("Loading import file...")) .html(__("Loading import file..."))
.appendTo(frm.get_field("import_preview").$wrapper); .appendTo(frm.get_field("import_preview").$wrapper);
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();
});
}
},
() => {
frm.call({ frm.call({
method: "get_preview_from_template", method: "get_preview_from_template",
args: { args: {
@@ -308,6 +347,8 @@ frappe.ui.form.on("Bank Statement Import", {
frm.events.show_import_warnings(frm, preview_data); frm.events.show_import_warnings(frm, preview_data);
}); });
}, },
]);
},
// method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template', // method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',
show_import_preview(frm, preview_data) { show_import_preview(frm, preview_data) {

View File

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

View File

@@ -3,15 +3,19 @@
import csv import csv
import io
import json import json
import re import re
from datetime import date, datetime
import frappe import frappe
import mt940
import openpyxl import openpyxl
from frappe import _ from frappe import _
from frappe.core.doctype.data_import.data_import import DataImport from frappe.core.doctype.data_import.data_import import DataImport
from frappe.core.doctype.data_import.importer import Importer, ImportFile from frappe.core.doctype.data_import.importer import Importer, ImportFile
from frappe.utils.background_jobs import enqueue 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 frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html
from openpyxl.styles import Font from openpyxl.styles import Font
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
@@ -35,6 +39,7 @@ class BankStatementImport(DataImport):
delimiter_options: DF.Data | None delimiter_options: DF.Data | None
google_sheets_url: DF.Data | None google_sheets_url: DF.Data | None
import_file: DF.Attach | None import_file: DF.Attach | None
import_mt940_fromat: DF.Check
import_type: DF.Literal["", "Insert New Records", "Update Existing Records"] import_type: DF.Literal["", "Insert New Records", "Update Existing Records"]
mute_emails: DF.Check mute_emails: DF.Check
reference_doctype: DF.Link reference_doctype: DF.Link
@@ -43,6 +48,7 @@ class BankStatementImport(DataImport):
submit_after_import: DF.Check submit_after_import: DF.Check
template_options: DF.Code | None template_options: DF.Code | None
template_warnings: DF.Code | None template_warnings: DF.Code | None
use_csv_sniffer: DF.Check
# end: auto-generated types # end: auto-generated types
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -65,6 +71,7 @@ class BankStatementImport(DataImport):
self.template_warnings = "" self.template_warnings = ""
if self.import_file and not self.import_file.lower().endswith(".txt"):
self.validate_import_file() self.validate_import_file()
self.validate_google_sheets_url() self.validate_google_sheets_url()
@@ -104,6 +111,68 @@ class BankStatementImport(DataImport):
return None 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() @frappe.whitelist()
def get_preview_from_template(data_import, import_file=None, google_sheets_url=None): 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( 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() 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): def parse_data_from_template(raw_data):
data = [] data = []

View File

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