Make StudentCard unique by customer

This commit is contained in:
imperosol 2024-11-15 15:19:30 +01:00
parent 1f6ad0a629
commit 74df077b31
9 changed files with 231 additions and 294 deletions

View File

@ -36,34 +36,31 @@
{% if profile.customer %}
<h3>{% trans %}Student cards{% endtrans %}</h3>
<h3>{% trans %}Student card{% endtrans %}</h3>
{% if profile.customer.student_cards.exists() %}
<ul class="student-cards">
{% for card in profile.customer.student_cards.all() %}
<li>
{{ card.uid }}
&nbsp;-&nbsp;
<a href="{{ url('counter:delete_student_card', customer_id=profile.customer.pk, card_id=card.id) }}">
{% trans %}Delete{% endtrans %}
</a>
</li>
{% endfor %}
</ul>
{% if profile.customer.student_card %}
<span class="student-cards">
{% trans %}Registered{% endtrans %} <i class="fa fa-check"></i>&nbsp;-&nbsp;
<a href="{{ url('counter:delete_student_card', customer_id=profile.customer.pk) }}">
{% trans %}Delete{% endtrans %}
</a>
</span>
{% else %}
<em class="no-cards">{% trans %}No student card registered.{% endtrans %}</em>
<p class="justify">
{% 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 %}
</p>
<form
class="form form-cards"
action="{{ url('counter:add_student_card', customer_id=profile.customer.pk) }}"
method="post"
>
{% csrf_token %}
{{ student_card_form.as_p() }}
<input class="form-submit-btn" type="submit" value="{% trans %}Save{% endtrans %}" />
</form>
{% endif %}
<form class="form form-cards" action="{{ url('counter:add_student_card', customer_id=profile.customer.pk) }}"
method="post">
{% csrf_token %}
{{ student_card_form.as_p() }}
<input class="form-submit-btn" type="submit" value="{% trans %}Save{% endtrans %}" />
</form>
{% endif %}
</div>
{% endblock %}

View File

@ -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()

View File

@ -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",
),
),
]

View File

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

View File

@ -29,26 +29,26 @@
{{ user_mini_profile(customer.user) }}
{{ user_subscription(customer.user) }}
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="add_student_card">
{% trans %}Add a student card{% endtrans %}
{{ student_card_input.student_card_uid }}
{% if request.session['not_valid_student_card_uid'] %}
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
{% endif %}
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>
<h6>{% trans %}Registered cards{% endtrans %}</h6>
{% if student_cards %}
<ul>
{% for card in student_cards %}
<li>{{ card.uid }}</li>
{% endfor %}
</ul>
<h6>{% trans %}Student card{% endtrans %}</h6>
{% if student_card %}
<p>
{% trans %}Registered{% endtrans %} <i class="fa fa-check"></i> &nbsp; - &nbsp;
<a href="{{ url('counter:delete_student_card', customer_id=customer.pk) }}">
{% trans %}Delete{% endtrans %}
</a>
</p>
{% else %}
{% trans %}No card registered{% endtrans %}
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user_id) }}">
{% csrf_token %}
<input type="hidden" name="action" value="add_student_card">
{% trans %}Add a student card{% endtrans %}
{{ student_card_input.student_card_uid }}
{% if request.session['not_valid_student_card_uid'] %}
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
{% endif %}
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
</form>
{% endif %}
</div>

View File

@ -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)

View File

@ -74,7 +74,7 @@ urlpatterns = [
name="add_student_card",
),
path(
"customer/<int:customer_id>/card/delete/<int:card_id>/",
"customer/<int:customer_id>/card/delete",
StudentCardDeleteView.as_view(),
name="delete_student_card",
),

View File

@ -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})

View File

@ -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 <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\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)"