Пример #1
0
class Segment(BaseSegment, models.Model):
    GA_STATUS_NOT_MEMBER = "N"
    GA_STATUS_MEMBER = "m"
    GA_STATUS_MANAGER = "M"
    GA_STATUS_REFERENT = "R"
    GA_STATUS_CHOICES = (
        (GA_STATUS_NOT_MEMBER, "Non membres de GA"),
        (GA_STATUS_MEMBER, "Membres de GA"),
        (GA_STATUS_MANAGER, "Animateur·ices et gestionnaires de GA"),
        (GA_STATUS_REFERENT, "Animateur·ices de GA"),
    )

    name = models.CharField("Nom", max_length=255)

    tags = models.ManyToManyField("people.PersonTag", blank=True)

    is_2022 = models.BooleanField("Inscrits NSP", null=True, blank=True)
    is_insoumise = models.BooleanField("Inscrits LFI",
                                       null=True,
                                       blank=True,
                                       default=True)

    newsletters = ChoiceArrayField(
        models.CharField(choices=Person.NEWSLETTERS_CHOICES, max_length=255),
        default=default_newsletters,
        help_text="Inclure les personnes abonnées aux newsletters suivantes.",
        blank=True,
    )
    supportgroup_status = models.CharField(
        "Limiter aux membres de groupes ayant ce statut",
        max_length=1,
        choices=GA_STATUS_CHOICES,
        blank=True,
    )
    supportgroup_subtypes = models.ManyToManyField(
        "groups.SupportGroupSubtype",
        verbose_name="Limiter aux membres de groupes d'un de ces sous-types",
        blank=True,
    )
    events = models.ManyToManyField(
        "events.Event",
        verbose_name="Limiter aux participant⋅e⋅s à un des événements",
        blank=True,
    )
    events_subtypes = models.ManyToManyField(
        "events.EventSubtype",
        verbose_name="Limiter aux participant⋅e⋅s à un événements de ce type",
        blank=True,
    )
    events_start_date = models.DateTimeField(
        "Limiter aux participant⋅e⋅s à des événements commençant après cette date",
        blank=True,
        null=True,
    )
    events_end_date = models.DateTimeField(
        "Limiter aux participant⋅e⋅s à des événements terminant avant cette date",
        blank=True,
        null=True,
    )
    events_organizer = models.BooleanField(
        "Limiter aux organisateurices (sans effet si pas d'autres filtres événements)",
        blank=True,
        default=False,
    )

    draw_status = models.BooleanField(
        "Limiter aux gens dont l'inscription au tirage au sort est",
        null=True,
        blank=True,
        default=None,
    )

    forms = models.ManyToManyField(
        "people.PersonForm",
        verbose_name="A répondu à au moins un de ces formulaires",
        blank=True,
        related_name="+",
    )

    polls = models.ManyToManyField(
        "polls.Poll",
        verbose_name="A participé à au moins une de ces consultations",
        blank=True,
        related_name="+",
    )

    countries = CountryField("Limiter aux pays", multiple=True, blank=True)
    departements = ChoiceArrayField(
        models.CharField(choices=data.departements_choices, max_length=3),
        verbose_name=
        "Limiter aux départements (calcul à partir du code postal)",
        default=list,
        blank=True,
    )
    area = MultiPolygonField("Limiter à un territoire définit manuellement",
                             blank=True,
                             null=True)

    campaigns = models.ManyToManyField(
        "nuntius.Campaign",
        related_name="+",
        verbose_name="Limiter aux personnes ayant reçu une des campagnes",
        blank=True,
    )

    last_open = models.IntegerField(
        "Limiter aux personnes ayant ouvert un email envoyé au court de derniers jours",
        help_text="Indiquer le nombre de jours",
        blank=True,
        null=True,
    )

    last_click = models.IntegerField(
        "Limiter aux personnes ayant cliqué dans un email envoyé au court des derniers jours",
        help_text="Indiquer le nombre de jours",
        blank=True,
        null=True,
    )

    FEEDBACK_OPEN = 1
    FEEDBACK_CLICKED = 2
    FEEDBACK_NOT_OPEN = 3
    FEEDBACK_NOT_CLICKED = 4
    FEEDBACK_OPEN_NOT_CLICKED = 5
    FEEDBACK_CHOICES = (
        (FEEDBACK_OPEN, "Personnes ayant ouvert"),
        (FEEDBACK_CLICKED, "Personnes ayant cliqué"),
        (FEEDBACK_NOT_OPEN, "Personnes n'ayant pas ouvert"),
        (FEEDBACK_NOT_CLICKED, "Personnes n'ayant pas cliqué"),
        (FEEDBACK_OPEN_NOT_CLICKED, "Personnes ayant ouvert mais pas cliqué"),
    )

    campaigns_feedback = models.PositiveSmallIntegerField(
        "Limiter en fonction de la réaction à ces campagnes",
        blank=True,
        null=True,
        choices=FEEDBACK_CHOICES,
        help_text=
        "Aucun effet si aucune campagne n'est sélectionnée dans le champ précédent",
    )

    registration_date = models.DateTimeField(
        "Limiter aux membres inscrit⋅e⋅s après cette date",
        blank=True,
        null=True)

    registration_duration = models.IntegerField(
        "Limiter aux membres inscrit⋅e⋅s depuis au moins un certain nombre d'heures",
        help_text="Indiquer le nombre d'heures",
        blank=True,
        null=True,
    )

    last_login = models.DateTimeField(
        "Limiter aux membres s'étant connecté⋅e pour la dernière fois après cette date",
        blank=True,
        null=True,
    )

    gender = models.CharField("Genre",
                              max_length=1,
                              blank=True,
                              choices=Person.GENDER_CHOICES)

    born_after = models.DateField("Personnes nées après le",
                                  blank=True,
                                  null=True,
                                  help_text=DATE_HELP_TEXT)
    born_before = models.DateField("Personnes nées avant le",
                                   blank=True,
                                   null=True,
                                   help_text=DATE_HELP_TEXT)

    donation_after = models.DateField(
        "A fait au moins un don (don mensuel inclus) après le",
        blank=True,
        null=True,
        help_text=DATE_HELP_TEXT,
    )
    donation_not_after = models.DateField(
        "N'a pas fait de don (don mensuel inclus) depuis le",
        blank=True,
        null=True,
        help_text=DATE_HELP_TEXT,
    )
    donation_total_min = AmountField(
        "Montant total des dons supérieur ou égal à", blank=True, null=True)
    donation_total_max = AmountField(
        "Montant total des dons inférieur ou égal à", blank=True, null=True)
    donation_total_range = DateRangeField(
        "Pour le filtre de montant total, prendre uniquement en compte les dons entre ces deux dates",
        blank=True,
        null=True,
        help_text=
        "Écrire sous la forme JJ/MM/AAAA. La date de début est incluse, pas la date de fin.",
    )

    subscription = models.BooleanField("A une souscription mensuelle active",
                                       blank=True,
                                       null=True)

    ELUS_NON = "N"
    ELUS_MEMBRE_RESEAU = "M"
    ELUS_REFERENCE = "R"
    ELUS_CHOICES = (
        ("", "Peu importe"),
        (ELUS_MEMBRE_RESEAU, "Uniquement les membres du réseau des élus"),
        (
            ELUS_REFERENCE,
            "Tous les élus, membres ou non du réseau, sauf les exclus du réseau",
        ),
    )

    elu = models.CharField("Est un élu",
                           max_length=1,
                           choices=ELUS_CHOICES,
                           blank=True)

    elu_municipal = models.BooleanField("Avec un mandat municipal",
                                        default=True)
    elu_departemental = models.BooleanField("Avec un mandat départemental",
                                            default=True)
    elu_regional = models.BooleanField("Avec un mandat régional", default=True)
    elu_consulaire = models.BooleanField("Avec un mandat consulaire",
                                         default=True)

    exclude_segments = models.ManyToManyField(
        "self",
        symmetrical=False,
        related_name="+",
        verbose_name="Exclure les personnes membres des segments suivants",
        blank=True,
    )

    add_segments = models.ManyToManyField(
        "self",
        symmetrical=False,
        related_name="+",
        verbose_name="Ajouter les personnes membres des segments suivants",
        blank=True,
    )

    def get_subscribers_q(self):
        # ne pas inclure les rôles inactifs dans les envois de mail
        q = ~Q(role__is_active=False)

        # permettre de créer des segments capables d'inclure des personnes inscrites à aucune des newsletters
        if self.newsletters:
            q &= Q(newsletters__overlap=self.newsletters)

        if self.is_insoumise is not None:
            q = q & Q(is_insoumise=self.is_insoumise)

        if self.is_2022 is not None:
            q = q & Q(is_2022=self.is_2022)

        if self.tags.all().count() > 0:
            q = q & Q(tags__in=self.tags.all())

        if self.supportgroup_status:
            if self.supportgroup_status == self.GA_STATUS_NOT_MEMBER:
                supportgroup_q = ~Q(memberships__supportgroup__published=True)
            elif self.supportgroup_status == self.GA_STATUS_MEMBER:
                supportgroup_q = Q(memberships__supportgroup__published=True)

            elif self.supportgroup_status == self.GA_STATUS_REFERENT:
                supportgroup_q = Q(
                    memberships__supportgroup__published=True,
                    memberships__membership_type__gte=Membership.
                    MEMBERSHIP_TYPE_REFERENT,
                )
            else:
                # ==> self.supportgroup_status == self.GA_STATUS_MANAGER
                supportgroup_q = Q(
                    memberships__supportgroup__published=True,
                    memberships__membership_type__gte=Membership.
                    MEMBERSHIP_TYPE_MANAGER,
                )

            if self.supportgroup_subtypes.all().count() > 0:
                supportgroup_q = supportgroup_q & Q(
                    memberships__supportgroup__subtypes__in=self.
                    supportgroup_subtypes.all())

            q = q & supportgroup_q

        events_filter = {}

        if self.events.all().count() > 0:
            events_filter["in"] = self.events.all()

        if self.events_subtypes.all().count() > 0:
            events_filter["subtype__in"] = self.events_subtypes.all()

        if self.events_start_date is not None:
            events_filter["start_time__gt"] = self.events_start_date

        if self.events_end_date is not None:
            events_filter["end_time__lt"] = self.events_end_date

        if events_filter:
            prefix = "organized_events" if self.events_organizer else "rsvps__event"
            q = q & Q(
                **{f"{prefix}__{k}": v
                   for k, v in events_filter.items()})

            if not self.events_organizer:
                q = q & Q(rsvps__status__in=[
                    RSVP.STATUS_CONFIRMED,
                    RSVP.STATUS_AWAITING_PAYMENT,
                ])

        if self.draw_status is not None:
            q = q & Q(draw_participation=self.draw_status)

        if self.forms.all().count() > 0:
            q = q & Q(form_submissions__form__in=self.forms.all())

        if self.polls.all().count() > 0:
            q = q & Q(poll_choices__poll__in=self.polls.all())

        if self.campaigns.all().count() > 0:
            if self.campaigns_feedback == self.FEEDBACK_OPEN:
                campaign__kwargs = {"campaignsentevent__open_count__gt": 0}
            elif self.campaigns_feedback == self.FEEDBACK_CLICKED:
                campaign__kwargs = {"campaignsentevent__click_count__gt": 0}
            elif self.campaigns_feedback == self.FEEDBACK_NOT_OPEN:
                campaign__kwargs = {"campaignsentevent__open_count": 0}
            elif self.campaigns_feedback == self.FEEDBACK_NOT_CLICKED:
                campaign__kwargs = {"campaignsentevent__click_count": 0}
            elif self.campaigns_feedback == self.FEEDBACK_OPEN_NOT_CLICKED:
                campaign__kwargs = {
                    "campaignsentevent__open_count__gt": 1,
                    "campaignsentevent__click_count": 0,
                }
            else:
                campaign__kwargs = {}

            q = q & Q(
                campaignsentevent__result__in=[
                    CampaignSentStatusType.UNKNOWN,
                    CampaignSentStatusType.OK,
                ],
                campaignsentevent__campaign__in=self.campaigns.all(),
                **campaign__kwargs,
            )

        if self.last_open is not None:
            q = q & Q(
                campaignsentevent__open_count__gt=0,
                campaignsentevent__datetime__gt=now() -
                timedelta(days=self.last_open),
            )

        if self.last_click is not None:
            q = q & Q(
                campaignsentevent__click_count__gt=0,
                campaignsentevent__datetime__gt=now() -
                timedelta(days=self.last_click),
            )

        if len(self.countries) > 0:
            q = q & Q(location_country__in=self.countries)

        if len(self.departements) > 0:
            q = q & Q(data.filtre_departements(*self.departements))

        if self.area is not None:
            q = q & Q(coordinates__intersects=self.area)

        if self.registration_date is not None:
            q = q & Q(created__gt=self.registration_date)

        if self.registration_duration:
            q = q & Q(created__lt=now() -
                      timedelta(hours=self.registration_duration))

        if self.last_login is not None:
            q = q & Q(role__last_login__gt=self.last_login)

        if self.gender:
            q = q & Q(gender=self.gender)

        if self.born_after is not None:
            q = q & Q(date_of_birth__gt=self.born_after)

        if self.born_before is not None:
            q = q & Q(date_of_birth__lt=self.born_before)

        if self.donation_after is not None:
            q = q & Q(payments__created__gt=self.donation_after,
                      **DONATION_FILTER)

        if self.donation_not_after is not None:
            q = q & ~Q(payments__created__gt=self.donation_not_after,
                       **DONATION_FILTER)

        if self.donation_total_min or self.donation_total_max:
            donation_range = (
                {
                    "payments__created__gt": self.donation_total_range.lower,
                    "payments__created__lt": self.donation_total_range.upper,
                } if self.donation_total_range else {})
            annotated_qs = Person.objects.annotate(donation_total=Sum(
                "payments__price",
                filter=Q(**DONATION_FILTER, **donation_range)))

            if self.donation_total_min:
                annotated_qs = annotated_qs.filter(
                    donation_total__gte=self.donation_total_min)

            if self.donation_total_max:
                annotated_qs = annotated_qs.filter(
                    donation_total__lte=self.donation_total_max)

            q = q & Q(id__in=annotated_qs.values_list("id"))

        if self.subscription is not None:
            if self.subscription:
                q = q & Q(subscriptions__status=Subscription.STATUS_ACTIVE)
            else:
                q = q & ~Q(subscriptions__status=Subscription.STATUS_ACTIVE)

        if self.elu:
            if self.elu == Segment.ELUS_MEMBRE_RESEAU:
                q &= Q(membre_reseau_elus=Person.MEMBRE_RESEAU_OUI)
            elif self.elu == Segment.ELUS_REFERENCE:
                q &= ~Q(membre_reseau_elus=Person.MEMBRE_RESEAU_EXCLUS)

            q_mandats = Q()
            for t in [
                    "elu_municipal",
                    "elu_departemental",
                    "elu_regional",
                    "elu_consulaire",
            ]:
                if getattr(self, t):
                    q_mandats |= Q(**{t: True})
            q &= q_mandats

        return q

    def _get_own_filters_queryset(self):
        qs = Person.objects.all()

        if self.elu:
            qs = qs.annotate_elus()

        return qs.filter(
            self.get_subscribers_q()).filter(emails___bounced=False)

    def get_subscribers_queryset(self):
        qs = self._get_own_filters_queryset()

        for s in self.add_segments.all():
            qs = Person.objects.filter(
                Q(pk__in=qs) | Q(pk__in=s.get_subscribers_queryset()))

        for s in self.exclude_segments.all():
            qs = qs.exclude(pk__in=s.get_subscribers_queryset())

        return qs.order_by("id").distinct("id")

    def get_subscribers_count(self):
        return (self._get_own_filters_queryset().order_by("id").distinct(
            "id").count() + sum(s.get_subscribers_count()
                                for s in self.add_segments.all()) -
                sum(s.get_subscribers_count()
                    for s in self.exclude_segments.all()))

    def is_subscriber(self, person):
        return self.get_subscribers_queryset().filter(pk=person.pk).exists()

    get_subscribers_count.short_description = "Personnes"
    get_subscribers_count.help_text = "Estimation du nombre d'inscrits"

    def __str__(self):
        return self.name
Пример #2
0
class Payment(ExportModelOperationsMixin("payment"), TimeStampedModel,
              LocationMixin):
    objects = PaymentManager()

    STATUS_WAITING = 0
    STATUS_COMPLETED = 1
    STATUS_ABANDONED = 2
    STATUS_CANCELED = 3
    STATUS_REFUSED = 4
    STATUS_REFUND = -1

    STATUS_CHOICES = (
        (STATUS_WAITING, "Paiement en attente"),
        (STATUS_COMPLETED, "Paiement terminé"),
        (STATUS_ABANDONED, "Paiement abandonné en cours"),
        (STATUS_CANCELED, "Paiement annulé avant encaissement"),
        (STATUS_REFUSED, "Paiement refusé par votre banque"),
        (STATUS_REFUND, "Paiement remboursé"),
    )

    person = models.ForeignKey("people.Person",
                               on_delete=models.SET_NULL,
                               null=True,
                               related_name="payments")

    email = models.EmailField("email", max_length=255)
    first_name = models.CharField("prénom", max_length=255)
    last_name = models.CharField("nom de famille", max_length=255)
    phone_number = PhoneNumberField("numéro de téléphone", null=True)

    type = models.CharField("type",
                            choices=get_payment_choices(),
                            max_length=255)
    mode = models.CharField(_("Mode de paiement"),
                            max_length=70,
                            null=False,
                            blank=False)

    price = AmountField(_("Prix"))
    status = models.IntegerField("status",
                                 choices=STATUS_CHOICES,
                                 default=STATUS_WAITING)
    meta = JSONField(blank=True, default=dict)
    events = JSONField(_("Événements de paiement"), blank=True, default=list)
    subscription = models.ForeignKey(
        "Subscription",
        on_delete=models.PROTECT,
        related_name="payments",
        null=True,
        blank=True,
    )

    def get_price_display(self):
        return "{} €".format(floatformat(self.price / 100, 2))

    get_price_display.short_description = "Prix"

    def get_mode_display(self):
        return (PAYMENT_MODES[self.mode].label
                if self.mode in PAYMENT_MODES else self.mode)

    get_mode_display.short_description = "Mode de paiement"

    def get_type_display(self):
        return (PAYMENT_TYPES[self.type].label
                if self.type in PAYMENT_TYPES else self.type)

    get_type_display.short_description = "Type de paiement"

    def get_payment_url(self):
        return front_url("payment_page", args=[self.pk])

    def can_retry(self):
        return (self.mode in PAYMENT_MODES
                and PAYMENT_MODES[self.mode].can_retry
                and self.status != self.STATUS_COMPLETED)

    def can_cancel(self):
        return (self.mode in PAYMENT_MODES
                and PAYMENT_MODES[self.mode].can_cancel
                and self.status != self.STATUS_COMPLETED)

    def html_full_address(self):
        return display_address(self)

    @property
    def description(self):
        from agir.payments.actions.payments import description_for_payment

        return description_for_payment(self)

    def __str__(self):
        return _("Paiement n°") + str(self.id)

    def __repr__(self):
        return "{klass}(id={id!r}, email={email!r}, status={status!r}, type={type!r}, mode={mode!r}, price={price!r})".format(
            klass=self.__class__.__name__,
            id=self.id,
            email=self.email,
            status=self.status,
            type=self.type,
            mode=self.mode,
            price=self.price,
        )

    class Meta:
        get_latest_by = "created"
        ordering = ("-created", )
        verbose_name = "Paiement"
        verbose_name_plural = "Paiements"
Пример #3
0
class SpendingRequest(HistoryMixin, TimeStampedModel):
    DIFFED_FIELDS = [
        "title",
        "event",
        "category",
        "category_precisions",
        "explanation",
        "amount",
        "spending_date",
        "provider",
        "iban",
    ]

    STATUS_DRAFT = "D"
    STATUS_AWAITING_GROUP_VALIDATION = "G"
    STATUS_AWAITING_REVIEW = "R"
    STATUS_AWAITING_SUPPLEMENTARY_INFORMATION = "I"
    STATUS_VALIDATED = "V"
    STATUS_TO_PAY = "T"
    STATUS_PAID = "P"
    STATUS_REFUSED = "B"
    STATUS_CHOICES = (
        (STATUS_DRAFT, _("Brouillon à compléter")),
        (
            STATUS_AWAITING_GROUP_VALIDATION,
            _("En attente de validation par un autre animateur"),
        ),
        (
            STATUS_AWAITING_REVIEW,
            _("En attente de vérification par l'équipe de suivi des questions financières"
              ),
        ),
        (
            STATUS_AWAITING_SUPPLEMENTARY_INFORMATION,
            _("Informations supplémentaires requises"),
        ),
        (STATUS_VALIDATED, _("Validée, en attente des fonds")),
        (STATUS_TO_PAY, _("Décomptée de l'allocation du groupe, à payer")),
        (STATUS_PAID, _("Payée")),
        (STATUS_REFUSED, _("Cette demande a été refusée")),
    )

    STATUS_NEED_ACTION = {
        STATUS_DRAFT,
        STATUS_AWAITING_GROUP_VALIDATION,
        STATUS_AWAITING_SUPPLEMENTARY_INFORMATION,
        STATUS_VALIDATED,
    }
    STATUS_ADMINISTRATOR_ACTION = {STATUS_AWAITING_REVIEW, STATUS_TO_PAY}
    STATUS_EDITION_MESSAGES = {
        STATUS_AWAITING_REVIEW:
        "Votre requête a déjà été transmise ! Si vous l'éditez, il vous faudra la retransmettre à nouveau.",
        STATUS_VALIDATED:
        "Votre requête a déjà été validée par l'équipe de suivi des questions financières. Si vous l'éditez, il vous faudra recommencer le processus de validation.",
    }

    CATEGORY_HARDWARE = "H"
    CATEGORY_VENUE = "V"
    CATEGORY_SERVICE = "S"
    CATEGORY_OTHER = "O"
    CATEGORY_CHOICES = (
        (CATEGORY_HARDWARE, _("Matériel militant")),
        (CATEGORY_VENUE, _("Location d'une salle")),
        (CATEGORY_SERVICE, _("Prestation de service")),
        (CATEGORY_SERVICE, _("Autres")),
    )

    HISTORY_MESSAGES = {
        STATUS_DRAFT:
        "Création de la demande",
        STATUS_AWAITING_GROUP_VALIDATION:
        "Validé par l'auteur d'origine",
        STATUS_AWAITING_REVIEW:
        "Renvoyé pour validation à l'équipe de suivi des questions financières",
        STATUS_AWAITING_SUPPLEMENTARY_INFORMATION:
        "Informations supplémentaires requises",
        STATUS_VALIDATED:
        "Demande validée par l'équipe de suivi des questions financières",
        STATUS_TO_PAY:
        "Demande en attente de réglement",
        STATUS_PAID:
        "Demande réglée",
        STATUS_REFUSED:
        "Demande rejetée par l'équipe de suivi des questions financières",
        (
            STATUS_AWAITING_GROUP_VALIDATION,
            STATUS_AWAITING_REVIEW,
        ):
        "Validé par un⋅e second⋅e animateur⋅rice",
    }
    for status, label in STATUS_CHOICES:
        HISTORY_MESSAGES[(status, status)] = "Modification de la demande"

    id = models.UUIDField(_("Identifiant"),
                          primary_key=True,
                          default=uuid.uuid4)

    title = models.CharField(_("Titre"), max_length=200)
    status = models.CharField(_("Statut"),
                              max_length=1,
                              default=STATUS_DRAFT,
                              choices=STATUS_CHOICES)

    operation = models.ForeignKey(Operation,
                                  on_delete=models.PROTECT,
                                  related_name="spending_request",
                                  null=True)

    group = models.ForeignKey(
        "groups.SupportGroup",
        on_delete=models.PROTECT,
        related_name="spending_requests",
        related_query_name="spending_request",
        blank=False,
        null=False,
    )
    event = models.ForeignKey(
        "events.Event",
        verbose_name=_("Événement lié à la dépense"),
        on_delete=models.SET_NULL,
        related_name="spending_requests",
        related_query_name="spending_request",
        blank=True,
        null=True,
        help_text=_(
            "Si c'est pertinent, l'événement concerné par la dépense. Il doit être organisé par le groupe pour"
            " pouvoir être sélectionné."),
    )

    category = models.CharField(
        _("Catégorie de demande"),
        max_length=1,
        blank=False,
        null=False,
        choices=CATEGORY_CHOICES,
    )
    category_precisions = models.CharField(
        _("Précisions sur le type de demande"),
        max_length=260,
        blank=False,
        null=False)

    explanation = models.TextField(
        _("Justification de la demande"),
        max_length=1500,
        help_text=
        _("Merci de justifier votre demande. Longueur conseillée : 500 signes."
          ),
    )

    amount = AmountField(
        _("Montant de la dépense"),
        null=False,
        blank=False,
        help_text=
        _("Pour que cette demande soit payée, la somme allouée à votre groupe doit être suffisante."
          ),
    )

    spending_date = models.DateField(
        _("Date de la dépense"),
        blank=False,
        null=False,
        help_text=
        _("Si la dépense n'a pas encore été effectuée, merci d'indiquer la date probable à laquelle elle surviendra."
          ),
    )

    provider = models.CharField(_("Raison sociale du prestataire"),
                                blank=False,
                                null=False,
                                max_length=200)

    iban = IBANField(
        _("RIB (format IBAN)"),
        blank=False,
        null=False,
        help_text=_(
            "Indiquez le RIB du prestataire s'il s'agit d'un réglement, ou le RIB de la personne qui a payé s'il s'agit"
            " d'un remboursement."),
        allowed_countries=["FR"],
    )

    payer_name = models.CharField(
        _("Nom de la personne qui a payé"),
        blank=True,
        max_length=200,
        help_text=
        "S'il s'agit du remboursement d'une dépense déjà faite, indiquez le nom de la personne qui a payé"
        " et à qui l'IBAN correspond. Sinon, laissez vide.",
    )

    class Meta:
        permissions = (("review_spendingrequest",
                        _("Peut traiter les demandes de dépenses")), )
        verbose_name = "Demande de dépense ou remboursement"
        verbose_name_plural = "Demandes de dépense ou remboursement"

    # noinspection PyMethodOverriding
    @classmethod
    def get_history_step(cls, old, new, *, admin=False, **kwargs):
        old_fields = old.field_dict if old else {}
        new_fields = new.field_dict
        old_status, new_status = old_fields.get("status"), new_fields["status"]
        revision = new.revision
        person = revision.user.person if revision and revision.user else None

        res = {
            "modified": new_fields["modified"],
            "comment": revision.get_comment(),
            "diff": cls.get_diff(old_fields, new_fields) if old_fields else [],
        }

        if person and admin:
            res["user"] = format_html(
                '<a href="{url}">{text}</a>',
                url=reverse("admin:people_person_change", args=[person.pk]),
                text=person.get_short_name(),
            )
        elif person:
            res["user"] = person.get_short_name()
        else:
            res["user"] = "******"

        # cas spécifique : si on revient à "attente d'informations supplémentaires suite à une modification par un non admin
        # c'est forcément une modification
        if (new_status == cls.STATUS_AWAITING_SUPPLEMENTARY_INFORMATION
                and person is not None):
            res["title"] = "Modification de la demande"
        # some couples (old_status, new_status)
        elif (old_status, new_status) in cls.HISTORY_MESSAGES:
            res["title"] = cls.HISTORY_MESSAGES[(old_status, new_status)]
        else:
            res["title"] = cls.HISTORY_MESSAGES.get(
                new_status, "[Modification non identifiée]")

        return res