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