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