diff --git a/sepaxml/shared.py b/sepaxml/shared.py index 6bdff06..04b2062 100644 --- a/sepaxml/shared.py +++ b/sepaxml/shared.py @@ -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 @@ -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') @@ -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"" + ET.tostring( - self._xml, "utf-8") + document._xml, "utf-8") if pretty_print: from xml.dom import minidom diff --git a/tests/test_repeated_export.py b/tests/test_repeated_export.py new file mode 100644 index 0000000..d69f3c0 --- /dev/null +++ b/tests/test_repeated_export.py @@ -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")