diff --git a/pycroft/lib/finance/__init__.py b/pycroft/lib/finance/__init__.py index 0287fef5b..0191fe4b0 100644 --- a/pycroft/lib/finance/__init__.py +++ b/pycroft/lib/finance/__init__.py @@ -42,8 +42,9 @@ get_pid_csv, ) from .retransfer import ( - get_activities_to_return, + attribute_activities_as_returned, generate_activities_return_sepaxml, + get_activities_to_return, ) from .transaction_crud import ( simple_transaction, diff --git a/pycroft/lib/finance/retransfer.py b/pycroft/lib/finance/retransfer.py index 231c5098c..a66fd94f4 100644 --- a/pycroft/lib/finance/retransfer.py +++ b/pycroft/lib/finance/retransfer.py @@ -1,9 +1,10 @@ from collections.abc import Sequence from datetime import datetime, timedelta +from schwifty import IBAN from sepaxml import SepaTransfer from sqlalchemy import select -from sqlalchemy.orm import joinedload, Session +from sqlalchemy.orm import Session, joinedload from pycroft import config from pycroft.helpers.utc import ensure_tz @@ -33,10 +34,11 @@ def generate_activities_return_sepaxml(activities: list[BankAccountActivity]) -> sepa = SepaTransfer(transfer_config, clean=False) for activity in activities: + bic = activity.other_routing_number or IBAN(activity.other_account_number).bic.compact payment = { "name": activity.other_name, "IBAN": activity.other_account_number, - "BIC": activity.other_routing_number, + "BIC": bic, "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}"[ @@ -46,3 +48,27 @@ def generate_activities_return_sepaxml(activities: list[BankAccountActivity]) -> sepa.add_payment(payment) return sepa.export() + + +def attribute_activities_as_returned( + session: Session, activities: list[BankAccountActivity] +) -> None: + for activity in activities: + debit_account = config.non_attributable_transactions_account + credit_account = activity.bank_account.account + + transaction = finance.simple_transaction( + description=activity.reference, + debit_account=debit_account, + credit_account=credit_account, + amount=activity.amount, + author=current_user, + valid_on=activity.valid_on, + confirmed=False, + ) + activity.split = next( + split for split in transaction.splits if split.account_id == credit_account.id + ) + session.add(activity) + + session.commit() diff --git a/pycroft/model/alembic/versions/2d7e4df39a3b_add_account_for_non_attributable_.py b/pycroft/model/alembic/versions/2d7e4df39a3b_add_account_for_non_attributable_.py new file mode 100644 index 000000000..aceafbae9 --- /dev/null +++ b/pycroft/model/alembic/versions/2d7e4df39a3b_add_account_for_non_attributable_.py @@ -0,0 +1,41 @@ +"""add account for non-attributable transfers to config + +Revision ID: 2d7e4df39a3b +Revises: bc0e0dd480d4 +Create Date: 2024-06-06 20:21:16.972195 + +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "2d7e4df39a3b" +down_revision = "bc0e0dd480d4" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "config", + sa.Column( + "non_attributable_transactions_account_id", + sa.Integer(), + nullable=False, + server_default="10", + ), + ) + op.create_foreign_key( + None, "config", "account", ["non_attributable_transactions_account_id"], ["id"] + ) + + +def downgrade(): + op.drop_constraint(None, "config", type_="foreignkey") + op.drop_column("config", "non_attributable_transactions_account_id") + op.create_index( + "bank_account_activity_imported_at", "bank_account_activity", ["imported_at"], unique=False + ) diff --git a/pycroft/model/config.py b/pycroft/model/config.py index 754eb1e96..734681004 100644 --- a/pycroft/model/config.py +++ b/pycroft/model/config.py @@ -72,6 +72,11 @@ class Config(IntegerIdModel): foreign_keys=[membership_fee_bank_account_id] ) + non_attributable_transactions_account_id: Mapped[int] = col(ForeignKey(Account.id)) + non_attributable_transactions_account: Mapped[Account] = relationship( + foreign_keys=[non_attributable_transactions_account_id] + ) + fints_product_id: Mapped[str | None] __table_args__ = (CheckConstraint("id = 1"),) diff --git a/pyproject.toml b/pyproject.toml index 8b6532eab..3cd2d725b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ dependencies = [ "pydantic ~= 2.4.0", "python-dotenv ~= 0.21.0", "reportlab ~= 3.6.13", # usersheet generation + "schwifty ~= 2024.06.1", "sentry-sdk[Flask] ~= 1.29.2", "simplejson ~= 3.11.1", # decimal serialization "SQLAlchemy >= 2.0.1", diff --git a/web/blueprints/finance/__init__.py b/web/blueprints/finance/__init__.py index 4479b51ca..9cf8a56e0 100644 --- a/web/blueprints/finance/__init__.py +++ b/web/blueprints/finance/__init__.py @@ -71,6 +71,7 @@ match_activities, get_activities_to_return, generate_activities_return_sepaxml, + attribute_activities_as_returned, get_all_bank_accounts, get_unassigned_bank_account_activities, get_all_mt940_errors, @@ -609,6 +610,8 @@ def bank_account_activities_return_do() -> ResponseReturnValue: sepa_xml: bytes = generate_activities_return_sepaxml(selected_activities) + attribute_activities_as_returned(session, selected_activities) + return send_file( BytesIO(sepa_xml), as_attachment=True, diff --git a/web/templates/finance/bank_account_activities_return.html b/web/templates/finance/bank_account_activities_return.html index 0afcad2f8..6f91f329d 100644 --- a/web/templates/finance/bank_account_activities_return.html +++ b/web/templates/finance/bank_account_activities_return.html @@ -34,7 +34,7 @@