Coverage for website/sales/admin/order_admin.py: 80.67%
230 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
1from functools import partial
3from django.contrib import admin, messages
4from django.contrib.admin import SimpleListFilter, register
5from django.forms import Field
6from django.http import HttpRequest
7from django.urls import resolve
8from django.utils import timezone
9from django.utils.translation import gettext_lazy as _
11from admin_auto_filters.filters import AutocompleteFilter
13from payments.widgets import PaymentWidget
14from sales import services
15from sales.models.order import Order, OrderItem
16from sales.models.shift import Shift
17from sales.services import is_manager
20class OrderItemInline(admin.TabularInline):
21 model = OrderItem
22 extra = 1
24 fields = ("product", "product_name", "amount", "total")
25 readonly_fields = ("product_name",)
27 def get_readonly_fields(self, request: HttpRequest, obj: Order = None):
28 default_fields = self.readonly_fields
30 if not (request.member and request.member.has_perm("sales.custom_prices")):
31 default_fields += ("total",)
33 return default_fields
35 def get_queryset(self, request):
36 queryset = super().get_queryset(request)
37 queryset = queryset.prefetch_related("product", "product__product")
38 return queryset
40 def has_add_permission(self, request, obj):
41 if obj and obj.shift.locked: 41 ↛ 42line 41 didn't jump to line 42 because the condition on line 41 was never true
42 return False
44 if obj and obj.payment:
45 return False
47 parent = self.get_parent_object_from_request(request)
48 if not parent: 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true
49 return False
51 return super().has_add_permission(request, obj)
53 def has_change_permission(self, request, obj=None):
54 if obj and obj.payment:
55 return False
56 if obj and obj.shift.locked: 56 ↛ 57line 56 didn't jump to line 57 because the condition on line 56 was never true
57 return False
58 if obj and not is_manager(request.member, obj.shift):
59 return False
60 return True
62 def has_delete_permission(self, request, obj=None):
63 if obj and obj.payment:
64 return False
65 if obj and obj.shift.locked: 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true
66 return False
67 if obj and not is_manager(request.member, obj.shift):
68 return False
69 return True
71 def get_parent_object_from_request(self, request):
72 """Get parent object to determine product list."""
73 resolved = resolve(request.path_info)
74 if resolved.kwargs: 74 ↛ 77line 74 didn't jump to line 77 because the condition on line 74 was always true
75 parent = self.parent_model.objects.get(pk=resolved.kwargs["object_id"])
76 return parent
77 return None
79 def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
80 """Limit product list items to items of order's shift."""
81 field = super().formfield_for_foreignkey(db_field, request, **kwargs)
83 if db_field.name == "product": 83 ↛ 91line 83 didn't jump to line 91 because the condition on line 83 was always true
84 if request is not None: 84 ↛ 89line 84 didn't jump to line 89 because the condition on line 84 was always true
85 parent = self.get_parent_object_from_request(request)
86 if parent: 86 ↛ 91line 86 didn't jump to line 91 because the condition on line 86 was always true
87 field.queryset = parent.shift.product_list.product_items
88 else:
89 field.queryset = field.queryset.none()
91 return field
94class OrderShiftFilter(AutocompleteFilter):
95 title = _("shift")
96 field_name = "shift"
97 rel_model = Order
98 use_pk_exact = False
100 def queryset(self, request, queryset):
101 if self.value(): 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true
102 return queryset.filter(shift=self.value())
103 return queryset
106class OrderMemberFilter(AutocompleteFilter):
107 title = _("member")
108 field_name = "payer"
109 rel_model = Order
110 use_pk_exact = False
112 def queryset(self, request, queryset):
113 if self.value(): 113 ↛ 114line 113 didn't jump to line 114 because the condition on line 113 was never true
114 return queryset.filter(payer=self.value())
115 return queryset
118class OrderPaymentFilter(SimpleListFilter):
119 title = _("payment")
120 parameter_name = "payment"
122 def lookups(self, request, model_admin):
123 return (
124 ("not_required", _("No payment required")),
125 ("paid", _("Paid")),
126 ("unpaid", _("Unpaid")),
127 )
129 def queryset(self, request, queryset):
130 if self.value() is None: 130 ↛ 132line 130 didn't jump to line 132 because the condition on line 130 was always true
131 return queryset
132 if self.value() == "paid":
133 return queryset.filter(payment__isnull=False)
134 if self.value() == "unpaid":
135 return queryset.filter(payment__isnull=True, total_amount__gt=0)
136 return queryset.filter(total_amount__exact=0)
139class OrderProductFilter(SimpleListFilter):
140 title = _("product")
141 parameter_name = "product"
143 def lookups(self, request, model_admin):
144 qs = model_admin.get_queryset(request)
145 types = qs.filter(order_items__product__product__isnull=False).values_list(
146 "order_items__product__product__id", "order_items__product__product__name"
147 )
148 return list(types.order_by("order_items__product__product__id").distinct())
150 def queryset(self, request, queryset):
151 if self.value() is None: 151 ↛ 153line 151 didn't jump to line 153 because the condition on line 151 was always true
152 return queryset
153 return queryset.filter(order_items__product__product__id__contains=self.value())
156@register(Order)
157class OrderAdmin(admin.ModelAdmin):
158 class Media:
159 pass
161 inlines = [
162 OrderItemInline,
163 ]
164 ordering = ("-created_at",)
165 date_hierarchy = "created_at"
166 search_fields = (
167 "id",
168 "payer__username",
169 "payer__first_name",
170 "payer__last_name",
171 "payer__profile__nickname",
172 )
174 list_display = (
175 "id",
176 "shift",
177 "created_at",
178 "order_description",
179 "num_items",
180 "discount",
181 "_total_amount",
182 "paid",
183 "payer",
184 )
185 list_filter = [
186 OrderShiftFilter,
187 OrderMemberFilter,
188 OrderPaymentFilter,
189 OrderProductFilter,
190 ]
192 fields = (
193 "id",
194 "shift",
195 "created_at",
196 "created_by",
197 "order_description",
198 "num_items",
199 "age_restricted",
200 "subtotal",
201 "discount",
202 "total_amount",
203 "payer",
204 "payment",
205 "payment_url",
206 )
208 readonly_fields = (
209 "id",
210 "created_at",
211 "created_by",
212 "order_description",
213 "num_items",
214 "subtotal",
215 "total_amount",
216 "_total_amount",
217 "_is_free",
218 "age_restricted",
219 "payment_url",
220 )
222 # Facet counts would crash for this admin. See #3585.
223 show_facets = admin.ShowFacets.NEVER
225 def get_readonly_fields(self, request: HttpRequest, obj: Order = None):
226 """Disallow changing shift when selected."""
227 default_fields = self.readonly_fields
229 if not (request.member and request.member.has_perm("sales.custom_prices")):
230 default_fields += ("discount",)
232 if obj and obj.shift:
233 default_fields += ("shift",)
235 return default_fields
237 def save_model(self, request, obj, form, change):
238 obj.created_by = request.user
239 obj.save()
241 def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
242 if object_id: 242 ↛ 252line 242 didn't jump to line 252 because the condition on line 242 was always true
243 obj = self.model.objects.get(pk=object_id)
244 if obj.age_restricted and obj.payer and not services.is_adult(obj.payer): 244 ↛ 245line 244 didn't jump to line 245 because the condition on line 244 was never true
245 self.message_user(
246 request,
247 _(
248 "The payer for this order is under-age while the order is age restricted!"
249 ),
250 messages.WARNING,
251 )
252 return super().changeform_view(request, object_id, form_url, extra_context)
254 def get_queryset(self, request):
255 queryset = super().get_queryset(request)
257 if not request.member: 257 ↛ 258line 257 didn't jump to line 258 because the condition on line 257 was never true
258 queryset = queryset.none()
259 elif not request.member.has_perm("sales.override_manager"): 259 ↛ 260line 259 didn't jump to line 260 because the condition on line 259 was never true
260 queryset = queryset.filter(
261 shift__managers__in=request.member.get_member_groups()
262 )
264 queryset = queryset.select_properties(
265 "total_amount", "subtotal", "num_items", "age_restricted"
266 )
267 queryset = queryset.prefetch_related(
268 "shift", "shift__event", "shift__product_list"
269 )
270 queryset = queryset.prefetch_related(
271 "order_items", "order_items__product", "order_items__product__product"
272 )
273 queryset = queryset.prefetch_related("payment")
274 queryset = queryset.prefetch_related("payer")
275 return queryset
277 def has_add_permission(self, request):
278 if not request.member: 278 ↛ 279line 278 didn't jump to line 279 because the condition on line 278 was never true
279 return False
280 if not request.member.has_perm("sales.override_manager"):
281 if not Shift.objects.filter(
282 start__lte=timezone.now(),
283 locked=False,
284 managers__in=request.member.get_member_groups(),
285 ).exists():
286 return False
287 return super().has_view_permission(request)
289 def has_view_permission(self, request, obj=None):
290 if obj and not is_manager(request.member, obj.shift):
291 return False
292 return super().has_view_permission(request, obj)
294 def has_change_permission(self, request, obj=None):
295 if obj and obj.shift.locked:
296 return False
297 if obj and obj.payment:
298 return False
300 if obj and not is_manager(request.member, obj.shift):
301 return False
303 return super().has_change_permission(request, obj)
305 def has_delete_permission(self, request, obj=None):
306 if obj and obj.shift.locked:
307 return False
308 if obj and obj.payment:
309 return False
311 if obj and not is_manager(request.member, obj.shift):
312 return False
314 return super().has_delete_permission(request, obj)
316 def get_form(self, request, obj=None, **kwargs):
317 """Override get form to use payment widget."""
318 return super().get_form(
319 request,
320 obj,
321 formfield_callback=partial(
322 self.formfield_for_dbfield, request=request, obj=obj
323 ),
324 **kwargs,
325 )
327 def formfield_for_dbfield(self, db_field, request, obj=None, **kwargs):
328 """Use payment widget for payments."""
329 field = super().formfield_for_dbfield(db_field, request, **kwargs)
330 if db_field.name == "payment":
331 return Field(
332 widget=PaymentWidget(obj=obj), initial=field.initial, required=False
333 )
334 if db_field.name == "shift": 334 ↛ 335line 334 didn't jump to line 335 because the condition on line 334 was never true
335 field.queryset = Shift.objects.filter(locked=False)
336 if not request.member:
337 field.queryset = field.queryset.none()
338 elif not request.member.has_perm("sales.override_manager"):
339 field.queryset = field.queryset.filter(
340 managers__in=request.member.get_member_groups()
341 ).distinct()
342 return field
344 def changelist_view(self, request, extra_context=None):
345 if not (request.member and request.member.has_perm("sales.override_manager")): 345 ↛ 346line 345 didn't jump to line 346 because the condition on line 345 was never true
346 self.message_user(
347 request,
348 _("You are only seeing orders that are relevant to you."),
349 messages.WARNING,
350 )
351 return super().changelist_view(request, extra_context)
353 def change_view(self, request, object_id, form_url="", extra_context=None):
354 if object_id: 354 ↛ 371line 354 didn't jump to line 371 because the condition on line 354 was always true
355 try:
356 obj = self.model.objects.get(pk=object_id)
357 if ( 357 ↛ 362line 357 didn't jump to line 362
358 obj.age_restricted
359 and obj.payer
360 and not services.is_adult(obj.payer)
361 ):
362 self.message_user(
363 request,
364 _(
365 "The payer for this order is under-age while the order is age restricted!"
366 ),
367 messages.WARNING,
368 )
369 except self.model.DoesNotExist:
370 pass
371 return super().change_view(request, object_id, form_url, extra_context)
373 def order_description(self, obj):
374 if obj.order_description:
375 return obj.order_description
376 return "-"
378 def num_items(self, obj):
379 return obj.num_items
381 def subtotal(self, obj):
382 if obj.subtotal:
383 return f"€{obj.subtotal:.2f}"
384 return "-"
386 def discount(self, obj):
387 if obj.discount:
388 return f"€{obj.discount:.2f}"
389 return "-"
391 def total_amount(self, obj):
392 if obj.total_amount:
393 return f"€{obj.total_amount:.2f}"
394 return "-"
396 def paid(self, obj):
397 if obj.total_amount is None or obj.total_amount == 0:
398 return None
399 return obj.payment is not None
401 paid.boolean = True
403 def age_restricted(self, obj):
404 return bool(obj.age_restricted) if obj else None
406 age_restricted.boolean = True