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

1from functools import partial 

2 

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 _ 

10 

11from admin_auto_filters.filters import AutocompleteFilter 

12 

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 

18 

19 

20class OrderItemInline(admin.TabularInline): 

21 model = OrderItem 

22 extra = 1 

23 

24 fields = ("product", "product_name", "amount", "total") 

25 readonly_fields = ("product_name",) 

26 

27 def get_readonly_fields(self, request: HttpRequest, obj: Order = None): 

28 default_fields = self.readonly_fields 

29 

30 if not (request.member and request.member.has_perm("sales.custom_prices")): 

31 default_fields += ("total",) 

32 

33 return default_fields 

34 

35 def get_queryset(self, request): 

36 queryset = super().get_queryset(request) 

37 queryset = queryset.prefetch_related("product", "product__product") 

38 return queryset 

39 

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 

43 

44 if obj and obj.payment: 

45 return False 

46 

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 

50 

51 return super().has_add_permission(request, obj) 

52 

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 

61 

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 

70 

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 

78 

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) 

82 

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

90 

91 return field 

92 

93 

94class OrderShiftFilter(AutocompleteFilter): 

95 title = _("shift") 

96 field_name = "shift" 

97 rel_model = Order 

98 use_pk_exact = False 

99 

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 

104 

105 

106class OrderMemberFilter(AutocompleteFilter): 

107 title = _("member") 

108 field_name = "payer" 

109 rel_model = Order 

110 use_pk_exact = False 

111 

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 

116 

117 

118class OrderPaymentFilter(SimpleListFilter): 

119 title = _("payment") 

120 parameter_name = "payment" 

121 

122 def lookups(self, request, model_admin): 

123 return ( 

124 ("not_required", _("No payment required")), 

125 ("paid", _("Paid")), 

126 ("unpaid", _("Unpaid")), 

127 ) 

128 

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) 

137 

138 

139class OrderProductFilter(SimpleListFilter): 

140 title = _("product") 

141 parameter_name = "product" 

142 

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

149 

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

154 

155 

156@register(Order) 

157class OrderAdmin(admin.ModelAdmin): 

158 class Media: 

159 pass 

160 

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 ) 

173 

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 ] 

191 

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 ) 

207 

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 ) 

221 

222 # Facet counts would crash for this admin. See #3585. 

223 show_facets = admin.ShowFacets.NEVER 

224 

225 def get_readonly_fields(self, request: HttpRequest, obj: Order = None): 

226 """Disallow changing shift when selected.""" 

227 default_fields = self.readonly_fields 

228 

229 if not (request.member and request.member.has_perm("sales.custom_prices")): 

230 default_fields += ("discount",) 

231 

232 if obj and obj.shift: 

233 default_fields += ("shift",) 

234 

235 return default_fields 

236 

237 def save_model(self, request, obj, form, change): 

238 obj.created_by = request.user 

239 obj.save() 

240 

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) 

253 

254 def get_queryset(self, request): 

255 queryset = super().get_queryset(request) 

256 

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 ) 

263 

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 

276 

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) 

288 

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) 

293 

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 

299 

300 if obj and not is_manager(request.member, obj.shift): 

301 return False 

302 

303 return super().has_change_permission(request, obj) 

304 

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 

310 

311 if obj and not is_manager(request.member, obj.shift): 

312 return False 

313 

314 return super().has_delete_permission(request, obj) 

315 

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 ) 

326 

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 

343 

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) 

352 

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) 

372 

373 def order_description(self, obj): 

374 if obj.order_description: 

375 return obj.order_description 

376 return "-" 

377 

378 def num_items(self, obj): 

379 return obj.num_items 

380 

381 def subtotal(self, obj): 

382 if obj.subtotal: 

383 return f"{obj.subtotal:.2f}" 

384 return "-" 

385 

386 def discount(self, obj): 

387 if obj.discount: 

388 return f"{obj.discount:.2f}" 

389 return "-" 

390 

391 def total_amount(self, obj): 

392 if obj.total_amount: 

393 return f"{obj.total_amount:.2f}" 

394 return "-" 

395 

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 

400 

401 paid.boolean = True 

402 

403 def age_restricted(self, obj): 

404 return bool(obj.age_restricted) if obj else None 

405 

406 age_restricted.boolean = True