Coverage for website/payments/admin_views.py: 100.00%
115 statements
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
1import csv
3from django.apps import apps
4from django.contrib import messages
5from django.contrib.admin.utils import model_ngettext
6from django.contrib.admin.views.decorators import staff_member_required
7from django.contrib.auth.decorators import permission_required
8from django.core.exceptions import (
9 DisallowedRedirect,
10 PermissionDenied,
11 SuspiciousOperation,
12)
13from django.db.models import Count, Max, Min, Sum
14from django.http import HttpResponse
15from django.shortcuts import get_object_or_404, redirect, render
16from django.utils import timezone
17from django.utils.decorators import method_decorator
18from django.utils.http import url_has_allowed_host_and_scheme
19from django.utils.text import capfirst
20from django.utils.translation import gettext_lazy as _
21from django.views import View
23from sentry_sdk import capture_exception
25from payments import services
26from payments.payables import payables
28from .models import Batch, Payment, PaymentUser
31@method_decorator(staff_member_required, name="dispatch")
32class PaymentAdminView(View):
33 """View that creates a payment."""
35 def post(self, request, *args, app_label, model_name, payable, **kwargs):
36 if "type" not in request.POST:
37 raise SuspiciousOperation("Missing POST parameters")
39 if "next" in request.POST and not url_has_allowed_host_and_scheme(
40 request.POST.get("next"), allowed_hosts={request.get_host()}
41 ):
42 raise DisallowedRedirect
44 payable_model = apps.get_model(app_label=app_label, model_name=model_name)
45 payable_obj = payables.get_payable(get_object_or_404(payable_model, pk=payable))
47 if not payable_obj.can_manage_payment(request.member):
48 raise PermissionDenied(_("You are not allowed to process this payment."))
50 try:
51 result = services.create_payment(
52 payable_obj,
53 self.request.member,
54 request.POST["type"],
55 )
56 except Exception as e:
57 capture_exception(e)
58 messages.error(
59 request,
60 _("Something went wrong paying %s: %s")
61 % (model_ngettext(payable_obj.model, 1), str(e)),
62 )
63 return redirect(f"admin:{app_label}_{model_name}_change", payable_obj.pk)
65 if result:
66 messages.success(
67 request,
68 _("Successfully paid %s.") % model_ngettext(payable_obj.model, 1),
69 )
70 else:
71 messages.error(
72 request,
73 _("Could not pay %s.") % model_ngettext(payable_obj.model, 1),
74 )
75 return redirect(
76 f"admin:{app_label}_{model_name}_change", payable_obj.model.pk
77 )
79 if "next" in request.POST:
80 return redirect(request.POST["next"])
82 return redirect("admin:payments_payment_change", result.pk)
85@method_decorator(staff_member_required, name="dispatch")
86@method_decorator(
87 permission_required("payments.process_batches"),
88 name="dispatch",
89)
90class BatchProcessAdminView(View):
91 """View that processes a batch."""
93 def post(self, request, *args, **kwargs):
94 batch = Batch.objects.get(pk=kwargs["pk"])
96 if "next" in request.POST and not url_has_allowed_host_and_scheme(
97 request.POST.get("next"), allowed_hosts={request.get_host()}
98 ):
99 raise DisallowedRedirect
101 if batch.processed:
102 messages.error(
103 request, _("{} already processed.").format(model_ngettext(batch, 1))
104 )
105 else:
106 services.process_batch(batch)
107 messages.success(
108 request,
109 _("Successfully processed {}.").format(model_ngettext(batch, 1)),
110 )
112 if "next" in request.POST:
113 return redirect(request.POST["next"])
115 return redirect("admin:payments_batch_change", kwargs["pk"])
118@method_decorator(staff_member_required, name="dispatch")
119@method_decorator(
120 permission_required("payments.process_batches"),
121 name="dispatch",
122)
123class BatchExportAdminView(View):
124 """View that exports a batch."""
126 def post(self, request, *args, **kwargs):
127 batch = Batch.objects.get(pk=kwargs["pk"])
129 response = HttpResponse(content_type="text/csv")
130 response["Content-Disposition"] = 'attachment;filename="batch.csv"'
131 writer = csv.writer(response)
132 headers = [
133 _("Account holder"),
134 _("IBAN"),
135 _("Mandate Reference"),
136 _("Amount"),
137 _("Description"),
138 _("Mandate Date"),
139 ]
140 writer.writerow([capfirst(x) for x in headers])
142 member_rows = batch.payments_set.values("paid_by").annotate(total=Sum("amount"))
144 for row in member_rows:
145 member = PaymentUser.objects.get(id=row["paid_by"])
146 bankaccount = member.bank_accounts.last()
147 writer.writerow(
148 [
149 bankaccount.name,
150 bankaccount.iban,
151 bankaccount.mandate_no,
152 f"{row['total']:.2f}",
153 batch.description,
154 bankaccount.valid_from,
155 ]
156 )
157 return response
160@method_decorator(staff_member_required, name="dispatch")
161@method_decorator(
162 permission_required("payments.process_batches"),
163 name="dispatch",
164)
165class BatchTopicExportAdminView(View):
166 """View that exports a batch per topic."""
168 def post(self, request, *args, **kwargs):
169 batch = Batch.objects.get(pk=kwargs["pk"])
171 response = HttpResponse(content_type="text/csv")
172 response["Content-Disposition"] = 'attachment;filename="batch-topic.csv"'
173 writer = csv.writer(response)
174 headers = [
175 _("Topic"),
176 _("No. of payments"),
177 _("First payment"),
178 _("Last payment"),
179 _("Total amount"),
180 ]
181 writer.writerow([capfirst(x) for x in headers])
183 topic_rows = (
184 batch.payments_set.values("topic")
185 .annotate(
186 total=Sum("amount"),
187 count=Count("paid_by"),
188 min_date=Min("created_at"),
189 max_date=Max("created_at"),
190 )
191 .order_by("topic")
192 )
194 for row in topic_rows:
195 writer.writerow(
196 [
197 row["topic"],
198 row["count"],
199 timezone.localtime(row["min_date"]).date(),
200 timezone.localtime(row["max_date"]).date(),
201 f"{row['total']:.2f}",
202 ]
203 )
204 return response
207@method_decorator(staff_member_required, name="dispatch")
208@method_decorator(
209 permission_required("payments.process_batches"),
210 name="dispatch",
211)
212class BatchTopicDescriptionAdminView(View):
213 """Shows the topic export as plain text."""
215 template_name = "admin/payments/batch_topic.html"
217 def post(self, request, *args, **kwargs):
218 context = {}
219 batch = get_object_or_404(Batch, pk=kwargs["pk"])
220 topic_rows = (
221 batch.payments_set.values("topic")
222 .annotate(
223 total=Sum("amount"),
224 count=Count("paid_by"),
225 min_date=Min("created_at"),
226 max_date=Max("created_at"),
227 )
228 .order_by("topic")
229 )
231 date = batch.processing_date if batch.processing_date else timezone.now().date()
232 description = f"Batch {batch.id} - {date}:\n"
233 for row in topic_rows:
234 min_date = timezone.localtime(row["min_date"]).date()
235 max_date = timezone.localtime(row["max_date"]).date()
236 description += f"- {row['topic']} ({row['count']}x) [{min_date} -- {max_date}], total €{row['total']:.2f}\n"
237 description += f"\n{batch.description}"
239 context["batch"] = batch
240 context["description"] = description
241 return render(request, self.template_name, context)
244@method_decorator(staff_member_required, name="dispatch")
245@method_decorator(
246 permission_required("payments.process_batches"),
247 name="dispatch",
248)
249class BatchNewFilledAdminView(View):
250 """View that adds a new batch filled with all payments that where not already in a batch."""
252 def post(self, request, *args, **kwargs):
253 batch = Batch()
254 batch.save()
256 payments = Payment.objects.filter(
257 type=Payment.TPAY,
258 batch=None,
259 )
261 payments.update(batch=batch)
263 return redirect("admin:payments_batch_change", object_id=batch.id)