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:
@@ -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) {
|
||||||
|
|||||||
@@ -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": [],
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user