Repository: svthalia/concrexit

Issue:
Add payment batch export per topic as plaintext view
### Is your feature request related to a problem? Please describe.
The current .csv export for Payment Batches is nice, but for Conscribo we **also** need it as just copy-pasteable plaintext. That would make things way easier.

### Describe the solution you'd like
An extra button, next to the 'export batch per topic'-button, 'show per topic', that will render a simple admin page with the content of the .csv, but as bulletpoints:

```
- Topic X, 21 payments, date first payment, date last payment, total amount
- Topic Y, 4 payments, first payment, last payment, total amount
```

Similar to the 'emails of attendees' page for events.

### Motivation
This makes things a lot easier for the treasurer and audit committee

### Describe alternatives you've considered
Doing this manually

### Additional context



Discussion:

Resolving patch:
diff --git a/website/payments/admin.py b/website/payments/admin.py
--- a/website/payments/admin.py
+++ b/website/payments/admin.py
@@ -400,6 +400,13 @@ def get_urls(self) -> list:
                 ),
                 name="payments_batch_export_topic",
             ),
+            path(
+                "<uuid:pk>/topic-description/",
+                self.admin_site.admin_view(
+                    admin_views.BatchTopicDescriptionAdminView.as_view()
+                ),
+                name="payments_batch_topic_description",
+            ),
             path(
                 "new_filled/",
                 self.admin_site.admin_view(
diff --git a/website/payments/admin_views.py b/website/payments/admin_views.py
--- a/website/payments/admin_views.py
+++ b/website/payments/admin_views.py
@@ -9,7 +9,7 @@
 from django.db.models import Sum, Count, Min, Max
 from django.http import HttpResponse
 from django.core.exceptions import SuspiciousOperation, DisallowedRedirect
-from django.shortcuts import redirect
+from django.shortcuts import redirect, get_object_or_404, render
 from django.utils import timezone
 from django.utils.text import capfirst
 from django.utils.decorators import method_decorator
@@ -199,6 +199,41 @@ def post(self, request, *args, **kwargs):
         return response
 
 
+@method_decorator(staff_member_required, name="dispatch")
+@method_decorator(
+    permission_required("payments.process_batches"), name="dispatch",
+)
+class BatchTopicDescriptionAdminView(View):
+    """
+    Shows the topic export as plain text
+    """
+
+    template_name = "admin/payments/batch_topic.html"
+
+    def post(self, request, *args, **kwargs):
+        context = {}
+        batch = get_object_or_404(Batch, pk=kwargs["pk"])
+        topic_rows = (
+            batch.payments_set.values("topic")
+            .annotate(
+                total=Sum("amount"),
+                count=Count("paid_by"),
+                min_date=Min("created_at"),
+                max_date=Max("created_at"),
+            )
+            .order_by("topic")
+        )
+
+        description = f"Batch {batch.id} - {batch.processing_date if batch.processing_date else timezone.now().date()}:\n"
+        for row in topic_rows:
+            description += f"- {row['topic']} ({row['count']}x) [{timezone.localtime(row['min_date']).date()} -- {timezone.localtime(row['max_date']).date()}], total €{row['total']:.2f}\n"
+        description += f"\n{batch.description}"
+
+        context["batch"] = batch
+        context["description"] = description
+        return render(request, self.template_name, context)
+
+
 @method_decorator(staff_member_required, name="dispatch")
 @method_decorator(
     permission_required("payments.process_batches"), name="dispatch",


Test patch:
diff --git a/website/payments/tests/test_admin_views.py b/website/payments/tests/test_admin_views.py
--- a/website/payments/tests/test_admin_views.py
+++ b/website/payments/tests/test_admin_views.py
@@ -439,6 +439,117 @@ def test_post(self):
         )
 
 
+@override_settings(SUSPEND_SIGNALS=True)
+@freeze_time("2020-01-01")
+class BatchTopicDescriptionAdminViewTest(TestCase):
+    @classmethod
+    def setUpTestData(cls):
+        cls.batch = Batch.objects.create()
+        cls.user = Member.objects.create(
+            username="test1",
+            first_name="Test1",
+            last_name="Example",
+            email="test1@example.org",
+        )
+        Profile.objects.create(user=cls.user)
+
+    def setUp(self):
+        self.client = Client()
+        self.client.force_login(self.user)
+        self.view = admin_views.BatchTopicDescriptionAdminView()
+
+    def _give_user_permissions(self):
+        content_type = ContentType.objects.get_for_model(Batch)
+        permissions = Permission.objects.filter(
+            content_type__app_label=content_type.app_label,
+        )
+        for p in permissions:
+            self.user.user_permissions.add(p)
+        self.user.is_staff = True
+        self.user.save()
+
+        self.client.logout()
+        self.client.force_login(self.user)
+
+    def test_permission(self):
+        url = f"/admin/payments/batch/{self.batch.id}/topic-description/"
+        response = self.client.post(url)
+        self.assertRedirects(response, "/admin/login/?next=%s" % url)
+
+        self._give_user_permissions()
+
+        response = self.client.post(url)
+        self.assertEqual(response.status_code, 200)
+
+    def test_post(self):
+        self._give_user_permissions()
+
+        self.user2 = Member.objects.create(
+            username="test2",
+            first_name="Test2",
+            last_name="Example",
+            email="test1@example.org",
+        )
+        Profile.objects.create(user=self.user2)
+
+        BankAccount.objects.create(
+            last_used=timezone.now(),
+            owner=self.user,
+            iban="DE75512108001245126199",
+            mandate_no="2",
+            valid_from=timezone.now(),
+        )
+        BankAccount.objects.create(
+            last_used=timezone.now(),
+            owner=self.user2,
+            iban="NL02ABNA0123456789",
+            mandate_no="1",
+            valid_from=timezone.now(),
+        )
+
+        Payment.objects.bulk_create(
+            [
+                Payment(
+                    amount=1,
+                    paid_by=self.user,
+                    type=Payment.TPAY,
+                    batch=self.batch,
+                    topic="test1",
+                ),
+                Payment(
+                    amount=2,
+                    paid_by=self.user,
+                    type=Payment.TPAY,
+                    batch=self.batch,
+                    topic="test2",
+                ),
+                Payment(
+                    amount=4,
+                    paid_by=self.user2,
+                    type=Payment.TPAY,
+                    batch=self.batch,
+                    topic="test1",
+                ),
+                Payment(
+                    amount=2,
+                    paid_by=self.user2,
+                    type=Payment.TPAY,
+                    batch=self.batch,
+                    topic="test2",
+                ),
+            ]
+        )
+
+        response = self.client.post(
+            f"/admin/payments/batch/{self.batch.id}/topic-description/"
+        )
+
+        self.assertContains(
+            response,
+            "- test1 (2x) [2020-01-01 -- 2020-01-01], total €5.00\n- test2 (2x) [2020-01-01 -- 2020-01-01], total €4.00\n",
+        )
+
+
 @override_settings(SUSPEND_SIGNALS=True)
 class BatchNewFilledAdminViewTest(TestCase):
     @classmethod