forked from DGNum/gestioCOF
Merge branch 'Aufinal/paid_attributions' into 'master'
Déplace le champ `paid` des participants aux attributions See merge request klub-dev-ens/gestioCOF!361
This commit is contained in:
commit
4598abc721
9 changed files with 213 additions and 46 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
- Le champ de paiement BdA se fait au niveau des attributions
|
||||||
- On peut supprimer des comptes et des articles K-Fêt
|
- On peut supprimer des comptes et des articles K-Fêt
|
||||||
- Passage à Django2
|
- Passage à Django2
|
||||||
- Dev : on peut désactiver la barre de debug avec une variable shell
|
- Dev : on peut désactiver la barre de debug avec une variable shell
|
||||||
|
|
57
bda/admin.py
57
bda/admin.py
|
@ -4,7 +4,7 @@ from custommail.shortcuts import send_mass_custom_mail
|
||||||
from dal.autocomplete import ModelSelect2
|
from dal.autocomplete import ModelSelect2
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.db.models import Count, Sum
|
from django.db.models import Count, Q, Sum
|
||||||
from django.template.defaultfilters import pluralize
|
from django.template.defaultfilters import pluralize
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
@ -81,11 +81,13 @@ class WithListingAttributionInline(AttributionInline):
|
||||||
exclude = ("given",)
|
exclude = ("given",)
|
||||||
form = WithListingAttributionTabularAdminForm
|
form = WithListingAttributionTabularAdminForm
|
||||||
listing = True
|
listing = True
|
||||||
|
verbose_name_plural = "Attributions sur listing"
|
||||||
|
|
||||||
|
|
||||||
class WithoutListingAttributionInline(AttributionInline):
|
class WithoutListingAttributionInline(AttributionInline):
|
||||||
form = WithoutListingAttributionTabularAdminForm
|
form = WithoutListingAttributionTabularAdminForm
|
||||||
listing = False
|
listing = False
|
||||||
|
verbose_name_plural = "Attributions hors listing"
|
||||||
|
|
||||||
|
|
||||||
class ParticipantAdminForm(forms.ModelForm):
|
class ParticipantAdminForm(forms.ModelForm):
|
||||||
|
@ -96,12 +98,31 @@ class ParticipantAdminForm(forms.ModelForm):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantPaidFilter(admin.SimpleListFilter):
|
||||||
|
"""
|
||||||
|
Permet de filtrer les participants sur s'ils ont payé leurs places ou pas
|
||||||
|
"""
|
||||||
|
|
||||||
|
title = "A payé"
|
||||||
|
parameter_name = "paid"
|
||||||
|
|
||||||
|
def lookups(self, request, model_admin):
|
||||||
|
return ((True, "Oui"), (False, "Non"))
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
return queryset.filter(paid=self.value())
|
||||||
|
|
||||||
|
|
||||||
class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
inlines = [WithListingAttributionInline, WithoutListingAttributionInline]
|
inlines = [WithListingAttributionInline, WithoutListingAttributionInline]
|
||||||
|
|
||||||
def get_queryset(self, request):
|
def get_queryset(self, request):
|
||||||
return Participant.objects.annotate(
|
return self.model.objects.annotate_paid().annotate(
|
||||||
nb_places=Count("attributions"), total=Sum("attributions__price")
|
nb_places=Count("attributions"),
|
||||||
|
remain=Sum(
|
||||||
|
"attribution__spectacle__price", filter=Q(attribution__paid=False)
|
||||||
|
),
|
||||||
|
total=Sum("attributions__price"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def nb_places(self, obj):
|
def nb_places(self, obj):
|
||||||
|
@ -110,6 +131,13 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
nb_places.admin_order_field = "nb_places"
|
nb_places.admin_order_field = "nb_places"
|
||||||
nb_places.short_description = "Nombre de places"
|
nb_places.short_description = "Nombre de places"
|
||||||
|
|
||||||
|
def paid(self, obj):
|
||||||
|
return obj.paid
|
||||||
|
|
||||||
|
paid.short_description = "A payé"
|
||||||
|
paid.boolean = True
|
||||||
|
paid.admin_order_field = "paid"
|
||||||
|
|
||||||
def total(self, obj):
|
def total(self, obj):
|
||||||
tot = obj.total
|
tot = obj.total
|
||||||
if tot:
|
if tot:
|
||||||
|
@ -118,14 +146,25 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
return "0 €"
|
return "0 €"
|
||||||
|
|
||||||
total.admin_order_field = "total"
|
total.admin_order_field = "total"
|
||||||
total.short_description = "Total à payer"
|
total.short_description = "Total des places"
|
||||||
list_display = ("user", "nb_places", "total", "paid", "paymenttype", "tirage")
|
|
||||||
list_filter = ("paid", "tirage")
|
def remain(self, obj):
|
||||||
|
rem = obj.remain
|
||||||
|
if rem:
|
||||||
|
return "%.02f €" % rem
|
||||||
|
else:
|
||||||
|
return "0 €"
|
||||||
|
|
||||||
|
remain.admin_order_field = "remain"
|
||||||
|
remain.short_description = "Reste à payer"
|
||||||
|
|
||||||
|
list_display = ("user", "nb_places", "total", "paid", "remain", "tirage")
|
||||||
|
list_filter = (ParticipantPaidFilter, "tirage")
|
||||||
search_fields = ("user__username", "user__first_name", "user__last_name")
|
search_fields = ("user__username", "user__first_name", "user__last_name")
|
||||||
actions = ["send_attribs"]
|
actions = ["send_attribs"]
|
||||||
actions_on_bottom = True
|
actions_on_bottom = True
|
||||||
list_per_page = 400
|
list_per_page = 400
|
||||||
readonly_fields = ("total",)
|
readonly_fields = ("total", "paid")
|
||||||
readonly_fields_update = ("user", "tirage")
|
readonly_fields_update = ("user", "tirage")
|
||||||
form = ParticipantAdminForm
|
form = ParticipantAdminForm
|
||||||
|
|
||||||
|
@ -183,11 +222,7 @@ class AttributionAdminForm(forms.ModelForm):
|
||||||
|
|
||||||
|
|
||||||
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
|
||||||
def paid(self, obj):
|
|
||||||
return obj.participant.paid
|
|
||||||
|
|
||||||
paid.short_description = "A payé"
|
|
||||||
paid.boolean = True
|
|
||||||
list_display = ("id", "spectacle", "participant", "given", "paid")
|
list_display = ("id", "spectacle", "participant", "given", "paid")
|
||||||
search_fields = (
|
search_fields = (
|
||||||
"spectacle__title",
|
"spectacle__title",
|
||||||
|
|
31
bda/migrations/0014_attribution_paid_field.py
Normal file
31
bda/migrations/0014_attribution_paid_field.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 2.2 on 2019-06-03 19:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("bda", "0013_merge_20180524_2123")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="attribution",
|
||||||
|
name="paid",
|
||||||
|
field=models.BooleanField(default=False, verbose_name="Payée"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="attribution",
|
||||||
|
name="paymenttype",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[
|
||||||
|
("cash", "Cash"),
|
||||||
|
("cb", "CB"),
|
||||||
|
("cheque", "Chèque"),
|
||||||
|
("autre", "Autre"),
|
||||||
|
],
|
||||||
|
max_length=6,
|
||||||
|
verbose_name="Moyen de paiement",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
37
bda/migrations/0015_move_bda_payment.py
Normal file
37
bda/migrations/0015_move_bda_payment.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Generated by Django 2.2 on 2019-06-03 19:30
|
||||||
|
|
||||||
|
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def set_attr_payment(apps, schema_editor):
|
||||||
|
Attribution = apps.get_model("bda", "Attribution")
|
||||||
|
for attr in Attribution.objects.all():
|
||||||
|
attr.paid = attr.participant.paid
|
||||||
|
attr.paymenttype = attr.participant.paymenttype
|
||||||
|
attr.save()
|
||||||
|
|
||||||
|
|
||||||
|
def set_participant_payment(apps, schema_editor):
|
||||||
|
Participant = apps.get_model("bda", "Participant")
|
||||||
|
for part in Participant.objects.all():
|
||||||
|
attr_set = part.attribution_set
|
||||||
|
part.paid = attr_set.exists() and not attr_set.filter(paid=False).exists()
|
||||||
|
try:
|
||||||
|
# S'il n'y a qu'un seul type de paiement, on le set
|
||||||
|
part.paymenttype = (
|
||||||
|
attr_set.values_list("paymenttype", flat=True).distinct().get()
|
||||||
|
)
|
||||||
|
# Sinon, whatever
|
||||||
|
except (ObjectDoesNotExist, MultipleObjectsReturned):
|
||||||
|
pass
|
||||||
|
part.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("bda", "0014_attribution_paid_field")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(set_attr_payment, set_participant_payment, atomic=True)
|
||||||
|
]
|
13
bda/migrations/0016_delete_participant_paid.py
Normal file
13
bda/migrations/0016_delete_participant_paid.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Generated by Django 2.2 on 2019-06-03 19:30
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [("bda", "0015_move_bda_payment")]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(model_name="participant", name="paid"),
|
||||||
|
migrations.RemoveField(model_name="participant", name="paymenttype"),
|
||||||
|
]
|
|
@ -9,7 +9,7 @@ from django.contrib.auth.models import User
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Count
|
from django.db.models import Count, Exists
|
||||||
from django.utils import formats, timezone
|
from django.utils import formats, timezone
|
||||||
|
|
||||||
|
|
||||||
|
@ -151,6 +151,41 @@ PAYMENT_TYPES = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Attribution(models.Model):
|
||||||
|
participant = models.ForeignKey("Participant", on_delete=models.CASCADE)
|
||||||
|
spectacle = models.ForeignKey(
|
||||||
|
Spectacle, on_delete=models.CASCADE, related_name="attribues"
|
||||||
|
)
|
||||||
|
given = models.BooleanField("Donnée", default=False)
|
||||||
|
paid = models.BooleanField("Payée", default=False)
|
||||||
|
paymenttype = models.CharField(
|
||||||
|
"Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s -- %s, %s" % (
|
||||||
|
self.participant.user,
|
||||||
|
self.spectacle.title,
|
||||||
|
self.spectacle.date,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ParticipantPaidQueryset(models.QuerySet):
|
||||||
|
"""
|
||||||
|
Un manager qui annote le queryset avec un champ `paid`,
|
||||||
|
indiquant si un participant a payé toutes ses attributions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def annotate_paid(self):
|
||||||
|
# OuterRef permet de se référer à un champ d'un modèle non encore fixé
|
||||||
|
# Voir:
|
||||||
|
# https://docs.djangoproject.com/en/2.2/ref/models/expressions/#django.db.models.OuterRef
|
||||||
|
unpaid = Attribution.objects.filter(
|
||||||
|
participant=models.OuterRef("pk"), paid=False
|
||||||
|
)
|
||||||
|
return self.annotate(paid=~Exists(unpaid))
|
||||||
|
|
||||||
|
|
||||||
class Participant(models.Model):
|
class Participant(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
choices = models.ManyToManyField(
|
choices = models.ManyToManyField(
|
||||||
|
@ -159,15 +194,13 @@ class Participant(models.Model):
|
||||||
attributions = models.ManyToManyField(
|
attributions = models.ManyToManyField(
|
||||||
Spectacle, through="Attribution", related_name="attributed_to"
|
Spectacle, through="Attribution", related_name="attributed_to"
|
||||||
)
|
)
|
||||||
paid = models.BooleanField("A payé", default=False)
|
|
||||||
paymenttype = models.CharField(
|
|
||||||
"Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True
|
|
||||||
)
|
|
||||||
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
|
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
|
||||||
choicesrevente = models.ManyToManyField(
|
choicesrevente = models.ManyToManyField(
|
||||||
Spectacle, related_name="subscribed", blank=True
|
Spectacle, related_name="subscribed", blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
objects = ParticipantPaidQueryset.as_manager()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "%s - %s" % (self.user, self.tirage.title)
|
return "%s - %s" % (self.user, self.tirage.title)
|
||||||
|
|
||||||
|
@ -212,21 +245,6 @@ class ChoixSpectacle(models.Model):
|
||||||
verbose_name_plural = "voeux"
|
verbose_name_plural = "voeux"
|
||||||
|
|
||||||
|
|
||||||
class Attribution(models.Model):
|
|
||||||
participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
|
|
||||||
spectacle = models.ForeignKey(
|
|
||||||
Spectacle, on_delete=models.CASCADE, related_name="attribues"
|
|
||||||
)
|
|
||||||
given = models.BooleanField("Donnée", default=False)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s -- %s, %s" % (
|
|
||||||
self.participant.user,
|
|
||||||
self.spectacle.title,
|
|
||||||
self.spectacle.date,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SpectacleRevente(models.Model):
|
class SpectacleRevente(models.Model):
|
||||||
attribution = models.OneToOneField(
|
attribution = models.OneToOneField(
|
||||||
Attribution, on_delete=models.CASCADE, related_name="revente"
|
Attribution, on_delete=models.CASCADE, related_name="revente"
|
||||||
|
|
|
@ -23,7 +23,11 @@ urlpatterns = [
|
||||||
views.spectacle,
|
views.spectacle,
|
||||||
name="bda-spectacle",
|
name="bda-spectacle",
|
||||||
),
|
),
|
||||||
url(r"^spectacles/unpaid/(?P<tirage_id>\d+)$", views.unpaid, name="bda-unpaid"),
|
url(
|
||||||
|
r"^spectacles/unpaid/(?P<tirage_id>\d+)$",
|
||||||
|
views.UnpaidParticipants.as_view(),
|
||||||
|
name="bda-unpaid",
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r"^spectacles/autocomplete$",
|
r"^spectacles/autocomplete$",
|
||||||
views.spectacle_autocomplete,
|
views.spectacle_autocomplete,
|
||||||
|
|
22
bda/views.py
22
bda/views.py
|
@ -41,7 +41,7 @@ from bda.models import (
|
||||||
SpectacleRevente,
|
SpectacleRevente,
|
||||||
Tirage,
|
Tirage,
|
||||||
)
|
)
|
||||||
from gestioncof.decorators import buro_required, cof_required
|
from gestioncof.decorators import BuroRequiredMixin, buro_required, cof_required
|
||||||
from utils.views.autocomplete import Select2QuerySetView
|
from utils.views.autocomplete import Select2QuerySetView
|
||||||
|
|
||||||
|
|
||||||
|
@ -378,7 +378,7 @@ def revente_manage(request, tirage_id):
|
||||||
- Annulation d'une revente après que le tirage a eu lieu
|
- Annulation d'une revente après que le tirage a eu lieu
|
||||||
"""
|
"""
|
||||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
participant, created = Participant.objects.get_or_create(
|
participant, created = Participant.annotate_paid().get_or_create(
|
||||||
user=request.user, tirage=tirage
|
user=request.user, tirage=tirage
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -695,12 +695,13 @@ def spectacle(request, tirage_id, spectacle_id):
|
||||||
"username": participant.user.username,
|
"username": participant.user.username,
|
||||||
"email": participant.user.email,
|
"email": participant.user.email,
|
||||||
"given": int(attrib.given),
|
"given": int(attrib.given),
|
||||||
"paid": participant.paid,
|
"paid": True,
|
||||||
"nb_places": 1,
|
"nb_places": 1,
|
||||||
}
|
}
|
||||||
if participant.id in participants:
|
if participant.id in participants:
|
||||||
participants[participant.id]["nb_places"] += 1
|
participants[participant.id]["nb_places"] += 1
|
||||||
participants[participant.id]["given"] += attrib.given
|
participants[participant.id]["given"] += attrib.given
|
||||||
|
participants[participant.id]["paid"] &= attrib.paid
|
||||||
else:
|
else:
|
||||||
participants[participant.id] = participant_info
|
participants[participant.id] = participant_info
|
||||||
|
|
||||||
|
@ -728,15 +729,16 @@ class SpectacleListView(ListView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@buro_required
|
class UnpaidParticipants(BuroRequiredMixin, ListView):
|
||||||
def unpaid(request, tirage_id):
|
context_object_name = "unpaid"
|
||||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
template_name = "bda-unpaid.html"
|
||||||
unpaid = (
|
|
||||||
tirage.participant_set.annotate(nb_attributions=Count("attribution"))
|
def get_queryset(self):
|
||||||
.filter(paid=False, nb_attributions__gt=0)
|
return (
|
||||||
|
Participant.objects.annotate_paid()
|
||||||
|
.filter(tirage__id=self.kwargs["tirage_id"], paid=False)
|
||||||
.select_related("user")
|
.select_related("user")
|
||||||
)
|
)
|
||||||
return render(request, "bda-unpaid.html", {"unpaid": unpaid})
|
|
||||||
|
|
||||||
|
|
||||||
@buro_required
|
@buro_required
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
import logging
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def cof_required(view_func):
|
def cof_required(view_func):
|
||||||
"""Décorateur qui vérifie que l'utilisateur est connecté et membre du COF.
|
"""Décorateur qui vérifie que l'utilisateur est connecté et membre du COF.
|
||||||
|
@ -53,3 +56,26 @@ def buro_required(view_func):
|
||||||
return render(request, "buro-denied.html", status=403)
|
return render(request, "buro-denied.html", status=403)
|
||||||
|
|
||||||
return login_required(_wrapped_view)
|
return login_required(_wrapped_view)
|
||||||
|
|
||||||
|
|
||||||
|
class CofRequiredMixin(PermissionRequiredMixin):
|
||||||
|
def has_permission(self):
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return self.request.user.profile.is_cof
|
||||||
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class BuroRequiredMixin(PermissionRequiredMixin):
|
||||||
|
def has_permission(self):
|
||||||
|
if not self.request.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return self.request.user.profile.is_buro
|
||||||
|
except AttributeError:
|
||||||
|
logger.error(
|
||||||
|
"L'utilisateur %s n'a pas de profil !", self.request.user.username
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
Loading…
Reference in a new issue