From 5a8052ae476cd011bb0a5c4782e91ccad28a7890 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 6 Oct 2024 22:24:20 +0200 Subject: [PATCH] send mail to inactive users --- counter/admin.py | 12 + counter/management/__init__.py | 0 counter/management/commands/__init__.py | 0 .../management/commands/dump_warning_mail.py | 88 ++++ ...untdump_accountdump_unique_ongoing_dump.py | 64 +++ counter/models.py | 47 +- .../counter/account_dump_warning_mail.jinja | 43 ++ counter/tests/test_account_dump.py | 7 + locale/fr/LC_MESSAGES/django.po | 475 ++++++++++-------- sith/settings.py | 12 +- 10 files changed, 541 insertions(+), 207 deletions(-) create mode 100644 counter/management/__init__.py create mode 100644 counter/management/commands/__init__.py create mode 100644 counter/management/commands/dump_warning_mail.py create mode 100644 counter/migrations/0024_accountdump_accountdump_unique_ongoing_dump.py create mode 100644 counter/templates/counter/account_dump_warning_mail.jinja create mode 100644 counter/tests/test_account_dump.py diff --git a/counter/admin.py b/counter/admin.py index 966f5b28..42943338 100644 --- a/counter/admin.py +++ b/counter/admin.py @@ -49,6 +49,18 @@ class BillingInfoAdmin(admin.ModelAdmin): autocomplete_fields = ("customer",) +@admin.register(AccountDump) +class AccountDumpAdmin(admin.ModelAdmin): + list_display = ( + "customer", + "warning_mail_sent_at", + "warning_mail_error", + "dump_operation", + ) + autocomplete_fields = ("customer",) + list_filter = ("warning_mail_error",) + + @admin.register(Counter) class CounterAdmin(admin.ModelAdmin): list_display = ("name", "club", "type") diff --git a/counter/management/__init__.py b/counter/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/counter/management/commands/__init__.py b/counter/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/counter/management/commands/dump_warning_mail.py b/counter/management/commands/dump_warning_mail.py new file mode 100644 index 00000000..0a1d7f78 --- /dev/null +++ b/counter/management/commands/dump_warning_mail.py @@ -0,0 +1,88 @@ +import logging +from smtplib import SMTPException + +from django.conf import settings +from django.core.mail import send_mail +from django.core.management.base import BaseCommand +from django.db.models import Exists, OuterRef, Subquery +from django.template.loader import render_to_string +from django.utils.timezone import localdate, now +from django.utils.translation import gettext as _ + +from core.models import User +from counter.models import AccountDump +from subscription.models import Subscription + + +class Command(BaseCommand): + """Send mail to inactive users, warning them that their account is about to be dumped. + + This command should be automated with a cron task. + """ + + def __init__(self, *args, **kwargs): + self.logger = logging.getLogger("account_dump_mail") + self.logger.setLevel(logging.INFO) + super().__init__(*args, **kwargs) + + def handle(self, *args, **options): + ongoing_dump_operation = AccountDump.objects.ongoing().filter( + customer__user=OuterRef("pk") + ) + users = list( + User.objects + .filter_inactive() + .filter(customer__amount__gt=0) + .exclude(Exists(ongoing_dump_operation)) + .annotate( + last_subscription_date=Subquery( + Subscription.objects.filter(member=OuterRef("pk")) + .order_by("-subscription_end") + .values("subscription_end")[:1] + ), + ) + .select_related("customer") + ) + self.stdout.write(f"{len(users)} users will be warned of their account dump") + dumps = [] + for user in users: + is_success = self._send_mail(user) + dumps.append( + AccountDump( + customer_id=user.id, + warning_mail_sent_at=now(), + warning_mail_error=not is_success, + ) + ) + AccountDump.objects.bulk_create(dumps) + self.stdout.write("Finished !") + + def _send_mail(self, user: User) -> bool: + """Send the warning email to the given user. + + Returns: + True if the mail was successfully sent, else False + """ + message = render_to_string( + "counter/account_dump_warning_mail.jinja", + { + "balance": user.customer.amount, + "last_subscription_date": user.last_subscription_date, + "dump_date": localdate() + settings.SITH_ACCOUNT_DUMP_DELTA, + }, + ) + try: + # sending mails one by one is long and ineffective, + # but it makes easier to know which emails failed (and how). + # Also, there won't be that much mails sent (except on the first run) + send_mail( + _("Clearing of your AE account"), + message, + settings.DEFAULT_FROM_EMAIL, + [user.email], + ) + self.logger.info(f"Mail successfully sent to {user.email}") + return True + except SMTPException as e: + self.logger.error(f"failed mail to {user.email} :\n{e}") + return False diff --git a/counter/migrations/0024_accountdump_accountdump_unique_ongoing_dump.py b/counter/migrations/0024_accountdump_accountdump_unique_ongoing_dump.py new file mode 100644 index 00000000..e5b478a7 --- /dev/null +++ b/counter/migrations/0024_accountdump_accountdump_unique_ongoing_dump.py @@ -0,0 +1,64 @@ +# Generated by Django 4.2.16 on 2024-10-06 14:33 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [("counter", "0023_billinginfo_phone_number")] + + operations = [ + migrations.CreateModel( + name="AccountDump", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "warning_mail_sent_at", + models.DateTimeField( + help_text="When the mail warning that the account was about to be dumped was sent." + ), + ), + ( + "warning_mail_error", + models.BooleanField( + default=False, + help_text="Set this to True if the warning mail received an error", + ), + ), + ( + "customer", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="dumps", + to="counter.customer", + ), + ), + ( + "dump_operation", + models.OneToOneField( + blank=True, + help_text="The operation that emptied the account.", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="counter.selling", + ), + ), + ], + ), + migrations.AddConstraint( + model_name="accountdump", + constraint=models.UniqueConstraint( + condition=models.Q(("dump_operation", None)), + fields=("customer",), + name="unique_ongoing_dump", + ), + ), + ] diff --git a/counter/models.py b/counter/models.py index 2e58760a..e6d5b061 100644 --- a/counter/models.py +++ b/counter/models.py @@ -26,7 +26,7 @@ from dict2xml import dict2xml from django.conf import settings from django.core.validators import MinLengthValidator from django.db import models -from django.db.models import Exists, F, OuterRef, QuerySet, Sum, Value +from django.db.models import Exists, F, OuterRef, Q, QuerySet, Sum, Value from django.db.models.functions import Concat, Length from django.forms import ValidationError from django.urls import reverse @@ -211,6 +211,51 @@ class BillingInfo(models.Model): return '' + xml +class AccountDumpQuerySet(models.QuerySet): + def ongoing(self) -> Self: + """Filter dump operations that are not completed yet.""" + return self.filter(dump_operation=None) + + +class AccountDump(models.Model): + """The process of dumping an account.""" + + customer = models.ForeignKey( + Customer, related_name="dumps", on_delete=models.CASCADE + ) + warning_mail_sent_at = models.DateTimeField( + help_text=_( + "When the mail warning that the account was about to be dumped was sent." + ) + ) + warning_mail_error = models.BooleanField( + default=False, + help_text=_("Set this to True if the warning mail received an error"), + ) + dump_operation = models.OneToOneField( + "Selling", + null=True, + blank=True, + on_delete=models.CASCADE, + help_text=_("The operation that emptied the account."), + ) + + objects = AccountDumpQuerySet.as_manager() + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["customer"], + condition=Q(dump_operation=None), + name="unique_ongoing_dump", + ), + ] + + def __str__(self): + status = "ongoing" if self.dump_operation is None else "finished" + return f"{self.customer} - {status}" + + class ProductType(models.Model): """A product type. diff --git a/counter/templates/counter/account_dump_warning_mail.jinja b/counter/templates/counter/account_dump_warning_mail.jinja new file mode 100644 index 00000000..faf52431 --- /dev/null +++ b/counter/templates/counter/account_dump_warning_mail.jinja @@ -0,0 +1,43 @@ +

+ Bonjour, +

+ +

+ {% trans date=last_subscription_date|date(DATETIME_FORMAT) %} + You received this email because your last subscription to the + Students' association ended on {{ date }}. + {% endtrans %} +

+ +

+ {% trans date=dump_date|date(DATETIME_FORMAT) %} + In accordance with the Internal Regulations, the balance of any + inactive AE account for more than 2 years automatically goes back + to the AE. + The money present on your account will therefore be recovered in full + on {{ date }}. + {% endtrans %} +

+ +

+ {% trans %}However, if your subscription is renewed by this date, + your right to keep the money in your AE account will be renewed.{% endtrans %} +

+ +{% if balance >= 10 %} +

+ {% trans %}You can also request a refund by sending an email to + ae@utbm.fr + before the aforementioned date.{% endtrans %} +

+{% endif %} + +

+ {% trans %}Sincerely{% endtrans %}, +

+ +

+ L'association des étudiants de l'UTBM
+ 6, Boulevard Anatole France
+ 90000 Belfort +

diff --git a/counter/tests/test_account_dump.py b/counter/tests/test_account_dump.py new file mode 100644 index 00000000..79228c26 --- /dev/null +++ b/counter/tests/test_account_dump.py @@ -0,0 +1,7 @@ +import pytest + + +@pytest.mark.django_db +def test_account_dump(): + # TODO write the fucking test + pass diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 02b4bcbe..5df95185 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -8,9 +8,9 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-10-10 19:37+0200\n" "PO-Revision-Date: 2016-07-18\n" -"Last-Translator: Skia \n" +"Last-Translator: Maréchal \n" -"Language: \n" +"Language: Français\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -18,8 +18,8 @@ msgstr "" #: accounting/models.py:62 accounting/models.py:103 accounting/models.py:136 #: accounting/models.py:203 club/models.py:55 com/models.py:274 -#: com/models.py:293 counter/models.py:220 counter/models.py:253 -#: counter/models.py:411 forum/models.py:59 launderette/models.py:29 +#: com/models.py:293 counter/models.py:265 counter/models.py:298 +#: counter/models.py:433 forum/models.py:59 launderette/models.py:29 #: launderette/models.py:84 launderette/models.py:122 msgid "name" msgstr "nom" @@ -40,7 +40,7 @@ msgstr "code postal" msgid "country" msgstr "pays" -#: accounting/models.py:67 core/models.py:365 +#: accounting/models.py:67 core/models.py:390 msgid "phone" msgstr "téléphone" @@ -218,7 +218,7 @@ msgstr "Compte" msgid "Company" msgstr "Entreprise" -#: accounting/models.py:324 core/models.py:312 sith/settings.py:411 +#: accounting/models.py:324 core/models.py:337 sith/settings.py:420 msgid "Other" msgstr "Autre" @@ -264,7 +264,7 @@ msgstr "" "Vous devez fournir soit un type comptable simplifié ou un type comptable " "standard" -#: accounting/models.py:442 counter/models.py:263 pedagogy/models.py:41 +#: accounting/models.py:442 counter/models.py:308 pedagogy/models.py:41 msgid "code" msgstr "code" @@ -517,7 +517,7 @@ msgid "Effective amount" msgstr "Montant effectif" #: accounting/templates/accounting/club_account_details.jinja:36 -#: sith/settings.py:457 +#: sith/settings.py:466 msgid "Closed" msgstr "Fermé" @@ -967,7 +967,7 @@ msgstr "Date de fin" #: club/forms.py:160 club/templates/club/club_sellings.jinja:49 #: core/templates/core/user_account_detail.jinja:17 #: core/templates/core/user_account_detail.jinja:56 -#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:138 +#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:141 msgid "Counter" msgstr "Comptoir" @@ -1029,11 +1029,11 @@ msgstr "actif" msgid "short description" msgstr "description courte" -#: club/models.py:81 core/models.py:367 +#: club/models.py:81 core/models.py:392 msgid "address" msgstr "Adresse" -#: club/models.py:98 core/models.py:278 +#: club/models.py:98 core/models.py:303 msgid "home" msgstr "home" @@ -1052,13 +1052,13 @@ msgstr "Un club avec ce nom UNIX existe déjà." msgid "user" msgstr "nom d'utilisateur" -#: club/models.py:354 core/models.py:331 election/models.py:178 +#: club/models.py:354 core/models.py:356 election/models.py:178 #: election/models.py:212 trombi/models.py:211 msgid "role" msgstr "rôle" -#: club/models.py:359 core/models.py:89 counter/models.py:221 -#: counter/models.py:254 election/models.py:13 election/models.py:115 +#: club/models.py:359 core/models.py:89 counter/models.py:266 +#: counter/models.py:299 election/models.py:13 election/models.py:115 #: election/models.py:188 forum/models.py:60 forum/models.py:244 msgid "description" msgstr "description" @@ -1072,7 +1072,7 @@ msgid "Enter a valid address. Only the root of the address is needed." msgstr "" "Entrez une adresse valide. Seule la racine de l'adresse est nécessaire." -#: club/models.py:429 com/models.py:82 com/models.py:309 core/models.py:941 +#: club/models.py:429 com/models.py:82 com/models.py:309 core/models.py:960 msgid "is moderated" msgstr "est modéré" @@ -1442,7 +1442,7 @@ msgstr "résumé" msgid "content" msgstr "contenu" -#: com/models.py:71 core/models.py:1503 launderette/models.py:92 +#: com/models.py:71 core/models.py:1522 launderette/models.py:92 #: launderette/models.py:130 launderette/models.py:181 msgid "type" msgstr "type" @@ -1492,7 +1492,7 @@ msgstr "weekmail" msgid "rank" msgstr "rang" -#: com/models.py:295 core/models.py:906 core/models.py:956 +#: com/models.py:295 core/models.py:925 core/models.py:975 msgid "file" msgstr "fichier" @@ -1988,17 +1988,17 @@ msgstr "Si un groupe est un meta-groupe ou pas" msgid "%(value)s is not a valid promo (between 0 and %(end)s)" msgstr "%(value)s n'est pas une promo valide (doit être entre 0 et %(end)s)" -#: core/models.py:231 +#: core/models.py:256 msgid "username" msgstr "nom d'utilisateur" -#: core/models.py:235 +#: core/models.py:260 msgid "Required. 254 characters or fewer. Letters, digits and ./+/-/_ only." msgstr "" "Requis. Pas plus de 254 caractères. Uniquement des lettres, numéros, et ./" "+/-/_" -#: core/models.py:241 +#: core/models.py:266 msgid "" "Enter a valid username. This value may contain only letters, numbers and ./" "+/-/_ characters." @@ -2006,43 +2006,43 @@ msgstr "" "Entrez un nom d'utilisateur correct. Uniquement des lettres, numéros, et ./" "+/-/_" -#: core/models.py:247 +#: core/models.py:272 msgid "A user with that username already exists." msgstr "Un utilisateur de ce nom existe déjà" -#: core/models.py:249 +#: core/models.py:274 msgid "first name" msgstr "Prénom" -#: core/models.py:250 +#: core/models.py:275 msgid "last name" msgstr "Nom" -#: core/models.py:251 +#: core/models.py:276 msgid "email address" msgstr "adresse email" -#: core/models.py:252 +#: core/models.py:277 msgid "date of birth" msgstr "date de naissance" -#: core/models.py:253 +#: core/models.py:278 msgid "nick name" msgstr "surnom" -#: core/models.py:255 +#: core/models.py:280 msgid "staff status" msgstr "status \"staff\"" -#: core/models.py:257 +#: core/models.py:282 msgid "Designates whether the user can log into this admin site." msgstr "Est-ce que l'utilisateur peut se logger à la partie admin du site." -#: core/models.py:260 +#: core/models.py:285 msgid "active" msgstr "actif" -#: core/models.py:263 +#: core/models.py:288 msgid "" "Designates whether this user should be treated as active. Unselect this " "instead of deleting accounts." @@ -2050,163 +2050,163 @@ msgstr "" "Est-ce que l'utilisateur doit être traité comme actif. Désélectionnez au " "lieu de supprimer les comptes." -#: core/models.py:267 +#: core/models.py:292 msgid "date joined" msgstr "date d'inscription" -#: core/models.py:268 +#: core/models.py:293 msgid "last update" msgstr "dernière mise à jour" -#: core/models.py:270 +#: core/models.py:295 msgid "superuser" msgstr "super-utilisateur" -#: core/models.py:272 +#: core/models.py:297 msgid "Designates whether this user is a superuser. " msgstr "Est-ce que l'utilisateur est super-utilisateur." -#: core/models.py:286 +#: core/models.py:311 msgid "profile" msgstr "profil" -#: core/models.py:294 +#: core/models.py:319 msgid "avatar" msgstr "avatar" -#: core/models.py:302 +#: core/models.py:327 msgid "scrub" msgstr "blouse" -#: core/models.py:308 +#: core/models.py:333 msgid "sex" msgstr "Genre" -#: core/models.py:312 +#: core/models.py:337 msgid "Man" msgstr "Homme" -#: core/models.py:312 +#: core/models.py:337 msgid "Woman" msgstr "Femme" -#: core/models.py:314 +#: core/models.py:339 msgid "pronouns" msgstr "pronoms" -#: core/models.py:316 +#: core/models.py:341 msgid "tshirt size" msgstr "taille de t-shirt" -#: core/models.py:319 +#: core/models.py:344 msgid "-" msgstr "-" -#: core/models.py:320 +#: core/models.py:345 msgid "XS" msgstr "XS" -#: core/models.py:321 +#: core/models.py:346 msgid "S" msgstr "S" -#: core/models.py:322 +#: core/models.py:347 msgid "M" msgstr "M" -#: core/models.py:323 +#: core/models.py:348 msgid "L" msgstr "L" -#: core/models.py:324 +#: core/models.py:349 msgid "XL" msgstr "XL" -#: core/models.py:325 +#: core/models.py:350 msgid "XXL" msgstr "XXL" -#: core/models.py:326 +#: core/models.py:351 msgid "XXXL" msgstr "XXXL" -#: core/models.py:334 +#: core/models.py:359 msgid "Student" msgstr "Étudiant" -#: core/models.py:335 +#: core/models.py:360 msgid "Administrative agent" msgstr "Personnel administratif" -#: core/models.py:336 +#: core/models.py:361 msgid "Teacher" msgstr "Enseignant" -#: core/models.py:337 +#: core/models.py:362 msgid "Agent" msgstr "Personnel" -#: core/models.py:338 +#: core/models.py:363 msgid "Doctor" msgstr "Doctorant" -#: core/models.py:339 +#: core/models.py:364 msgid "Former student" msgstr "Ancien étudiant" -#: core/models.py:340 +#: core/models.py:365 msgid "Service" msgstr "Service" -#: core/models.py:346 +#: core/models.py:371 msgid "department" msgstr "département" -#: core/models.py:353 +#: core/models.py:378 msgid "dpt option" msgstr "Filière" -#: core/models.py:355 pedagogy/models.py:69 pedagogy/models.py:293 +#: core/models.py:380 pedagogy/models.py:69 pedagogy/models.py:293 msgid "semester" msgstr "semestre" -#: core/models.py:356 +#: core/models.py:381 msgid "quote" msgstr "citation" -#: core/models.py:357 +#: core/models.py:382 msgid "school" msgstr "école" -#: core/models.py:359 +#: core/models.py:384 msgid "promo" msgstr "promo" -#: core/models.py:362 +#: core/models.py:387 msgid "forum signature" msgstr "signature du forum" -#: core/models.py:364 +#: core/models.py:389 msgid "second email address" msgstr "adresse email secondaire" -#: core/models.py:366 +#: core/models.py:391 msgid "parent phone" msgstr "téléphone des parents" -#: core/models.py:369 +#: core/models.py:394 msgid "parent address" msgstr "adresse des parents" -#: core/models.py:372 +#: core/models.py:397 msgid "is subscriber viewable" msgstr "profil visible par les cotisants" -#: core/models.py:572 +#: core/models.py:591 msgid "A user with that username already exists" msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" -#: core/models.py:737 core/templates/core/macros.jinja:75 +#: core/models.py:756 core/templates/core/macros.jinja:75 #: core/templates/core/macros.jinja:77 core/templates/core/macros.jinja:78 #: core/templates/core/user_detail.jinja:100 #: core/templates/core/user_detail.jinja:101 @@ -2226,101 +2226,101 @@ msgstr "Un utilisateur de ce nom d'utilisateur existe déjà" msgid "Profile" msgstr "Profil" -#: core/models.py:856 +#: core/models.py:875 msgid "Visitor" msgstr "Visiteur" -#: core/models.py:863 +#: core/models.py:882 msgid "receive the Weekmail" msgstr "recevoir le Weekmail" -#: core/models.py:864 +#: core/models.py:883 msgid "show your stats to others" msgstr "montrez vos statistiques aux autres" -#: core/models.py:866 +#: core/models.py:885 msgid "get a notification for every click" msgstr "avoir une notification pour chaque click" -#: core/models.py:869 +#: core/models.py:888 msgid "get a notification for every refilling" msgstr "avoir une notification pour chaque rechargement" -#: core/models.py:895 sas/views.py:309 +#: core/models.py:914 sas/views.py:309 msgid "file name" msgstr "nom du fichier" -#: core/models.py:899 core/models.py:1252 +#: core/models.py:918 core/models.py:1271 msgid "parent" msgstr "parent" -#: core/models.py:913 +#: core/models.py:932 msgid "compressed file" msgstr "version allégée" -#: core/models.py:920 +#: core/models.py:939 msgid "thumbnail" msgstr "miniature" -#: core/models.py:928 core/models.py:945 +#: core/models.py:947 core/models.py:964 msgid "owner" msgstr "propriétaire" -#: core/models.py:932 core/models.py:1269 core/views/files.py:223 +#: core/models.py:951 core/models.py:1288 core/views/files.py:223 msgid "edit group" msgstr "groupe d'édition" -#: core/models.py:935 core/models.py:1272 core/views/files.py:226 +#: core/models.py:954 core/models.py:1291 core/views/files.py:226 msgid "view group" msgstr "groupe de vue" -#: core/models.py:937 +#: core/models.py:956 msgid "is folder" msgstr "est un dossier" -#: core/models.py:938 +#: core/models.py:957 msgid "mime type" msgstr "type mime" -#: core/models.py:939 +#: core/models.py:958 msgid "size" msgstr "taille" -#: core/models.py:950 +#: core/models.py:969 msgid "asked for removal" msgstr "retrait demandé" -#: core/models.py:952 +#: core/models.py:971 msgid "is in the SAS" msgstr "est dans le SAS" -#: core/models.py:1021 +#: core/models.py:1040 msgid "Character '/' not authorized in name" msgstr "Le caractère '/' n'est pas autorisé dans les noms de fichier" -#: core/models.py:1023 core/models.py:1027 +#: core/models.py:1042 core/models.py:1046 msgid "Loop in folder tree" msgstr "Boucle dans l'arborescence des dossiers" -#: core/models.py:1030 +#: core/models.py:1049 msgid "You can not make a file be a children of a non folder file" msgstr "" "Vous ne pouvez pas mettre un fichier enfant de quelque chose qui n'est pas " "un dossier" -#: core/models.py:1041 +#: core/models.py:1060 msgid "Duplicate file" msgstr "Un fichier de ce nom existe déjà" -#: core/models.py:1058 +#: core/models.py:1077 msgid "You must provide a file" msgstr "Vous devez fournir un fichier" -#: core/models.py:1235 +#: core/models.py:1254 msgid "page unix name" msgstr "nom unix de la page" -#: core/models.py:1241 +#: core/models.py:1260 msgid "" "Enter a valid page name. This value may contain only unaccented letters, " "numbers and ./+/-/_ characters." @@ -2328,55 +2328,55 @@ msgstr "" "Entrez un nom de page correct. Uniquement des lettres non accentuées, " "numéros, et ./+/-/_" -#: core/models.py:1259 +#: core/models.py:1278 msgid "page name" msgstr "nom de la page" -#: core/models.py:1264 +#: core/models.py:1283 msgid "owner group" msgstr "groupe propriétaire" -#: core/models.py:1277 +#: core/models.py:1296 msgid "lock user" msgstr "utilisateur bloquant" -#: core/models.py:1284 +#: core/models.py:1303 msgid "lock_timeout" msgstr "décompte du déblocage" -#: core/models.py:1334 +#: core/models.py:1353 msgid "Duplicate page" msgstr "Une page de ce nom existe déjà" -#: core/models.py:1337 +#: core/models.py:1356 msgid "Loop in page tree" msgstr "Boucle dans l'arborescence des pages" -#: core/models.py:1457 +#: core/models.py:1476 msgid "revision" msgstr "révision" -#: core/models.py:1458 +#: core/models.py:1477 msgid "page title" msgstr "titre de la page" -#: core/models.py:1459 +#: core/models.py:1478 msgid "page content" msgstr "contenu de la page" -#: core/models.py:1500 +#: core/models.py:1519 msgid "url" msgstr "url" -#: core/models.py:1501 +#: core/models.py:1520 msgid "param" msgstr "param" -#: core/models.py:1506 +#: core/models.py:1525 msgid "viewed" msgstr "vue" -#: core/models.py:1564 +#: core/models.py:1583 msgid "operation type" msgstr "type d'opération" @@ -2480,7 +2480,7 @@ msgstr "Photos" #: eboutic/templates/eboutic/eboutic_main.jinja:22 #: eboutic/templates/eboutic/eboutic_makecommand.jinja:16 #: eboutic/templates/eboutic/eboutic_payment_result.jinja:4 -#: sith/settings.py:410 sith/settings.py:418 +#: sith/settings.py:419 sith/settings.py:427 msgid "Eboutic" msgstr "Eboutic" @@ -3567,8 +3567,7 @@ msgid "%s is already your godfather" msgstr "%s est déjà votre parrain/marraine" #: core/views/forms.py:347 -#, fuzzy, python-format -#| msgid "This user has already commented on this UV" +#, python-format msgid "%s is already your godchild" msgstr "%s est déjà votre fillot/fillote" @@ -3629,6 +3628,10 @@ msgstr "Produit parent" msgid "Buying groups" msgstr "Groupes d'achat" +#: counter/management/commands/dump_warning_mail.py:82 +msgid "Clearing of your AE account" +msgstr "Vidange de votre compte AE" + #: counter/migrations/0013_customer_recorded_products.py:25 msgid "Ecocup regularization" msgstr "Régularization des ecocups" @@ -3685,43 +3688,55 @@ msgstr "Pays" msgid "Phone number" msgstr "Numéro de téléphone" -#: counter/models.py:232 counter/models.py:258 +#: counter/models.py:228 +msgid "When the mail warning that the account was about to be dumped was sent." +msgstr "Quand le mail d'avertissement de la vidange du compte a été envoyé." + +#: counter/models.py:233 +msgid "Set this to True if the warning mail received an error" +msgstr "Mettre à True si le mail a reçu une erreur" + +#: counter/models.py:240 +msgid "The operation that emptied the account." +msgstr "L'opération qui a vidé le compte." + +#: counter/models.py:277 counter/models.py:303 msgid "product type" msgstr "type du produit" -#: counter/models.py:264 +#: counter/models.py:309 msgid "purchase price" msgstr "prix d'achat" -#: counter/models.py:265 +#: counter/models.py:310 msgid "selling price" msgstr "prix de vente" -#: counter/models.py:266 +#: counter/models.py:311 msgid "special selling price" msgstr "prix de vente spécial" -#: counter/models.py:273 +#: counter/models.py:318 msgid "icon" msgstr "icône" -#: counter/models.py:278 +#: counter/models.py:323 msgid "limit age" msgstr "âge limite" -#: counter/models.py:279 +#: counter/models.py:324 msgid "tray price" msgstr "prix plateau" -#: counter/models.py:283 +#: counter/models.py:328 msgid "parent product" msgstr "produit parent" -#: counter/models.py:289 +#: counter/models.py:334 msgid "buying groups" msgstr "groupe d'achat" -#: counter/models.py:291 election/models.py:50 +#: counter/models.py:336 election/models.py:50 msgid "archived" msgstr "archivé" @@ -3867,6 +3882,58 @@ msgstr "uid" msgid "student cards" msgstr "cartes étudiante" +#: counter/templates/counter/account_dump_warning_mail.jinja:6 +#, python-format +msgid "" +"\n" +" You received this email because your last subscription to the\n" +" Students' association ended on %(date)s.\n" +" " +msgstr "" +"\n" +"Vous recevez ce mail car votre dernière cotisation à l'assocation des " +"étudiants de l'UTBM s'est achevée le %(date)s.\n" +" " + +#: counter/templates/counter/account_dump_warning_mail.jinja:13 +#, python-format +msgid "" +"\n" +" In accordance with the Internal Regulations, the balance of any\n" +" inactive AE account for more than 2 years automatically goes back\n" +" to the AE.\n" +" The money present on your account will therefore be recovered in full\n" +" on %(date)s.\n" +" " +msgstr "" +"\n" +"Conformément au Règlement intérieur, le solde de tout compte AE inactif " +"depuis plus de 2 ans revient de droit à l'AE. L'argent présent sur votre " +"compte sera donc récupéré en totalité le %(date)s.\n" +" " + +#: counter/templates/counter/account_dump_warning_mail.jinja:23 +msgid "" +"However, if your subscription is renewed by this date,\n" +" your right to keep the money in your AE account will be renewed." +msgstr "" +"Cependant, si votre cotisation est renouvelée d'ici cette date, votre droit " +"à conserver l'argent de votre compte AE sera renouvelé." + +#: counter/templates/counter/account_dump_warning_mail.jinja:29 +msgid "" +"You can also request a refund by sending an email to\n" +" ae@utbm.fr\n" +" before the aforementioned date." +msgstr "" +"Vous pouvez également effectuer une demande de remboursement par mail à " +"l'adresse ae@utbm.fr avant la date " +"susmentionnée." + +#: counter/templates/counter/account_dump_warning_mail.jinja:36 +msgid "Sincerely" +msgstr "Cordialement" + #: counter/templates/counter/activity.jinja:5 #: counter/templates/counter/activity.jinja:13 #, python-format @@ -4136,23 +4203,23 @@ msgstr "Temps" msgid "Top 100 barman %(counter_name)s (all semesters)" msgstr "Top 100 barman %(counter_name)s (tous les semestres)" -#: counter/views.py:148 +#: counter/views.py:151 msgid "Cash summary" msgstr "Relevé de caisse" -#: counter/views.py:157 +#: counter/views.py:160 msgid "Last operations" msgstr "Dernières opérations" -#: counter/views.py:204 +#: counter/views.py:207 msgid "Bad credentials" msgstr "Mauvais identifiants" -#: counter/views.py:206 +#: counter/views.py:209 msgid "User is not barman" msgstr "L'utilisateur n'est pas barman." -#: counter/views.py:211 +#: counter/views.py:214 msgid "Bad location, someone is already logged in somewhere else" msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs" @@ -5266,10 +5333,8 @@ msgid "Ask for removal" msgstr "Demander le retrait" #: sas/templates/sas/picture.jinja:118 sas/templates/sas/picture.jinja:129 -#, fuzzy -#| msgid "Previous" msgid "Previous picture" -msgstr "Précédent" +msgstr "Image précédente" #: sas/templates/sas/picture.jinja:137 msgid "People" @@ -5292,311 +5357,311 @@ msgstr "Erreur de création de l'album %(album)s : %(msg)s" msgid "Add user" msgstr "Ajouter une personne" -#: sith/settings.py:246 sith/settings.py:465 +#: sith/settings.py:255 sith/settings.py:474 msgid "English" msgstr "Anglais" -#: sith/settings.py:246 sith/settings.py:464 +#: sith/settings.py:255 sith/settings.py:473 msgid "French" msgstr "Français" -#: sith/settings.py:384 +#: sith/settings.py:393 msgid "TC" msgstr "TC" -#: sith/settings.py:385 +#: sith/settings.py:394 msgid "IMSI" msgstr "IMSI" -#: sith/settings.py:386 +#: sith/settings.py:395 msgid "IMAP" msgstr "IMAP" -#: sith/settings.py:387 +#: sith/settings.py:396 msgid "INFO" msgstr "INFO" -#: sith/settings.py:388 +#: sith/settings.py:397 msgid "GI" msgstr "GI" -#: sith/settings.py:389 sith/settings.py:475 +#: sith/settings.py:398 sith/settings.py:484 msgid "E" msgstr "E" -#: sith/settings.py:390 +#: sith/settings.py:399 msgid "EE" msgstr "EE" -#: sith/settings.py:391 +#: sith/settings.py:400 msgid "GESC" msgstr "GESC" -#: sith/settings.py:392 +#: sith/settings.py:401 msgid "GMC" msgstr "GMC" -#: sith/settings.py:393 +#: sith/settings.py:402 msgid "MC" msgstr "MC" -#: sith/settings.py:394 +#: sith/settings.py:403 msgid "EDIM" msgstr "EDIM" -#: sith/settings.py:395 +#: sith/settings.py:404 msgid "Humanities" msgstr "Humanités" -#: sith/settings.py:396 +#: sith/settings.py:405 msgid "N/A" msgstr "N/A" -#: sith/settings.py:400 sith/settings.py:407 sith/settings.py:426 +#: sith/settings.py:409 sith/settings.py:416 sith/settings.py:435 msgid "Check" msgstr "Chèque" -#: sith/settings.py:401 sith/settings.py:409 sith/settings.py:427 +#: sith/settings.py:410 sith/settings.py:418 sith/settings.py:436 msgid "Cash" msgstr "Espèces" -#: sith/settings.py:402 +#: sith/settings.py:411 msgid "Transfert" msgstr "Virement" -#: sith/settings.py:415 +#: sith/settings.py:424 msgid "Belfort" msgstr "Belfort" -#: sith/settings.py:416 +#: sith/settings.py:425 msgid "Sevenans" msgstr "Sevenans" -#: sith/settings.py:417 +#: sith/settings.py:426 msgid "Montbéliard" msgstr "Montbéliard" -#: sith/settings.py:445 +#: sith/settings.py:454 msgid "Free" msgstr "Libre" -#: sith/settings.py:446 +#: sith/settings.py:455 msgid "CS" msgstr "CS" -#: sith/settings.py:447 +#: sith/settings.py:456 msgid "TM" msgstr "TM" -#: sith/settings.py:448 +#: sith/settings.py:457 msgid "OM" msgstr "OM" -#: sith/settings.py:449 +#: sith/settings.py:458 msgid "QC" msgstr "QC" -#: sith/settings.py:450 +#: sith/settings.py:459 msgid "EC" msgstr "EC" -#: sith/settings.py:451 +#: sith/settings.py:460 msgid "RN" msgstr "RN" -#: sith/settings.py:452 +#: sith/settings.py:461 msgid "ST" msgstr "ST" -#: sith/settings.py:453 +#: sith/settings.py:462 msgid "EXT" msgstr "EXT" -#: sith/settings.py:458 +#: sith/settings.py:467 msgid "Autumn" msgstr "Automne" -#: sith/settings.py:459 +#: sith/settings.py:468 msgid "Spring" msgstr "Printemps" -#: sith/settings.py:460 +#: sith/settings.py:469 msgid "Autumn and spring" msgstr "Automne et printemps" -#: sith/settings.py:466 +#: sith/settings.py:475 msgid "German" msgstr "Allemand" -#: sith/settings.py:467 +#: sith/settings.py:476 msgid "Spanish" msgstr "Espagnol" -#: sith/settings.py:471 +#: sith/settings.py:480 msgid "A" msgstr "A" -#: sith/settings.py:472 +#: sith/settings.py:481 msgid "B" msgstr "B" -#: sith/settings.py:473 +#: sith/settings.py:482 msgid "C" msgstr "C" -#: sith/settings.py:474 +#: sith/settings.py:483 msgid "D" msgstr "D" -#: sith/settings.py:476 +#: sith/settings.py:485 msgid "FX" msgstr "FX" -#: sith/settings.py:477 +#: sith/settings.py:486 msgid "F" msgstr "F" -#: sith/settings.py:478 +#: sith/settings.py:487 msgid "Abs" msgstr "Abs" -#: sith/settings.py:482 +#: sith/settings.py:491 msgid "Selling deletion" msgstr "Suppression de vente" -#: sith/settings.py:483 +#: sith/settings.py:492 msgid "Refilling deletion" msgstr "Suppression de rechargement" -#: sith/settings.py:520 +#: sith/settings.py:534 msgid "One semester" msgstr "Un semestre, 20 €" -#: sith/settings.py:521 +#: sith/settings.py:535 msgid "Two semesters" msgstr "Deux semestres, 35 €" -#: sith/settings.py:523 +#: sith/settings.py:537 msgid "Common core cursus" msgstr "Cursus tronc commun, 60 €" -#: sith/settings.py:527 +#: sith/settings.py:541 msgid "Branch cursus" msgstr "Cursus branche, 60 €" -#: sith/settings.py:528 +#: sith/settings.py:542 msgid "Alternating cursus" msgstr "Cursus alternant, 30 €" -#: sith/settings.py:529 +#: sith/settings.py:543 msgid "Honorary member" msgstr "Membre honoraire, 0 €" -#: sith/settings.py:530 +#: sith/settings.py:544 msgid "Assidu member" msgstr "Membre d'Assidu, 0 €" -#: sith/settings.py:531 +#: sith/settings.py:545 msgid "Amicale/DOCEO member" msgstr "Membre de l'Amicale/DOCEO, 0 €" -#: sith/settings.py:532 +#: sith/settings.py:546 msgid "UT network member" msgstr "Cotisant du réseau UT, 0 €" -#: sith/settings.py:533 +#: sith/settings.py:547 msgid "CROUS member" msgstr "Membres du CROUS, 0 €" -#: sith/settings.py:534 +#: sith/settings.py:548 msgid "Sbarro/ESTA member" msgstr "Membre de Sbarro ou de l'ESTA, 20 €" -#: sith/settings.py:536 +#: sith/settings.py:550 msgid "One semester Welcome Week" msgstr "Un semestre Welcome Week" -#: sith/settings.py:540 +#: sith/settings.py:554 msgid "One month for free" msgstr "Un mois gratuit" -#: sith/settings.py:541 +#: sith/settings.py:555 msgid "Two months for free" msgstr "Deux mois gratuits" -#: sith/settings.py:542 +#: sith/settings.py:556 msgid "Eurok's volunteer" msgstr "Bénévole Eurockéennes" -#: sith/settings.py:544 +#: sith/settings.py:558 msgid "Six weeks for free" msgstr "6 semaines gratuites" -#: sith/settings.py:548 +#: sith/settings.py:562 msgid "One day" msgstr "Un jour" -#: sith/settings.py:549 +#: sith/settings.py:563 msgid "GA staff member" msgstr "Membre staff GA (2 semaines), 1 €" -#: sith/settings.py:552 +#: sith/settings.py:566 msgid "One semester (-20%)" msgstr "Un semestre (-20%), 12 €" -#: sith/settings.py:557 +#: sith/settings.py:571 msgid "Two semesters (-20%)" msgstr "Deux semestres (-20%), 22 €" -#: sith/settings.py:562 +#: sith/settings.py:576 msgid "Common core cursus (-20%)" msgstr "Cursus tronc commun (-20%), 36 €" -#: sith/settings.py:567 +#: sith/settings.py:581 msgid "Branch cursus (-20%)" msgstr "Cursus branche (-20%), 36 €" -#: sith/settings.py:572 +#: sith/settings.py:586 msgid "Alternating cursus (-20%)" msgstr "Cursus alternant (-20%), 24 €" -#: sith/settings.py:578 +#: sith/settings.py:592 msgid "One year for free(CA offer)" msgstr "Une année offerte (Offre CA)" -#: sith/settings.py:598 +#: sith/settings.py:612 msgid "President" msgstr "Président⸱e" -#: sith/settings.py:599 +#: sith/settings.py:613 msgid "Vice-President" msgstr "Vice-Président⸱e" -#: sith/settings.py:600 +#: sith/settings.py:614 msgid "Treasurer" msgstr "Trésorier⸱e" -#: sith/settings.py:601 +#: sith/settings.py:615 msgid "Communication supervisor" msgstr "Responsable communication" -#: sith/settings.py:602 +#: sith/settings.py:616 msgid "Secretary" msgstr "Secrétaire" -#: sith/settings.py:603 +#: sith/settings.py:617 msgid "IT supervisor" msgstr "Responsable info" -#: sith/settings.py:604 +#: sith/settings.py:618 msgid "Board member" msgstr "Membre du bureau" -#: sith/settings.py:605 +#: sith/settings.py:619 msgid "Active member" msgstr "Membre actif⸱ve" -#: sith/settings.py:606 +#: sith/settings.py:620 msgid "Curious" msgstr "Curieux⸱euse" diff --git a/sith/settings.py b/sith/settings.py index a4eca6b3..9b484559 100644 --- a/sith/settings.py +++ b/sith/settings.py @@ -37,6 +37,7 @@ import binascii import logging import os import sys +from datetime import timedelta from pathlib import Path import sentry_sdk @@ -229,13 +230,20 @@ LOGGING = { "class": "logging.StreamHandler", "formatter": "simple", }, + "dump_mail_file": { + "level": "DEBUG", + "class": "logging.FileHandler", + "filename": "account_dump_mail.log", + "formatter": "simple", + }, }, "loggers": { "main": { "handlers": ["log_to_stdout"], "level": "INFO", "propagate": True, - } + }, + "account_dump_mail": {"handlers": ["dump_mail_file", "log_to_stdout"]}, }, } @@ -498,6 +506,8 @@ SITH_COUNTER_CASH_SUMMARY_LENGTH = 50 SITH_ACCOUNT_INACTIVITY_DELTA = relativedelta(years=2) """Time before which a user account is considered inactive""" +SITH_ACCOUNT_DUMP_DELTA = timedelta(days=30) +"""timedelta between the warning mail and the actual account dump""" # Defines which product type is the refilling type, and thus increases the account amount SITH_COUNTER_PRODUCTTYPE_REFILLING = 3