Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions banking/ebics/doctype/ebics_request/ebics_request.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Successful\nNo Data Available\nFailed"
"options": "Successful\nNo Data Available\nFailed\nSkipped"
},
{
"fieldname": "response",
Expand All @@ -53,7 +53,7 @@
],
"in_create": 1,
"links": [],
"modified": "2025-03-12 19:06:17.168084",
"modified": "2026-01-21 13:31:17.168084",
"modified_by": "Administrator",
"module": "EBICS",
"name": "EBICS Request",
Expand Down
63 changes: 46 additions & 17 deletions banking/ebics/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,23 @@
from banking.ebics.manager import EBICSManager, EbicsRequest
from banking.ebics.types import MT940Statement, MT940Transaction


class UnresolvedBatchTransactionError(Exception):
"""Raised when a batch transaction cannot be resolved due to missing camt.054 data.

This is a transient error - the camt.054 file may become available on a later sync.
Scheduled jobs should catch this and log a warning instead of failing the entire sync.
"""

pass


if TYPE_CHECKING:
from datetime import date

from fintech.sepa import SEPATransaction
from fintech.sepa import CAMTDocument, SEPATransaction

from banking.ebics.doctype.ebics_request.ebics_request import EBICSRequest as EBICSRequestDoc
from banking.ebics.doctype.ebics_user.ebics_user import EBICSUser
from banking.overrides.bank_transaction import CustomBankTransaction

Expand Down Expand Up @@ -88,7 +100,7 @@ def execute_ebics_download(
ebics_request: EbicsRequest,
requested_by: Literal["User", "System"],
permitted_types: list[str],
) -> dict | None:
) -> tuple[dict | None, "EBICSRequestDoc"]:
"""Execute a single EBICS download request with logging and error handling.

Args:
Expand All @@ -99,7 +111,7 @@ def execute_ebics_download(
permitted_types: List of permitted order types for validation

Returns:
dict: The downloaded XML files, or None if failed or no data available
tuple: (downloaded XML files or None, request log document)
"""
if manager.protocol_version == "H004":
validated_perms(ebics_user, permitted_types, ebics_request.order_type)
Expand Down Expand Up @@ -132,7 +144,7 @@ def execute_ebics_download(
),
}
)
return xml_files
return xml_files, request_log
except fintech.ebics.EbicsNoDataAvailable:
request_log.db_set({"status": "Successful", "response": "No Data Available"})
except Exception as e:
Expand All @@ -143,7 +155,7 @@ def execute_ebics_download(
reference_name=ebics_user,
)

return None
return None, request_log


def sync_ebics_transactions(
Expand All @@ -164,15 +176,19 @@ def sync_ebics_transactions(
permitted_types = manager.get_permitted_order_types()

# Download main statements
main_xml = execute_ebics_download(manager, user.name, main_request, requested_by, permitted_types)
main_xml, main_request_log = execute_ebics_download(
manager, user.name, main_request, requested_by, permitted_types
)
if not main_xml:
return

# Download batch transactions if enabled
batch_xml = None
if user.download_batch_transactions:
batch_request = request_map["batch"]
batch_xml = execute_ebics_download(manager, user.name, batch_request, requested_by, permitted_types)
batch_xml, _batch_request_log = execute_ebics_download(
manager, user.name, batch_request, requested_by, permitted_types
)
# Continue even if batch download fails - main_xml is what matters

# Keep the request logs, no matter what happens next.
Expand All @@ -182,6 +198,19 @@ def sync_ebics_transactions(
# want to rollback the entire transaction and report an error.
try:
import_ebics_json(user, main_xml, batch_xml)
except UnresolvedBatchTransactionError:
# This is a transient error - camt.054 may become available on the next sync.
# Log a warning instead of an error and tell the bank to resend the data.
frappe.db.rollback()
main_request_log.db_set("status", "Skipped")
Comment thread
barredterra marked this conversation as resolved.
frappe.log_error(
title=_("Banking Warning"),
message=_("Unable to resolve batch transaction: camt.054 not available. Please try again later."),
reference_doctype="EBICS User",
reference_name=user.name,
)
Comment thread
barredterra marked this conversation as resolved.
manager.confirm_download(success=False)
return
except Exception:
frappe.db.rollback()
frappe.log_error(
Expand Down Expand Up @@ -256,7 +285,7 @@ def get_bank_account(iban: str, bank: str, company: str) -> str | None:


def process_camt_document(
camt_document,
camt_document: "CAMTDocument",
bank_account: str,
company: "str | None" = None,
earliest_date: "date | None" = None,
Expand All @@ -272,14 +301,14 @@ def process_camt_document(

transaction_id = get_transaction_id(transaction)

if (
transaction.batch
and (split_batch_transactions or len(transaction) == 1)
and len(transaction) >= 1
):
# Split batch transactions into sub-transactions, based on info
# from camt.054 that is sometimes available.
# If that's not possible, create a single transaction
if transaction.batch and split_batch_transactions:
# Split batch transactions into sub-transactions, based on info from camt.054.

if len(transaction) == 0:
# camt.054 might become available at a later time than camt.053.
# In this case, we want to block the processing of camt.053 until camt.054 is available.
raise UnresolvedBatchTransactionError()

for sub_transaction_index, sub_transaction in enumerate(transaction):
sub_transaction_id = get_transaction_id(sub_transaction, sub_transaction_index)
create_sepa_bank_transaction(
Expand Down Expand Up @@ -415,7 +444,7 @@ def upload_camt_file():
from fintech.sepa import CAMTDocument

camt_document = CAMTDocument(file_bytes.decode())
process_camt_document(camt_document, bank_account)
process_camt_document(camt_document, bank_account, split_batch_transactions=False)


def decode_mt940_bytes(file_bytes: bytes) -> str:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ readme = "README.md"
dynamic = ["version"]
dependencies = [
# frappe -- https://github.com/frappe/frappe is installed via 'bench init'
"fintech==7.8.8",
"fintech==7.9.1",
"kontocheck~=6.15.0",
]

Expand Down
Loading