Skip to content

Commit

Permalink
Enable export of non-attributable transfers
Browse files Browse the repository at this point in the history
  • Loading branch information
FestplattenSchnitzel committed Mar 15, 2024
1 parent f07a07c commit 9eb88cf
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 1 deletion.
4 changes: 4 additions & 0 deletions pycroft/lib/finance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
take_actions_for_payment_in_default_users,
get_pid_csv,
)
from .retransfer import (
get_activities_to_return,
generate_activities_return_sepaxml,
)
from .transaction_crud import (
simple_transaction,
complex_transaction,
Expand Down
49 changes: 49 additions & 0 deletions pycroft/lib/finance/retransfer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from collections.abc import Sequence
from datetime import datetime, timedelta

from sepaxml import SepaTransfer
from sqlalchemy import select
from sqlalchemy.orm import joinedload

from pycroft.helpers.utc import ensure_tz
from pycroft.model import session
from pycroft.model.finance import BankAccountActivity


def get_activities_to_return() -> Sequence[BankAccountActivity]:
statement = (
select(BankAccountActivity)
.options(joinedload(BankAccountActivity.bank_account))
.filter(BankAccountActivity.transaction_id.is_(None))
.filter(BankAccountActivity.amount > 0)
.filter(
BankAccountActivity.imported_at
< ensure_tz(datetime.utcnow() - timedelta(days=14))
)
)

return session.session.scalars(statement).all()


def generate_activities_return_sepaxml(activities: list[BankAccountActivity]) -> bytes:
config = {
"name": "Studierendenrat der TU Dresden",
"IBAN": "DE61850503003120219540",
"BIC": "OSDDDE81",
"batch": False,
"currency": "EUR",
}
sepa = SepaTransfer(config, clean=False)

for activity in activities:
payment = {
"name": activity.other_name,
"IBAN": activity.other_account_number,
"BIC": activity.other_routing_number,
"amount": int(activity.amount * 100),
"execution_date": datetime.now().date(),
"description": f"Rücküberweisung nicht zuordenbarer Überweisung vom {activity.posted_on} mit Referenz {activity.reference}",
}
sepa.add_payment(payment)

return sepa.export()
66 changes: 65 additions & 1 deletion web/blueprints/finance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
"""
import typing as t
from decimal import Decimal
from collections.abc import Iterable
from collections.abc import Iterable, Sequence
from datetime import date
from datetime import timedelta, datetime
from functools import partial
from itertools import zip_longest, chain
from io import BytesIO

import wtforms
from fints.dialog import FinTSDialogError
Expand All @@ -34,6 +35,7 @@
request,
url_for,
make_response,
send_file,
)
from flask.typing import ResponseReturnValue
from flask_login import current_user
Expand Down Expand Up @@ -67,6 +69,8 @@
get_system_accounts,
ImportedTransactions,
match_activities,
get_activities_to_return,
generate_activities_return_sepaxml,
get_all_bank_accounts,
get_unassigned_bank_account_activities,
get_all_mt940_errors,
Expand Down Expand Up @@ -548,6 +552,66 @@ def bank_account_activities_match() -> ResponseReturnValue:
activities_team=matched_activities_team)


class ActivityEntry(t.TypedDict):
bank_account: str
name: str
valid_on: date
reference: str
amount: int


@bp.route("/bank-account-activities/return/")
@access.require("finance_change")
def bank_account_activities_return() -> ResponseReturnValue:
field_list: BooleanFieldList = []
activities: dict[str, ActivityEntry] = {}

for activity in get_activities_to_return():
activities[str(activity.id)] = {
"bank_account": activity.bank_account.name,
"name": activity.other_name,
"valid_on": activity.valid_on,
"reference": activity.reference,
"amount": activity.amount,
}

field_list.append((str(activity.id), BooleanField(str(activity.id), default=True)))

form: t.Any = _create_form(field_list)

return render_template(
"finance/bank_account_activities_return.html",
form=form(),
activities=activities,
)


@bp.route("/bank-account-activities/return/do/", methods=["POST"])
@access.require("finance_change")
def bank_account_activities_return_do() -> ResponseReturnValue:
field_list: BooleanFieldList = []
activities_to_return: Sequence[BankAccountActivity] = get_activities_to_return()

for activity in activities_to_return:
field_list.append((str(activity.id), BooleanField(str(activity.id), default=True)))

form: t.Any = _create_form(field_list)()

if form.validate_on_submit():
selected_activities: list[BankAccountActivity] = [
activity for activity in activities_to_return if form[str(activity.id)].data
]

sepa_xml: bytes = generate_activities_return_sepaxml(selected_activities)

return send_file(
BytesIO(sepa_xml),
as_attachment=True,
download_name=f"non-attributable-transactions-{datetime.now().date()}.xml",
)



class UserMatch(t.TypedDict):
purpose: str
name: str
Expand Down
46 changes: 46 additions & 0 deletions web/templates/finance/bank_account_activities_return.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{#
SPDX-FileCopyrightText: 2024 Gregor Düster <git@gdstr.eu>

SPDX-License-Identifier: Apache-2.0
#}
{% extends "layout.html" %}

{% set page_title = "Unzuordenbare Überweisungen zurücküberweisen" %}

{% block content %}
<form action="{{ url_for('.bank_account_activities_return_do') }}" method="POST">
<div class="row">
<div class="col-md-12">
{{ form.csrf_token }}
<table class="table table-striped table-responsive activities">
<thead>
<th></th>
<th>Bankkonto</th>
<th>Name</th>
<th>Gültig am</th>
<th>Verwendungszweck</th>
<th>Betrag</th>
</thead>
<tbody>
{% for field in form %}{% if field.type != 'CSRFTokenField' %}
<tr>
<td>{{ field }}</td>
<td>{{ activities[field.id]["bank_account"] }}</td>
<td>{{ activities[field.id]["name"] }}</td>
<td>{{ activities[field.id]["valid_on"] }}</td>
<td>{{ activities[field.id]["reference"] }}</td>
<td>{{ activities[field.id]["amount"] }} &euro;</td>
</tr>
{% endif %}{% endfor %}
</tbody>
</table>
</div>
</div>

<div class="row">
<div class="col-md-12">
<button type="submit" class="btn btn-primary">SEPA-XML generieren</button>
</div>
</div>
</form>
{% endblock %}
1 change: 1 addition & 0 deletions web/templates/finance/bank_accounts_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ <h2 class="page-header">{{ _("Übersicht") }}</h2>
<section>
<h2 class="page-header">{{ _("Unzugeordnete Kontobewegungen") }}</h2>
<a href="{{ url_for('.bank_account_activities_match') }}" class="btn btn-primary">Kontobewegungen matchen</a>
<a href="{{ url_for('.bank_account_activities_return') }}" class="btn btn-outline-secondary">Unzugeordnete Kontobewegungen rücküberweisen</a>
{{ bank_account_activity_table.render('bank_accounts_activities') }}
</section>
{% endblock %}

0 comments on commit 9eb88cf

Please sign in to comment.