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 %} {% if profile.customer %}
<h3>{% trans %}Student cards{% endtrans %}</h3> <h3>{% trans %}Student card{% endtrans %}</h3>
{% if profile.customer.student_cards.exists() %} {% if profile.customer.student_card %}
<ul class="student-cards"> <span class="student-cards">
{% for card in profile.customer.student_cards.all() %} {% trans %}Registered{% endtrans %} <i class="fa fa-check"></i>&nbsp;-&nbsp;
<li> <a href="{{ url('counter:delete_student_card', customer_id=profile.customer.pk) }}">
{{ card.uid }} {% trans %}Delete{% endtrans %}
&nbsp;-&nbsp; </a>
<a href="{{ url('counter:delete_student_card', customer_id=profile.customer.pk, card_id=card.id) }}"> </span>
{% trans %}Delete{% endtrans %}
</a>
</li>
{% endfor %}
</ul>
{% else %} {% else %}
<em class="no-cards">{% trans %}No student card registered.{% endtrans %}</em> <em class="no-cards">{% trans %}No student card registered.{% endtrans %}</em>
<p class="justify"> <p class="justify">
{% trans %}You can add a card by asking at a counter or add it yourself here. If you want to manually {% 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 %} 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> </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 %} {% 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 %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -52,9 +52,7 @@ class StudentCardForm(forms.ModelForm):
class Meta: class Meta:
model = StudentCard model = StudentCard
fields = ["uid"] fields = ["uid"]
widgets = { widgets = {"uid": NFCTextInput}
"uid": NFCTextInput,
}
def clean(self): def clean(self):
cleaned_data = super().clean() 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 = models.CharField(
_("uid"), max_length=UID_SIZE, unique=True, validators=[MinLengthValidator(4)] _("uid"), max_length=UID_SIZE, unique=True, validators=[MinLengthValidator(4)]
) )
customer = models.ForeignKey( customer = models.OneToOneField(
Customer, Customer,
related_name="student_cards", related_name="student_card",
verbose_name=_("student cards"), verbose_name=_("student card"),
null=False,
blank=False,
on_delete=models.CASCADE, on_delete=models.CASCADE,
) )
@ -1145,7 +1143,7 @@ class StudentCard(models.Model):
return self.uid return self.uid
@staticmethod @staticmethod
def is_valid(uid): def is_valid(uid: str):
return ( return (
(uid.isupper() or uid.isnumeric()) (uid.isupper() or uid.isnumeric())
and len(uid) == StudentCard.UID_SIZE and len(uid) == StudentCard.UID_SIZE

View File

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

View File

@ -1,3 +1,4 @@
import itertools
import json import json
import string import string
@ -188,216 +189,102 @@ class TestStudentCard(TestCase):
) )
def test_add_student_card_from_counter(self): def test_add_student_card_from_counter(self):
# Test card with mixed letters and numbers for uid in ["8B90734A802A8F", "ABCAAAFAAFAAAB", "15248196326518"]:
response = self.client.post( self.sli.customer.student_card.delete()
reverse( self.client.post(
"counter:click", reverse(
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, "counter:click",
), kwargs={"counter_id": self.counter.id, "user_id": self.sli.id},
{"student_card_uid": "8B90734A802A8F", "action": "add_student_card"}, ),
) {"student_card_uid": uid, "action": "add_student_card"},
self.assertContains(response, text="8B90734A802A8F") )
self.sli.customer.refresh_from_db()
# Test card with only numbers assert self.sli.customer.student_card.uid == uid
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")
def test_add_student_card_from_counter_fail(self): def test_add_student_card_from_counter_fail(self):
# UID too short # UID too short
response = self.client.post( self.sli.customer.student_card.delete()
reverse( # too short, then too long, then containing lowercase, then spaces
"counter:click", for uid in ["8B90734A802A8", "8B90734A802A8FA", "8b90734a802a9f", " " * 14]:
kwargs={"counter_id": self.counter.id, "user_id": self.sli.id}, response = self.client.post(
), reverse(
{"student_card_uid": "8B90734A802A8", "action": "add_student_card"}, "counter:click",
) kwargs={"counter_id": self.counter.id, "user_id": self.sli.id},
self.assertContains( ),
response, text="Ce n'est pas un UID de carte étudiante valide" {"student_card_uid": uid, "action": "add_student_card"},
) )
self.assertContains(
# UID too long response, text="Ce n'est pas un UID de carte étudiante valide"
response = self.client.post( )
reverse( self.sli.customer.refresh_from_db()
"counter:click", assert not hasattr(self.sli.customer, "student_card")
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"
)
def test_delete_student_card_with_owner(self): def test_delete_student_card_with_owner(self):
self.client.force_login(self.sli) self.client.force_login(self.sli)
self.client.post( self.client.post(
reverse( reverse(
"counter:delete_student_card", "counter:delete_student_card",
kwargs={ kwargs={"customer_id": self.sli.customer.pk},
"customer_id": self.sli.customer.pk,
"card_id": self.sli.customer.student_cards.first().id,
},
) )
) )
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): def test_delete_student_card_with_board_member(self):
self.client.force_login(self.skia) self.client.force_login(self.skia)
self.client.post( self.client.post(
reverse( reverse(
"counter:delete_student_card", "counter:delete_student_card",
kwargs={ kwargs={"customer_id": self.sli.customer.pk},
"customer_id": self.sli.customer.pk,
"card_id": self.sli.customer.student_cards.first().id,
},
) )
) )
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): def test_delete_student_card_with_root(self):
self.client.force_login(self.root) self.client.force_login(self.root)
self.client.post( self.client.post(
reverse( reverse(
"counter:delete_student_card", "counter:delete_student_card",
kwargs={ kwargs={"customer_id": self.sli.customer.pk},
"customer_id": self.sli.customer.pk,
"card_id": self.sli.customer.student_cards.first().id,
},
) )
) )
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): def test_delete_student_card_fail(self):
self.client.force_login(self.krophil) self.client.force_login(self.krophil)
response = self.client.post( response = self.client.post(
reverse( reverse(
"counter:delete_student_card", "counter:delete_student_card",
kwargs={ kwargs={"customer_id": self.sli.customer.pk},
"customer_id": self.sli.customer.pk,
"card_id": self.sli.customer.student_cards.first().id,
},
) )
) )
assert response.status_code == 403 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): def test_add_student_card_from_user_preferences(self):
# Test with owner of the card # Test with owner of the card
self.client.force_login(self.sli) users = [self.sli, self.skia, self.root]
self.client.post( uids = ["8B90734A802A8F", "ABCAAAFAAFAAAB", "15248196326518"]
reverse( for user, uid in itertools.product(users, uids):
"counter:add_student_card", kwargs={"customer_id": self.sli.customer.pk} self.sli.customer.student_card.delete()
), self.client.force_login(user)
{"uid": "8B90734A802A8F"}, self.client.post(
) reverse(
"counter:add_student_card",
kwargs={"customer_id": self.sli.customer.pk},
),
{"uid": uid},
)
response = self.client.get( response = self.client.get(
reverse("core:user_prefs", kwargs={"user_id": self.sli.id}) reverse("core:user_prefs", kwargs={"user_id": self.sli.id})
) )
self.assertContains(response, text="8B90734A802A8F") self.sli.customer.refresh_from_db()
assert self.sli.customer.student_card.uid == uid
# Test with board member self.assertContains(response, text="Enregistré")
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")
def test_add_student_card_from_user_preferences_fail(self): def test_add_student_card_from_user_preferences_fail(self):
self.client.force_login(self.sli) self.client.force_login(self.sli)

View File

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

View File

@ -111,16 +111,23 @@ class StudentCardDeleteView(DeleteView, CanEditMixin):
model = StudentCard model = StudentCard
template_name = "core/delete_confirm.jinja" template_name = "core/delete_confirm.jinja"
pk_url_kwarg = "card_id"
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"]) self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"])
return super().dispatch(request, *args, **kwargs) 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): def get_success_url(self, **kwargs):
return reverse_lazy( return reverse("core:user_prefs", kwargs={"user_id": self.customer.user_id})
"core:user_prefs", kwargs={"user_id": self.customer.user.pk}
)
class CounterTabsMixin(TabedViewMixin): class CounterTabsMixin(TabedViewMixin):
@ -627,7 +634,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
product product
) )
kwargs["customer"] = self.customer 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["student_card_input"] = NFCCardForm()
kwargs["basket_total"] = self.sum_basket(self.request) kwargs["basket_total"] = self.sum_basket(self.request)
kwargs["refill_form"] = self.refill_form or RefillForm() kwargs["refill_form"] = self.refill_form or RefillForm()
@ -1527,11 +1534,10 @@ class StudentCardFormView(FormView):
def form_valid(self, form): def form_valid(self, form):
data = form.clean() data = form.clean()
res = super(FormView, self).form_valid(form) StudentCard.objects.update_or_create(
StudentCard(customer=self.customer, uid=data["uid"]).save() customer=self.customer, defaults={"uid": data["uid"]}
return res )
return super(FormView, self).form_valid(form)
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
return reverse_lazy( return reverse("core:user_prefs", kwargs={"user_id": self.customer.user_id})
"core:user_prefs", kwargs={"user_id": self.customer.user.pk}
)

View File

@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "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" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@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:34
#: core/templates/core/user_clubs.jinja:63 #: core/templates/core/user_clubs.jinja:63
#: core/templates/core/user_edit.jinja:62 #: 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:35
#: counter/templates/counter/last_ops.jinja:65 #: counter/templates/counter/last_ops.jinja:65
#: election/templates/election/election_detail.jinja:187 #: election/templates/election/election_detail.jinja:187
@ -650,7 +651,7 @@ msgid "Done"
msgstr "Effectuées" msgstr "Effectuées"
#: accounting/templates/accounting/journal_details.jinja:41 #: 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/moderation.jinja:13
#: pedagogy/templates/pedagogy/uv_detail.jinja:142 #: pedagogy/templates/pedagogy/uv_detail.jinja:142
#: trombi/templates/trombi/comment.jinja:4 #: 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_godfathers_tree.jinja:85
#: core/templates/core/user_preferences.jinja:18 #: core/templates/core/user_preferences.jinja:18
#: core/templates/core/user_preferences.jinja:27 #: 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 #: counter/templates/counter/cash_register_summary.jinja:28
#: forum/templates/forum/reply.jinja:39 #: forum/templates/forum/reply.jinja:39
#: subscription/templates/subscription/subscription.jinja:25 #: 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" 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" 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" msgid "Begin date"
msgstr "Date de début" 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 #: election/views.py:170 subscription/views.py:38
msgid "End date" msgid "End date"
msgstr "Date de fin" msgstr "Date de fin"
@ -963,15 +964,15 @@ msgstr "Date de fin"
#: club/forms.py:156 club/templates/club/club_sellings.jinja:49 #: 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:17
#: core/templates/core/user_account_detail.jinja:56 #: 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" msgid "Counter"
msgstr "Comptoir" msgstr "Comptoir"
#: club/forms.py:163 counter/views.py:683 #: club/forms.py:163 counter/views.py:685
msgid "Products" msgid "Products"
msgstr "Produits" msgstr "Produits"
#: club/forms.py:168 counter/views.py:688 #: club/forms.py:168 counter/views.py:690
msgid "Archived products" msgid "Archived products"
msgstr "Produits archivés" msgstr "Produits archivés"
@ -3041,11 +3042,11 @@ msgid "Eboutic invoices"
msgstr "Facture eboutic" msgstr "Facture eboutic"
#: core/templates/core/user_account.jinja:54 #: 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" msgid "Etickets"
msgstr "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" msgid "User has no account"
msgstr "L'utilisateur n'a pas de compte" msgstr "L'utilisateur n'a pas de compte"
@ -3295,14 +3296,20 @@ msgid "Go to my Trombi tools"
msgstr "Allez à mes outils de Trombi" msgstr "Allez à mes outils de Trombi"
#: core/templates/core/user_preferences.jinja:39 #: core/templates/core/user_preferences.jinja:39
msgid "Student cards" #: counter/templates/counter/counter_click.jinja:32
msgstr "Cartes étudiante" 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." msgid "No student card registered."
msgstr "Aucune carte étudiante enregistrée." msgstr "Aucune carte étudiante enregistrée."
#: core/templates/core/user_preferences.jinja:56 #: core/templates/core/user_preferences.jinja:51
msgid "" msgid ""
"You can add a card by asking at a counter or add it yourself here. If you " "You can add a card by asking at a counter or add it yourself here. If you "
"want to manually\n" "want to manually\n"
@ -3376,8 +3383,8 @@ msgstr "Cotisations"
msgid "Subscription stats" msgid "Subscription stats"
msgstr "Statistiques de cotisation" msgstr "Statistiques de cotisation"
#: core/templates/core/user_tools.jinja:48 counter/forms.py:176 #: core/templates/core/user_tools.jinja:48 counter/forms.py:174
#: counter/views.py:678 #: counter/views.py:680
msgid "Counters" msgid "Counters"
msgstr "Comptoirs" msgstr "Comptoirs"
@ -3394,12 +3401,12 @@ msgid "Product types management"
msgstr "Gestion des types de produit" msgstr "Gestion des types de produit"
#: core/templates/core/user_tools.jinja:56 #: 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" msgid "Cash register summaries"
msgstr "Relevés de caisse" msgstr "Relevés de caisse"
#: core/templates/core/user_tools.jinja:57 #: 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" msgid "Invoices call"
msgstr "Appels à facture" msgstr "Appels à facture"
@ -3547,7 +3554,7 @@ msgstr "Parrain / Marraine"
msgid "Godchild" msgid "Godchild"
msgstr "Fillot / Fillote" 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" msgid "Select user"
msgstr "Choisir un utilisateur" msgstr "Choisir un utilisateur"
@ -3600,11 +3607,11 @@ msgstr "Galaxie"
msgid "counter" msgid "counter"
msgstr "comptoir" msgstr "comptoir"
#: counter/forms.py:63 #: counter/forms.py:61
msgid "This UID is invalid" msgid "This UID is invalid"
msgstr "Cet UID est invalide" msgstr "Cet UID est invalide"
#: counter/forms.py:111 #: counter/forms.py:109
msgid "User not found" msgid "User not found"
msgstr "Utilisateur non trouvé" msgstr "Utilisateur non trouvé"
@ -3632,7 +3639,7 @@ msgstr "client"
msgid "customers" msgid "customers"
msgstr "clients" msgstr "clients"
#: counter/models.py:110 counter/views.py:261 #: counter/models.py:110 counter/views.py:263
msgid "Not enough money" msgid "Not enough money"
msgstr "Solde insuffisant" msgstr "Solde insuffisant"
@ -3858,10 +3865,6 @@ msgstr "secret"
msgid "uid" msgid "uid"
msgstr "uid" msgstr "uid"
#: counter/models.py:1138
msgid "student cards"
msgstr "cartes étudiante"
#: counter/templates/counter/account_dump_warning_mail.jinja:1 #: counter/templates/counter/account_dump_warning_mail.jinja:1
msgid "Hello" msgid "Hello"
msgstr "Bonjour" msgstr "Bonjour"
@ -3963,7 +3966,7 @@ msgstr "Liste des relevés de caisse"
msgid "Theoric sums" msgid "Theoric sums"
msgstr "Sommes théoriques" 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" msgid "Emptied"
msgstr "Coffre vidé" msgstr "Coffre vidé"
@ -3975,15 +3978,19 @@ msgstr "oui"
msgid "There is no cash register summary in this website." msgid "There is no cash register summary in this website."
msgstr "Il n'y a pas de relevé de caisse dans ce site web." 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" msgid "Add a student card"
msgstr "Ajouter une carte étudiante" 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" msgid "This is not a valid student card UID"
msgstr "Ce n'est pas un UID de carte étudiante valide" 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:67
#: counter/templates/counter/counter_click.jinja:132 #: counter/templates/counter/counter_click.jinja:132
#: counter/templates/counter/invoices_call.jinja:16 #: counter/templates/counter/invoices_call.jinja:16
@ -3994,14 +4001,6 @@ msgstr "Ce n'est pas un UID de carte étudiante valide"
msgid "Go" msgid "Go"
msgstr "Valider" 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 #: counter/templates/counter/counter_click.jinja:56
#: launderette/templates/launderette/launderette_admin.jinja:8 #: launderette/templates/launderette/launderette_admin.jinja:8
msgid "Selling" msgid "Selling"
@ -4189,101 +4188,101 @@ msgstr "Temps"
msgid "Top 100 barman %(counter_name)s (all semesters)" msgid "Top 100 barman %(counter_name)s (all semesters)"
msgstr "Top 100 barman %(counter_name)s (tous les semestres)" msgstr "Top 100 barman %(counter_name)s (tous les semestres)"
#: counter/views.py:147 #: counter/views.py:149
msgid "Cash summary" msgid "Cash summary"
msgstr "Relevé de caisse" msgstr "Relevé de caisse"
#: counter/views.py:156 #: counter/views.py:158
msgid "Last operations" msgid "Last operations"
msgstr "Dernières opérations" msgstr "Dernières opérations"
#: counter/views.py:203 #: counter/views.py:205
msgid "Bad credentials" msgid "Bad credentials"
msgstr "Mauvais identifiants" msgstr "Mauvais identifiants"
#: counter/views.py:205 #: counter/views.py:207
msgid "User is not barman" msgid "User is not barman"
msgstr "L'utilisateur n'est pas 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" msgid "Bad location, someone is already logged in somewhere else"
msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs" msgstr "Mauvais comptoir, quelqu'un est déjà connecté ailleurs"
#: counter/views.py:252 #: counter/views.py:254
msgid "Too young for that product" msgid "Too young for that product"
msgstr "Trop jeune pour ce produit" msgstr "Trop jeune pour ce produit"
#: counter/views.py:255 #: counter/views.py:257
msgid "Not allowed for that product" msgid "Not allowed for that product"
msgstr "Non autorisé pour ce produit" msgstr "Non autorisé pour ce produit"
#: counter/views.py:258 #: counter/views.py:260
msgid "No date of birth provided" msgid "No date of birth provided"
msgstr "Pas de date de naissance renseignée" 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" msgid "You have not enough money to buy all the basket"
msgstr "Vous n'avez pas assez d'argent pour acheter le panier" msgstr "Vous n'avez pas assez d'argent pour acheter le panier"
#: counter/views.py:673 #: counter/views.py:675
msgid "Counter administration" msgid "Counter administration"
msgstr "Administration des comptoirs" msgstr "Administration des comptoirs"
#: counter/views.py:693 #: counter/views.py:695
msgid "Product types" msgid "Product types"
msgstr "Types de produit" msgstr "Types de produit"
#: counter/views.py:913 #: counter/views.py:915
msgid "10 cents" msgid "10 cents"
msgstr "10 centimes" msgstr "10 centimes"
#: counter/views.py:914 #: counter/views.py:916
msgid "20 cents" msgid "20 cents"
msgstr "20 centimes" msgstr "20 centimes"
#: counter/views.py:915 #: counter/views.py:917
msgid "50 cents" msgid "50 cents"
msgstr "50 centimes" msgstr "50 centimes"
#: counter/views.py:916 #: counter/views.py:918
msgid "1 euro" msgid "1 euro"
msgstr "1 €" msgstr "1 €"
#: counter/views.py:917 #: counter/views.py:919
msgid "2 euros" msgid "2 euros"
msgstr "2 €" msgstr "2 €"
#: counter/views.py:918 #: counter/views.py:920
msgid "5 euros" msgid "5 euros"
msgstr "5 €" msgstr "5 €"
#: counter/views.py:919 #: counter/views.py:921
msgid "10 euros" msgid "10 euros"
msgstr "10 €" msgstr "10 €"
#: counter/views.py:920 #: counter/views.py:922
msgid "20 euros" msgid "20 euros"
msgstr "20 €" msgstr "20 €"
#: counter/views.py:921 #: counter/views.py:923
msgid "50 euros" msgid "50 euros"
msgstr "50 €" msgstr "50 €"
#: counter/views.py:923 #: counter/views.py:925
msgid "100 euros" msgid "100 euros"
msgstr "100 €" msgstr "100 €"
#: counter/views.py:926 counter/views.py:932 counter/views.py:938 #: counter/views.py:928 counter/views.py:934 counter/views.py:940
#: counter/views.py:944 counter/views.py:950 #: counter/views.py:946 counter/views.py:952
msgid "Check amount" msgid "Check amount"
msgstr "Montant du chèque" msgstr "Montant du chèque"
#: counter/views.py:929 counter/views.py:935 counter/views.py:941 #: counter/views.py:931 counter/views.py:937 counter/views.py:943
#: counter/views.py:947 counter/views.py:953 #: counter/views.py:949 counter/views.py:955
msgid "Check quantity" msgid "Check quantity"
msgstr "Nombre de chèque" msgstr "Nombre de chèque"
#: counter/views.py:1473 #: counter/views.py:1475
msgid "people(s)" msgid "people(s)"
msgstr "personne(s)" msgstr "personne(s)"