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

1import csv 

2 

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 

22 

23from sentry_sdk import capture_exception 

24 

25from payments import services 

26from payments.payables import payables 

27 

28from .models import Batch, Payment, PaymentUser 

29 

30 

31@method_decorator(staff_member_required, name="dispatch") 

32class PaymentAdminView(View): 

33 """View that creates a payment.""" 

34 

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") 

38 

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 

43 

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)) 

46 

47 if not payable_obj.can_manage_payment(request.member): 

48 raise PermissionDenied(_("You are not allowed to process this payment.")) 

49 

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) 

64 

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 ) 

78 

79 if "next" in request.POST: 

80 return redirect(request.POST["next"]) 

81 

82 return redirect("admin:payments_payment_change", result.pk) 

83 

84 

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.""" 

92 

93 def post(self, request, *args, **kwargs): 

94 batch = Batch.objects.get(pk=kwargs["pk"]) 

95 

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 

100 

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 ) 

111 

112 if "next" in request.POST: 

113 return redirect(request.POST["next"]) 

114 

115 return redirect("admin:payments_batch_change", kwargs["pk"]) 

116 

117 

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.""" 

125 

126 def post(self, request, *args, **kwargs): 

127 batch = Batch.objects.get(pk=kwargs["pk"]) 

128 

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]) 

141 

142 member_rows = batch.payments_set.values("paid_by").annotate(total=Sum("amount")) 

143 

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 

158 

159 

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.""" 

167 

168 def post(self, request, *args, **kwargs): 

169 batch = Batch.objects.get(pk=kwargs["pk"]) 

170 

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]) 

182 

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 ) 

193 

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 

205 

206 

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.""" 

214 

215 template_name = "admin/payments/batch_topic.html" 

216 

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 ) 

230 

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}" 

238 

239 context["batch"] = batch 

240 context["description"] = description 

241 return render(request, self.template_name, context) 

242 

243 

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.""" 

251 

252 def post(self, request, *args, **kwargs): 

253 batch = Batch() 

254 batch.save() 

255 

256 payments = Payment.objects.filter( 

257 type=Payment.TPAY, 

258 batch=None, 

259 ) 

260 

261 payments.update(batch=batch) 

262 

263 return redirect("admin:payments_batch_change", object_id=batch.id)