Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ Limitations
* Fetching balances
* Fetching holdings
* SEPA transfers and debits (only with required TAN and with specific TAN methods)
* Supports Python 3.9+
* SEPA standing orders (fetching, creating, changing and deleting)
* Supports Python 3.11+


Security
Expand Down
10 changes: 10 additions & 0 deletions docs/developer/segments/all.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ fints.segments.saldo module
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset

fints.segments.standing_order module
------------------------------------

.. automodule:: fints.segments.standing_order
:members:
:inherited-members:
:undoc-members:
:show-inheritance:
:exclude-members: print_nested, naive_parse, find_subclass, is_unset

fints.segments.statement module
-------------------------------

Expand Down
47 changes: 47 additions & 0 deletions docs/reading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,50 @@ You can fetch the holdings of an account with the ``get_holdings`` method:
This method will return a list of ``Holding`` objects:

.. autoclass:: fints.models.Holding


.. _standing-orders:

Fetching standing orders
------------------------

You can fetch the SEPA standing orders ("Daueraufträge") of an account with the ``get_standing_orders`` method:

.. autoclass:: fints.client.FinTS3Client
:members: get_standing_orders
:noindex:

This method will return a list of ``HICDB1`` segments, one per standing order. The schedule is available in
``standing_order_details`` (see :class:`~fints.formals.StandingOrderDetails3`), a suspension, if any, in
``suspension`` (see :class:`~fints.formals.StandingOrderSuspension3`). Recipient, amount and purpose are contained in
the SEPA pain.001 message in ``sepa_pain_message``, which you can read with ``parse_sepa_transfer_pain``:

.. autofunction:: fints.utils.parse_sepa_transfer_pain

.. autoclass:: fints.models.SEPATransferPayment

The first and last execution dates are *virtual dates*, which allow 31 days in every month (e.g. 2025-04-31 for a
standing order executed on the last day of each month). They are returned as ``VirtualDate`` objects:

.. autoclass:: fints.models.VirtualDate
:members: to_date

Example:

.. code-block:: python

from fints.utils import parse_sepa_transfer_pain

standing_orders = client.get_standing_orders(account)
while isinstance(standing_orders, NeedTANResponse):
standing_orders = ask_for_tan(standing_orders)

for order in standing_orders:
details = order.standing_order_details
payment = parse_sepa_transfer_pain(order.sepa_pain_message)
print("Task ID:", order.task_id)
print("First/last execution:", details.first_execution_date, details.last_execution_date)
print("Interval:", details.interval, details.time_unit, "on day", details.execution_day)
print("Recipient:", payment.recipient_name, payment.iban, payment.bic)
print("Amount:", payment.amount.amount, payment.amount.currency)
print("Purpose:", payment.reason)
148 changes: 148 additions & 0 deletions docs/transfers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,151 @@ Full example
print(res.status)
print(res.responses)



Creating standing orders
------------------------

You can create a SEPA standing order ("Dauerauftrag") with the same convenience as a simple transfer. The schedule
is given by the date of the first execution, the time unit (monthly or weekly), the interval (every n months or weeks)
and the execution day (day of the month or of the week):

.. autoclass:: fints.client.FinTS3Client
:members: simple_create_standing_order
:noindex:

.. autoclass:: fints.formals.StandingOrderTimeUnit1
:noindex:

The first and last execution dates can be given as ``datetime.date`` or as ``VirtualDate`` (see
:ref:`standing-orders`). A virtual date also allows days beyond the end of the month, e.g.
``VirtualDate(2030, 4, 31)`` as first execution of a standing order executed on the 31st of each month.

Which intervals, execution days and advance notices the bank allows, e.g. to only offer allowed values to the user,
can be found out with ``get_standing_order_parameters``:

.. autoclass:: fints.client.FinTS3Client
:members: get_standing_order_parameters
:noindex:

If you want to supply your own SEPA XML, use ``create_standing_order``. The requested execution date
(``ReqdExctnDt``) in the pain message must be ``1999-01-01``, as for a simple transfer:

.. autoclass:: fints.client.FinTS3Client
:members: create_standing_order
:noindex:

As with transfers, the return value may be a `NeedVOPResponse` (call `approve_vop_response` to proceed) or a
`NeedTANResponse`. If the bank assigns a task id to the new standing order, it is returned in ``data['task_id']``
of the final ``TransactionResponse``.

.. note::

Returning the task id (Auftragsidentifikation) on creation is optional for banks, and many banks do not do it:
``data['task_id']`` is then not set, even though the standing order was created successfully (e.g. response code
``0010 Auftrag entgegengenommen``). In this case, call ``get_standing_orders`` afterwards to obtain the task id
of the new standing order. You need it for example to delete the standing order later on. As with changes (see
below), it may take a while until the new standing order shows up there.

.. code-block:: python

from fints.formals import StandingOrderTimeUnit1

res = client.simple_create_standing_order(
account=account,
iban='DE89370400440532013000',
bic='COBADEFFXXX',
recipient_name='Max Mustermann',
amount=Decimal('25.00'),
account_name='Your name',
reason='Monthly rent',
first_execution_date=datetime.date(2030, 1, 1),
time_unit=StandingOrderTimeUnit1.MONTHLY,
interval=1,
execution_day=1,
)
while isinstance(res, NeedTANResponse | NeedVOPResponse):
if isinstance(res, NeedTANResponse):
res = ask_for_tan(res)
elif isinstance(res, NeedVOPResponse):
res = client.approve_vop_response(res)

print(res.status, res.data.get('task_id'))


Changing standing orders
------------------------

To change a SEPA standing order, pass one of the objects returned by ``get_standing_orders`` (see
:ref:`standing-orders`) together with the values to change. All values that are not given are kept. To make a
standing order unlimited, pass ``remove_last_execution_date=True``:

.. autoclass:: fints.client.FinTS3Client
:members: simple_change_standing_order
:noindex:

Which values may be changed is decided by the bank. The bank may assign a new task id to the changed standing order,
it is returned in ``data['task_id']`` (and the previous one in ``data['task_id_old']``) of the final
``TransactionResponse`` if the bank reports it. Otherwise, call ``get_standing_orders`` again to obtain it.

.. note::

Banks may process changes asynchronously, especially if they answer with response code
``0010 Auftrag entgegengenommen`` (order accepted) instead of ``0020`` (order executed). It can then take a while
until the change is visible in online banking and in ``get_standing_orders``. In the meantime, the standing order
may even be missing completely from ``get_standing_orders`` for a minute or so. Wait a bit before fetching the
standing orders again, and do not submit the change a second time.

.. code-block:: python

standing_orders = client.get_standing_orders(account)
while isinstance(standing_orders, NeedTANResponse):
standing_orders = ask_for_tan(standing_orders)

order = standing_orders[0]
res = client.simple_change_standing_order(account, order, amount=Decimal('30.00'), execution_day=15)
while isinstance(res, NeedTANResponse | NeedVOPResponse):
if isinstance(res, NeedTANResponse):
res = ask_for_tan(res)
elif isinstance(res, NeedVOPResponse):
res = client.approve_vop_response(res)

print(res.status, res.data.get('task_id'))

If you want to supply your own SEPA XML, use ``change_standing_order``. The bank requires the complete new pain
message in the same schema as the pain message of the existing standing order (``order.sepa_descriptor``). To change
single values of the existing pain message while keeping its schema, you can use ``modify_sepa_transfer_pain``:

.. autoclass:: fints.client.FinTS3Client
:members: change_standing_order
:noindex:

.. autofunction:: fints.utils.modify_sepa_transfer_pain


Deleting standing orders
------------------------

You can delete a SEPA standing order ("Dauerauftrag") by passing one of the objects returned by
``get_standing_orders`` (see :ref:`standing-orders`) to ``delete_standing_order``:

.. autoclass:: fints.client.FinTS3Client
:members: delete_standing_order
:noindex:

At any point, you might receive a `NeedTANResponse`.
You should then enter a TAN, read our chapter :ref:`tans` to find out more.

.. code-block:: python

standing_orders = client.get_standing_orders(account)
while isinstance(standing_orders, NeedTANResponse):
standing_orders = ask_for_tan(standing_orders)

order = standing_orders[0]
res = client.delete_standing_order(account, order)
while isinstance(res, NeedTANResponse):
res = ask_for_tan(res)

print(res.status)
print(res.responses)
6 changes: 6 additions & 0 deletions docs/trouble.rst
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ the problem.
"Fetch status protocol",
"Make a simple transfer",
"Fetch statements as PDF",
"Fetch standing orders",
]

print("Choose an operation")
Expand Down Expand Up @@ -209,5 +210,10 @@ the problem.
with open(output_pdf, 'wb') as file:
file.write(res.data)
print("Written to", output_pdf)
elif choice == 12:
res = f.get_standing_orders(account)
while isinstance(res, NeedTANResponse):
res = ask_for_tan(res)
print(res)
except FinTSUnsupportedOperation as e:
print("This operation is not supported by this bank:", e)
Loading
Loading