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:
Ludovic Stephan 2019-06-17 21:59:01 +02:00
commit 4598abc721
9 changed files with 213 additions and 46 deletions

View file

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

View file

@ -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",

View 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",
),
),
]

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

View 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"),
]

View file

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

View file

@ -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,

View file

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

View file

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