Merge branch 'consos' into 'master'

Page de consommations

Closes #21

See merge request bde/nk20!57
This commit is contained in:
Pierre-antoine Comby 2020-03-21 23:18:24 +01:00
commit 0ecef74a1c
19 changed files with 1416 additions and 333 deletions

View File

@ -124,7 +124,7 @@ class UserDetailView(LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
user = context['user_object'] user = context['user_object']
history_list = \ history_list = \
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)) Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")
context['history_list'] = HistoryTable(history_list) context['history_list'] = HistoryTable(history_list)
club_list = \ club_list = \
Membership.objects.all().filter(user=user).only("club") Membership.objects.all().filter(user=user).only("club")

View File

@ -6,7 +6,7 @@ from rest_polymorphic.serializers import PolymorphicSerializer
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \ from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
TemplateTransaction TemplateTransaction, SpecialTransaction
class NoteSerializer(serializers.ModelSerializer): class NoteSerializer(serializers.ModelSerializer):
@ -18,12 +18,6 @@ class NoteSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Note model = Note
fields = '__all__' fields = '__all__'
extra_kwargs = {
'url': {
'view_name': 'project-detail',
'lookup_field': 'pk'
},
}
class NoteClubSerializer(serializers.ModelSerializer): class NoteClubSerializer(serializers.ModelSerializer):
@ -31,44 +25,60 @@ class NoteClubSerializer(serializers.ModelSerializer):
REST API Serializer for Club's notes. REST API Serializer for Club's notes.
The djangorestframework plugin will analyse the model `NoteClub` and parse all fields in the API. The djangorestframework plugin will analyse the model `NoteClub` and parse all fields in the API.
""" """
name = serializers.SerializerMethodField()
class Meta: class Meta:
model = NoteClub model = NoteClub
fields = '__all__' fields = '__all__'
def get_name(self, obj):
return str(obj)
class NoteSpecialSerializer(serializers.ModelSerializer): class NoteSpecialSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for special notes. REST API Serializer for special notes.
The djangorestframework plugin will analyse the model `NoteSpecial` and parse all fields in the API. The djangorestframework plugin will analyse the model `NoteSpecial` and parse all fields in the API.
""" """
name = serializers.SerializerMethodField()
class Meta: class Meta:
model = NoteSpecial model = NoteSpecial
fields = '__all__' fields = '__all__'
def get_name(self, obj):
return str(obj)
class NoteUserSerializer(serializers.ModelSerializer): class NoteUserSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for User's notes. REST API Serializer for User's notes.
The djangorestframework plugin will analyse the model `NoteUser` and parse all fields in the API. The djangorestframework plugin will analyse the model `NoteUser` and parse all fields in the API.
""" """
name = serializers.SerializerMethodField()
class Meta: class Meta:
model = NoteUser model = NoteUser
fields = '__all__' fields = '__all__'
def get_name(self, obj):
return str(obj)
class AliasSerializer(serializers.ModelSerializer): class AliasSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for Aliases. REST API Serializer for Aliases.
The djangorestframework plugin will analyse the model `Alias` and parse all fields in the API. The djangorestframework plugin will analyse the model `Alias` and parse all fields in the API.
""" """
note = serializers.SerializerMethodField()
class Meta: class Meta:
model = Alias model = Alias
fields = '__all__' fields = '__all__'
def get_note(self, alias):
return NotePolymorphicSerializer().to_representation(alias.note)
class NotePolymorphicSerializer(PolymorphicSerializer): class NotePolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = { model_serializer_mapping = {
@ -134,9 +144,21 @@ class MembershipTransactionSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
class SpecialTransactionSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Special transactions.
The djangorestframework plugin will analyse the model `SpecialTransaction` and parse all fields in the API.
"""
class Meta:
model = SpecialTransaction
fields = '__all__'
class TransactionPolymorphicSerializer(PolymorphicSerializer): class TransactionPolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = { model_serializer_mapping = {
Transaction: TransactionSerializer, Transaction: TransactionSerializer,
TemplateTransaction: TemplateTransactionSerializer, TemplateTransaction: TemplateTransactionSerializer,
MembershipTransaction: MembershipTransactionSerializer, MembershipTransaction: MembershipTransactionSerializer,
SpecialTransaction: SpecialTransactionSerializer,
} }

View File

@ -4,7 +4,7 @@
from django.db.models import Q from django.db.models import Q
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.filters import SearchFilter from rest_framework.filters import OrderingFilter, SearchFilter
from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \ from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \
NoteUserSerializer, AliasSerializer, \ NoteUserSerializer, AliasSerializer, \
@ -61,6 +61,9 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
""" """
queryset = Note.objects.all() queryset = Note.objects.all()
serializer_class = NotePolymorphicSerializer serializer_class = NotePolymorphicSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ]
ordering_fields = ['alias__name', 'alias__normalized_name']
def get_queryset(self): def get_queryset(self):
""" """
@ -82,12 +85,11 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
elif "club" in types: elif "club" in types:
queryset = queryset.filter(polymorphic_ctype__model="noteclub") queryset = queryset.filter(polymorphic_ctype__model="noteclub")
elif "special" in types: elif "special" in types:
queryset = queryset.filter( queryset = queryset.filter(polymorphic_ctype__model="notespecial")
polymorphic_ctype__model="notespecial")
else: else:
queryset = queryset.none() queryset = queryset.none()
return queryset return queryset.distinct()
class AliasViewSet(viewsets.ModelViewSet): class AliasViewSet(viewsets.ModelViewSet):
@ -98,6 +100,9 @@ class AliasViewSet(viewsets.ModelViewSet):
""" """
queryset = Alias.objects.all() queryset = Alias.objects.all()
serializer_class = AliasSerializer serializer_class = AliasSerializer
filter_backends = [SearchFilter, OrderingFilter]
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
ordering_fields = ['name', 'normalized_name']
def get_queryset(self): def get_queryset(self):
""" """

View File

@ -6,7 +6,7 @@ from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .models import Alias from .models import Alias
from .models import Transaction, TransactionTemplate from .models import TransactionTemplate
class AliasForm(forms.ModelForm): class AliasForm(forms.ModelForm):
@ -50,52 +50,3 @@ class TransactionTemplateForm(forms.ModelForm):
}, },
), ),
} }
class TransactionForm(forms.ModelForm):
def save(self, commit=True):
super().save(commit)
def clean(self):
"""
If the user has no right to transfer funds, then it will be the source of the transfer by default.
Transactions between a note and the same note are not authorized.
"""
cleaned_data = super().clean()
if "source" not in cleaned_data: # TODO Replace it with "if %user has no right to transfer funds"
cleaned_data["source"] = self.user.note
if cleaned_data["source"].pk == cleaned_data["destination"].pk:
self.add_error("destination", _("Source and destination must be different."))
return cleaned_data
class Meta:
model = Transaction
fields = (
'source',
'destination',
'reason',
'amount',
)
# Voir ci-dessus
widgets = {
'source':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
},
),
'destination':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
},
),
}

View File

@ -7,7 +7,7 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
from .notes import Note, NoteClub from .notes import Note, NoteClub, NoteSpecial
""" """
Defines transactions Defines transactions
@ -68,6 +68,7 @@ class TransactionTemplate(models.Model):
description = models.CharField( description = models.CharField(
verbose_name=_('description'), verbose_name=_('description'),
max_length=255, max_length=255,
blank=True,
) )
class Meta: class Meta:
@ -106,7 +107,10 @@ class Transaction(PolymorphicModel):
verbose_name=_('quantity'), verbose_name=_('quantity'),
default=1, default=1,
) )
amount = models.PositiveIntegerField(verbose_name=_('amount'), ) amount = models.PositiveIntegerField(
verbose_name=_('amount'),
)
reason = models.CharField( reason = models.CharField(
verbose_name=_('reason'), verbose_name=_('reason'),
max_length=255, max_length=255,
@ -132,6 +136,7 @@ class Transaction(PolymorphicModel):
if self.source.pk == self.destination.pk: if self.source.pk == self.destination.pk:
# When source == destination, no money is transfered # When source == destination, no money is transfered
super().save(*args, **kwargs)
return return
created = self.pk is None created = self.pk is None
@ -156,11 +161,14 @@ class Transaction(PolymorphicModel):
def total(self): def total(self):
return self.amount * self.quantity return self.amount * self.quantity
@property
def type(self):
return _('Transfer')
class TemplateTransaction(Transaction): class TemplateTransaction(Transaction):
""" """
Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`. Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
""" """
template = models.ForeignKey( template = models.ForeignKey(
@ -173,6 +181,36 @@ class TemplateTransaction(Transaction):
on_delete=models.PROTECT, on_delete=models.PROTECT,
) )
@property
def type(self):
return _('Template')
class SpecialTransaction(Transaction):
"""
Special type of :model:`note.Transaction` associated to transactions with special notes
"""
last_name = models.CharField(
max_length=255,
verbose_name=_("name"),
)
first_name = models.CharField(
max_length=255,
verbose_name=_("first_name"),
)
bank = models.CharField(
max_length=255,
verbose_name=_("bank"),
blank=True,
)
@property
def type(self):
return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit")
class MembershipTransaction(Transaction): class MembershipTransaction(Transaction):
""" """
@ -189,3 +227,7 @@ class MembershipTransaction(Transaction):
class Meta: class Meta:
verbose_name = _("membership transaction") verbose_name = _("membership transaction")
verbose_name_plural = _("membership transactions") verbose_name_plural = _("membership transactions")
@property
def type(self):
return _('membership transaction')

View File

@ -1,9 +1,12 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import html
import django_tables2 as tables import django_tables2 as tables
from django.db.models import F from django.db.models import F
from django_tables2.utils import A from django_tables2.utils import A
from django.utils.translation import gettext_lazy as _
from .models.notes import Alias from .models.notes import Alias
from .models.transactions import Transaction from .models.transactions import Transaction
@ -17,17 +20,25 @@ class HistoryTable(tables.Table):
'table table-condensed table-striped table-hover' 'table table-condensed table-striped table-hover'
} }
model = Transaction model = Transaction
exclude = ("polymorphic_ctype", ) exclude = ("id", "polymorphic_ctype", )
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
sequence = ('...', 'total', 'valid') sequence = ('...', 'type', 'total', 'valid', )
orderable = False
type = tables.Column()
total = tables.Column() # will use Transaction.total() !! total = tables.Column() # will use Transaction.total() !!
valid = tables.Column(attrs={"td": {"id": lambda record: "validate_" + str(record.id),
"class": lambda record: str(record.valid).lower() + ' validate',
"onclick": lambda record: 'de_validate(' + str(record.id) + ', '
+ str(record.valid).lower() + ')'}})
def order_total(self, queryset, is_descending): def order_total(self, queryset, is_descending):
# needed for rendering # needed for rendering
queryset = queryset.annotate(total=F('amount') * F('quantity')) \ queryset = queryset.annotate(total=F('amount') * F('quantity')) \
.order_by(('-' if is_descending else '') + 'total') .order_by(('-' if is_descending else '') + 'total')
return (queryset, True) return queryset, True
def render_amount(self, value): def render_amount(self, value):
return pretty_money(value) return pretty_money(value)
@ -35,6 +46,16 @@ class HistoryTable(tables.Table):
def render_total(self, value): def render_total(self, value):
return pretty_money(value) return pretty_money(value)
def render_type(self, value):
return _(value)
# Django-tables escape strings. That's a wrong thing.
def render_reason(self, value):
return html.unescape(value)
def render_valid(self, value):
return "" if value else ""
class AliasTable(tables.Table): class AliasTable(tables.Table):
class Meta: class Meta:

View File

@ -11,7 +11,7 @@ def pretty_money(value):
abs(value) // 100, abs(value) // 100,
) )
else: else:
return "{:s}{:d}{:02d}".format( return "{:s}{:d}.{:02d}".format(
"- " if value < 0 else "", "- " if value < 0 else "",
abs(value) // 100, abs(value) // 100,
abs(value) % 100, abs(value) % 100,

View File

@ -3,53 +3,43 @@
from dal import autocomplete from dal import autocomplete
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, ListView, UpdateView from django.views.generic import CreateView, ListView, UpdateView
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from .forms import TransactionForm, TransactionTemplateForm from .forms import TransactionTemplateForm
from .models import Transaction, TransactionTemplate, Alias from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction, NoteSpecial
from .models.transactions import SpecialTransaction
from .tables import HistoryTable from .tables import HistoryTable
class TransactionCreate(LoginRequiredMixin, CreateView): class TransactionCreate(LoginRequiredMixin, SingleTableView):
""" """
Show transfer page Show transfer page
TODO: If user have sufficient rights, they can transfer from an other note TODO: If user have sufficient rights, they can transfer from an other note
""" """
model = Transaction queryset = Transaction.objects.order_by("-id").all()[:50]
form_class = TransactionForm template_name = "note/transaction_form.html"
# Transaction history table
table_class = HistoryTable
table_pagination = {"per_page": 50}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" """
Add some context variables in template such as page title Add some context variables in template such as page title
""" """
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['title'] = _('Transfer money from your account ' context['title'] = _('Transfer money')
'to one or others') context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
context['no_cache'] = True context['special_types'] = NoteSpecial.objects.order_by("special_type").all()
return context return context
def get_form(self, form_class=None):
"""
If the user has no right to transfer funds, then it won't have the choice of the source of the transfer.
"""
form = super().get_form(form_class)
if False: # TODO: fix it with "if %user has no right to transfer funds"
del form.fields['source']
form.user = self.request.user
return form
def get_success_url(self):
return reverse('note:transfer')
class NoteAutocomplete(autocomplete.Select2QuerySetView): class NoteAutocomplete(autocomplete.Select2QuerySetView):
""" """
@ -127,21 +117,25 @@ class ConsoView(LoginRequiredMixin, SingleTableView):
""" """
Consume Consume
""" """
model = Transaction queryset = Transaction.objects.order_by("-id").all()[:50]
template_name = "note/conso_form.html" template_name = "note/conso_form.html"
# Transaction history table # Transaction history table
table_class = HistoryTable table_class = HistoryTable
table_pagination = {"per_page": 10} table_pagination = {"per_page": 50}
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" """
Add some context variables in template such as page title Add some context variables in template such as page title
""" """
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['transaction_templates'] = TransactionTemplate.objects.filter(display=True) \ from django.db.models import Count
.order_by('category') buttons = TransactionTemplate.objects.filter(display=True) \
.annotate(clicks=Count('templatetransaction')).order_by('category__name', 'name')
context['transaction_templates'] = buttons
context['most_used'] = buttons.order_by('-clicks', 'name')[:10]
context['title'] = _("Consumptions") context['title'] = _("Consumptions")
context['polymorphic_ctype'] = ContentType.objects.get_for_model(TemplateTransaction).pk
# select2 compatibility # select2 compatibility
context['no_cache'] = True context['no_cache'] = True

@ -1 +0,0 @@
Subproject commit 123466cfa914422422cd372197e64adf65ef05f7

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-11 11:44+0100\n" "POT-Creation-Date: 2020-03-16 11:53+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -23,9 +23,10 @@ msgid "activity"
msgstr "" msgstr ""
#: apps/activity/models.py:19 apps/activity/models.py:44 #: apps/activity/models.py:19 apps/activity/models.py:44
#: apps/member/models.py:60 apps/member/models.py:111 #: apps/member/models.py:61 apps/member/models.py:112
#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 #: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:15 #: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202
#: templates/member/profile_detail.html:15
msgid "name" msgid "name"
msgstr "" msgstr ""
@ -50,7 +51,7 @@ msgid "description"
msgstr "" msgstr ""
#: apps/activity/models.py:54 apps/note/models/notes.py:164 #: apps/activity/models.py:54 apps/note/models/notes.py:164
#: apps/note/models/transactions.py:62 #: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115
msgid "type" msgid "type"
msgstr "" msgstr ""
@ -86,7 +87,7 @@ msgstr ""
msgid "API" msgid "API"
msgstr "" msgstr ""
#: apps/logs/apps.py:10 #: apps/logs/apps.py:11
msgid "Logs" msgid "Logs"
msgstr "" msgstr ""
@ -166,73 +167,73 @@ msgstr ""
msgid "user profile" msgid "user profile"
msgstr "" msgstr ""
#: apps/member/models.py:65 #: apps/member/models.py:66
msgid "email" msgid "email"
msgstr "" msgstr ""
#: apps/member/models.py:70 #: apps/member/models.py:71
msgid "membership fee" msgid "membership fee"
msgstr "" msgstr ""
#: apps/member/models.py:74 #: apps/member/models.py:75
msgid "membership duration" msgid "membership duration"
msgstr "" msgstr ""
#: apps/member/models.py:75 #: apps/member/models.py:76
msgid "The longest time a membership can last (NULL = infinite)." msgid "The longest time a membership can last (NULL = infinite)."
msgstr "" msgstr ""
#: apps/member/models.py:80 #: apps/member/models.py:81
msgid "membership start" msgid "membership start"
msgstr "" msgstr ""
#: apps/member/models.py:81 #: apps/member/models.py:82
msgid "How long after January 1st the members can renew their membership." msgid "How long after January 1st the members can renew their membership."
msgstr "" msgstr ""
#: apps/member/models.py:86 #: apps/member/models.py:87
msgid "membership end" msgid "membership end"
msgstr "" msgstr ""
#: apps/member/models.py:87 #: apps/member/models.py:88
msgid "" msgid ""
"How long the membership can last after January 1st of the next year after " "How long the membership can last after January 1st of the next year after "
"members can renew their membership." "members can renew their membership."
msgstr "" msgstr ""
#: apps/member/models.py:93 apps/note/models/notes.py:139 #: apps/member/models.py:94 apps/note/models/notes.py:139
msgid "club" msgid "club"
msgstr "" msgstr ""
#: apps/member/models.py:94 #: apps/member/models.py:95
msgid "clubs" msgid "clubs"
msgstr "" msgstr ""
#: apps/member/models.py:117 #: apps/member/models.py:118
msgid "role" msgid "role"
msgstr "" msgstr ""
#: apps/member/models.py:118 #: apps/member/models.py:119
msgid "roles" msgid "roles"
msgstr "" msgstr ""
#: apps/member/models.py:142 #: apps/member/models.py:143
msgid "membership starts on" msgid "membership starts on"
msgstr "" msgstr ""
#: apps/member/models.py:145 #: apps/member/models.py:146
msgid "membership ends on" msgid "membership ends on"
msgstr "" msgstr ""
#: apps/member/models.py:149 #: apps/member/models.py:150
msgid "fee" msgid "fee"
msgstr "" msgstr ""
#: apps/member/models.py:153 #: apps/member/models.py:154
msgid "membership" msgid "membership"
msgstr "" msgstr ""
#: apps/member/models.py:154 #: apps/member/models.py:155
msgid "memberships" msgid "memberships"
msgstr "" msgstr ""
@ -253,12 +254,12 @@ msgstr ""
msgid "Alias successfully deleted" msgid "Alias successfully deleted"
msgstr "" msgstr ""
#: apps/note/admin.py:120 apps/note/models/transactions.py:93 #: apps/note/admin.py:120 apps/note/models/transactions.py:94
msgid "source" msgid "source"
msgstr "" msgstr ""
#: apps/note/admin.py:128 apps/note/admin.py:156 #: apps/note/admin.py:128 apps/note/admin.py:156
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:99 #: apps/note/models/transactions.py:53 apps/note/models/transactions.py:100
msgid "destination" msgid "destination"
msgstr "" msgstr ""
@ -278,10 +279,6 @@ msgstr ""
msgid "Maximal size: 2MB" msgid "Maximal size: 2MB"
msgstr "" msgstr ""
#: apps/note/forms.py:70
msgid "Source and destination must be different."
msgstr ""
#: apps/note/models/notes.py:27 #: apps/note/models/notes.py:27
msgid "account balance" msgid "account balance"
msgstr "" msgstr ""
@ -312,7 +309,7 @@ msgstr ""
msgid "display image" msgid "display image"
msgstr "" msgstr ""
#: apps/note/models/notes.py:53 apps/note/models/transactions.py:102 #: apps/note/models/notes.py:53 apps/note/models/transactions.py:103
msgid "created at" msgid "created at"
msgstr "" msgstr ""
@ -374,15 +371,15 @@ msgstr ""
msgid "aliases" msgid "aliases"
msgstr "" msgstr ""
#: apps/note/models/notes.py:229 #: apps/note/models/notes.py:233
msgid "Alias is too long." msgid "Alias is too long."
msgstr "" msgstr ""
#: apps/note/models/notes.py:234 #: apps/note/models/notes.py:238
msgid "An alias with a similar name already exists: {} " msgid "An alias with a similar name already exists: {} "
msgstr "" msgstr ""
#: apps/note/models/notes.py:243 #: apps/note/models/notes.py:247
msgid "You can't delete your main alias." msgid "You can't delete your main alias."
msgstr "" msgstr ""
@ -398,7 +395,7 @@ msgstr ""
msgid "A template with this name already exist" msgid "A template with this name already exist"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109 #: apps/note/models/transactions.py:56 apps/note/models/transactions.py:111
msgid "amount" msgid "amount"
msgstr "" msgstr ""
@ -406,51 +403,81 @@ msgstr ""
msgid "in centimes" msgid "in centimes"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:74 #: apps/note/models/transactions.py:75
msgid "transaction template" msgid "transaction template"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:75 #: apps/note/models/transactions.py:76
msgid "transaction templates" msgid "transaction templates"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:106 #: apps/note/models/transactions.py:107
msgid "quantity" msgid "quantity"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:111 #: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15
msgid "reason" msgid "Gift"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:115 #: apps/note/models/transactions.py:118 templates/base.html:90
msgid "valid" #: templates/note/transaction_form.html:19
#: templates/note/transaction_form.html:126
msgid "Transfer"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:120 #: apps/note/models/transactions.py:119
msgid "transaction" msgid "Template"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:121 #: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23
msgid "transactions" msgid "Credit"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:185 #: apps/note/models/transactions.py:121 templates/note/transaction_form.html:27
msgid "Debit"
msgstr ""
#: apps/note/models/transactions.py:122 apps/note/models/transactions.py:230
msgid "membership transaction" msgid "membership transaction"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:186 #: apps/note/models/transactions.py:129
msgid "reason"
msgstr ""
#: apps/note/models/transactions.py:133
msgid "valid"
msgstr ""
#: apps/note/models/transactions.py:138
msgid "transaction"
msgstr ""
#: apps/note/models/transactions.py:139
msgid "transactions"
msgstr ""
#: apps/note/models/transactions.py:207
msgid "first_name"
msgstr ""
#: apps/note/models/transactions.py:212
msgid "bank"
msgstr ""
#: apps/note/models/transactions.py:231
msgid "membership transactions" msgid "membership transactions"
msgstr "" msgstr ""
#: apps/note/views.py:29 #: apps/note/views.py:31
msgid "Transfer money from your account to one or others" msgid "Transfer money"
msgstr "" msgstr ""
#: apps/note/views.py:139 #: apps/note/views.py:132 templates/base.html:78
msgid "Consommations" msgid "Consumptions"
msgstr "" msgstr ""
#: note_kfet/settings/__init__.py:63 #: note_kfet/settings/__init__.py:61
msgid "" msgid ""
"The Central Authentication Service grants you access to most of our websites " "The Central Authentication Service grants you access to most of our websites "
"by authenticating only once, so you don't need to type your credentials " "by authenticating only once, so you don't need to type your credentials "
@ -473,24 +500,16 @@ msgstr ""
msgid "The ENS Paris-Saclay BDE note." msgid "The ENS Paris-Saclay BDE note."
msgstr "" msgstr ""
#: templates/base.html:70 #: templates/base.html:81
msgid "Consumptions"
msgstr ""
#: templates/base.html:73
msgid "Clubs" msgid "Clubs"
msgstr "" msgstr ""
#: templates/base.html:76 #: templates/base.html:84
msgid "Activities" msgid "Activities"
msgstr "" msgstr ""
#: templates/base.html:79 #: templates/base.html:87
msgid "Button" msgid "Buttons"
msgstr ""
#: templates/base.html:82 templates/note/transaction_form.html:35
msgid "Transfer"
msgstr "" msgstr ""
#: templates/cas_server/base.html:7 #: templates/cas_server/base.html:7
@ -549,6 +568,7 @@ msgid "Field filters"
msgstr "" msgstr ""
#: templates/django_filters/rest_framework/form.html:5 #: templates/django_filters/rest_framework/form.html:5
#: templates/member/club_form.html:10
msgid "Submit" msgid "Submit"
msgstr "" msgstr ""
@ -572,6 +592,14 @@ msgstr ""
msgid "Transaction history" msgid "Transaction history"
msgstr "" msgstr ""
#: templates/member/club_form.html:6
msgid "Clubs list"
msgstr ""
#: templates/member/club_list.html:8
msgid "New club"
msgstr ""
#: templates/member/manage_auth_tokens.html:16 #: templates/member/manage_auth_tokens.html:16
msgid "Token" msgid "Token"
msgstr "" msgstr ""
@ -620,8 +648,87 @@ msgstr ""
msgid "Save Changes" msgid "Save Changes"
msgstr "" msgstr ""
#: templates/member/signup.html:5 templates/member/signup.html:8
#: templates/member/signup.html:14 #: templates/member/signup.html:14
msgid "Sign Up" msgid "Sign up"
msgstr ""
#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38
msgid "Select emitters"
msgstr ""
#: templates/note/conso_form.html:45
msgid "Select consumptions"
msgstr ""
#: templates/note/conso_form.html:51
msgid "Consume!"
msgstr ""
#: templates/note/conso_form.html:64
msgid "Most used buttons"
msgstr ""
#: templates/note/conso_form.html:121
msgid "Edit"
msgstr ""
#: templates/note/conso_form.html:126
msgid "Single consumptions"
msgstr ""
#: templates/note/conso_form.html:130
msgid "Double consumptions"
msgstr ""
#: templates/note/conso_form.html:141
msgid "Recent transactions history"
msgstr ""
#: templates/note/transaction_form.html:55
msgid "External payment"
msgstr ""
#: templates/note/transaction_form.html:63
msgid "Transfer type"
msgstr ""
#: templates/note/transaction_form.html:73
msgid "Name"
msgstr ""
#: templates/note/transaction_form.html:79
msgid "First name"
msgstr ""
#: templates/note/transaction_form.html:85
msgid "Bank"
msgstr ""
#: templates/note/transaction_form.html:97
#: templates/note/transaction_form.html:179
#: templates/note/transaction_form.html:186
msgid "Select receivers"
msgstr ""
#: templates/note/transaction_form.html:114
msgid "Amount"
msgstr ""
#: templates/note/transaction_form.html:119
msgid "Reason"
msgstr ""
#: templates/note/transaction_form.html:193
msgid "Credit note"
msgstr ""
#: templates/note/transaction_form.html:200
msgid "Debit note"
msgstr ""
#: templates/note/transactiontemplate_form.html:6
msgid "Buttons list"
msgstr "" msgstr ""
#: templates/registration/logged_out.html:8 #: templates/registration/logged_out.html:8

View File

@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-11 11:44+0100\n" "POT-Creation-Date: 2020-03-16 11:53+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,9 +18,10 @@ msgid "activity"
msgstr "activité" msgstr "activité"
#: apps/activity/models.py:19 apps/activity/models.py:44 #: apps/activity/models.py:19 apps/activity/models.py:44
#: apps/member/models.py:60 apps/member/models.py:111 #: apps/member/models.py:61 apps/member/models.py:112
#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 #: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:15 #: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202
#: templates/member/profile_detail.html:15
msgid "name" msgid "name"
msgstr "nom" msgstr "nom"
@ -45,7 +46,7 @@ msgid "description"
msgstr "description" msgstr "description"
#: apps/activity/models.py:54 apps/note/models/notes.py:164 #: apps/activity/models.py:54 apps/note/models/notes.py:164
#: apps/note/models/transactions.py:62 #: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115
msgid "type" msgid "type"
msgstr "type" msgstr "type"
@ -81,7 +82,7 @@ msgstr "invités"
msgid "API" msgid "API"
msgstr "" msgstr ""
#: apps/logs/apps.py:10 #: apps/logs/apps.py:11
msgid "Logs" msgid "Logs"
msgstr "" msgstr ""
@ -161,37 +162,37 @@ msgstr "payé"
msgid "user profile" msgid "user profile"
msgstr "profil utilisateur" msgstr "profil utilisateur"
#: apps/member/models.py:65 #: apps/member/models.py:66
msgid "email" msgid "email"
msgstr "courriel" msgstr "courriel"
#: apps/member/models.py:70 #: apps/member/models.py:71
msgid "membership fee" msgid "membership fee"
msgstr "cotisation pour adhérer" msgstr "cotisation pour adhérer"
#: apps/member/models.py:74 #: apps/member/models.py:75
msgid "membership duration" msgid "membership duration"
msgstr "durée de l'adhésion" msgstr "durée de l'adhésion"
#: apps/member/models.py:75 #: apps/member/models.py:76
msgid "The longest time a membership can last (NULL = infinite)." msgid "The longest time a membership can last (NULL = infinite)."
msgstr "La durée maximale d'une adhésion (NULL = infinie)." msgstr "La durée maximale d'une adhésion (NULL = infinie)."
#: apps/member/models.py:80 #: apps/member/models.py:81
msgid "membership start" msgid "membership start"
msgstr "début de l'adhésion" msgstr "début de l'adhésion"
#: apps/member/models.py:81 #: apps/member/models.py:82
msgid "How long after January 1st the members can renew their membership." msgid "How long after January 1st the members can renew their membership."
msgstr "" msgstr ""
"Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur " "Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur "
"adhésion." "adhésion."
#: apps/member/models.py:86 #: apps/member/models.py:87
msgid "membership end" msgid "membership end"
msgstr "fin de l'adhésion" msgstr "fin de l'adhésion"
#: apps/member/models.py:87 #: apps/member/models.py:88
msgid "" msgid ""
"How long the membership can last after January 1st of the next year after " "How long the membership can last after January 1st of the next year after "
"members can renew their membership." "members can renew their membership."
@ -199,45 +200,39 @@ msgstr ""
"Combien de temps l'adhésion peut durer après le 1er Janvier de l'année " "Combien de temps l'adhésion peut durer après le 1er Janvier de l'année "
"suivante avant que les adhérents peuvent renouveler leur adhésion." "suivante avant que les adhérents peuvent renouveler leur adhésion."
#: apps/member/models.py:93 apps/note/models/notes.py:139 #: apps/member/models.py:94 apps/note/models/notes.py:139
msgid "club" msgid "club"
msgstr "club" msgstr "club"
msgid "New club" #: apps/member/models.py:95
msgstr "Nouveau club"
msgid "Clubs list"
msgstr "Liste des clubs"
#: apps/member/models.py:94
msgid "clubs" msgid "clubs"
msgstr "clubs" msgstr "clubs"
#: apps/member/models.py:117 #: apps/member/models.py:118
msgid "role" msgid "role"
msgstr "rôle" msgstr "rôle"
#: apps/member/models.py:118 #: apps/member/models.py:119
msgid "roles" msgid "roles"
msgstr "rôles" msgstr "rôles"
#: apps/member/models.py:142 #: apps/member/models.py:143
msgid "membership starts on" msgid "membership starts on"
msgstr "l'adhésion commence le" msgstr "l'adhésion commence le"
#: apps/member/models.py:145 #: apps/member/models.py:146
msgid "membership ends on" msgid "membership ends on"
msgstr "l'adhésion finie le" msgstr "l'adhésion finie le"
#: apps/member/models.py:149 #: apps/member/models.py:150
msgid "fee" msgid "fee"
msgstr "cotisation" msgstr "cotisation"
#: apps/member/models.py:153 #: apps/member/models.py:154
msgid "membership" msgid "membership"
msgstr "adhésion" msgstr "adhésion"
#: apps/member/models.py:154 #: apps/member/models.py:155
msgid "memberships" msgid "memberships"
msgstr "adhésions" msgstr "adhésions"
@ -258,12 +253,12 @@ msgstr "Compte n°%(id)s : %(username)s"
msgid "Alias successfully deleted" msgid "Alias successfully deleted"
msgstr "L'alias a bien été supprimé" msgstr "L'alias a bien été supprimé"
#: apps/note/admin.py:120 apps/note/models/transactions.py:93 #: apps/note/admin.py:120 apps/note/models/transactions.py:94
msgid "source" msgid "source"
msgstr "source" msgstr "source"
#: apps/note/admin.py:128 apps/note/admin.py:156 #: apps/note/admin.py:128 apps/note/admin.py:156
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:99 #: apps/note/models/transactions.py:53 apps/note/models/transactions.py:100
msgid "destination" msgid "destination"
msgstr "destination" msgstr "destination"
@ -283,10 +278,6 @@ msgstr "Choisissez une image"
msgid "Maximal size: 2MB" msgid "Maximal size: 2MB"
msgstr "Taille maximale : 2 Mo" msgstr "Taille maximale : 2 Mo"
#: apps/note/forms.py:70
msgid "Source and destination must be different."
msgstr "La source et la destination doivent être différentes."
#: apps/note/models/notes.py:27 #: apps/note/models/notes.py:27
msgid "account balance" msgid "account balance"
msgstr "solde du compte" msgstr "solde du compte"
@ -318,7 +309,7 @@ msgstr ""
msgid "display image" msgid "display image"
msgstr "image affichée" msgstr "image affichée"
#: apps/note/models/notes.py:53 apps/note/models/transactions.py:102 #: apps/note/models/notes.py:53 apps/note/models/transactions.py:103
msgid "created at" msgid "created at"
msgstr "créée le" msgstr "créée le"
@ -380,15 +371,15 @@ msgstr "alias"
msgid "aliases" msgid "aliases"
msgstr "alias" msgstr "alias"
#: apps/note/models/notes.py:229 #: apps/note/models/notes.py:233
msgid "Alias is too long." msgid "Alias is too long."
msgstr "L'alias est trop long." msgstr "L'alias est trop long."
#: apps/note/models/notes.py:234 #: apps/note/models/notes.py:238
msgid "An alias with a similar name already exists: {} " msgid "An alias with a similar name already exists: {} "
msgstr "Un alias avec un nom similaire existe déjà : {}" msgstr "Un alias avec un nom similaire existe déjà : {}"
#: apps/note/models/notes.py:243 #: apps/note/models/notes.py:247
msgid "You can't delete your main alias." msgid "You can't delete your main alias."
msgstr "Vous ne pouvez pas supprimer votre alias principal." msgstr "Vous ne pouvez pas supprimer votre alias principal."
@ -404,7 +395,7 @@ msgstr "catégories de transaction"
msgid "A template with this name already exist" msgid "A template with this name already exist"
msgstr "Un modèle de transaction avec un nom similaire existe déjà." msgstr "Un modèle de transaction avec un nom similaire existe déjà."
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109 #: apps/note/models/transactions.py:56 apps/note/models/transactions.py:111
msgid "amount" msgid "amount"
msgstr "montant" msgstr "montant"
@ -412,47 +403,81 @@ msgstr "montant"
msgid "in centimes" msgid "in centimes"
msgstr "en centimes" msgstr "en centimes"
#: apps/note/models/transactions.py:74 #: apps/note/models/transactions.py:75
msgid "transaction template" msgid "transaction template"
msgstr "modèle de transaction" msgstr "modèle de transaction"
#: apps/note/models/transactions.py:75 #: apps/note/models/transactions.py:76
msgid "transaction templates" msgid "transaction templates"
msgstr "modèles de transaction" msgstr "modèles de transaction"
#: apps/note/models/transactions.py:106 #: apps/note/models/transactions.py:107
msgid "quantity" msgid "quantity"
msgstr "quantité" msgstr "quantité"
#: apps/note/models/transactions.py:111 #: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15
msgid "reason" msgid "Gift"
msgstr "raison" msgstr "Don"
#: apps/note/models/transactions.py:115 #: apps/note/models/transactions.py:118 templates/base.html:90
msgid "valid" #: templates/note/transaction_form.html:19
msgstr "valide" #: templates/note/transaction_form.html:126
msgid "Transfer"
msgstr "Virement"
#: apps/note/models/transactions.py:120 #: apps/note/models/transactions.py:119
msgid "transaction" msgid "Template"
msgstr "transaction" msgstr "Bouton"
#: apps/note/models/transactions.py:121 #: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23
msgid "transactions" msgid "Credit"
msgstr "transactions" msgstr "Crédit"
#: apps/note/models/transactions.py:185 #: apps/note/models/transactions.py:121 templates/note/transaction_form.html:27
msgid "Debit"
msgstr "Retrait"
#: apps/note/models/transactions.py:122 apps/note/models/transactions.py:230
msgid "membership transaction" msgid "membership transaction"
msgstr "transaction d'adhésion" msgstr "transaction d'adhésion"
#: apps/note/models/transactions.py:186 #: apps/note/models/transactions.py:129
msgid "reason"
msgstr "raison"
#: apps/note/models/transactions.py:133
msgid "valid"
msgstr "valide"
#: apps/note/models/transactions.py:138
msgid "transaction"
msgstr "transaction"
#: apps/note/models/transactions.py:139
msgid "transactions"
msgstr "transactions"
#: apps/note/models/transactions.py:207
msgid "first_name"
msgstr "Prénom"
#: apps/note/models/transactions.py:212
msgid "bank"
msgstr "Banque"
#: apps/note/models/transactions.py:231
msgid "membership transactions" msgid "membership transactions"
msgstr "transactions d'adhésion" msgstr "transactions d'adhésion"
#: apps/note/views.py:29 #: apps/note/views.py:31
msgid "Transfer money from your account to one or others" msgid "Transfer money"
msgstr "Transfert d'argent de ton compte vers un ou plusieurs autres" msgstr "Transferts d'argent"
#: note_kfet/settings/__init__.py:63 #: apps/note/views.py:132 templates/base.html:78
msgid "Consumptions"
msgstr "Consommations"
#: note_kfet/settings/__init__.py:61
msgid "" msgid ""
"The Central Authentication Service grants you access to most of our websites " "The Central Authentication Service grants you access to most of our websites "
"by authenticating only once, so you don't need to type your credentials " "by authenticating only once, so you don't need to type your credentials "
@ -475,29 +500,18 @@ msgstr ""
msgid "The ENS Paris-Saclay BDE note." msgid "The ENS Paris-Saclay BDE note."
msgstr "La note du BDE de l'ENS Paris-Saclay." msgstr "La note du BDE de l'ENS Paris-Saclay."
#: templates/base.html:70 #: templates/base.html:81
msgid "Consumptions"
msgstr "Consommations"
#: templates/base.html:73
msgid "Clubs" msgid "Clubs"
msgstr "Clubs" msgstr "Clubs"
#: templates/base.html:76 #: templates/base.html:84
msgid "Activities" msgid "Activities"
msgstr "Activités" msgstr "Activités"
#: templates/base.html:79 #: templates/base.html:87
msgid "Buttons" msgid "Buttons"
msgstr "Boutons" msgstr "Boutons"
msgid "Buttons list"
msgstr "Liste des boutons"
#: templates/base.html:82 templates/note/transaction_form.html:35
msgid "Transfer"
msgstr "Virement"
#: templates/cas_server/base.html:7 #: templates/cas_server/base.html:7
msgid "Central Authentication Service" msgid "Central Authentication Service"
msgstr "" msgstr ""
@ -556,8 +570,9 @@ msgid "Field filters"
msgstr "" msgstr ""
#: templates/django_filters/rest_framework/form.html:5 #: templates/django_filters/rest_framework/form.html:5
#: templates/member/club_form.html:10
msgid "Submit" msgid "Submit"
msgstr "" msgstr "Envoyer"
#: templates/member/club_detail.html:10 #: templates/member/club_detail.html:10
msgid "Membership starts on" msgid "Membership starts on"
@ -579,6 +594,14 @@ msgstr "solde du compte"
msgid "Transaction history" msgid "Transaction history"
msgstr "Historique des transactions" msgstr "Historique des transactions"
#: templates/member/club_form.html:6
msgid "Clubs list"
msgstr "Liste des clubs"
#: templates/member/club_list.html:8
msgid "New club"
msgstr "Nouveau club"
#: templates/member/manage_auth_tokens.html:16 #: templates/member/manage_auth_tokens.html:16
msgid "Token" msgid "Token"
msgstr "Jeton" msgstr "Jeton"
@ -627,11 +650,89 @@ msgstr "Voir mes adhésions"
msgid "Save Changes" msgid "Save Changes"
msgstr "Sauvegarder les changements" msgstr "Sauvegarder les changements"
#: templates/member/signup.html:8 #: templates/member/signup.html:5 templates/member/signup.html:8
#: templates/member/signup.html:14 #: templates/member/signup.html:14
msgid "Sign up" msgid "Sign up"
msgstr "Inscription" msgstr "Inscription"
#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38
msgid "Select emitters"
msgstr "Sélection des émetteurs"
#: templates/note/conso_form.html:45
msgid "Select consumptions"
msgstr "Consommations"
#: templates/note/conso_form.html:51
msgid "Consume!"
msgstr "Consommer !"
#: templates/note/conso_form.html:64
msgid "Most used buttons"
msgstr "Boutons les plus utilisés"
#: templates/note/conso_form.html:121
msgid "Edit"
msgstr "Éditer"
#: templates/note/conso_form.html:126
msgid "Single consumptions"
msgstr "Consos simples"
#: templates/note/conso_form.html:130
msgid "Double consumptions"
msgstr "Consos doubles"
#: templates/note/conso_form.html:141
msgid "Recent transactions history"
msgstr "Historique des transactions récentes"
#: templates/note/transaction_form.html:55
msgid "External payment"
msgstr "Paiement extérieur"
#: templates/note/transaction_form.html:63
msgid "Transfer type"
msgstr "Type de transfert"
#: templates/note/transaction_form.html:73
msgid "Name"
msgstr "Nom"
#: templates/note/transaction_form.html:79
msgid "First name"
msgstr "Prénom"
#: templates/note/transaction_form.html:85
msgid "Bank"
msgstr "Banque"
#: templates/note/transaction_form.html:97
#: templates/note/transaction_form.html:179
#: templates/note/transaction_form.html:186
msgid "Select receivers"
msgstr "Sélection des destinataires"
#: templates/note/transaction_form.html:114
msgid "Amount"
msgstr "Montant"
#: templates/note/transaction_form.html:119
msgid "Reason"
msgstr "Raison"
#: templates/note/transaction_form.html:193
msgid "Credit note"
msgstr "Note à créditer"
#: templates/note/transaction_form.html:200
msgid "Debit note"
msgstr "Note à débiter"
#: templates/note/transactiontemplate_form.html:6
msgid "Buttons list"
msgstr "Liste des boutons"
#: templates/registration/logged_out.html:8 #: templates/registration/logged_out.html:8
msgid "Thanks for spending some quality time with the Web site today." msgid "Thanks for spending some quality time with the Web site today."
msgstr "" msgstr ""

281
static/js/base.js Normal file
View File

@ -0,0 +1,281 @@
// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later
/**
* Convert balance in cents to a human readable amount
* @param value the balance, in cents
* @returns {string}
*/
function pretty_money(value) {
if (value % 100 === 0)
return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + " €";
else
return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + "."
+ (Math.abs(value) % 100 < 10 ? "0" : "") + (Math.abs(value) % 100) + " €";
}
/**
* Add a message on the top of the page.
* @param msg The message to display
* @param alert_type The type of the alert. Choices: info, success, warning, danger
*/
function addMsg(msg, alert_type) {
let msgDiv = $("#messages");
let html = msgDiv.html();
html += "<div class=\"alert alert-" + alert_type + " alert-dismissible\">" +
"<button class=\"close\" data-dismiss=\"alert\" href=\"#\"><span aria-hidden=\"true\">×</span></button>"
+ msg + "</div>\n";
msgDiv.html(html);
}
/**
* Reload the balance of the user on the right top corner
*/
function refreshBalance() {
$("#user_balance").load("/ #user_balance");
}
/**
* Query the 20 first matched notes with a given pattern
* @param pattern The pattern that is queried
* @param fun For each found note with the matched alias `alias`, fun(note, alias) is called.
*/
function getMatchedNotes(pattern, fun) {
$.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", fun);
}
/**
* Generate a <li> entry with a given id and text
*/
function li(id, text) {
return "<li class=\"list-group-item py-1 d-flex justify-content-between align-items-center\"" +
" id=\"" + id + "\">" + text + "</li>\n";
}
/**
* Render note name and picture
* @param note The note to render
* @param alias The alias to be displayed
* @param user_note_field
* @param profile_pic_field
*/
function displayNote(note, alias, user_note_field=null, profile_pic_field=null) {
let img = note == null ? null : note.display_image;
if (img == null)
img = '/media/pic/default.png';
if (note !== null && alias !== note.name)
alias += " (aka. " + note.name + ")";
if (note !== null && user_note_field !== null)
$("#" + user_note_field).text(alias + " : " + pretty_money(note.balance));
if (profile_pic_field != null)
$("#" + profile_pic_field).attr('src', img);
}
/**
* Remove a note from the emitters.
* @param d The note to remove
* @param note_prefix The prefix of the identifiers of the <li> blocks of the emitters
* @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity]
* @param note_list_id The div block identifier where the notes of the buyers are displayed
* @param user_note_field The identifier of the field that display the note of the hovered note (useful in
* consumptions, put null if not used)
* @param profile_pic_field The identifier of the field that display the profile picture of the hovered note
* (useful in consumptions, put null if not used)
* @returns an anonymous function to be compatible with jQuery events
*/
function removeNote(d, note_prefix="note", notes_display, note_list_id, user_note_field=null, profile_pic_field=null) {
return (function() {
let new_notes_display = [];
let html = "";
notes_display.forEach(function (disp) {
if (disp.quantity > 1 || disp.id !== d.id) {
disp.quantity -= disp.id === d.id ? 1 : 0;
new_notes_display.push(disp);
html += li(note_prefix + "_" + disp.id, disp.name
+ "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>");
}
});
notes_display.length = 0;
new_notes_display.forEach(function(disp) {
notes_display.push(disp);
});
$("#" + note_list_id).html(html);
notes_display.forEach(function (disp) {
let obj = $("#" + note_prefix + "_" + disp.id);
obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field));
obj.hover(function() {
if (disp.note)
displayNote(disp.note, disp.name, user_note_field, profile_pic_field);
});
});
});
}
/**
* Generate an auto-complete field to query a note with its alias
* @param field_id The identifier of the text field where the alias is typed
* @param alias_matched_id The div block identifier where the matched aliases are displayed
* @param note_list_id The div block identifier where the notes of the buyers are displayed
* @param notes An array containing the note objects of the buyers
* @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity]
* @param alias_prefix The prefix of the <li> blocks for the matched aliases
* @param note_prefix The prefix of the <li> blocks for the notes of the buyers
* @param user_note_field The identifier of the field that display the note of the hovered note (useful in
* consumptions, put null if not used)
* @param profile_pic_field The identifier of the field that display the profile picture of the hovered note
* (useful in consumptions, put null if not used)
* @param alias_click Function that is called when an alias is clicked. If this method exists and doesn't return true,
* the associated note is not displayed.
* Useful for a consumption if the item is selected before.
*/
function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes_display, alias_prefix="alias",
note_prefix="note", user_note_field=null, profile_pic_field=null, alias_click=null) {
let field = $("#" + field_id);
// When the user clicks on the search field, it is immediately cleared
field.click(function() {
field.val("");
});
let old_pattern = null;
// When the user type "Enter", the first alias is clicked
field.keypress(function(event) {
if (event.originalEvent.charCode === 13)
$("#" + alias_matched_id + " li").first().trigger("click");
});
// When the user type something, the matched aliases are refreshed
field.keyup(function(e) {
if (e.originalEvent.charCode === 13)
return;
let pattern = field.val();
// If the pattern is not modified, we don't query the API
if (pattern === old_pattern || pattern === "")
return;
old_pattern = pattern;
// Clear old matched notes
notes.length = 0;
let aliases_matched_obj = $("#" + alias_matched_id);
let aliases_matched_html = "";
// Get matched notes with the given pattern
getMatchedNotes(pattern, function(aliases) {
// The response arrived too late, we stop the request
if (pattern !== $("#" + field_id).val())
return;
aliases.results.forEach(function (alias) {
let note = alias.note;
aliases_matched_html += li(alias_prefix + "_" + alias.id, alias.name);
note.alias = alias;
notes.push(note);
});
// Display the list of matched aliases
aliases_matched_obj.html(aliases_matched_html);
notes.forEach(function (note) {
let alias = note.alias;
let alias_obj = $("#" + alias_prefix + "_" + alias.id);
// When an alias is hovered, the profile picture and the balance are displayed at the right place
alias_obj.hover(function () {
displayNote(note, alias.name, user_note_field, profile_pic_field);
});
// When the user click on an alias, the associated note is added to the emitters
alias_obj.click(function () {
field.val("");
// If the note is already an emitter, we increase the quantity
var disp = null;
notes_display.forEach(function (d) {
// We compare the note ids
if (d.id === note.id) {
d.quantity += 1;
disp = d;
}
});
// In the other case, we add a new emitter
if (disp == null) {
disp = {
name: alias.name,
id: note.id,
note: note,
quantity: 1
};
notes_display.push(disp);
}
// If the function alias_click exists, it is called. If it doesn't return true, then the notes are
// note displayed. Useful for a consumption when a button is already clicked
if (alias_click && !alias_click())
return;
let note_list = $("#" + note_list_id);
let html = "";
notes_display.forEach(function (disp) {
html += li(note_prefix + "_" + disp.id, disp.name
+ "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>");
});
// Emitters are displayed
note_list.html(html);
notes_display.forEach(function (disp) {
let line_obj = $("#" + note_prefix + "_" + disp.id);
// Hover an emitter display also the profile picture
line_obj.hover(function () {
displayNote(disp.note, disp.name, user_note_field, profile_pic_field);
});
// When an emitter is clicked, it is removed
line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field,
profile_pic_field));
});
});
});
});
});
}
// When a validate button is clicked, we switch the validation status
function de_validate(id, validated) {
$("#validate_" + id).html("<strong style=\"font-size: 16pt;\">⟳ ...</strong>");
// Perform a PATCH request to the API in order to update the transaction
// If the user has insuffisent rights, an error message will appear
$.ajax({
"url": "/api/note/transaction/transaction/" + id + "/",
type: "PATCH",
dataType: "json",
headers: {
"X-CSRFTOKEN": CSRF_TOKEN
},
data: {
"resourcetype": "TemplateTransaction",
valid: !validated
},
success: function () {
// Refresh jQuery objects
$(".validate").click(de_validate);
refreshBalance();
// error if this method doesn't exist. Please define it.
refreshHistory();
},
error: function(err) {
addMsg("Une erreur est survenue lors de la validation/dévalidation " +
"de cette transaction : " + err.responseText, "danger");
refreshBalance();
// error if this method doesn't exist. Please define it.
refreshHistory();
}
});
}

205
static/js/consos.js Normal file
View File

@ -0,0 +1,205 @@
// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later
/**
* Refresh the history table on the consumptions page.
*/
function refreshHistory() {
$("#history").load("/note/consos/ #history");
$("#most_used").load("/note/consos/ #most_used");
}
$(document).ready(function() {
// If hash of a category in the URL, then select this category
// else select the first one
if (location.hash) {
$("a[href='" + location.hash + "']").tab("show");
} else {
$("a[data-toggle='tab']").first().tab("show");
}
// When selecting a category, change URL
$(document.body).on("click", "a[data-toggle='tab']", function() {
location.hash = this.getAttribute("href");
});
// Switching in double consumptions mode should update the layout
let double_conso_obj = $("#double_conso");
double_conso_obj.click(function() {
$("#consos_list_div").show();
$("#infos_div").attr('class', 'col-sm-5 col-xl-6');
$("#note_infos_div").attr('class', 'col-xl-3');
$("#user_select_div").attr('class', 'col-xl-4');
$("#buttons_div").attr('class', 'col-sm-7 col-xl-6');
let note_list_obj = $("#note_list");
if (buttons.length > 0 && note_list_obj.text().length > 0) {
$("#consos_list").html(note_list_obj.html());
note_list_obj.html("");
buttons.forEach(function(button) {
$("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons,
"consos_list"));
});
}
});
let single_conso_obj = $("#single_conso");
single_conso_obj.click(function() {
$("#consos_list_div").hide();
$("#infos_div").attr('class', 'col-sm-5 col-md-4');
$("#note_infos_div").attr('class', 'col-xl-5');
$("#user_select_div").attr('class', 'col-xl-7');
$("#buttons_div").attr('class', 'col-sm-7 col-md-8');
let consos_list_obj = $("#consos_list");
if (buttons.length > 0) {
if (notes_display.length === 0 && consos_list_obj.text().length > 0) {
$("#note_list").html(consos_list_obj.html());
consos_list_obj.html("");
buttons.forEach(function(button) {
$("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons,
"note_list"));
});
}
else {
buttons.length = 0;
consos_list_obj.html("");
}
}
});
// Ensure we begin in single consumption. Removing these lines may cause problems when reloading.
single_conso_obj.prop('checked', 'true');
double_conso_obj.removeAttr('checked');
$("label[for='double_conso']").attr('class', 'btn btn-sm btn-outline-primary');
$("#consos_list_div").hide();
$("#consume_all").click(consumeAll);
});
notes = [];
notes_display = [];
buttons = [];
// When the user searches an alias, we update the auto-completion
autoCompleteNote("note", "alias_matched", "note_list", notes, notes_display,
"alias", "note", "user_note", "profile_pic", function() {
if (buttons.length > 0 && $("#single_conso").is(":checked")) {
consumeAll();
return false;
}
return true;
});
/**
* Add a transaction from a button.
* @param dest Where the money goes
* @param amount The price of the item
* @param type The type of the transaction (content type id for TemplateTransaction)
* @param category_id The category identifier
* @param category_name The category name
* @param template_id The identifier of the button
* @param template_name The name of the button
*/
function addConso(dest, amount, type, category_id, category_name, template_id, template_name) {
var button = null;
buttons.forEach(function(b) {
if (b.id === template_id) {
b.quantity += 1;
button = b;
}
});
if (button == null) {
button = {
id: template_id,
name: template_name,
dest: dest,
quantity: 1,
amount: amount,
type: type,
category_id: category_id,
category_name: category_name
};
buttons.push(button);
}
let dc_obj = $("#double_conso");
if (dc_obj.is(":checked") || notes_display.length === 0) {
let list = dc_obj.is(":checked") ? "consos_list" : "note_list";
let html = "";
buttons.forEach(function(button) {
html += li("conso_button_" + button.id, button.name
+ "<span class=\"badge badge-dark badge-pill\">" + button.quantity + "</span>");
});
$("#" + list).html(html);
buttons.forEach(function(button) {
$("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons, list));
});
}
else
consumeAll();
}
/**
* Reset the page as its initial state.
*/
function reset() {
notes_display.length = 0;
notes.length = 0;
buttons.length = 0;
$("#note_list").html("");
$("#alias_matched").html("");
$("#consos_list").html("");
displayNote(null, "");
refreshHistory();
refreshBalance();
}
/**
* Apply all transactions: all notes in `notes` buy each item in `buttons`
*/
function consumeAll() {
notes_display.forEach(function(note_display) {
buttons.forEach(function(button) {
consume(note_display.id, button.dest, button.quantity * note_display.quantity, button.amount,
button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id);
});
});
}
/**
* Create a new transaction from a button through the API.
* @param source The note that paid the item (type: int)
* @param dest The note that sold the item (type: int)
* @param quantity The quantity sold (type: int)
* @param amount The price of one item, in cents (type: int)
* @param reason The transaction details (type: str)
* @param type The type of the transaction (content type id for TemplateTransaction)
* @param category The category id of the button (type: int)
* @param template The button id (type: int)
*/
function consume(source, dest, quantity, amount, reason, type, category, template) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": quantity,
"amount": amount,
"reason": reason,
"valid": true,
"polymorphic_ctype": type,
"resourcetype": "TemplateTransaction",
"source": source,
"destination": dest,
"category": category,
"template": template
}, reset).fail(function (e) {
reset();
addMsg("Une erreur est survenue lors de la transaction : " + e.responseText, "danger");
});
}

157
static/js/transfer.js Normal file
View File

@ -0,0 +1,157 @@
sources = [];
sources_notes_display = [];
dests = [];
dests_notes_display = [];
function refreshHistory() {
$("#history").load("/note/transfer/ #history");
}
function reset() {
sources_notes_display.length = 0;
sources.length = 0;
dests_notes_display.length = 0;
dests.length = 0;
$("#source_note_list").html("");
$("#dest_note_list").html("");
$("#source_alias_matched").html("");
$("#dest_alias_matched").html("");
$("#amount").val("");
$("#reason").val("");
$("#last_name").val("");
$("#first_name").val("");
$("#bank").val("");
refreshBalance();
refreshHistory();
}
$(document).ready(function() {
autoCompleteNote("source_note", "source_alias_matched", "source_note_list", sources, sources_notes_display,
"source_alias", "source_note", "user_note", "profile_pic");
autoCompleteNote("dest_note", "dest_alias_matched", "dest_note_list", dests, dests_notes_display,
"dest_alias", "dest_note", "user_note", "profile_pic", function() {
let last = dests_notes_display[dests_notes_display.length - 1];
dests_notes_display.length = 0;
dests_notes_display.push(last);
last.quantity = 1;
$.getJSON("/api/user/" + last.note.user + "/", function(user) {
$("#last_name").val(user.last_name);
$("#first_name").val(user.first_name);
});
return true;
});
// Ensure we begin in gift mode. Removing these lines may cause problems when reloading.
$("#type_gift").prop('checked', 'true');
$("#type_transfer").removeAttr('checked');
$("#type_credit").removeAttr('checked');
$("#type_debit").removeAttr('checked');
$("label[for='type_transfer']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_credit']").attr('class', 'btn btn-sm btn-outline-primary');
$("label[for='type_debit']").attr('class', 'btn btn-sm btn-outline-primary');
});
$("#transfer").click(function() {
if ($("#type_gift").is(':checked')) {
dests_notes_display.forEach(function (dest) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": dest.quantity,
"amount": 100 * $("#amount").val(),
"reason": $("#reason").val(),
"valid": true,
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction",
"source": user_id,
"destination": dest.id
}, function () {
addMsg("Le transfert de "
+ pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note "
+ " vers la note " + dest.name + " a été fait avec succès !", "success");
reset();
}).fail(function (err) {
addMsg("Le transfert de "
+ pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note "
+ " vers la note " + dest.name + " a échoué : " + err.responseText, "danger");
reset();
});
});
}
else if ($("#type_transfer").is(':checked')) {
sources_notes_display.forEach(function (source) {
dests_notes_display.forEach(function (dest) {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": source.quantity * dest.quantity,
"amount": 100 * $("#amount").val(),
"reason": $("#reason").val(),
"valid": true,
"polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "Transaction",
"source": source.id,
"destination": dest.id
}, function () {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name
+ " vers la note " + dest.name + " a été fait avec succès !", "success");
reset();
}).fail(function (err) {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name
+ " vers la note " + dest.name + " a échoué : " + err.responseText, "danger");
reset();
});
});
});
} else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) {
let special_note = $("#credit_type").val();
let user_note = dests_notes_display[0].id;
let given_reason = $("#reason").val();
let source, dest, reason;
if ($("#type_credit").is(':checked')) {
source = special_note;
dest = user_note;
reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0)
reason += " (" + given_reason + ")";
}
else {
source = user_note;
dest = special_note;
reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0)
reason += " (" + given_reason + ")";
}
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": 1,
"amount": 100 * $("#amount").val(),
"reason": reason,
"valid": true,
"polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "SpecialTransaction",
"source": source,
"destination": dest,
"last_name": $("#last_name").val(),
"first_name": $("#first_name").val(),
"bank": $("#bank").val()
}, function () {
addMsg("Le crédit/retrait a bien été effectué !", "success");
reset();
}).fail(function (err) {
addMsg("Le crédit/transfert a échoué : " + err.responseText, "danger");
reset();
});
}
});

View File

@ -46,12 +46,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
<script src="/static/js/base.js"></script>
{# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #} {# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #}
{% if form.media %} {% if form.media %}
{{ form.media }} {{ form.media }}
{% endif %} {% endif %}
<style>
.validate:hover {
cursor: pointer;
text-decoration: underline;
}
</style>
{% block extracss %}{% endblock %} {% block extracss %}{% endblock %}
</head> </head>
<body class="d-flex w-100 h-100 flex-column"> <body class="d-flex w-100 h-100 flex-column">
@ -86,7 +94,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li class="dropdown"> <li class="dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-user"></i> {{ user.username }} ({{ user.note.balance | pretty_money }}) <i class="fa fa-user"></i>
<span id="user_balance">{{ user.username }} ({{ user.note.balance | pretty_money }})</span>
</a> </a>
<div class="dropdown-menu dropdown-menu-right" <div class="dropdown-menu dropdown-menu-right"
aria-labelledby="navbarDropdownMenuLink"> aria-labelledby="navbarDropdownMenuLink">
@ -115,6 +124,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</nav> </nav>
<div class="container-fluid my-3" style="max-width: 1600px;"> <div class="container-fluid my-3" style="max-width: 1600px;">
{% block contenttitle %}<h1>{{ title }}</h1>{% endblock %} {% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
<div id="messages"></div>
{% block content %} {% block content %}
<p>Default content...</p> <p>Default content...</p>
{% endblock content %} {% endblock content %}
@ -158,6 +168,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
</footer> </footer>
<script>
CSRF_TOKEN = "{{ csrf_token }}";
</script>
{% block extrajavascript %} {% block extrajavascript %}
{% endblock extrajavascript %} {% endblock extrajavascript %}
</body> </body>

View File

@ -10,7 +10,7 @@
<img src="{{ object.note.display_image.url }}" class="img-thumbnail mt-2" > <img src="{{ object.note.display_image.url }}" class="img-thumbnail mt-2" >
</a> </a>
</div> </div>
<div class="card-body"> <div class="card-body" id="profile_infos">
<dl class="row"> <dl class="row">
<dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt> <dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt>
<dd class="col-xl-6">{{ object.last_name }} {{ object.first_name }}</dd> <dd class="col-xl-6">{{ object.last_name }} {{ object.first_name }}</dd>
@ -76,7 +76,9 @@
</a> </a>
</div> </div>
<div id="historyListCollapse" class="collapse" style="overflow:auto hidden" aria-labelledby="historyListHeading" data-parent="#accordionProfile"> <div id="historyListCollapse" class="collapse" style="overflow:auto hidden" aria-labelledby="historyListHeading" data-parent="#accordionProfile">
{% render_table history_list %} <div id="history_list">
{% render_table history_list %}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -84,3 +86,12 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extrajavascript %}
<script>
function refreshHistory() {
$("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list");
$("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos");
}
</script>
{% endblock %}

View File

@ -7,67 +7,88 @@
{% block content %} {% block content %}
<div class="row mt-4"> <div class="row mt-4">
<div class="col-sm-5 col-md-4"> <div class="col-sm-5 col-md-4" id="infos_div">
<div class="row"> <div class="row">
{# User details column #} {# User details column #}
<div class="col-xl-5"> <div class="col-xl-5" id="note_infos_div">
<div class="card border-success shadow mb-4"> <div class="card border-success shadow mb-4">
<img src="https://perso.crans.org/erdnaxe/site-crans/img/logo.svg" <img src="/media/pic/default.png"
alt="" class="img-fluid rounded mx-auto d-block"> id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block">
<div class="card-body text-center"> <div class="card-body text-center">
Paquito (aka. PAC) : -230 € <span id="user_note"></span>
</div> </div>
</div> </div>
</div> </div>
{# User selection column #} {# User selection column #}
<div class="col-xl-7"> <div class="col-xl-7" id="user_select_div">
<div class="card border-success shadow mb-4"> <div class="card border-success shadow mb-4">
<div class="card-header"> <div class="card-header">
<p class="card-text font-weight-bold"> <p class="card-text font-weight-bold">
Sélection des émitteurs {% trans "Select emitters" %}
</p> </p>
</div> </div>
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush" id="note_list">
<li class="list-group-item py-1 d-flex justify-content-between align-items-center">
Cras justo odio
<span class="badge badge-dark badge-pill">14</span>
</li>
<li class="list-group-item py-1 d-flex justify-content-between align-items-center">
Dapibus ac facilisis in
<span class="badge badge-dark badge-pill">1</span>
</li>
</ul> </ul>
<div class="card-body"> <div class="card-body">
TODO: reimplement select2 here in JS <input class="form-control mx-auto d-block" type="text" id="note" />
<ul class="list-group list-group-flush" id="alias_matched">
</ul>
</div> </div>
</div> </div>
</div> </div>
<div class="col-xl-5" id="consos_list_div">
<div class="card border-info shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Select consumptions" %}
</p>
</div>
<ul class="list-group list-group-flush" id="consos_list">
</ul>
<button id="consume_all" class="form-control btn btn-primary">
{% trans "Consume!" %}
</button>
</div>
</div>
</div> </div>
</div> </div>
{# Buttons column #} {# Buttons column #}
<div class="col-sm-7 col-md-8"> <div class="col-sm-7 col-md-8" id="buttons_div">
{# Show last used buttons #} {# Show last used buttons #}
<div class="card shadow mb-4"> <div class="card shadow mb-4">
<div class="card-body text-nowrap" style="overflow:auto hidden"> <div class="card-header">
<p class="card-text text-muted font-weight-light font-italic"> <p class="card-text font-weight-bold">
Les boutons les plus utilisés s'afficheront ici. {% trans "Most used buttons" %}
</p> </p>
</div> </div>
<div class="card-body text-nowrap" style="overflow:auto hidden">
<div class="d-inline-flex flex-wrap justify-content-center" id="most_used">
{% for button in most_used %}
{% if button.display %}
<button class="btn btn-outline-dark rounded-0 flex-fill"
id="most_used_button{{ button.id }}" name="button" value="{{ button.name }}">
{{ button.name }} ({{ button.amount | pretty_money }})
</button>
{% endif %}
{% endfor %}
</div>
</div>
</div> </div>
{# Regroup buttons under categories #} {# Regroup buttons under categories #}
{% regroup transaction_templates by template_type as template_types %} {% regroup transaction_templates by category as categories %}
<div class="card border-primary text-center shadow mb-4"> <div class="card border-primary text-center shadow mb-4">
{# Tabs for button categories #} {# Tabs for button categories #}
<div class="card-header"> <div class="card-header">
<ul class="nav nav-tabs nav-fill card-header-tabs"> <ul class="nav nav-tabs nav-fill card-header-tabs">
{% for template_type in template_types %} {% for category in categories %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link font-weight-bold" data-toggle="tab" href="#{{ template_type.grouper|slugify }}"> <a class="nav-link font-weight-bold" data-toggle="tab" href="#{{ category.grouper|slugify }}">
{{ template_type.grouper }} {{ category.grouper }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}
@ -77,14 +98,16 @@
{# Tabs content #} {# Tabs content #}
<div class="card-body"> <div class="card-body">
<div class="tab-content"> <div class="tab-content">
{% for template_type in template_types %} {% for category in categories %}
<div class="tab-pane" id="{{ template_type.grouper|slugify }}"> <div class="tab-pane" id="{{ category.grouper|slugify }}">
<div class="d-inline-flex flex-wrap justify-content-center"> <div class="d-inline-flex flex-wrap justify-content-center">
{% for button in template_type.list %} {% for button in category.list %}
<button class="btn btn-outline-dark rounded-0 flex-fill" {% if button.display %}
name="button" value="{{ button.name }}"> <button class="btn btn-outline-dark rounded-0 flex-fill"
{{ button.name }} ({{ button.amount | pretty_money }}) id="button{{ button.id }}" name="button" value="{{ button.name }}">
</button> {{ button.name }} ({{ button.amount | pretty_money }})
</button>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -95,16 +118,16 @@
{# Mode switch #} {# Mode switch #}
<div class="card-footer border-primary"> <div class="card-footer border-primary">
<a class="btn btn-sm btn-secondary float-left" href="{% url 'note:template_list' %}"> <a class="btn btn-sm btn-secondary float-left" href="{% url 'note:template_list' %}">
<i class="fa fa-edit"></i> Éditer <i class="fa fa-edit"></i> {% trans "Edit" %}
</a> </a>
<div class="btn-group btn-group-toggle float-right" data-toggle="buttons"> <div class="btn-group btn-group-toggle float-right" data-toggle="buttons">
<label class="btn btn-sm btn-outline-primary active"> <label for="single_conso" class="btn btn-sm btn-outline-primary active">
<input type="radio" name="options" id="option1" checked> <input type="radio" name="conso_type" id="single_conso" checked>
Consomations simples {% trans "Single consumptions" %}
</label> </label>
<label class="btn btn-sm btn-outline-primary"> <label for="double_conso" class="btn btn-sm btn-outline-primary">
<input type="radio" name="options" id="option2"> <input type="radio" name="conso_type" id="double_conso">
Consomations doubles {% trans "Double consumptions" %}
</label> </label>
</div> </div>
</div> </div>
@ -112,40 +135,37 @@
</div> </div>
</div> </div>
<div class="card shadow mb-4"> <div class="card shadow mb-4" id="history">
<div class="card-header"> <div class="card-header">
<p class="card-text font-weight-bold"> <p class="card-text font-weight-bold">
Historique des transactions récentes {% trans "Recent transactions history" %}
</p> </p>
</div> </div>
{% render_table table %} {% render_table table %}
</div> </div>
{% endblock %} {% endblock %}
{% block extracss %}
<style>
.select2-container{
max-width: 100%;
min-width: 100%;
}
</style>
{% endblock %}
{% block extrajavascript %} {% block extrajavascript %}
<script type="text/javascript" src="/static/js/consos.js"></script>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { {% for button in most_used %}
// If hash of a category in the URL, then select this category {% if button.display %}
// else select the first one $("#most_used_button{{ button.id }}").click(function() {
if (location.hash) { addConso({{ button.destination.id }}, {{ button.amount }},
$("a[href='" + location.hash + "']").tab("show"); {{ polymorphic_ctype }}, {{ button.category.id }}, "{{ button.category.name }}",
} else { {{ button.id }}, "{{ button.name }}");
$("a[data-toggle='tab']").first().tab("show"); });
} {% endif %}
{% endfor %}
// When selecting a category, change URL {% for button in transaction_templates %}
$(document.body).on("click", "a[data-toggle='tab']", function(event) { {% if button.display %}
location.hash = this.getAttribute("href"); $("#button{{ button.id }}").click(function() {
}); addConso({{ button.destination.id }}, {{ button.amount }},
}); {{ polymorphic_ctype }}, {{ button.category.id }}, "{{ button.category.name }}",
{{ button.id }}, "{{ button.name }}");
});
{% endif %}
{% endfor %}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -3,35 +3,188 @@
SPDX-License-Identifier: GPL-2.0-or-later SPDX-License-Identifier: GPL-2.0-or-later
{% endcomment %} {% endcomment %}
{% load i18n static %} {% load i18n static django_tables2 %}
{% block content %} {% block content %}
<form method="post" onsubmit="window.onbeforeunload=null">{% csrf_token %}
{% if form.non_field_errors %} <div class="row">
<p class="errornote"> <div class="col-xl-12">
{% for error in form.non_field_errors %} <div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
{{ error }} <label for="type_gift" class="btn btn-sm btn-outline-primary active">
{% endfor %} <input type="radio" name="transaction_type" id="type_gift" checked>
</p> {% trans "Gift" %}
{% endif %} </label>
<fieldset class="module aligned"> <label for="type_transfer" class="btn btn-sm btn-outline-primary">
{% for field in form %} <input type="radio" name="transaction_type" id="type_transfer">
<div class="form-row{% if field.errors %} errors{% endif %}"> {% trans "Transfer" %}
{{ field.errors }} </label>
<div> <label for="type_credit" class="btn btn-sm btn-outline-primary">
{{ field.label_tag }} <input type="radio" name="transaction_type" id="type_credit">
{% if field.is_readonly %} {% trans "Credit" %}
<div class="readonly">{{ field.contents }}</div> </label>
{% else %} <label type="type_debit" class="btn btn-sm btn-outline-primary">
{{ field }} <input type="radio" name="transaction_type" id="type_debit">
{% endif %} {% trans "Debit" %}
{% if field.field.help_text %} </label>
<div class="help">{{ field.field.help_text|safe }}</div> </div>
{% endif %} </div>
</div>
<div class="row">
<div class="col-md-4" id="emitters_div" style="display: none;">
<div class="card border-success shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Select emitters" %}
</p>
</div>
<ul class="list-group list-group-flush" id="source_note_list">
</ul>
<div class="card-body">
<input class="form-control mx-auto d-block" type="text" id="source_note" />
<ul class="list-group list-group-flush" id="source_alias_matched">
</ul>
</div>
</div>
</div>
<div class="col-xl-4" id="note_infos_div">
<div class="card border-success shadow mb-4">
<img src="/media/pic/default.png"
id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block">
<div class="card-body text-center">
<span id="user_note"></span>
</div>
</div>
</div>
<div class="col-md-4" id="external_div" style="display: none;">
<div class="card border-success shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "External payment" %}
</p>
</div>
<ul class="list-group list-group-flush" id="source_note_list">
</ul>
<div class="card-body">
<div class="form-row">
<div class="col-md-12">
<label for="credit_type">{% trans "Transfer type" %} :</label>
<select id="credit_type" class="custom-select">
{% for special_type in special_types %}
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<label for="last_name">{% trans "Name" %} :</label>
<input type="text" id="last_name" class="form-control" />
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<label for="first_name">{% trans "First name" %} :</label>
<input type="text" id="first_name" class="form-control" />
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<label for="bank">{% trans "Bank" %} :</label>
<input type="text" id="bank" class="form-control" />
</div>
</div> </div>
</div> </div>
{% endfor %} </div>
</fieldset> </div>
<input type="submit" value="{% trans 'Transfer' %}">
</form> <div class="col-md-8" id="dests_div">
<div class="card border-info shadow mb-4">
<div class="card-header">
<p class="card-text font-weight-bold" id="dest_title">
{% trans "Select receivers" %}
</p>
</div>
<ul class="list-group list-group-flush" id="dest_note_list">
</ul>
<div class="card-body">
<input class="form-control mx-auto d-block" type="text" id="dest_note" />
<ul class="list-group list-group-flush" id="dest_alias_matched">
</ul>
</div>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-6">
<label for="amount">{% trans "Amount" %} :</label>
<div class="input-group">
<input class="form-control mx-auto d-block" type="number" min="0" step="0.01" id="amount" />
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
</div>
</div>
<div class="form-group col-md-6">
<label for="reason">{% trans "Reason" %} :</label>
<input class="form-control mx-auto d-block" type="text" id="reason" required />
</div>
</div>
<div class="form-row">
<div class="col-md-12">
<button id="transfer" class="form-control btn btn-primary">{% trans 'Transfer' %}</button>
</div>
</div>
<div class="card shadow mb-4" id="history">
<div class="card-header">
<p class="card-text font-weight-bold">
{% trans "Recent transactions history" %}
</p>
</div>
{% render_table table %}
</div>
{% endblock %}
{% block extrajavascript %}
<script>
TRANSFER_POLYMORPHIC_CTYPE = {{ polymorphic_ctype }};
SPECIAL_TRANSFER_POLYMORPHIC_CTYPE = {{ special_polymorphic_ctype }};
user_id = {{ user.note.pk }};
$("#type_gift").click(function() {
$("#emitters_div").hide();
$("#external_div").hide();
$("#dests_div").attr('class', 'col-md-8');
$("#dest_title").text("{% trans "Select receivers" %}");
});
$("#type_transfer").click(function() {
$("#emitters_div").show();
$("#external_div").hide();
$("#dests_div").attr('class', 'col-md-4');
$("#dest_title").text("{% trans "Select receivers" %}");
});
$("#type_credit").click(function() {
$("#emitters_div").hide();
$("#external_div").show();
$("#dests_div").attr('class', 'col-md-4');
$("#dest_title").text("{% trans "Credit note" %}");
});
$("#type_debit").click(function() {
$("#emitters_div").hide();
$("#external_div").show();
$("#dests_div").attr('class', 'col-md-4');
$("#dest_title").text("{% trans "Debit note" %}");
});
</script>
<script src="/static/js/transfer.js"></script>
{% endblock %} {% endblock %}

View File

@ -15,7 +15,7 @@
<td><a href="{{object.get_absolute_url}}">{{ object.name }}</a></td> <td><a href="{{object.get_absolute_url}}">{{ object.name }}</a></td>
<td>{{ object.destination }}</td> <td>{{ object.destination }}</td>
<td>{{ object.amount | pretty_money }}</td> <td>{{ object.amount | pretty_money }}</td>
<td>{{ object.template_type }}</td> <td>{{ object.category }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>