Skip to content
Open
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
17 changes: 12 additions & 5 deletions sepaxml/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"""
import xml.etree.ElementTree as ET
from collections import OrderedDict
from copy import copy, deepcopy

from .utils import decimal_str_to_int, int_to_decimal_str, make_msg_id
from .validation import try_valid_xml
Expand Down Expand Up @@ -84,25 +85,31 @@ def export(self, validate=True, pretty_print=False):
Method to output the xml as string. It will finalize the batches and
then calculate the checksums (amount sum and transaction count),
fill these into the group header and output the XML.
Exporting repeatedly does not add payments or change the totals.

@param pretty_print: uses Python's xml.dom.minidom.Node.toprettyxml to make it easier to read for humans
"""
self._finalize_batch()
# Finalization appends batches and fills the group totals. Keep those
# changes on a working document so exporting again starts from the
# original payments, including any added since the previous export.
document = copy(self)
document._xml = deepcopy(self._xml)
document._finalize_batch()

ctrl_sum_total = 0
nb_of_txs_total = 0

for ctrl_sum in self._xml.iter('CtrlSum'):
for ctrl_sum in document._xml.iter('CtrlSum'):
if ctrl_sum.text is None:
continue
ctrl_sum_total += decimal_str_to_int(ctrl_sum.text)

for nb_of_txs in self._xml.iter('NbOfTxs'):
for nb_of_txs in document._xml.iter('NbOfTxs'):
if nb_of_txs.text is None:
continue
nb_of_txs_total += int(nb_of_txs.text)

n = self._xml.find(self.root_el)
n = document._xml.find(self.root_el)
GrpHdr_node = n.find('GrpHdr')
CtrlSum_node = GrpHdr_node.find('CtrlSum')
NbOfTxs_node = GrpHdr_node.find('NbOfTxs')
Expand All @@ -112,7 +119,7 @@ def export(self, validate=True, pretty_print=False):
# Prepending the XML version is hacky, but cElementTree only offers this
# automatically if you write to a file, which we don't necessarily want.
out = b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + ET.tostring(
self._xml, "utf-8")
document._xml, "utf-8")

if pretty_print:
from xml.dom import minidom
Expand Down
56 changes: 56 additions & 0 deletions tests/test_repeated_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import datetime
from decimal import Decimal
from xml.etree import ElementTree as ET

import pytest

from sepaxml import SepaDD, SepaTransfer


@pytest.mark.parametrize("batch", [True, False])
@pytest.mark.parametrize("payment_class,schema,transaction_tag", [
(SepaDD, "pain.008.001.02", "DrctDbtTxInf"),
(SepaTransfer, "pain.001.001.03", "CdtTrfTxInf"),
])
def test_repeated_export_preserves_payments(batch, payment_class, schema, transaction_tag):
document = payment_class({
"name": "TestCreditor",
"IBAN": "NL50BANK1234567890",
"BIC": "BANKNL2A",
"batch": batch,
"creditor_id": "DE26ZZZ00000000000",
"currency": "EUR",
}, schema=schema)
payment = {
"name": "Test Debtor",
"IBAN": "NL50BANK1234567890",
"BIC": "BANKNL2A",
"amount": 1012,
"type": "RCUR",
"execution_date": datetime.date(2026, 1, 5),
"collection_date": datetime.date(2026, 1, 5),
"mandate_id": "1234",
"mandate_date": datetime.date(2025, 1, 5),
"description": "Test transaction",
}
document.add_payment(payment.copy())
document.add_payment(dict(payment, amount=5000))
namespace = {"s": "urn:iso:std:iso:20022:tech:xsd:" + schema}

def check_export(expected_count, expected_sum, pretty_print=False):
xml = ET.fromstring(document.export(pretty_print=pretty_print))
header = xml.find(".//s:GrpHdr", namespace)
assert header.find("s:NbOfTxs", namespace).text == str(expected_count)
assert header.find("s:CtrlSum", namespace).text == expected_sum
assert len(xml.findall(".//s:" + transaction_tag, namespace)) == expected_count
batches = xml.findall(".//s:PmtInf", namespace)
assert sum(int(b.find("s:NbOfTxs", namespace).text) for b in batches) == expected_count
assert sum(Decimal(b.find("s:CtrlSum", namespace).text) for b in batches) == Decimal(expected_sum)

check_export(2, "60.12")
check_export(2, "60.12", pretty_print=True)
check_export(2, "60.12")

document.add_payment(dict(payment, amount=123))
check_export(3, "61.35")
check_export(3, "61.35")