From 74df077b31cff8d555933b4f0137ea3b76478d34 Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 15 Nov 2024 15:19:30 +0100 Subject: [PATCH] Make StudentCard unique by customer --- core/templates/core/user_preferences.jinja | 37 ++- counter/forms.py | 4 +- .../0025_alter_studentcard_customer.py | 52 ++++ counter/models.py | 10 +- counter/templates/counter/counter_click.jinja | 36 +-- counter/tests/test_customer.py | 227 +++++------------- counter/urls.py | 2 +- counter/views.py | 28 ++- locale/fr/LC_MESSAGES/django.po | 129 +++++----- 9 files changed, 231 insertions(+), 294 deletions(-) create mode 100644 counter/migrations/0025_alter_studentcard_customer.py diff --git a/core/templates/core/user_preferences.jinja b/core/templates/core/user_preferences.jinja index 0cf4bd57..ac41944e 100644 --- a/core/templates/core/user_preferences.jinja +++ b/core/templates/core/user_preferences.jinja @@ -36,34 +36,31 @@ {% if profile.customer %} -

{% trans %}Student cards{% endtrans %}

+

{% trans %}Student card{% endtrans %}

- {% if profile.customer.student_cards.exists() %} - + {% if profile.customer.student_card %} + + {% trans %}Registered{% endtrans %}  -  + + {% trans %}Delete{% endtrans %} + + {% else %} {% trans %}No student card registered.{% endtrans %}

{% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually add a student card yourself, you'll need a NFC reader. We store the UID of the card which is 14 characters long.{% endtrans %}

+
+ {% csrf_token %} + {{ student_card_form.as_p() }} + +
{% endif %} - -
- {% csrf_token %} - {{ student_card_form.as_p() }} - -
{% endif %} {% endblock %} \ No newline at end of file diff --git a/counter/forms.py b/counter/forms.py index 84a92512..6de8bf70 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -52,9 +52,7 @@ class StudentCardForm(forms.ModelForm): class Meta: model = StudentCard fields = ["uid"] - widgets = { - "uid": NFCTextInput, - } + widgets = {"uid": NFCTextInput} def clean(self): cleaned_data = super().clean() diff --git a/counter/migrations/0025_alter_studentcard_customer.py b/counter/migrations/0025_alter_studentcard_customer.py new file mode 100644 index 00000000..f15bb39e --- /dev/null +++ b/counter/migrations/0025_alter_studentcard_customer.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.16 on 2024-11-15 12:34 +from __future__ import annotations + +from operator import attrgetter +from typing import TYPE_CHECKING + +import django.db.models.deletion +from django.db import migrations, models +from django.db.models import Count + +if TYPE_CHECKING: + from django.db.backends.postgresql.schema import DatabaseSchemaEditor + from django.db.migrations.state import StateApps + + +def delete_duplicates(apps: StateApps, schema_editor: DatabaseSchemaEditor): + """Delete cards of users with more than one student cards. + + For all users who have more than one registered student card, all + the cards except the last one are deleted. + """ + Customer = apps.get_model("counter", "Customer") + StudentCard = apps.get_model("counter", "StudentCard") + customers = ( + Customer.objects.annotate(nb_cards=Count("student_cards")) + .filter(nb_cards__gt=1) + .prefetch_related("student_cards") + ) + to_delete = [ + card.id + for customer in customers + for card in sorted(customer.student_cards.all(), key=attrgetter("id"))[:-1] + ] + StudentCard.objects.filter(id__in=to_delete).delete() + + +class Migration(migrations.Migration): + dependencies = [("counter", "0024_accountdump_accountdump_unique_ongoing_dump")] + + operations = [ + migrations.RunPython(delete_duplicates, migrations.RunPython.noop), + migrations.AlterField( + model_name="studentcard", + name="customer", + field=models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + related_name="student_card", + to="counter.customer", + verbose_name="student card", + ), + ), + ] diff --git a/counter/models.py b/counter/models.py index 1666572b..281f2069 100644 --- a/counter/models.py +++ b/counter/models.py @@ -1132,12 +1132,10 @@ class StudentCard(models.Model): uid = models.CharField( _("uid"), max_length=UID_SIZE, unique=True, validators=[MinLengthValidator(4)] ) - customer = models.ForeignKey( + customer = models.OneToOneField( Customer, - related_name="student_cards", - verbose_name=_("student cards"), - null=False, - blank=False, + related_name="student_card", + verbose_name=_("student card"), on_delete=models.CASCADE, ) @@ -1145,7 +1143,7 @@ class StudentCard(models.Model): return self.uid @staticmethod - def is_valid(uid): + def is_valid(uid: str): return ( (uid.isupper() or uid.isnumeric()) and len(uid) == StudentCard.UID_SIZE diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index cb6bb9cf..72c0a872 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -29,26 +29,26 @@ {{ user_mini_profile(customer.user) }} {{ user_subscription(customer.user) }}

{% trans %}Amount: {% endtrans %}{{ customer.amount }} €

-
- {% csrf_token %} - - {% trans %}Add a student card{% endtrans %} - {{ student_card_input.student_card_uid }} - {% if request.session['not_valid_student_card_uid'] %} -

{% trans %}This is not a valid student card UID{% endtrans %}

- {% endif %} - -
-
{% trans %}Registered cards{% endtrans %}
- {% if student_cards %} - - +
{% trans %}Student card{% endtrans %}
+ {% if student_card %} +

+ {% trans %}Registered{% endtrans %}   -   + + {% trans %}Delete{% endtrans %} + +

{% else %} {% trans %}No card registered{% endtrans %} +
+ {% csrf_token %} + + {% trans %}Add a student card{% endtrans %} + {{ student_card_input.student_card_uid }} + {% if request.session['not_valid_student_card_uid'] %} +

{% trans %}This is not a valid student card UID{% endtrans %}

+ {% endif %} + +
{% endif %} diff --git a/counter/tests/test_customer.py b/counter/tests/test_customer.py index 2d7e1c60..74a1e79a 100644 --- a/counter/tests/test_customer.py +++ b/counter/tests/test_customer.py @@ -1,3 +1,4 @@ +import itertools import json import string @@ -188,216 +189,102 @@ class TestStudentCard(TestCase): ) def test_add_student_card_from_counter(self): - # Test card with mixed letters and numbers - response = self.client.post( - reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, - ), - {"student_card_uid": "8B90734A802A8F", "action": "add_student_card"}, - ) - self.assertContains(response, text="8B90734A802A8F") - - # Test card with only numbers - response = self.client.post( - reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, - ), - {"student_card_uid": "04786547890123", "action": "add_student_card"}, - ) - self.assertContains(response, text="04786547890123") - - # Test card with only letters - response = self.client.post( - reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, - ), - {"student_card_uid": "ABCAAAFAAFAAAB", "action": "add_student_card"}, - ) - self.assertContains(response, text="ABCAAAFAAFAAAB") + for uid in ["8B90734A802A8F", "ABCAAAFAAFAAAB", "15248196326518"]: + self.sli.customer.student_card.delete() + self.client.post( + reverse( + "counter:click", + kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + ), + {"student_card_uid": uid, "action": "add_student_card"}, + ) + self.sli.customer.refresh_from_db() + assert self.sli.customer.student_card.uid == uid def test_add_student_card_from_counter_fail(self): # UID too short - response = self.client.post( - reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, - ), - {"student_card_uid": "8B90734A802A8", "action": "add_student_card"}, - ) - self.assertContains( - response, text="Ce n'est pas un UID de carte étudiante valide" - ) - - # UID too long - response = self.client.post( - reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, - ), - {"student_card_uid": "8B90734A802A8FA", "action": "add_student_card"}, - ) - self.assertContains( - response, text="Ce n'est pas un UID de carte étudiante valide" - ) - - # Test with already existing card - response = self.client.post( - reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, - ), - {"student_card_uid": "9A89B82018B0A0", "action": "add_student_card"}, - ) - self.assertContains( - response, text="Ce n'est pas un UID de carte étudiante valide" - ) - - # Test with lowercase - response = self.client.post( - reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, - ), - {"student_card_uid": "8b90734a802a9f", "action": "add_student_card"}, - ) - self.assertContains( - response, text="Ce n'est pas un UID de carte étudiante valide" - ) - - # Test with white spaces - response = self.client.post( - reverse( - "counter:click", - kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, - ), - {"student_card_uid": " ", "action": "add_student_card"}, - ) - self.assertContains( - response, text="Ce n'est pas un UID de carte étudiante valide" - ) + self.sli.customer.student_card.delete() + # too short, then too long, then containing lowercase, then spaces + for uid in ["8B90734A802A8", "8B90734A802A8FA", "8b90734a802a9f", " " * 14]: + response = self.client.post( + reverse( + "counter:click", + kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, + ), + {"student_card_uid": uid, "action": "add_student_card"}, + ) + self.assertContains( + response, text="Ce n'est pas un UID de carte étudiante valide" + ) + self.sli.customer.refresh_from_db() + assert not hasattr(self.sli.customer, "student_card") def test_delete_student_card_with_owner(self): self.client.force_login(self.sli) self.client.post( reverse( "counter:delete_student_card", - kwargs={ - "customer_id": self.sli.customer.pk, - "card_id": self.sli.customer.student_cards.first().id, - }, + kwargs={"customer_id": self.sli.customer.pk}, ) ) - assert not self.sli.customer.student_cards.exists() + self.sli.customer.refresh_from_db() + assert not hasattr(self.sli.customer, "student_card") def test_delete_student_card_with_board_member(self): self.client.force_login(self.skia) self.client.post( reverse( "counter:delete_student_card", - kwargs={ - "customer_id": self.sli.customer.pk, - "card_id": self.sli.customer.student_cards.first().id, - }, + kwargs={"customer_id": self.sli.customer.pk}, ) ) - assert not self.sli.customer.student_cards.exists() + self.sli.customer.refresh_from_db() + assert not hasattr(self.sli.customer, "student_card") def test_delete_student_card_with_root(self): self.client.force_login(self.root) self.client.post( reverse( "counter:delete_student_card", - kwargs={ - "customer_id": self.sli.customer.pk, - "card_id": self.sli.customer.student_cards.first().id, - }, + kwargs={"customer_id": self.sli.customer.pk}, ) ) - assert not self.sli.customer.student_cards.exists() + self.sli.customer.refresh_from_db() + assert not hasattr(self.sli.customer, "student_card") def test_delete_student_card_fail(self): self.client.force_login(self.krophil) response = self.client.post( reverse( "counter:delete_student_card", - kwargs={ - "customer_id": self.sli.customer.pk, - "card_id": self.sli.customer.student_cards.first().id, - }, + kwargs={"customer_id": self.sli.customer.pk}, ) ) assert response.status_code == 403 - assert self.sli.customer.student_cards.exists() + self.sli.customer.refresh_from_db() + assert hasattr(self.sli.customer, "student_card") def test_add_student_card_from_user_preferences(self): # Test with owner of the card - self.client.force_login(self.sli) - self.client.post( - reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} - ), - {"uid": "8B90734A802A8F"}, - ) + users = [self.sli, self.skia, self.root] + uids = ["8B90734A802A8F", "ABCAAAFAAFAAAB", "15248196326518"] + for user, uid in itertools.product(users, uids): + self.sli.customer.student_card.delete() + self.client.force_login(user) + self.client.post( + reverse( + "counter:add_student_card", + kwargs={"customer_id": self.sli.customer.pk}, + ), + {"uid": uid}, + ) - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) - self.assertContains(response, text="8B90734A802A8F") - - # Test with board member - self.client.force_login(self.skia) - self.client.post( - reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} - ), - {"uid": "8B90734A802A8A"}, - ) - - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) - self.assertContains(response, text="8B90734A802A8A") - - # Test card with only numbers - self.client.post( - reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} - ), - {"uid": "04786547890123"}, - ) - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) - self.assertContains(response, text="04786547890123") - - # Test card with only letters - self.client.post( - reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} - ), - {"uid": "ABCAAAFAAFAAAB"}, - ) - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) - self.assertContains(response, text="ABCAAAFAAFAAAB") - - # Test with root - self.client.force_login(self.root) - self.client.post( - reverse( - "counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} - ), - {"uid": "8B90734A802A8B"}, - ) - - response = self.client.get( - reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) - ) - self.assertContains(response, text="8B90734A802A8B") + response = self.client.get( + reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) + ) + self.sli.customer.refresh_from_db() + assert self.sli.customer.student_card.uid == uid + self.assertContains(response, text="Enregistré") def test_add_student_card_from_user_preferences_fail(self): self.client.force_login(self.sli) diff --git a/counter/urls.py b/counter/urls.py index a2732925..92582935 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -74,7 +74,7 @@ urlpatterns = [ name="add_student_card", ), path( - "customer//card/delete//", + "customer//card/delete", StudentCardDeleteView.as_view(), name="delete_student_card", ), diff --git a/counter/views.py b/counter/views.py index 9483d335..b44e9ea1 100644 --- a/counter/views.py +++ b/counter/views.py @@ -111,16 +111,23 @@ class StudentCardDeleteView(DeleteView, CanEditMixin): model = StudentCard template_name = "core/delete_confirm.jinja" - pk_url_kwarg = "card_id" def dispatch(self, request, *args, **kwargs): self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"]) return super().dispatch(request, *args, **kwargs) + def get_object(self, queryset=None): + if not hasattr(self.customer, "student_card"): + raise Http404( + _( + "The customer %s has no registered student card", + self.customer.user.get_full_name(), + ) + ) + return self.customer.student_card + def get_success_url(self, **kwargs): - return reverse_lazy( - "core:user_prefs", kwargs={"user_id": self.customer.user.pk} - ) + return reverse("core:user_prefs", kwargs={"user_id": self.customer.user_id}) class CounterTabsMixin(TabedViewMixin): @@ -627,7 +634,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView): product ) kwargs["customer"] = self.customer - kwargs["student_cards"] = self.customer.student_cards.all() + kwargs["student_card"] = getattr(self.customer, "student_card", None) kwargs["student_card_input"] = NFCCardForm() kwargs["basket_total"] = self.sum_basket(self.request) kwargs["refill_form"] = self.refill_form or RefillForm() @@ -1527,11 +1534,10 @@ class StudentCardFormView(FormView): def form_valid(self, form): data = form.clean() - res = super(FormView, self).form_valid(form) - StudentCard(customer=self.customer, uid=data["uid"]).save() - return res + StudentCard.objects.update_or_create( + customer=self.customer, defaults={"uid": data["uid"]} + ) + return super(FormView, self).form_valid(form) def get_success_url(self, **kwargs): - return reverse_lazy( - "core:user_prefs", kwargs={"user_id": self.customer.user.pk} - ) + return reverse("core:user_prefs", kwargs={"user_id": self.customer.user_id}) diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index d357d31c..00548c2c 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-11-14 10:26+0100\n" +"POT-Creation-Date: 2024-11-15 13:03+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -369,7 +369,8 @@ msgstr "Compte en banque : " #: core/templates/core/user_clubs.jinja:34 #: core/templates/core/user_clubs.jinja:63 #: core/templates/core/user_edit.jinja:62 -#: core/templates/core/user_preferences.jinja:48 +#: core/templates/core/user_preferences.jinja:45 +#: counter/templates/counter/counter_click.jinja:37 #: counter/templates/counter/last_ops.jinja:35 #: counter/templates/counter/last_ops.jinja:65 #: election/templates/election/election_detail.jinja:187 @@ -650,7 +651,7 @@ msgid "Done" msgstr "Effectuées" #: accounting/templates/accounting/journal_details.jinja:41 -#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:955 +#: counter/templates/counter/cash_summary_list.jinja:37 counter/views.py:957 #: pedagogy/templates/pedagogy/moderation.jinja:13 #: pedagogy/templates/pedagogy/uv_detail.jinja:142 #: trombi/templates/trombi/comment.jinja:4 @@ -771,7 +772,7 @@ msgstr "Opération liée : " #: core/templates/core/user_godfathers_tree.jinja:85 #: core/templates/core/user_preferences.jinja:18 #: core/templates/core/user_preferences.jinja:27 -#: core/templates/core/user_preferences.jinja:65 +#: core/templates/core/user_preferences.jinja:61 #: counter/templates/counter/cash_register_summary.jinja:28 #: forum/templates/forum/reply.jinja:39 #: subscription/templates/subscription/subscription.jinja:25 @@ -951,11 +952,11 @@ msgstr "Une action est requise" msgid "You must specify at least an user or an email address" msgstr "vous devez spécifier au moins un utilisateur ou une adresse email" -#: club/forms.py:149 counter/forms.py:203 +#: club/forms.py:149 counter/forms.py:201 msgid "Begin date" msgstr "Date de début" -#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:206 +#: club/forms.py:152 com/views.py:84 com/views.py:202 counter/forms.py:204 #: election/views.py:170 subscription/views.py:38 msgid "End date" msgstr "Date de fin" @@ -963,15 +964,15 @@ msgstr "Date de fin" #: club/forms.py:156 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:137 +#: counter/templates/counter/cash_summary_list.jinja:33 counter/views.py:139 msgid "Counter" msgstr "Comptoir" -#: club/forms.py:163 counter/views.py:683 +#: club/forms.py:163 counter/views.py:685 msgid "Products" msgstr "Produits" -#: club/forms.py:168 counter/views.py:688 +#: club/forms.py:168 counter/views.py:690 msgid "Archived products" msgstr "Produits archivés" @@ -3041,11 +3042,11 @@ msgid "Eboutic invoices" msgstr "Facture eboutic" #: core/templates/core/user_account.jinja:54 -#: core/templates/core/user_tools.jinja:58 counter/views.py:708 +#: core/templates/core/user_tools.jinja:58 counter/views.py:710 msgid "Etickets" msgstr "Etickets" -#: core/templates/core/user_account.jinja:69 core/views/user.py:638 +#: core/templates/core/user_account.jinja:69 core/views/user.py:634 msgid "User has no account" msgstr "L'utilisateur n'a pas de compte" @@ -3295,14 +3296,20 @@ msgid "Go to my Trombi tools" msgstr "Allez à mes outils de Trombi" #: core/templates/core/user_preferences.jinja:39 -msgid "Student cards" -msgstr "Cartes étudiante" +#: counter/templates/counter/counter_click.jinja:32 +msgid "Student card" +msgstr "Carte étudiante" -#: core/templates/core/user_preferences.jinja:54 +#: core/templates/core/user_preferences.jinja:43 +#: counter/templates/counter/counter_click.jinja:35 +msgid "Registered" +msgstr "Enregistré" + +#: core/templates/core/user_preferences.jinja:49 msgid "No student card registered." msgstr "Aucune carte étudiante enregistrée." -#: core/templates/core/user_preferences.jinja:56 +#: core/templates/core/user_preferences.jinja:51 msgid "" "You can add a card by asking at a counter or add it yourself here. If you " "want to manually\n" @@ -3376,8 +3383,8 @@ msgstr "Cotisations" msgid "Subscription stats" msgstr "Statistiques de cotisation" -#: core/templates/core/user_tools.jinja:48 counter/forms.py:176 -#: counter/views.py:678 +#: core/templates/core/user_tools.jinja:48 counter/forms.py:174 +#: counter/views.py:680 msgid "Counters" msgstr "Comptoirs" @@ -3394,12 +3401,12 @@ msgid "Product types management" msgstr "Gestion des types de produit" #: core/templates/core/user_tools.jinja:56 -#: counter/templates/counter/cash_summary_list.jinja:23 counter/views.py:698 +#: counter/templates/counter/cash_summary_list.jinja:23 counter/views.py:700 msgid "Cash register summaries" msgstr "Relevés de caisse" #: core/templates/core/user_tools.jinja:57 -#: counter/templates/counter/invoices_call.jinja:4 counter/views.py:703 +#: counter/templates/counter/invoices_call.jinja:4 counter/views.py:705 msgid "Invoices call" msgstr "Appels à facture" @@ -3547,7 +3554,7 @@ msgstr "Parrain / Marraine" msgid "Godchild" msgstr "Fillot / Fillote" -#: core/views/forms.py:311 counter/forms.py:82 trombi/views.py:151 +#: core/views/forms.py:311 counter/forms.py:80 trombi/views.py:151 msgid "Select user" msgstr "Choisir un utilisateur" @@ -3600,11 +3607,11 @@ msgstr "Galaxie" msgid "counter" msgstr "comptoir" -#: counter/forms.py:63 +#: counter/forms.py:61 msgid "This UID is invalid" msgstr "Cet UID est invalide" -#: counter/forms.py:111 +#: counter/forms.py:109 msgid "User not found" msgstr "Utilisateur non trouvé" @@ -3632,7 +3639,7 @@ msgstr "client" msgid "customers" msgstr "clients" -#: counter/models.py:110 counter/views.py:261 +#: counter/models.py:110 counter/views.py:263 msgid "Not enough money" msgstr "Solde insuffisant" @@ -3858,10 +3865,6 @@ msgstr "secret" msgid "uid" msgstr "uid" -#: counter/models.py:1138 -msgid "student cards" -msgstr "cartes étudiante" - #: counter/templates/counter/account_dump_warning_mail.jinja:1 msgid "Hello" msgstr "Bonjour" @@ -3963,7 +3966,7 @@ msgstr "Liste des relevés de caisse" msgid "Theoric sums" msgstr "Sommes théoriques" -#: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:956 +#: counter/templates/counter/cash_summary_list.jinja:36 counter/views.py:958 msgid "Emptied" msgstr "Coffre vidé" @@ -3975,15 +3978,19 @@ msgstr "oui" msgid "There is no cash register summary in this website." msgstr "Il n'y a pas de relevé de caisse dans ce site web." -#: counter/templates/counter/counter_click.jinja:35 +#: counter/templates/counter/counter_click.jinja:41 +msgid "No card registered" +msgstr "Aucune carte enregistrée" + +#: counter/templates/counter/counter_click.jinja:45 msgid "Add a student card" msgstr "Ajouter une carte étudiante" -#: counter/templates/counter/counter_click.jinja:38 +#: counter/templates/counter/counter_click.jinja:48 msgid "This is not a valid student card UID" msgstr "Ce n'est pas un UID de carte étudiante valide" -#: counter/templates/counter/counter_click.jinja:40 +#: counter/templates/counter/counter_click.jinja:50 #: counter/templates/counter/counter_click.jinja:67 #: counter/templates/counter/counter_click.jinja:132 #: counter/templates/counter/invoices_call.jinja:16 @@ -3994,14 +4001,6 @@ msgstr "Ce n'est pas un UID de carte étudiante valide" msgid "Go" msgstr "Valider" -#: counter/templates/counter/counter_click.jinja:42 -msgid "Registered cards" -msgstr "Cartes enregistrées" - -#: counter/templates/counter/counter_click.jinja:51 -msgid "No card registered" -msgstr "Aucune carte enregistrée" - #: counter/templates/counter/counter_click.jinja:56 #: launderette/templates/launderette/launderette_admin.jinja:8 msgid "Selling" @@ -4189,101 +4188,101 @@ msgstr "Temps" msgid "Top 100 barman %(counter_name)s (all semesters)" msgstr "Top 100 barman %(counter_name)s (tous les semestres)" -#: counter/views.py:147 +#: counter/views.py:149 msgid "Cash summary" msgstr "Relevé de caisse" -#: counter/views.py:156 +#: counter/views.py:158 msgid "Last operations" msgstr "Dernières opérations" -#: counter/views.py:203 +#: counter/views.py:205 msgid "Bad credentials" msgstr "Mauvais identifiants" -#: counter/views.py:205 +#: counter/views.py:207 msgid "User is not barman" msgstr "L'utilisateur n'est pas barman." -#: counter/views.py:210 +#: counter/views.py:212 msgid "Bad location, someone is already logged in somewhere else" msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs" -#: counter/views.py:252 +#: counter/views.py:254 msgid "Too young for that product" msgstr "Trop jeune pour ce produit" -#: counter/views.py:255 +#: counter/views.py:257 msgid "Not allowed for that product" msgstr "Non autorisé pour ce produit" -#: counter/views.py:258 +#: counter/views.py:260 msgid "No date of birth provided" msgstr "Pas de date de naissance renseignée" -#: counter/views.py:546 +#: counter/views.py:548 msgid "You have not enough money to buy all the basket" msgstr "Vous n'avez pas assez d'argent pour acheter le panier" -#: counter/views.py:673 +#: counter/views.py:675 msgid "Counter administration" msgstr "Administration des comptoirs" -#: counter/views.py:693 +#: counter/views.py:695 msgid "Product types" msgstr "Types de produit" -#: counter/views.py:913 +#: counter/views.py:915 msgid "10 cents" msgstr "10 centimes" -#: counter/views.py:914 +#: counter/views.py:916 msgid "20 cents" msgstr "20 centimes" -#: counter/views.py:915 +#: counter/views.py:917 msgid "50 cents" msgstr "50 centimes" -#: counter/views.py:916 +#: counter/views.py:918 msgid "1 euro" msgstr "1 €" -#: counter/views.py:917 +#: counter/views.py:919 msgid "2 euros" msgstr "2 €" -#: counter/views.py:918 +#: counter/views.py:920 msgid "5 euros" msgstr "5 €" -#: counter/views.py:919 +#: counter/views.py:921 msgid "10 euros" msgstr "10 €" -#: counter/views.py:920 +#: counter/views.py:922 msgid "20 euros" msgstr "20 €" -#: counter/views.py:921 +#: counter/views.py:923 msgid "50 euros" msgstr "50 €" -#: counter/views.py:923 +#: counter/views.py:925 msgid "100 euros" msgstr "100 €" -#: counter/views.py:926 counter/views.py:932 counter/views.py:938 -#: counter/views.py:944 counter/views.py:950 +#: counter/views.py:928 counter/views.py:934 counter/views.py:940 +#: counter/views.py:946 counter/views.py:952 msgid "Check amount" msgstr "Montant du chèque" -#: counter/views.py:929 counter/views.py:935 counter/views.py:941 -#: counter/views.py:947 counter/views.py:953 +#: counter/views.py:931 counter/views.py:937 counter/views.py:943 +#: counter/views.py:949 counter/views.py:955 msgid "Check quantity" msgstr "Nombre de chèque" -#: counter/views.py:1473 +#: counter/views.py:1475 msgid "people(s)" msgstr "personne(s)"