diff --git a/Dockerfile b/Dockerfile index d42bdd1f..dfc49d04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,11 @@ RUN apt update && \ apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \ rm -rf /var/lib/apt/lists/* +# Install LaTeX requirements +RUN apt update && \ + apt install -y texlive-latex-extra texlive-fonts-extra texlive-lang-french && \ + rm -rf /var/lib/apt/lists/* + COPY . /code/ # Comment what is not needed diff --git a/README.md b/README.md index 1ffe8793..9b0c927e 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,17 @@ ## Installation sur un serveur -On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout nu ou bien configuré. +On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout nu ou bien configuré. 1. Paquets nécessaires $ sudo apt install nginx python3 python3-pip python3-dev uwsgi $ sudo apt install uwsgi-plugin-python3 python3-venv git acl + La génération des factures de l'application trésorerie nécessite une installation de LaTeX suffisante : + + $ sudo apt install texlive-latex-extra texlive-fonts-extra texlive-lang-french + 2. Clonage du dépot on se met au bon endroit : diff --git a/apps/api/urls.py b/apps/api/urls.py index b275a0b8..67fdba30 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -12,6 +12,7 @@ from activity.api.urls import register_activity_urls from api.viewsets import ReadProtectedModelViewSet from member.api.urls import register_members_urls from note.api.urls import register_note_urls +from treasury.api.urls import register_treasury_urls from logs.api.urls import register_logs_urls from permission.api.urls import register_permission_urls @@ -74,6 +75,7 @@ router.register('user', UserViewSet) register_members_urls(router, 'members') register_activity_urls(router, 'activity') register_note_urls(router, 'note') +register_treasury_urls(router, 'treasury') register_permission_urls(router, 'permission') register_logs_urls(router, 'logs') diff --git a/apps/member/fixtures/initial.json b/apps/member/fixtures/initial.json index 769650a0..bba1e7ac 100644 --- a/apps/member/fixtures/initial.json +++ b/apps/member/fixtures/initial.json @@ -5,7 +5,7 @@ "fields": { "name": "BDE", "email": "tresorerie.bde@example.com", - "membership_fee": 5, + "membership_fee": 500, "membership_duration": "396 00:00:00", "membership_start": "213 00:00:00", "membership_end": "273 00:00:00" @@ -17,7 +17,7 @@ "fields": { "name": "Kfet", "email": "tresorerie.bde@example.com", - "membership_fee": 35, + "membership_fee": 3500, "membership_duration": "396 00:00:00", "membership_start": "213 00:00:00", "membership_end": "273 00:00:00" diff --git a/apps/member/models.py b/apps/member/models.py index cdbb9332..d0051e59 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -4,6 +4,7 @@ import datetime from django.conf import settings +from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ @@ -67,6 +68,13 @@ class Club(models.Model): email = models.EmailField( verbose_name=_('email'), ) + parent_club = models.ForeignKey( + 'self', + null=True, + blank=True, + on_delete=models.PROTECT, + verbose_name=_('parent club'), + ) # Memberships membership_fee = models.PositiveIntegerField( @@ -158,6 +166,12 @@ class Membership(models.Model): else: return self.date_start.toordinal() <= datetime.datetime.now().toordinal() + def save(self, *args, **kwargs): + if self.club.parent_club is not None: + if not Membership.objects.filter(user=self.user, club=self.club.parent_club): + raise ValidationError(_('User is not a member of the parent club')) + super().save(*args, **kwargs) + class Meta: verbose_name = _('membership') verbose_name_plural = _('memberships') diff --git a/apps/member/tables.py b/apps/member/tables.py index a6de17d2..d0c37a6e 100644 --- a/apps/member/tables.py +++ b/apps/member/tables.py @@ -17,6 +17,7 @@ class ClubTable(tables.Table): fields = ('id', 'name', 'email') row_attrs = { 'class': 'table-row', + 'id': lambda record: "row-" + str(record.pk), 'data-href': lambda record: record.pk } diff --git a/apps/member/urls.py b/apps/member/urls.py index d9dfd181..0b705bfd 100644 --- a/apps/member/urls.py +++ b/apps/member/urls.py @@ -12,12 +12,14 @@ urlpatterns = [ path('club//', views.ClubDetailView.as_view(), name="club_detail"), path('club//add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"), path('club/create/', views.ClubCreateView.as_view(), name="club_create"), + path('club//update', views.ClubUpdateView.as_view(), name="club_update"), + path('club//update_pic', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), + path('club//aliases', views.ClubAliasView.as_view(), name="club_alias"), path('user/', views.UserListView.as_view(), name="user_list"), path('user/', views.UserDetailView.as_view(), name="user_detail"), path('user//update', views.UserUpdateView.as_view(), name="user_update_profile"), path('user//update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"), - path('user//aliases', views.AliasView.as_view(), name="user_alias"), - path('user/aliases/delete/', views.DeleteAliasView.as_view(), name="user_alias_delete"), + path('user//aliases', views.ProfileAliasView.as_view(), name="user_alias"), path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'), # API for the user autocompleter path('user/user-autocomplete', views.UserAutocomplete.as_view(), name="user_autocomplete"), diff --git a/apps/member/views.py b/apps/member/views.py index 0ba76d6a..2992f76e 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -20,7 +20,8 @@ from django.views.generic import CreateView, DetailView, UpdateView, TemplateVie from django.views.generic.edit import FormMixin from django_tables2.views import SingleTableView from rest_framework.authtoken.models import Token -from note.forms import AliasForm, ImageForm +from note.forms import ImageForm +#from note.forms import AliasForm, ImageForm from note.models import Alias, NoteUser from note.models.transactions import Transaction from note.tables import HistoryTable, AliasTable @@ -143,10 +144,6 @@ class UserDetailView(LoginRequiredMixin, DetailView): club_list = \ Membership.objects.all().filter(user=user).only("club") context['club_list'] = ClubTable(club_list) - context['title'] = _("Account #%(id)s: %(username)s") % { - 'id': user.pk, - 'username': user.username, - } return context @@ -171,62 +168,20 @@ class UserListView(LoginRequiredMixin, SingleTableView): context["filter"] = self.filter return context - -class AliasView(LoginRequiredMixin, FormMixin, DetailView): + +class ProfileAliasView(LoginRequiredMixin, DetailView): model = User template_name = 'member/profile_alias.html' context_object_name = 'user_object' - form_class = AliasForm - + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - note = context['user_object'].note + note = context['object'].note context["aliases"] = AliasTable(note.alias_set.all()) return context - def get_success_url(self): - return reverse_lazy('member:user_alias', kwargs={'pk': self.object.id}) - def post(self, request, *args, **kwargs): - self.object = self.get_object() - form = self.get_form() - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_invalid(form) - - def form_valid(self, form): - alias = form.save(commit=False) - alias.note = self.object.note - alias.save() - return super().form_valid(form) - - -class DeleteAliasView(LoginRequiredMixin, DeleteView): - model = Alias - - def delete(self, request, *args, **kwargs): - try: - self.object = self.get_object() - self.object.delete() - except ValidationError as e: - # TODO: pass message to redirected view. - messages.error(self.request, str(e)) - else: - messages.success(self.request, _("Alias successfully deleted")) - return HttpResponseRedirect(self.get_success_url()) - - def get_success_url(self): - return reverse_lazy('member:user_alias', kwargs={'pk': self.object.note.user.pk}) - - def get(self, request, *args, **kwargs): - return self.post(request, *args, **kwargs) - - -class ProfilePictureUpdateView(LoginRequiredMixin, FormMixin, DetailView): - model = User - template_name = 'member/profile_picture_update.html' - context_object_name = 'user_object' +class PictureUpdateView(LoginRequiredMixin, FormMixin, DetailView): form_class = ImageForm def get_context_data(self, *args, **kwargs): @@ -273,6 +228,12 @@ class ProfilePictureUpdateView(LoginRequiredMixin, FormMixin, DetailView): return super().form_valid(form) +class ProfilePictureUpdateView(PictureUpdateView): + model = User + template_name = 'member/profile_picture_update.html' + context_object_name = 'user_object' + + class ManageAuthTokens(LoginRequiredMixin, TemplateView): """ Affiche le jeton d'authentification, et permet de le regénérer @@ -329,10 +290,11 @@ class ClubCreateView(LoginRequiredMixin, CreateView): """ model = Club form_class = ClubForm + success_url = reverse_lazy('member:club_list') def form_valid(self, form): return super().form_valid(form) - + class ClubListView(LoginRequiredMixin, SingleTableView): """ @@ -364,6 +326,34 @@ class ClubDetailView(LoginRequiredMixin, DetailView): context['member_list'] = club_member return context +class ClubAliasView(LoginRequiredMixin, DetailView): + model = Club + template_name = 'member/club_alias.html' + context_object_name = 'club' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + note = context['object'].note + context["aliases"] = AliasTable(note.alias_set.all()) + return context + + +class ClubUpdateView(LoginRequiredMixin, UpdateView): + model = Club + context_object_name = "club" + form_class = ClubForm + template_name = "member/club_form.html" + success_url = reverse_lazy("member:club_detail") + + +class ClubPictureUpdateView(PictureUpdateView): + model = Club + template_name = 'member/club_picture_update.html' + context_object_name = 'club' + + def get_success_url(self): + return reverse_lazy('member:club_detail', kwargs={'pk': self.object.id}) + class ClubAddMemberView(LoginRequiredMixin, CreateView): model = Membership @@ -374,12 +364,12 @@ class ClubAddMemberView(LoginRequiredMixin, CreateView): return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view") | PermissionBackend.filter_queryset(self.request.user, Membership, "change")) - def get_context_data(self, **kwargs): + club = Club.objects.get(pk=self.kwargs["pk"]) context = super().get_context_data(**kwargs) context['formset'] = MemberFormSet() context['helper'] = FormSetHelper() - + context['club'] = club context['no_cache'] = True return context diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index 27ff6d65..02379b04 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -78,7 +78,11 @@ class AliasSerializer(serializers.ModelSerializer): class Meta: model = Alias fields = '__all__' - read_only_fields = ('note', ) + + def validate(self, attrs): + instance = Alias(**attrs) + instance.clean() + return attrs class NotePolymorphicSerializer(PolymorphicSerializer): diff --git a/apps/note/api/views.py b/apps/note/api/views.py index af02dc8b..b5064ae4 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -2,8 +2,13 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.db.models import Q +from django.core.exceptions import ValidationError from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter, SearchFilter +from rest_framework import viewsets +from rest_framework.response import Response +from rest_framework import status + from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\ @@ -52,6 +57,22 @@ class AliasViewSet(ReadProtectedModelViewSet): search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] ordering_fields = ['name', 'normalized_name'] + def get_serializer_class(self): + serializer_class = self.serializer_class + if self.request.method in ['PUT', 'PATCH']: + #alias owner cannot be change once establish + setattr(serializer_class.Meta, 'read_only_fields', ('note',)) + return serializer_class + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + try: + self.perform_destroy(instance) + except ValidationError as e: + print(e) + return Response({e.code:e.message},status.HTTP_400_BAD_REQUEST) + return Response(status=status.HTTP_204_NO_CONTENT) + def get_queryset(self): """ Parse query and apply filters. @@ -104,7 +125,7 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet): search_fields = ['$name', ] -class TransactionTemplateViewSet(ReadProtectedModelViewSet): +class TransactionTemplateViewSet(viewsets.ModelViewSet): """ REST API View set. The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer, @@ -112,8 +133,9 @@ class TransactionTemplateViewSet(ReadProtectedModelViewSet): """ queryset = TransactionTemplate.objects.all() serializer_class = TransactionTemplateSerializer - filter_backends = [DjangoFilterBackend] + filter_backends = [SearchFilter, DjangoFilterBackend] filterset_fields = ['name', 'amount', 'display', 'category', ] + search_fields = ['$name', ] class TransactionViewSet(ReadProtectedModelViewSet): diff --git a/apps/note/fixtures/button.json b/apps/note/fixtures/button.json new file mode 100644 index 00000000..39f880b5 --- /dev/null +++ b/apps/note/fixtures/button.json @@ -0,0 +1,58 @@ +[ + { + "model": "note.templatecategory", + "pk": 1, + "fields": { + "name": "Soft" + } + }, + { + "model": "note.templatecategory", + "pk": 2, + "fields": { + "name": "Pulls" + } + }, + { + "model": "note.templatecategory", + "pk": 3, + "fields": { + "name": "Gala" + } + }, + { + "model": "note.templatecategory", + "pk": 4, + "fields": { + "name": "Clubs" + } + }, + { + "model": "note.templatecategory", + "pk": 5, + "fields": { + "name": "Bouffe" + } + }, + { + "model": "note.templatecategory", + "pk": 6, + "fields": { + "name": "BDA" + } + }, + { + "model": "note.templatecategory", + "pk": 7, + "fields": { + "name": "Autre" + } + }, + { + "model": "note.templatecategory", + "pk": 8, + "fields": { + "name": "Alcool" + } + } +] diff --git a/apps/note/fixtures/initial.json b/apps/note/fixtures/initial.json index 63285e34..72853eb7 100644 --- a/apps/note/fixtures/initial.json +++ b/apps/note/fixtures/initial.json @@ -70,7 +70,7 @@ "balance": 0, "last_negative": null, "is_active": true, - "display_image": "", + "display_image": "pic/default.png", "created_at": "2020-02-20T20:09:38.615Z" } }, @@ -85,23 +85,8 @@ "balance": 0, "last_negative": null, "is_active": true, - "display_image": "", - "created_at": "2020-02-20T20:16:14.753Z" - } - }, - { - "model": "note.note", - "pk": 7, - "fields": { - "polymorphic_ctype": [ - "note", - "noteuser" - ], - "balance": 0, - "last_negative": null, - "is_active": true, "display_image": "pic/default.png", - "created_at": "2020-03-22T13:01:35.680Z" + "created_at": "2020-02-20T20:16:14.753Z" } }, { @@ -199,61 +184,5 @@ "normalized_name": "kfet", "note": 6 } - }, - { - "model": "note.templatecategory", - "pk": 1, - "fields": { - "name": "Soft" - } - }, - { - "model": "note.templatecategory", - "pk": 2, - "fields": { - "name": "Pulls" - } - }, - { - "model": "note.templatecategory", - "pk": 3, - "fields": { - "name": "Gala" - } - }, - { - "model": "note.templatecategory", - "pk": 4, - "fields": { - "name": "Clubs" - } - }, - { - "model": "note.templatecategory", - "pk": 5, - "fields": { - "name": "Bouffe" - } - }, - { - "model": "note.templatecategory", - "pk": 6, - "fields": { - "name": "BDA" - } - }, - { - "model": "note.templatecategory", - "pk": 7, - "fields": { - "name": "Autre" - } - }, - { - "model": "note.templatecategory", - "pk": 8, - "fields": { - "name": "Alcool" - } } -] +] \ No newline at end of file diff --git a/apps/note/forms.py b/apps/note/forms.py index ac6adaaf..60252ad5 100644 --- a/apps/note/forms.py +++ b/apps/note/forms.py @@ -9,17 +9,6 @@ from .models import Alias from .models import TransactionTemplate -class AliasForm(forms.ModelForm): - class Meta: - model = Alias - fields = ("name",) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["name"].label = False - self.fields["name"].widget.attrs = {"placeholder": _('New Alias')} - - class ImageForm(forms.Form): image = forms.ImageField(required=False, label=_('select an image'), diff --git a/apps/note/models/__init__.py b/apps/note/models/__init__.py index 8f1921f9..e9c8a0a9 100644 --- a/apps/note/models/__init__.py +++ b/apps/note/models/__init__.py @@ -3,12 +3,12 @@ from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser from .transactions import MembershipTransaction, Transaction, \ - TemplateCategory, TransactionTemplate, RecurrentTransaction + TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction __all__ = [ # Notes 'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser', # Transactions 'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate', - 'RecurrentTransaction', + 'RecurrentTransaction', 'SpecialTransaction', ] diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py index b6b00aa8..43faabfe 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -228,7 +228,7 @@ class Alias(models.Model): for cat in {'M', 'P', 'Z', 'C'})).casefold() def clean(self): - normalized_name = Alias.normalize(self.name) + normalized_name = self.normalize(self.name) if len(normalized_name) >= 255: raise ValidationError(_('Alias is too long.'), code='alias_too_long') @@ -242,8 +242,12 @@ class Alias(models.Model): pass self.normalized_name = normalized_name + def save(self,*args,**kwargs): + self.normalized_name = self.normalize(self.name) + super().save(*args,**kwargs) + def delete(self, using=None, keep_parents=False): if self.name == str(self.note): raise ValidationError(_("You can't delete your main alias."), - code="cant_delete_main_alias") + code="main_alias") return super().delete(using, keep_parents) diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index 0e40edf6..c6b8baa6 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.db import models +from django.db.models import F from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -93,12 +94,26 @@ class Transaction(PolymorphicModel): related_name='+', verbose_name=_('source'), ) + + source_alias = models.CharField( + max_length=255, + default="", # Will be remplaced by the name of the note on save + verbose_name=_('used alias'), + ) + destination = models.ForeignKey( Note, on_delete=models.PROTECT, related_name='+', verbose_name=_('destination'), ) + + destination_alias = models.CharField( + max_length=255, + default="", # Will be remplaced by the name of the note on save + verbose_name=_('used alias'), + ) + created_at = models.DateTimeField( verbose_name=_('created at'), default=timezone.now, @@ -115,11 +130,19 @@ class Transaction(PolymorphicModel): verbose_name=_('reason'), max_length=255, ) + valid = models.BooleanField( verbose_name=_('valid'), default=True, ) + invalidity_reason = models.CharField( + verbose_name=_('invalidity reason'), + max_length=255, + default=None, + null=True, + ) + class Meta: verbose_name = _("transaction") verbose_name_plural = _("transactions") @@ -134,6 +157,13 @@ class Transaction(PolymorphicModel): When saving, also transfer money between two notes """ + # If the aliases are not entered, we assume that the used alias is the name of the note + if not self.source_alias: + self.source_alias = str(self.source) + + if not self.destination_alias: + self.destination_alias = str(self.destination) + if self.source.pk == self.destination.pk: # When source == destination, no money is transfered super().save(*args, **kwargs) @@ -152,6 +182,10 @@ class Transaction(PolymorphicModel): self.source.balance -= to_transfer self.destination.balance += to_transfer + # When a transaction is declared valid, we ensure that the invalidity reason is null, if it was + # previously invalid + self.invalidity_reason = None + # We save first the transaction, in case of the user has no right to transfer money super().save(*args, **kwargs) diff --git a/apps/note/tables.py b/apps/note/tables.py index b9dac051..201b6c43 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -5,11 +5,12 @@ import html import django_tables2 as tables from django.db.models import F +from django.utils.html import format_html from django_tables2.utils import A from django.utils.translation import gettext_lazy as _ from .models.notes import Alias -from .models.transactions import Transaction +from .models.transactions import Transaction, TransactionTemplate from .templatetags.pretty_money import pretty_money @@ -20,19 +21,48 @@ class HistoryTable(tables.Table): 'table table-condensed table-striped table-hover' } model = Transaction - exclude = ("id", "polymorphic_ctype", ) + exclude = ("id", "polymorphic_ctype", "invalidity_reason", "source_alias", "destination_alias",) template_name = 'django_tables2/bootstrap4.html' - sequence = ('...', 'type', 'total', 'valid', ) + sequence = ('...', 'type', 'total', 'valid',) orderable = False + source = tables.Column( + attrs={ + "td": { + "data-toggle": "tooltip", + "title": lambda record: _("used alias").capitalize() + " : " + record.source_alias, + } + } + ) + + destination = tables.Column( + attrs={ + "td": { + "data-toggle": "tooltip", + "title": lambda record: _("used alias").capitalize() + " : " + record.destination_alias, + } + } + ) + type = tables.Column() 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() + ')'}}) + valid = tables.Column( + attrs={ + "td": { + "id": lambda record: "validate_" + str(record.id), + "class": lambda record: str(record.valid).lower() + ' validate', + "data-toggle": "tooltip", + "title": lambda record: _("Click to invalidate") if record.valid else _("Click to validate"), + "onclick": lambda record: 'in_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')', + "onmouseover": lambda record: '$("#invalidity_reason_' + + str(record.id) + '").show();$("#invalidity_reason_' + + str(record.id) + '").focus();', + "onmouseout": lambda record: '$("#invalidity_reason_' + str(record.id) + '").hide()', + } + } + ) def order_total(self, queryset, is_descending): # needed for rendering @@ -53,15 +83,32 @@ class HistoryTable(tables.Table): def render_reason(self, value): return html.unescape(value) - def render_valid(self, value): - return "✔" if value else "✖" + def render_valid(self, value, record): + """ + When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason + """ + val = "✔" if value else "✖" + val += "" + return format_html(val) + + +# function delete_button(id) provided in template file +DELETE_TEMPLATE = """ + +""" class AliasTable(tables.Table): class Meta: attrs = { 'class': - 'table table condensed table-striped table-hover' + 'table table condensed table-striped table-hover', + 'id':"alias_table" } model = Alias fields = ('name',) @@ -69,9 +116,37 @@ class AliasTable(tables.Table): show_header = False name = tables.Column(attrs={'td': {'class': 'text-center'}}) - delete = tables.LinkColumn('member:user_alias_delete', - args=[A('pk')], - attrs={ - 'td': {'class': 'col-sm-2'}, - 'a': {'class': 'btn btn-danger'}}, - text='delete', accessor='pk') + + delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE, + extra_context={"delete_trans": _('delete')}, + attrs={'td': {'class': 'col-sm-1'}}) + + + +class ButtonTable(tables.Table): + class Meta: + attrs = { + 'class': + 'table table-bordered condensed table-hover' + } + row_attrs = { + 'class': lambda record: 'table-row ' + 'table-success' if record.display else 'table-danger', + 'id': lambda record: "row-" + str(record.pk), + 'data-href': lambda record: record.pk + } + + model = TransactionTemplate + + edit = tables.LinkColumn('note:template_update', + args=[A('pk')], + attrs={'td': {'class': 'col-sm-1'}, + 'a': {'class': 'btn btn-sm btn-primary'}}, + text=_('edit'), + accessor='pk') + + delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE, + extra_context={"delete_trans": _('delete')}, + attrs={'td': {'class': 'col-sm-1'}}) + + def render_amount(self, value): + return pretty_money(value) diff --git a/apps/note/templatetags/pretty_money.py b/apps/note/templatetags/pretty_money.py index 265870a8..ba527f9b 100644 --- a/apps/note/templatetags/pretty_money.py +++ b/apps/note/templatetags/pretty_money.py @@ -18,5 +18,10 @@ def pretty_money(value): ) +def cents_to_euros(value): + return "{:.02f}".format(value / 100) if value else "" + + register = template.Library() register.filter('pretty_money', pretty_money) +register.filter('cents_to_euros', cents_to_euros) diff --git a/apps/note/urls.py b/apps/note/urls.py index fea911f6..59316069 100644 --- a/apps/note/urls.py +++ b/apps/note/urls.py @@ -8,7 +8,7 @@ from .models import Note app_name = 'note' urlpatterns = [ - path('transfer/', views.TransactionCreate.as_view(), name='transfer'), + path('transfer/', views.TransactionCreateView.as_view(), name='transfer'), path('buttons/create/', views.TransactionTemplateCreateView.as_view(), name='template_create'), path('buttons/update//', views.TransactionTemplateUpdateView.as_view(), name='template_update'), path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'), diff --git a/apps/note/views.py b/apps/note/views.py index 84df2bd7..ddf5ee6f 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -6,22 +6,25 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType from django.db.models import Q from django.utils.translation import gettext_lazy as _ -from django.views.generic import CreateView, ListView, UpdateView +from django.views.generic import CreateView, UpdateView from django_tables2 import SingleTableView +from django.urls import reverse_lazy from permission.backends import PermissionBackend from .forms import TransactionTemplateForm from .models import Transaction, TransactionTemplate, Alias, RecurrentTransaction, NoteSpecial from .models.transactions import SpecialTransaction -from .tables import HistoryTable +from .tables import HistoryTable, ButtonTable -class TransactionCreate(LoginRequiredMixin, SingleTableView): +class TransactionCreateView(LoginRequiredMixin, SingleTableView): """ - Show transfer page + View for the creation of Transaction between two note which are not :models:`transactions.RecurrentTransaction`. + e.g. for donation/transfer between people and clubs or for credit/debit with :models:`note.NoteSpecial` """ template_name = "note/transaction_form.html" + model = Transaction # Transaction history table table_class = HistoryTable table_pagination = {"per_page": 50} @@ -46,13 +49,14 @@ class TransactionCreate(LoginRequiredMixin, SingleTableView): class NoteAutocomplete(autocomplete.Select2QuerySetView): """ - Auto complete note by aliases + Auto complete note by aliases. Used in every search field for note + ex: :view:`ConsoView`, :view:`TransactionCreateView` """ def get_queryset(self): """ - Quand une personne cherche un alias, une requête est envoyée sur l'API dédiée à l'auto-complétion. - Cette fonction récupère la requête, et renvoie la liste filtrée des aliases. + When someone look for an :models:`note.Alias`, a query is sent to the dedicated API. + This function handles the result and return a filtered list of aliases. """ # Un utilisateur non connecté n'a accès à aucune information if not self.request.user.is_authenticated: @@ -81,6 +85,10 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView): return qs def get_result_label(self, result): + """ + Show the selected alias and the username associated + (aka. ) + """ # Gère l'affichage de l'alias dans la recherche res = result.name note_name = str(result.note) @@ -89,7 +97,9 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView): return res def get_result_value(self, result): - # Le résultat renvoyé doit être l'identifiant de la note, et non de l'alias + """ + The value used for the transactions will be the id of the Note. + """ return str(result.note.pk) @@ -99,14 +109,15 @@ class TransactionTemplateCreateView(LoginRequiredMixin, CreateView): """ model = TransactionTemplate form_class = TransactionTemplateForm + success_url = reverse_lazy('note:template_list') -class TransactionTemplateListView(LoginRequiredMixin, ListView): +class TransactionTemplateListView(LoginRequiredMixin, SingleTableView): """ List TransactionsTemplates """ model = TransactionTemplate - form_class = TransactionTemplateForm + table_class = ButtonTable class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView): @@ -114,11 +125,13 @@ class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView): """ model = TransactionTemplate form_class = TransactionTemplateForm + success_url = reverse_lazy('note:template_list') class ConsoView(LoginRequiredMixin, SingleTableView): """ - Consume + The Magic View that make people pay their beer and burgers. + (Most of the magic happens in the dark world of Javascript see consos.js) """ template_name = "note/conso_form.html" diff --git a/apps/permission/admin.py b/apps/permission/admin.py index aaa6f661..4312f4b0 100644 --- a/apps/permission/admin.py +++ b/apps/permission/admin.py @@ -28,4 +28,3 @@ class RolePermissionsAdmin(admin.ModelAdmin): Admin customisation for RolePermissions """ list_display = ('role', ) - diff --git a/apps/permission/api/views.py b/apps/permission/api/views.py index 6087c83e..965e82c9 100644 --- a/apps/permission/api/views.py +++ b/apps/permission/api/views.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django_filters.rest_framework import DjangoFilterBackend - from api.viewsets import ReadOnlyProtectedModelViewSet + from .serializers import PermissionSerializer from ..models import Permission diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 4c7de16d..43d39a36 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -327,7 +327,7 @@ "note", "transaction" ], - "query": "[\"AND\", {\"source\": [\"user\", \"note\"]}, {\"amount__lte\": [\"user\", \"note\", \"balance\"]}]", + "query": "[\"AND\", {\"source\": [\"user\", \"note\"]}, [\"OR\", {\"amount__lte\": [\"user\", \"note\", \"balance\"]}, {\"valid\": false}]]", "type": "add", "mask": 1, "field": "", @@ -387,7 +387,7 @@ "note", "recurrenttransaction" ], - "query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}]", + "query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]", "type": "add", "mask": 2, "field": "", diff --git a/apps/permission/models.py b/apps/permission/models.py index 109c1875..205f5b41 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -10,7 +10,6 @@ from django.core.exceptions import ValidationError from django.db import models from django.db.models import F, Q, Model from django.utils.translation import gettext_lazy as _ - from member.models import Role @@ -281,4 +280,3 @@ class RolePermissions(models.Model): def __str__(self): return str(self.role) - diff --git a/apps/permission/permissions.py b/apps/permission/permissions.py index d9816a63..7097085f 100644 --- a/apps/permission/permissions.py +++ b/apps/permission/permissions.py @@ -3,6 +3,8 @@ from rest_framework.permissions import DjangoObjectPermissions +from .backends import PermissionBackend + SAFE_METHODS = ('HEAD', 'OPTIONS', ) @@ -41,8 +43,8 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions): user = request.user perms = self.get_required_object_permissions(request.method, model_cls) - - if not user.has_perms(perms, obj): + # if not user.has_perms(perms, obj): + if not all(PermissionBackend().has_perm(user, perm, obj) for perm in perms): # If the user does not have permissions we need to determine if # they have read permissions to see 403, or not, and simply see # a 404 response. diff --git a/apps/permission/signals.py b/apps/permission/signals.py index aebca39d..1e30f56f 100644 --- a/apps/permission/signals.py +++ b/apps/permission/signals.py @@ -3,10 +3,9 @@ from django.core.exceptions import PermissionDenied from django.db.models.signals import pre_save, pre_delete, post_save, post_delete - from logs import signals as logs_signals -from permission.backends import PermissionBackend from note_kfet.middlewares import get_current_authenticated_user +from permission.backends import PermissionBackend EXCLUDED = [ diff --git a/apps/permission/templatetags/perms.py b/apps/permission/templatetags/perms.py index 8f2a0006..8bcd3597 100644 --- a/apps/permission/templatetags/perms.py +++ b/apps/permission/templatetags/perms.py @@ -3,10 +3,8 @@ from django.contrib.contenttypes.models import ContentType from django.template.defaultfilters import stringfilter - -from note_kfet.middlewares import get_current_authenticated_user, get_current_session from django import template - +from note_kfet.middlewares import get_current_authenticated_user, get_current_session from permission.backends import PermissionBackend diff --git a/apps/treasury/__init__.py b/apps/treasury/__init__.py new file mode 100644 index 00000000..c9c6150e --- /dev/null +++ b/apps/treasury/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +default_app_config = 'treasury.apps.TreasuryConfig' diff --git a/apps/treasury/admin.py b/apps/treasury/admin.py new file mode 100644 index 00000000..abeec3e3 --- /dev/null +++ b/apps/treasury/admin.py @@ -0,0 +1,27 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-lateré + +from django.contrib import admin + +from .models import RemittanceType, Remittance + + +@admin.register(RemittanceType) +class RemittanceTypeAdmin(admin.ModelAdmin): + """ + Admin customisation for RemiitanceType + """ + list_display = ('note', ) + + +@admin.register(Remittance) +class RemittanceAdmin(admin.ModelAdmin): + """ + Admin customisation for Remittance + """ + list_display = ('remittance_type', 'date', 'comment', 'count', 'amount', 'closed', ) + + def has_change_permission(self, request, obj=None): + if not obj: + return True + return not obj.closed and super().has_change_permission(request, obj) diff --git a/apps/treasury/api/__init__.py b/apps/treasury/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/treasury/api/serializers.py b/apps/treasury/api/serializers.py new file mode 100644 index 00000000..f1bbef75 --- /dev/null +++ b/apps/treasury/api/serializers.py @@ -0,0 +1,62 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from rest_framework import serializers +from note.api.serializers import SpecialTransactionSerializer + +from ..models import Invoice, Product, RemittanceType, Remittance + + +class ProductSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Product types. + The djangorestframework plugin will analyse the model `Product` and parse all fields in the API. + """ + + class Meta: + model = Product + fields = '__all__' + + +class InvoiceSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Invoice types. + The djangorestframework plugin will analyse the model `Invoice` and parse all fields in the API. + """ + class Meta: + model = Invoice + fields = '__all__' + read_only_fields = ('bde',) + + products = serializers.SerializerMethodField() + + def get_products(self, obj): + return serializers.ListSerializer(child=ProductSerializer())\ + .to_representation(Product.objects.filter(invoice=obj).all()) + + +class RemittanceTypeSerializer(serializers.ModelSerializer): + """ + REST API Serializer for RemittanceType types. + The djangorestframework plugin will analyse the model `RemittanceType` and parse all fields in the API. + """ + + class Meta: + model = RemittanceType + fields = '__all__' + + +class RemittanceSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Remittance types. + The djangorestframework plugin will analyse the model `Remittance` and parse all fields in the API. + """ + + transactions = serializers.SerializerMethodField() + + class Meta: + model = Remittance + fields = '__all__' + + def get_transactions(self, obj): + return serializers.ListSerializer(child=SpecialTransactionSerializer()).to_representation(obj.transactions) diff --git a/apps/treasury/api/urls.py b/apps/treasury/api/urls.py new file mode 100644 index 00000000..30ac00e1 --- /dev/null +++ b/apps/treasury/api/urls.py @@ -0,0 +1,14 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from .views import InvoiceViewSet, ProductViewSet, RemittanceViewSet, RemittanceTypeViewSet + + +def register_treasury_urls(router, path): + """ + Configure router for treasury REST API. + """ + router.register(path + '/invoice', InvoiceViewSet) + router.register(path + '/product', ProductViewSet) + router.register(path + '/remittance_type', RemittanceTypeViewSet) + router.register(path + '/remittance', RemittanceViewSet) diff --git a/apps/treasury/api/views.py b/apps/treasury/api/views.py new file mode 100644 index 00000000..7a70fd24 --- /dev/null +++ b/apps/treasury/api/views.py @@ -0,0 +1,53 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.filters import SearchFilter +from api.viewsets import ReadProtectedModelViewSet + +from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer +from ..models import Invoice, Product, RemittanceType, Remittance + + +class InvoiceViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Invoice` objects, serialize it to JSON with the given serializer, + then render it on /api/treasury/invoice/ + """ + queryset = Invoice.objects.all() + serializer_class = InvoiceSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['bde', ] + + +class ProductViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Product` objects, serialize it to JSON with the given serializer, + then render it on /api/treasury/product/ + """ + queryset = Product.objects.all() + serializer_class = ProductSerializer + filter_backends = [SearchFilter] + search_fields = ['$designation', ] + + +class RemittanceTypeViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer + then render it on /api/treasury/remittance_type/ + """ + queryset = RemittanceType.objects.all() + serializer_class = RemittanceTypeSerializer + + +class RemittanceViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer, + then render it on /api/treasury/remittance/ + """ + queryset = Remittance.objects.all() + serializer_class = RemittanceSerializer diff --git a/apps/treasury/apps.py b/apps/treasury/apps.py new file mode 100644 index 00000000..e2873ea2 --- /dev/null +++ b/apps/treasury/apps.py @@ -0,0 +1,33 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.apps import AppConfig +from django.db.models import Q +from django.db.models.signals import post_save, post_migrate +from django.utils.translation import gettext_lazy as _ + + +class TreasuryConfig(AppConfig): + name = 'treasury' + verbose_name = _('Treasury') + + def ready(self): + """ + Define app internal signals to interact with other apps + """ + + from . import signals + from note.models import SpecialTransaction, NoteSpecial + from treasury.models import SpecialTransactionProxy + post_save.connect(signals.save_special_transaction, sender=SpecialTransaction) + + def setup_specialtransactions_proxies(**kwargs): + # If the treasury app was disabled for any reason during a certain amount of time, + # we ensure that each special transaction is linked to a proxy + for transaction in SpecialTransaction.objects.filter( + source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), + specialtransactionproxy=None, + ): + SpecialTransactionProxy.objects.create(transaction=transaction, remittance=None) + + post_migrate.connect(setup_specialtransactions_proxies, sender=SpecialTransactionProxy) diff --git a/apps/treasury/fixtures/initial.json b/apps/treasury/fixtures/initial.json new file mode 100644 index 00000000..143d2101 --- /dev/null +++ b/apps/treasury/fixtures/initial.json @@ -0,0 +1,9 @@ +[ + { + "model": "treasury.remittancetype", + "pk": 1, + "fields": { + "note": 3 + } + } +] diff --git a/apps/treasury/forms.py b/apps/treasury/forms.py new file mode 100644 index 00000000..caaa365f --- /dev/null +++ b/apps/treasury/forms.py @@ -0,0 +1,156 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import datetime + +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Submit +from django import forms +from django.utils.translation import gettext_lazy as _ + +from .models import Invoice, Product, Remittance, SpecialTransactionProxy + + +class InvoiceForm(forms.ModelForm): + """ + Create and generate invoices. + """ + + # Django forms don't support date fields. We have to add it manually + date = forms.DateField( + initial=datetime.date.today, + widget=forms.TextInput(attrs={'type': 'date'}) + ) + + def clean_date(self): + self.instance.date = self.data.get("date") + + class Meta: + model = Invoice + exclude = ('bde', ) + + +# Add a subform per product in the invoice form, and manage correctly the link between the invoice and +# its products. The FormSet will search automatically the ForeignKey in the Product model. +ProductFormSet = forms.inlineformset_factory( + Invoice, + Product, + fields='__all__', + extra=1, +) + + +class ProductFormSetHelper(FormHelper): + """ + Specify some template informations for the product form. + """ + + def __init__(self, form=None): + super().__init__(form) + self.form_tag = False + self.form_method = 'POST' + self.form_class = 'form-inline' + self.template = 'bootstrap4/table_inline_formset.html' + + +class RemittanceForm(forms.ModelForm): + """ + Create remittances. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.helper = FormHelper() + + # We can't update the type of the remittance once created. + if self.instance.pk: + self.fields["remittance_type"].disabled = True + self.fields["remittance_type"].required = False + + # We display the submit button iff the remittance is open, + # the close button iff it is open and has a linked transaction + if not self.instance.closed: + self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'})) + if self.instance.transactions: + self.helper.add_input(Submit("close", _("Close"), css_class='btn btn-success')) + else: + # If the remittance is closed, we can't change anything + self.fields["comment"].disabled = True + self.fields["comment"].required = False + + def clean(self): + # We can't update anything if the remittance is already closed. + if self.instance.closed: + self.add_error("comment", _("Remittance is already closed.")) + + cleaned_data = super().clean() + + if self.instance.pk and cleaned_data.get("remittance_type") != self.instance.remittance_type: + self.add_error("remittance_type", _("You can't change the type of the remittance.")) + + # The close button is manually handled + if "close" in self.data: + self.instance.closed = True + self.cleaned_data["closed"] = True + + return cleaned_data + + class Meta: + model = Remittance + fields = ('remittance_type', 'comment',) + + +class LinkTransactionToRemittanceForm(forms.ModelForm): + """ + Attach a special transaction to a remittance. + """ + + # Since we use a proxy model for special transactions, we add manually the fields related to the transaction + last_name = forms.CharField(label=_("Last name")) + + first_name = forms.Field(label=_("First name")) + + bank = forms.Field(label=_("Bank")) + + amount = forms.IntegerField(label=_("Amount"), min_value=0) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + # Add submit button + self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'})) + + self.fields["remittance"].queryset = Remittance.objects.filter(closed=False) + + def clean_last_name(self): + """ + Replace the first name in the information of the transaction. + """ + self.instance.transaction.last_name = self.data.get("last_name") + self.instance.transaction.clean() + + def clean_first_name(self): + """ + Replace the last name in the information of the transaction. + """ + self.instance.transaction.first_name = self.data.get("first_name") + self.instance.transaction.clean() + + def clean_bank(self): + """ + Replace the bank in the information of the transaction. + """ + self.instance.transaction.bank = self.data.get("bank") + self.instance.transaction.clean() + + def clean_amount(self): + """ + Replace the amount of the transaction. + """ + self.instance.transaction.amount = self.data.get("amount") + self.instance.transaction.clean() + + class Meta: + model = SpecialTransactionProxy + fields = ('remittance', ) diff --git a/apps/treasury/migrations/__init__.py b/apps/treasury/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/treasury/models.py b/apps/treasury/models.py new file mode 100644 index 00000000..bcd89db9 --- /dev/null +++ b/apps/treasury/models.py @@ -0,0 +1,189 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.core.exceptions import ValidationError +from django.db import models +from django.db.models import Q +from django.utils.translation import gettext_lazy as _ +from note.models import NoteSpecial, SpecialTransaction + + +class Invoice(models.Model): + """ + An invoice model that can generates a true invoice. + """ + + id = models.PositiveIntegerField( + primary_key=True, + verbose_name=_("Invoice identifier"), + ) + + bde = models.CharField( + max_length=32, + default='Saperlistpopette.png', + choices=( + ('Saperlistpopette.png', 'Saper[list]popette'), + ('Finalist.png', 'Fina[list]'), + ('Listorique.png', '[List]orique'), + ('Satellist.png', 'Satel[list]'), + ('Monopolist.png', 'Monopo[list]'), + ('Kataclist.png', 'Katac[list]'), + ), + verbose_name=_("BDE"), + ) + + object = models.CharField( + max_length=255, + verbose_name=_("Object"), + ) + + description = models.TextField( + verbose_name=_("Description") + ) + + name = models.CharField( + max_length=255, + verbose_name=_("Name"), + ) + + address = models.TextField( + verbose_name=_("Address"), + ) + + date = models.DateField( + auto_now_add=True, + verbose_name=_("Place"), + ) + + acquitted = models.BooleanField( + verbose_name=_("Acquitted"), + ) + + +class Product(models.Model): + """ + Product that appears on an invoice. + """ + + invoice = models.ForeignKey( + Invoice, + on_delete=models.PROTECT, + ) + + designation = models.CharField( + max_length=255, + verbose_name=_("Designation"), + ) + + quantity = models.PositiveIntegerField( + verbose_name=_("Quantity") + ) + + amount = models.IntegerField( + verbose_name=_("Unit price") + ) + + @property + def amount_euros(self): + return self.amount / 100 + + @property + def total(self): + return self.quantity * self.amount + + @property + def total_euros(self): + return self.total / 100 + + +class RemittanceType(models.Model): + """ + Store what kind of remittances can be stored. + """ + + note = models.OneToOneField( + NoteSpecial, + on_delete=models.CASCADE, + ) + + def __str__(self): + return str(self.note) + + +class Remittance(models.Model): + """ + Treasurers want to regroup checks or bank transfers in bank remittances. + """ + + date = models.DateTimeField( + auto_now_add=True, + verbose_name=_("Date"), + ) + + remittance_type = models.ForeignKey( + RemittanceType, + on_delete=models.PROTECT, + verbose_name=_("Type"), + ) + + comment = models.CharField( + max_length=255, + verbose_name=_("Comment"), + ) + + closed = models.BooleanField( + default=False, + verbose_name=_("Closed"), + ) + + @property + def transactions(self): + """ + :return: Transactions linked to this remittance. + """ + if not self.pk: + return SpecialTransaction.objects.none() + return SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self) + + def count(self): + """ + Linked transactions count. + """ + return self.transactions.count() + + @property + def amount(self): + """ + Total amount of the remittance. + """ + return sum(transaction.total for transaction in self.transactions.all()) + + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + # Check if all transactions have the right type. + if self.transactions.filter(~Q(source=self.remittance_type.note)).exists(): + raise ValidationError("All transactions in a remittance must have the same type") + + return super().save(force_insert, force_update, using, update_fields) + + def __str__(self): + return _("Remittance #{:d}: {}").format(self.id, self.comment, ) + + +class SpecialTransactionProxy(models.Model): + """ + In order to keep modularity, we don't that the Note app depends on the treasury app. + That's why we create a proxy in this app, to link special transactions and remittances. + If it isn't very clean, that makes what we want. + """ + + transaction = models.OneToOneField( + SpecialTransaction, + on_delete=models.CASCADE, + ) + + remittance = models.ForeignKey( + Remittance, + on_delete=models.PROTECT, + null=True, + verbose_name=_("Remittance"), + ) diff --git a/apps/treasury/signals.py b/apps/treasury/signals.py new file mode 100644 index 00000000..54c19c09 --- /dev/null +++ b/apps/treasury/signals.py @@ -0,0 +1,12 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from treasury.models import SpecialTransactionProxy, RemittanceType + + +def save_special_transaction(instance, created, **kwargs): + """ + When a special transaction is created, we create its linked proxy + """ + if created and RemittanceType.objects.filter(note=instance.source).exists(): + SpecialTransactionProxy.objects.create(transaction=instance, remittance=None).save() diff --git a/apps/treasury/tables.py b/apps/treasury/tables.py new file mode 100644 index 00000000..1ecc04db --- /dev/null +++ b/apps/treasury/tables.py @@ -0,0 +1,103 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ +from django_tables2 import A +from note.models import SpecialTransaction +from note.templatetags.pretty_money import pretty_money + +from .models import Invoice, Remittance + + +class InvoiceTable(tables.Table): + """ + List all invoices. + """ + id = tables.LinkColumn("treasury:invoice_update", + args=[A("pk")], + text=lambda record: _("Invoice #{:d}").format(record.id), ) + + invoice = tables.LinkColumn("treasury:invoice_render", + verbose_name=_("Invoice"), + args=[A("pk")], + accessor="pk", + text="", + attrs={ + 'a': {'class': 'fa fa-file-pdf-o'}, + 'td': {'data-turbolinks': 'false'} + }) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = Invoice + template_name = 'django_tables2/bootstrap4.html' + fields = ('id', 'name', 'object', 'acquitted', 'invoice',) + + +class RemittanceTable(tables.Table): + """ + List all remittances. + """ + + count = tables.Column(verbose_name=_("Transaction count")) + + amount = tables.Column(verbose_name=_("Amount")) + + view = tables.LinkColumn("treasury:remittance_update", + verbose_name=_("View"), + args=[A("pk")], + text=_("View"), + attrs={ + 'a': {'class': 'btn btn-primary'} + }, ) + + def render_amount(self, value): + return pretty_money(value) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = Remittance + template_name = 'django_tables2/bootstrap4.html' + fields = ('id', 'date', 'remittance_type', 'comment', 'count', 'amount', 'view',) + + +class SpecialTransactionTable(tables.Table): + """ + List special credit transactions that are (or not, following the queryset) attached to a remittance. + """ + + # Display add and remove buttons. Use the `exclude` field to select what is needed. + remittance_add = tables.LinkColumn("treasury:link_transaction", + verbose_name=_("Remittance"), + args=[A("specialtransactionproxy.pk")], + text=_("Add"), + attrs={ + 'a': {'class': 'btn btn-primary'} + }, ) + + remittance_remove = tables.LinkColumn("treasury:unlink_transaction", + verbose_name=_("Remittance"), + args=[A("specialtransactionproxy.pk")], + text=_("Remove"), + attrs={ + 'a': {'class': 'btn btn-primary btn-danger'} + }, ) + + def render_id(self, record): + return record.specialtransactionproxy.pk + + def render_amount(self, value): + return pretty_money(value) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = SpecialTransaction + template_name = 'django_tables2/bootstrap4.html' + fields = ('id', 'source', 'destination', 'last_name', 'first_name', 'bank', 'amount', 'reason',) diff --git a/apps/treasury/urls.py b/apps/treasury/urls.py new file mode 100644 index 00000000..d44cc414 --- /dev/null +++ b/apps/treasury/urls.py @@ -0,0 +1,24 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.urls import path + +from .views import InvoiceCreateView, InvoiceListView, InvoiceUpdateView, InvoiceRenderView, RemittanceListView,\ + RemittanceCreateView, RemittanceUpdateView, LinkTransactionToRemittanceView, UnlinkTransactionToRemittanceView + +app_name = 'treasury' +urlpatterns = [ + # Invoice app paths + path('invoice/', InvoiceListView.as_view(), name='invoice_list'), + path('invoice/create/', InvoiceCreateView.as_view(), name='invoice_create'), + path('invoice//', InvoiceUpdateView.as_view(), name='invoice_update'), + path('invoice/render//', InvoiceRenderView.as_view(), name='invoice_render'), + + # Remittance app paths + path('remittance/', RemittanceListView.as_view(), name='remittance_list'), + path('remittance/create/', RemittanceCreateView.as_view(), name='remittance_create'), + path('remittance//', RemittanceUpdateView.as_view(), name='remittance_update'), + path('remittance/link_transaction//', LinkTransactionToRemittanceView.as_view(), name='link_transaction'), + path('remittance/unlink_transaction//', UnlinkTransactionToRemittanceView.as_view(), + name='unlink_transaction'), +] diff --git a/apps/treasury/views.py b/apps/treasury/views.py new file mode 100644 index 00000000..90440566 --- /dev/null +++ b/apps/treasury/views.py @@ -0,0 +1,316 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import os +import shutil +import subprocess +from tempfile import mkdtemp + +from crispy_forms.helper import FormHelper +from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.exceptions import ValidationError +from django.db.models import Q +from django.http import HttpResponse +from django.shortcuts import redirect +from django.template.loader import render_to_string +from django.urls import reverse_lazy +from django.views.generic import CreateView, UpdateView +from django.views.generic.base import View, TemplateView +from django_tables2 import SingleTableView +from note.models import SpecialTransaction, NoteSpecial +from note_kfet.settings.base import BASE_DIR + +from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm +from .models import Invoice, Product, Remittance, SpecialTransactionProxy +from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable + + +class InvoiceCreateView(LoginRequiredMixin, CreateView): + """ + Create Invoice + """ + model = Invoice + form_class = InvoiceForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + form = context['form'] + form.helper = FormHelper() + # Remove form tag on the generation of the form in the template (already present on the template) + form.helper.form_tag = False + # The formset handles the set of the products + form_set = ProductFormSet(instance=form.instance) + context['formset'] = form_set + context['helper'] = ProductFormSetHelper() + context['no_cache'] = True + + return context + + def form_valid(self, form): + ret = super().form_valid(form) + + kwargs = {} + + # The user type amounts in cents. We convert it in euros. + for key in self.request.POST: + value = self.request.POST[key] + if key.endswith("amount") and value: + kwargs[key] = str(int(100 * float(value))) + elif value: + kwargs[key] = value + + # For each product, we save it + formset = ProductFormSet(kwargs, instance=form.instance) + if formset.is_valid(): + for f in formset: + # We don't save the product if the designation is not entered, ie. if the line is empty + if f.is_valid() and f.instance.designation: + f.save() + f.instance.save() + else: + f.instance = None + + return ret + + def get_success_url(self): + return reverse_lazy('treasury:invoice_list') + + +class InvoiceListView(LoginRequiredMixin, SingleTableView): + """ + List existing Invoices + """ + model = Invoice + table_class = InvoiceTable + + +class InvoiceUpdateView(LoginRequiredMixin, UpdateView): + """ + Create Invoice + """ + model = Invoice + form_class = InvoiceForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + form = context['form'] + form.helper = FormHelper() + # Remove form tag on the generation of the form in the template (already present on the template) + form.helper.form_tag = False + # Fill the intial value for the date field, with the initial date of the model instance + form.fields['date'].initial = form.instance.date + # The formset handles the set of the products + form_set = ProductFormSet(instance=form.instance) + context['formset'] = form_set + context['helper'] = ProductFormSetHelper() + context['no_cache'] = True + + return context + + def form_valid(self, form): + ret = super().form_valid(form) + + kwargs = {} + # The user type amounts in cents. We convert it in euros. + for key in self.request.POST: + value = self.request.POST[key] + if key.endswith("amount") and value: + kwargs[key] = str(int(100 * float(value))) + elif value: + kwargs[key] = value + + formset = ProductFormSet(kwargs, instance=form.instance) + saved = [] + # For each product, we save it + if formset.is_valid(): + for f in formset: + # We don't save the product if the designation is not entered, ie. if the line is empty + if f.is_valid() and f.instance.designation: + f.save() + f.instance.save() + saved.append(f.instance.pk) + else: + f.instance = None + # Remove old products that weren't given in the form + Product.objects.filter(~Q(pk__in=saved), invoice=form.instance).delete() + + return ret + + def get_success_url(self): + return reverse_lazy('treasury:invoice_list') + + +class InvoiceRenderView(LoginRequiredMixin, View): + """ + Render Invoice as a generated PDF with the given information and a LaTeX template + """ + + def get(self, request, **kwargs): + pk = kwargs["pk"] + invoice = Invoice.objects.get(pk=pk) + products = Product.objects.filter(invoice=invoice).all() + + # Informations of the BDE. Should be updated when the school will move. + invoice.place = "Cachan" + invoice.my_name = "BDE ENS Cachan" + invoice.my_address_street = "61 avenue du Président Wilson" + invoice.my_city = "94230 Cachan" + invoice.bank_code = 30003 + invoice.desk_code = 3894 + invoice.account_number = 37280662 + invoice.rib_key = 14 + invoice.bic = "SOGEFRPP" + + # Replace line breaks with the LaTeX equivalent + invoice.description = invoice.description.replace("\r", "").replace("\n", "\\\\ ") + invoice.address = invoice.address.replace("\r", "").replace("\n", "\\\\ ") + # Fill the template with the information + tex = render_to_string("treasury/invoice_sample.tex", dict(obj=invoice, products=products)) + + try: + os.mkdir(BASE_DIR + "/tmp") + except FileExistsError: + pass + # We render the file in a temporary directory + tmp_dir = mkdtemp(prefix=BASE_DIR + "/tmp/") + + try: + with open("{}/invoice-{:d}.tex".format(tmp_dir, pk), "wb") as f: + f.write(tex.encode("UTF-8")) + del tex + + # The file has to be rendered twice + for _ in range(2): + error = subprocess.Popen( + ["pdflatex", "invoice-{}.tex".format(pk)], + cwd=tmp_dir, + stdin=open(os.devnull, "r"), + stderr=open(os.devnull, "wb"), + stdout=open(os.devnull, "wb"), + ).wait() + + if error: + raise IOError("An error attempted while generating a invoice (code=" + str(error) + ")") + + # Display the generated pdf as a HTTP Response + pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read() + response = HttpResponse(pdf, content_type="application/pdf") + response['Content-Disposition'] = "inline;filename=invoice-{:d}.pdf".format(pk) + except IOError as e: + raise e + finally: + # Delete all temporary files + shutil.rmtree(tmp_dir) + + return response + + +class RemittanceCreateView(LoginRequiredMixin, CreateView): + """ + Create Remittance + """ + model = Remittance + form_class = RemittanceForm + + def get_success_url(self): + return reverse_lazy('treasury:remittance_list') + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + ctx["table"] = RemittanceTable(data=Remittance.objects.all()) + ctx["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none()) + + return ctx + + +class RemittanceListView(LoginRequiredMixin, TemplateView): + """ + List existing Remittances + """ + template_name = "treasury/remittance_list.html" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + ctx["opened_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=False).all()) + ctx["closed_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=True).reverse().all()) + + ctx["special_transactions_no_remittance"] = SpecialTransactionTable( + data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), + specialtransactionproxy__remittance=None).all(), + exclude=('remittance_remove', )) + ctx["special_transactions_with_remittance"] = SpecialTransactionTable( + data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), + specialtransactionproxy__remittance__closed=False).all(), + exclude=('remittance_add', )) + + return ctx + + +class RemittanceUpdateView(LoginRequiredMixin, UpdateView): + """ + Update Remittance + """ + model = Remittance + form_class = RemittanceForm + + def get_success_url(self): + return reverse_lazy('treasury:remittance_list') + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + ctx["table"] = RemittanceTable(data=Remittance.objects.all()) + data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).all() + ctx["special_transactions"] = SpecialTransactionTable( + data=data, + exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', )) + + return ctx + + +class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView): + """ + Attach a special transaction to a remittance + """ + + model = SpecialTransactionProxy + form_class = LinkTransactionToRemittanceForm + + def get_success_url(self): + return reverse_lazy('treasury:remittance_list') + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + form = ctx["form"] + form.fields["last_name"].initial = self.object.transaction.last_name + form.fields["first_name"].initial = self.object.transaction.first_name + form.fields["bank"].initial = self.object.transaction.bank + form.fields["amount"].initial = self.object.transaction.amount + form.fields["remittance"].queryset = form.fields["remittance"] \ + .queryset.filter(remittance_type__note=self.object.transaction.source) + + return ctx + + +class UnlinkTransactionToRemittanceView(LoginRequiredMixin, View): + """ + Unlink a special transaction and its remittance + """ + + def get(self, *args, **kwargs): + pk = kwargs["pk"] + transaction = SpecialTransactionProxy.objects.get(pk=pk) + + # The remittance must be open (or inexistant) + if transaction.remittance and transaction.remittance.closed: + raise ValidationError("Remittance is already closed.") + + transaction.remittance = None + transaction.save() + + return redirect('treasury:remittance_list') diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index e61efb2a..c9eda5aa 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-03-16 11:53+0100\n" +"POT-Creation-Date: 2020-03-26 14:40+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -23,9 +23,9 @@ msgid "activity" msgstr "" #: apps/activity/models.py:19 apps/activity/models.py:44 -#: 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/transactions.py:44 apps/note/models/transactions.py:202 +#: apps/member/models.py:63 apps/member/models.py:114 +#: apps/note/models/notes.py:188 apps/note/models/transactions.py:25 +#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:232 #: templates/member/profile_detail.html:15 msgid "name" msgstr "" @@ -46,12 +46,13 @@ msgstr "" msgid "activity types" msgstr "" -#: apps/activity/models.py:48 apps/note/models/transactions.py:69 +#: apps/activity/models.py:48 apps/note/models/transactions.py:70 +#: apps/permission/models.py:91 msgid "description" msgstr "" #: apps/activity/models.py:54 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115 +#: apps/note/models/transactions.py:63 msgid "type" msgstr "" @@ -119,11 +120,11 @@ msgstr "" msgid "create" msgstr "" -#: apps/logs/models.py:61 +#: apps/logs/models.py:61 apps/note/tables.py:147 msgid "edit" msgstr "" -#: apps/logs/models.py:62 +#: apps/logs/models.py:62 apps/note/tables.py:151 msgid "delete" msgstr "" @@ -143,123 +144,123 @@ msgstr "" msgid "member" msgstr "" -#: apps/member/models.py:23 +#: apps/member/models.py:25 msgid "phone number" msgstr "" -#: apps/member/models.py:29 templates/member/profile_detail.html:28 +#: apps/member/models.py:31 templates/member/profile_detail.html:28 msgid "section" msgstr "" -#: apps/member/models.py:30 +#: apps/member/models.py:32 msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "" -#: apps/member/models.py:36 templates/member/profile_detail.html:31 +#: apps/member/models.py:38 templates/member/profile_detail.html:31 msgid "address" msgstr "" -#: apps/member/models.py:42 +#: apps/member/models.py:44 msgid "paid" msgstr "" -#: apps/member/models.py:47 apps/member/models.py:48 +#: apps/member/models.py:49 apps/member/models.py:50 msgid "user profile" msgstr "" -#: apps/member/models.py:66 +#: apps/member/models.py:68 msgid "email" msgstr "" -#: apps/member/models.py:71 +#: apps/member/models.py:73 msgid "membership fee" msgstr "" -#: apps/member/models.py:75 +#: apps/member/models.py:77 msgid "membership duration" msgstr "" -#: apps/member/models.py:76 +#: apps/member/models.py:78 msgid "The longest time a membership can last (NULL = infinite)." msgstr "" -#: apps/member/models.py:81 +#: apps/member/models.py:83 msgid "membership start" msgstr "" -#: apps/member/models.py:82 +#: apps/member/models.py:84 msgid "How long after January 1st the members can renew their membership." msgstr "" -#: apps/member/models.py:87 +#: apps/member/models.py:89 msgid "membership end" msgstr "" -#: apps/member/models.py:88 +#: apps/member/models.py:90 msgid "" "How long the membership can last after January 1st of the next year after " "members can renew their membership." msgstr "" -#: apps/member/models.py:94 apps/note/models/notes.py:139 +#: apps/member/models.py:96 apps/note/models/notes.py:139 msgid "club" msgstr "" -#: apps/member/models.py:95 +#: apps/member/models.py:97 msgid "clubs" msgstr "" -#: apps/member/models.py:118 +#: apps/member/models.py:120 apps/permission/models.py:276 msgid "role" msgstr "" -#: apps/member/models.py:119 +#: apps/member/models.py:121 msgid "roles" msgstr "" -#: apps/member/models.py:143 +#: apps/member/models.py:145 msgid "membership starts on" msgstr "" -#: apps/member/models.py:146 +#: apps/member/models.py:148 msgid "membership ends on" msgstr "" -#: apps/member/models.py:150 +#: apps/member/models.py:152 msgid "fee" msgstr "" -#: apps/member/models.py:154 +#: apps/member/models.py:162 msgid "membership" msgstr "" -#: apps/member/models.py:155 +#: apps/member/models.py:163 msgid "memberships" msgstr "" -#: apps/member/views.py:69 templates/member/profile_detail.html:46 +#: apps/member/views.py:80 templates/member/profile_detail.html:46 msgid "Update Profile" msgstr "" -#: apps/member/views.py:82 +#: apps/member/views.py:93 msgid "An alias with a similar name already exists." msgstr "" -#: apps/member/views.py:132 +#: apps/member/views.py:146 #, python-format msgid "Account #%(id)s: %(username)s" msgstr "" -#: apps/member/views.py:202 +#: apps/member/views.py:216 msgid "Alias successfully deleted" msgstr "" -#: apps/note/admin.py:120 apps/note/models/transactions.py:94 +#: apps/note/admin.py:120 apps/note/models/transactions.py:95 msgid "source" msgstr "" #: apps/note/admin.py:128 apps/note/admin.py:156 -#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:100 +#: apps/note/models/transactions.py:54 apps/note/models/transactions.py:108 msgid "destination" msgstr "" @@ -309,7 +310,7 @@ msgstr "" msgid "display image" msgstr "" -#: apps/note/models/notes.py:53 apps/note/models/transactions.py:103 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:118 msgid "created at" msgstr "" @@ -383,116 +384,274 @@ msgstr "" msgid "You can't delete your main alias." msgstr "" -#: apps/note/models/transactions.py:30 +#: apps/note/models/transactions.py:31 msgid "transaction category" msgstr "" -#: apps/note/models/transactions.py:31 +#: apps/note/models/transactions.py:32 msgid "transaction categories" msgstr "" -#: apps/note/models/transactions.py:47 +#: apps/note/models/transactions.py:48 msgid "A template with this name already exist" msgstr "" -#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:111 +#: apps/note/models/transactions.py:57 apps/note/models/transactions.py:126 msgid "amount" msgstr "" -#: apps/note/models/transactions.py:57 +#: apps/note/models/transactions.py:58 msgid "in centimes" msgstr "" -#: apps/note/models/transactions.py:75 +#: apps/note/models/transactions.py:76 msgid "transaction template" msgstr "" -#: apps/note/models/transactions.py:76 +#: apps/note/models/transactions.py:77 msgid "transaction templates" msgstr "" -#: apps/note/models/transactions.py:107 +#: apps/note/models/transactions.py:101 apps/note/models/transactions.py:114 +#: apps/note/tables.py:33 apps/note/tables.py:42 +msgid "used alias" +msgstr "" + +#: apps/note/models/transactions.py:122 msgid "quantity" msgstr "" -#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15 -msgid "Gift" -msgstr "" - -#: apps/note/models/transactions.py:118 templates/base.html:90 -#: templates/note/transaction_form.html:19 -#: templates/note/transaction_form.html:126 -msgid "Transfer" -msgstr "" - -#: apps/note/models/transactions.py:119 -msgid "Template" -msgstr "" - -#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23 -msgid "Credit" -msgstr "" - -#: 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" -msgstr "" - -#: apps/note/models/transactions.py:129 +#: apps/note/models/transactions.py:130 msgid "reason" msgstr "" -#: apps/note/models/transactions.py:133 +#: apps/note/models/transactions.py:135 msgid "valid" msgstr "" -#: apps/note/models/transactions.py:138 +#: apps/note/models/transactions.py:140 apps/note/tables.py:95 +msgid "invalidity reason" +msgstr "" + +#: apps/note/models/transactions.py:147 msgid "transaction" msgstr "" -#: apps/note/models/transactions.py:139 +#: apps/note/models/transactions.py:148 msgid "transactions" msgstr "" -#: apps/note/models/transactions.py:207 +#: apps/note/models/transactions.py:202 templates/base.html:83 +#: templates/note/transaction_form.html:19 +#: templates/note/transaction_form.html:145 +msgid "Transfer" +msgstr "" + +#: apps/note/models/transactions.py:188 +msgid "Template" +msgstr "" + +#: apps/note/models/transactions.py:203 msgid "first_name" msgstr "" -#: apps/note/models/transactions.py:212 +#: apps/note/models/transactions.py:208 msgid "bank" msgstr "" +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24 +msgid "Credit" +msgstr "" + +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28 +msgid "Debit" +msgstr "" + +#: apps/note/models/transactions.py:230 apps/note/models/transactions.py:235 +msgid "membership transaction" +msgstr "" + #: apps/note/models/transactions.py:231 msgid "membership transactions" msgstr "" -#: apps/note/views.py:31 +#: apps/note/views.py:39 msgid "Transfer money" msgstr "" -#: apps/note/views.py:132 templates/base.html:78 +#: apps/note/views.py:145 templates/base.html:79 msgid "Consumptions" msgstr "" -#: note_kfet/settings/__init__.py:61 +#: apps/permission/models.py:69 apps/permission/models.py:262 +#, python-brace-format +msgid "Can {type} {model}.{field} in {query}" +msgstr "" + +#: apps/permission/models.py:71 apps/permission/models.py:264 +#, python-brace-format +msgid "Can {type} {model} in {query}" +msgstr "" + +#: apps/permission/models.py:84 +msgid "rank" +msgstr "" + +#: apps/permission/models.py:147 +msgid "Specifying field applies only to view and change permission types." +msgstr "" + +#: apps/treasury/apps.py:11 templates/base.html:102 +msgid "Treasury" +msgstr "" + +#: apps/treasury/forms.py:56 apps/treasury/forms.py:95 +#: templates/django_filters/rest_framework/form.html:5 +#: templates/member/club_form.html:10 templates/treasury/invoice_form.html:47 +msgid "Submit" +msgstr "" + +#: apps/treasury/forms.py:58 +msgid "Close" +msgstr "" + +#: apps/treasury/forms.py:65 +msgid "Remittance is already closed." +msgstr "" + +#: apps/treasury/forms.py:70 +msgid "You can't change the type of the remittance." +msgstr "" + +#: apps/treasury/forms.py:84 +msgid "Last name" +msgstr "" + +#: apps/treasury/forms.py:86 templates/note/transaction_form.html:92 +msgid "First name" +msgstr "" + +#: apps/treasury/forms.py:88 templates/note/transaction_form.html:98 +msgid "Bank" +msgstr "" + +#: apps/treasury/forms.py:90 apps/treasury/tables.py:40 +#: templates/note/transaction_form.html:128 +#: templates/treasury/remittance_form.html:18 +msgid "Amount" +msgstr "" + +#: apps/treasury/models.py:18 +msgid "Invoice identifier" +msgstr "" + +#: apps/treasury/models.py:32 +msgid "BDE" +msgstr "" + +#: apps/treasury/models.py:37 +msgid "Object" +msgstr "" + +#: apps/treasury/models.py:41 +msgid "Description" +msgstr "" + +#: apps/treasury/models.py:46 templates/note/transaction_form.html:86 +msgid "Name" +msgstr "" + +#: apps/treasury/models.py:50 +msgid "Address" +msgstr "" + +#: apps/treasury/models.py:55 +msgid "Place" +msgstr "" + +#: apps/treasury/models.py:59 +msgid "Acquitted" +msgstr "" + +#: apps/treasury/models.py:75 +msgid "Designation" +msgstr "" + +#: apps/treasury/models.py:79 +msgid "Quantity" +msgstr "" + +#: apps/treasury/models.py:83 +msgid "Unit price" +msgstr "" + +#: apps/treasury/models.py:120 +msgid "Date" +msgstr "" + +#: apps/treasury/models.py:126 +msgid "Type" +msgstr "" + +#: apps/treasury/models.py:131 +msgid "Comment" +msgstr "" + +#: apps/treasury/models.py:136 +msgid "Closed" +msgstr "" + +#: apps/treasury/models.py:159 +msgid "Remittance #{:d}: {}" +msgstr "" + +#: apps/treasury/models.py:178 apps/treasury/tables.py:64 +#: apps/treasury/tables.py:72 templates/treasury/invoice_list.html:13 +#: templates/treasury/remittance_list.html:13 +msgid "Remittance" +msgstr "" + +#: apps/treasury/tables.py:16 +msgid "Invoice #{:d}" +msgstr "" + +#: apps/treasury/tables.py:19 templates/treasury/invoice_list.html:10 +#: templates/treasury/remittance_list.html:10 +msgid "Invoice" +msgstr "" + +#: apps/treasury/tables.py:38 +msgid "Transaction count" +msgstr "" + +#: apps/treasury/tables.py:43 apps/treasury/tables.py:45 +msgid "View" +msgstr "" + +#: apps/treasury/tables.py:66 +msgid "Add" +msgstr "" + +#: apps/treasury/tables.py:74 +msgid "Remove" +msgstr "" + +#: note_kfet/settings/__init__.py:63 msgid "" "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 " "again unless your session expires or you logout." msgstr "" -#: note_kfet/settings/base.py:156 +#: note_kfet/settings/base.py:151 msgid "German" msgstr "" -#: note_kfet/settings/base.py:157 +#: note_kfet/settings/base.py:152 msgid "English" msgstr "" -#: note_kfet/settings/base.py:158 +#: note_kfet/settings/base.py:153 msgid "French" msgstr "" @@ -500,18 +659,14 @@ msgstr "" msgid "The ENS Paris-Saclay BDE note." msgstr "" -#: templates/base.html:81 +#: templates/base.html:87 msgid "Clubs" msgstr "" -#: templates/base.html:84 +#: templates/base.html:92 msgid "Activities" msgstr "" -#: templates/base.html:87 -msgid "Buttons" -msgstr "" - #: templates/cas_server/base.html:7 msgid "Central Authentication Service" msgstr "" @@ -567,11 +722,6 @@ msgstr "" msgid "Field filters" msgstr "" -#: templates/django_filters/rest_framework/form.html:5 -#: templates/member/club_form.html:10 -msgid "Submit" -msgstr "" - #: templates/member/club_detail.html:10 msgid "Membership starts on" msgstr "" @@ -653,7 +803,7 @@ msgstr "" msgid "Sign up" msgstr "" -#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38 +#: templates/note/conso_form.html:28 templates/note/transaction_form.html:50 msgid "Select emitters" msgstr "" @@ -681,49 +831,53 @@ msgstr "" msgid "Double consumptions" msgstr "" -#: templates/note/conso_form.html:141 +#: templates/note/conso_form.html:141 templates/note/transaction_form.html:152 msgid "Recent transactions history" msgstr "" -#: templates/note/transaction_form.html:55 +#: templates/note/transaction_form.html:15 +msgid "Gift" +msgstr "" + +#: templates/note/transaction_form.html:68 msgid "External payment" msgstr "" -#: templates/note/transaction_form.html:63 +#: templates/note/transaction_form.html:76 msgid "Transfer type" msgstr "" -#: templates/note/transaction_form.html:73 +#: templates/note/transaction_form.html:86 msgid "Name" msgstr "" -#: templates/note/transaction_form.html:79 +#: templates/note/transaction_form.html:92 msgid "First name" msgstr "" -#: templates/note/transaction_form.html:85 +#: templates/note/transaction_form.html:98 msgid "Bank" msgstr "" -#: templates/note/transaction_form.html:97 -#: templates/note/transaction_form.html:179 -#: templates/note/transaction_form.html:186 +#: templates/note/transaction_form.html:111 +#: templates/note/transaction_form.html:169 +#: templates/note/transaction_form.html:176 msgid "Select receivers" msgstr "" -#: templates/note/transaction_form.html:114 +#: templates/note/transaction_form.html:128 msgid "Amount" msgstr "" -#: templates/note/transaction_form.html:119 +#: templates/note/transaction_form.html:138 msgid "Reason" msgstr "" -#: templates/note/transaction_form.html:193 +#: templates/note/transaction_form.html:183 msgid "Credit note" msgstr "" -#: templates/note/transaction_form.html:200 +#: templates/note/transaction_form.html:190 msgid "Debit note" msgstr "" @@ -731,6 +885,22 @@ msgstr "" msgid "Buttons list" msgstr "" +#: templates/note/transactiontemplate_list.html:9 +msgid "search button" +msgstr "" + +#: templates/note/transactiontemplate_list.html:20 +msgid "buttons listing " +msgstr "" + +#: templates/note/transactiontemplate_list.html:71 +msgid "button successfully deleted " +msgstr "" + +#: templates/note/transactiontemplate_list.html:75 +msgid "Unable to delete button " +msgstr "" + #: templates/registration/logged_out.html:8 msgid "Thanks for spending some quality time with the Web site today." msgstr "" @@ -740,7 +910,7 @@ msgid "Log in again" msgstr "" #: templates/registration/login.html:7 templates/registration/login.html:8 -#: templates/registration/login.html:26 +#: templates/registration/login.html:28 #: templates/registration/password_reset_complete.html:10 msgid "Log in" msgstr "" @@ -752,7 +922,15 @@ msgid "" "page. Would you like to login to a different account?" msgstr "" -#: templates/registration/login.html:27 +#: templates/registration/login.html:22 +msgid "You can also register via the central authentification server " +msgstr "" + +#: templates/registration/login.html:23 +msgid "using this link " +msgstr "" + +#: templates/registration/login.html:29 msgid "Forgotten your password or username?" msgstr "" @@ -808,3 +986,72 @@ msgstr "" #: templates/registration/password_reset_form.html:11 msgid "Reset my password" msgstr "" + +#: templates/treasury/invoice_form.html:6 +msgid "Invoices list" +msgstr "" + +#: templates/treasury/invoice_form.html:42 +msgid "Add product" +msgstr "" + +#: templates/treasury/invoice_form.html:43 +msgid "Remove product" +msgstr "" + +#: templates/treasury/invoice_list.html:21 +msgid "New invoice" +msgstr "" + +#: templates/treasury/remittance_form.html:7 +msgid "Remittance #" +msgstr "" + +#: templates/treasury/remittance_form.html:9 +#: templates/treasury/specialtransactionproxy_form.html:7 +msgid "Remittances list" +msgstr "" + +#: templates/treasury/remittance_form.html:12 +msgid "Count" +msgstr "" + +#: templates/treasury/remittance_form.html:29 +msgid "Linked transactions" +msgstr "" + +#: templates/treasury/remittance_form.html:34 +msgid "There is no transaction linked with this remittance." +msgstr "" + +#: templates/treasury/remittance_list.html:19 +msgid "Opened remittances" +msgstr "" + +#: templates/treasury/remittance_list.html:24 +msgid "There is no opened remittance." +msgstr "" + +#: templates/treasury/remittance_list.html:28 +msgid "New remittance" +msgstr "" + +#: templates/treasury/remittance_list.html:32 +msgid "Transfers without remittances" +msgstr "" + +#: templates/treasury/remittance_list.html:37 +msgid "There is no transaction without any linked remittance." +msgstr "" + +#: templates/treasury/remittance_list.html:43 +msgid "Transfers with opened remittances" +msgstr "" + +#: templates/treasury/remittance_list.html:48 +msgid "There is no transaction with an opened linked remittance." +msgstr "" + +#: templates/treasury/remittance_list.html:54 +msgid "Closed remittances" +msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 5e6e9470..ca43d5a4 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-03-16 11:53+0100\n" +"POT-Creation-Date: 2020-03-26 14:40+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,9 +18,9 @@ msgid "activity" msgstr "activité" #: apps/activity/models.py:19 apps/activity/models.py:44 -#: 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/transactions.py:44 apps/note/models/transactions.py:202 +#: apps/member/models.py:63 apps/member/models.py:114 +#: apps/note/models/notes.py:188 apps/note/models/transactions.py:25 +#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:232 #: templates/member/profile_detail.html:15 msgid "name" msgstr "nom" @@ -41,12 +41,13 @@ msgstr "type d'activité" msgid "activity types" msgstr "types d'activité" -#: apps/activity/models.py:48 apps/note/models/transactions.py:69 +#: apps/activity/models.py:48 apps/note/models/transactions.py:70 +#: apps/permission/models.py:91 msgid "description" msgstr "description" #: apps/activity/models.py:54 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115 +#: apps/note/models/transactions.py:63 msgid "type" msgstr "type" @@ -114,11 +115,11 @@ msgstr "Nouvelles données" msgid "create" msgstr "Créer" -#: apps/logs/models.py:61 +#: apps/logs/models.py:61 apps/note/tables.py:147 msgid "edit" msgstr "Modifier" -#: apps/logs/models.py:62 +#: apps/logs/models.py:62 apps/note/tables.py:151 msgid "delete" msgstr "Supprimer" @@ -138,61 +139,61 @@ msgstr "Les logs ne peuvent pas être détruits." msgid "member" msgstr "adhérent" -#: apps/member/models.py:23 +#: apps/member/models.py:25 msgid "phone number" msgstr "numéro de téléphone" -#: apps/member/models.py:29 templates/member/profile_detail.html:28 +#: apps/member/models.py:31 templates/member/profile_detail.html:28 msgid "section" msgstr "section" -#: apps/member/models.py:30 +#: apps/member/models.py:32 msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" -#: apps/member/models.py:36 templates/member/profile_detail.html:31 +#: apps/member/models.py:38 templates/member/profile_detail.html:31 msgid "address" msgstr "adresse" -#: apps/member/models.py:42 +#: apps/member/models.py:44 msgid "paid" msgstr "payé" -#: apps/member/models.py:47 apps/member/models.py:48 +#: apps/member/models.py:49 apps/member/models.py:50 msgid "user profile" msgstr "profil utilisateur" -#: apps/member/models.py:66 +#: apps/member/models.py:68 msgid "email" msgstr "courriel" -#: apps/member/models.py:71 +#: apps/member/models.py:73 msgid "membership fee" msgstr "cotisation pour adhérer" -#: apps/member/models.py:75 +#: apps/member/models.py:77 msgid "membership duration" msgstr "durée de l'adhésion" -#: apps/member/models.py:76 +#: apps/member/models.py:78 msgid "The longest time a membership can last (NULL = infinite)." msgstr "La durée maximale d'une adhésion (NULL = infinie)." -#: apps/member/models.py:81 +#: apps/member/models.py:83 msgid "membership start" msgstr "début de l'adhésion" -#: apps/member/models.py:82 +#: apps/member/models.py:84 msgid "How long after January 1st the members can renew their membership." msgstr "" "Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur " "adhésion." -#: apps/member/models.py:87 +#: apps/member/models.py:89 msgid "membership end" msgstr "fin de l'adhésion" -#: apps/member/models.py:88 +#: apps/member/models.py:90 msgid "" "How long the membership can last after January 1st of the next year after " "members can renew their membership." @@ -200,65 +201,65 @@ msgstr "" "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." -#: apps/member/models.py:94 apps/note/models/notes.py:139 +#: apps/member/models.py:96 apps/note/models/notes.py:139 msgid "club" msgstr "club" -#: apps/member/models.py:95 +#: apps/member/models.py:97 msgid "clubs" msgstr "clubs" -#: apps/member/models.py:118 +#: apps/member/models.py:120 apps/permission/models.py:276 msgid "role" msgstr "rôle" -#: apps/member/models.py:119 +#: apps/member/models.py:121 msgid "roles" msgstr "rôles" -#: apps/member/models.py:143 +#: apps/member/models.py:145 msgid "membership starts on" msgstr "l'adhésion commence le" -#: apps/member/models.py:146 +#: apps/member/models.py:148 msgid "membership ends on" msgstr "l'adhésion finie le" -#: apps/member/models.py:150 +#: apps/member/models.py:152 msgid "fee" msgstr "cotisation" -#: apps/member/models.py:154 +#: apps/member/models.py:162 msgid "membership" msgstr "adhésion" -#: apps/member/models.py:155 +#: apps/member/models.py:163 msgid "memberships" msgstr "adhésions" -#: apps/member/views.py:69 templates/member/profile_detail.html:46 +#: apps/member/views.py:80 templates/member/profile_detail.html:46 msgid "Update Profile" msgstr "Modifier le profil" -#: apps/member/views.py:82 +#: apps/member/views.py:93 msgid "An alias with a similar name already exists." msgstr "Un alias avec un nom similaire existe déjà." -#: apps/member/views.py:132 +#: apps/member/views.py:146 #, python-format msgid "Account #%(id)s: %(username)s" msgstr "Compte n°%(id)s : %(username)s" -#: apps/member/views.py:202 +#: apps/member/views.py:216 msgid "Alias successfully deleted" msgstr "L'alias a bien été supprimé" -#: apps/note/admin.py:120 apps/note/models/transactions.py:94 +#: apps/note/admin.py:120 apps/note/models/transactions.py:95 msgid "source" msgstr "source" #: apps/note/admin.py:128 apps/note/admin.py:156 -#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:100 +#: apps/note/models/transactions.py:54 apps/note/models/transactions.py:108 msgid "destination" msgstr "destination" @@ -309,7 +310,7 @@ msgstr "" msgid "display image" msgstr "image affichée" -#: apps/note/models/notes.py:53 apps/note/models/transactions.py:103 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:118 msgid "created at" msgstr "créée le" @@ -383,116 +384,270 @@ msgstr "Un alias avec un nom similaire existe déjà : {}" msgid "You can't delete your main alias." msgstr "Vous ne pouvez pas supprimer votre alias principal." -#: apps/note/models/transactions.py:30 +#: apps/note/models/transactions.py:31 msgid "transaction category" msgstr "catégorie de transaction" -#: apps/note/models/transactions.py:31 +#: apps/note/models/transactions.py:32 msgid "transaction categories" msgstr "catégories de transaction" -#: apps/note/models/transactions.py:47 +#: apps/note/models/transactions.py:48 msgid "A template with this name already exist" msgstr "Un modèle de transaction avec un nom similaire existe déjà." -#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:111 +#: apps/note/models/transactions.py:57 apps/note/models/transactions.py:126 msgid "amount" msgstr "montant" -#: apps/note/models/transactions.py:57 +#: apps/note/models/transactions.py:58 msgid "in centimes" msgstr "en centimes" -#: apps/note/models/transactions.py:75 +#: apps/note/models/transactions.py:76 msgid "transaction template" msgstr "modèle de transaction" -#: apps/note/models/transactions.py:76 +#: apps/note/models/transactions.py:77 msgid "transaction templates" msgstr "modèles de transaction" -#: apps/note/models/transactions.py:107 +#: apps/note/models/transactions.py:101 apps/note/models/transactions.py:114 +#: apps/note/tables.py:33 apps/note/tables.py:42 +msgid "used alias" +msgstr "alias utilisé" + +#: apps/note/models/transactions.py:122 msgid "quantity" msgstr "quantité" -#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15 -msgid "Gift" -msgstr "Don" - -#: apps/note/models/transactions.py:118 templates/base.html:90 -#: templates/note/transaction_form.html:19 -#: templates/note/transaction_form.html:126 -msgid "Transfer" -msgstr "Virement" - -#: apps/note/models/transactions.py:119 -msgid "Template" -msgstr "Bouton" - -#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23 -msgid "Credit" -msgstr "Crédit" - -#: 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" -msgstr "transaction d'adhésion" - -#: apps/note/models/transactions.py:129 +#: apps/note/models/transactions.py:115 msgid "reason" msgstr "raison" -#: apps/note/models/transactions.py:133 +#: apps/note/models/transactions.py:119 msgid "valid" msgstr "valide" -#: apps/note/models/transactions.py:138 +#: apps/note/models/transactions.py:124 msgid "transaction" msgstr "transaction" -#: apps/note/models/transactions.py:139 +#: apps/note/models/transactions.py:125 msgid "transactions" msgstr "transactions" -#: apps/note/models/transactions.py:207 -msgid "first_name" -msgstr "Prénom" +#: apps/note/models/transactions.py:168 templates/base.html:98 +#: templates/note/transaction_form.html:19 +#: templates/note/transaction_form.html:145 +msgid "Transfer" +msgstr "Virement" -#: apps/note/models/transactions.py:212 +#: apps/note/models/transactions.py:188 +msgid "Template" +msgstr "Bouton" + +#: apps/note/models/transactions.py:203 +msgid "first_name" +msgstr "prénom" + +#: apps/note/models/transactions.py:208 msgid "bank" -msgstr "Banque" +msgstr "banque" + +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24 +msgid "Credit" +msgstr "Crédit" + +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28 +msgid "Debit" +msgstr "Débit" + +#: apps/note/models/transactions.py:230 apps/note/models/transactions.py:235 +msgid "membership transaction" +msgstr "transaction d'adhésion" #: apps/note/models/transactions.py:231 msgid "membership transactions" msgstr "transactions d'adhésion" -#: apps/note/views.py:31 +#: apps/note/views.py:39 msgid "Transfer money" -msgstr "Transferts d'argent" +msgstr "Transférer de l'argent" -#: apps/note/views.py:132 templates/base.html:78 +#: apps/note/views.py:145 templates/base.html:79 msgid "Consumptions" msgstr "Consommations" -#: note_kfet/settings/__init__.py:61 +#: apps/permission/models.py:69 apps/permission/models.py:262 +#, python-brace-format +msgid "Can {type} {model}.{field} in {query}" +msgstr "" + +#: apps/permission/models.py:71 apps/permission/models.py:264 +#, python-brace-format +msgid "Can {type} {model} in {query}" +msgstr "" + +#: apps/permission/models.py:84 +msgid "rank" +msgstr "Rang" + +#: apps/permission/models.py:147 +msgid "Specifying field applies only to view and change permission types." +msgstr "" + +#: apps/treasury/apps.py:11 templates/base.html:102 +msgid "Treasury" +msgstr "Trésorerie" + +#: apps/treasury/forms.py:56 apps/treasury/forms.py:95 +#: templates/django_filters/rest_framework/form.html:5 +#: templates/member/club_form.html:10 templates/treasury/invoice_form.html:47 +msgid "Submit" +msgstr "Envoyer" + +#: apps/treasury/forms.py:58 +msgid "Close" +msgstr "Fermer" + +#: apps/treasury/forms.py:65 +msgid "Remittance is already closed." +msgstr "La remise est déjà fermée." + +#: apps/treasury/forms.py:70 +msgid "You can't change the type of the remittance." +msgstr "Vous ne pouvez pas changer le type de la remise." + +#: apps/treasury/forms.py:84 +msgid "Last name" +msgstr "Nom de famille" + +#: apps/treasury/forms.py:86 templates/note/transaction_form.html:92 +msgid "First name" +msgstr "Prénom" + +#: apps/treasury/forms.py:88 templates/note/transaction_form.html:98 +msgid "Bank" +msgstr "Banque" + +#: apps/treasury/forms.py:90 apps/treasury/tables.py:40 +#: templates/note/transaction_form.html:128 +#: templates/treasury/remittance_form.html:18 +msgid "Amount" +msgstr "Montant" + +#: apps/treasury/models.py:18 +msgid "Invoice identifier" +msgstr "Numéro de facture" + +#: apps/treasury/models.py:32 +msgid "BDE" +msgstr "BDE" + +#: apps/treasury/models.py:37 +msgid "Object" +msgstr "Objet" + +#: apps/treasury/models.py:41 +msgid "Description" +msgstr "Description" + +#: apps/treasury/models.py:46 templates/note/transaction_form.html:86 +msgid "Name" +msgstr "Nom" + +#: apps/treasury/models.py:50 +msgid "Address" +msgstr "Adresse" + +#: apps/treasury/models.py:55 +msgid "Place" +msgstr "Lieu" + +#: apps/treasury/models.py:59 +msgid "Acquitted" +msgstr "Acquittée" + +#: apps/treasury/models.py:75 +msgid "Designation" +msgstr "Désignation" + +#: apps/treasury/models.py:79 +msgid "Quantity" +msgstr "Quantité" + +#: apps/treasury/models.py:83 +msgid "Unit price" +msgstr "Prix unitaire" + +#: apps/treasury/models.py:120 +msgid "Date" +msgstr "Date" + +#: apps/treasury/models.py:126 +msgid "Type" +msgstr "Type" + +#: apps/treasury/models.py:131 +msgid "Comment" +msgstr "Commentaire" + +#: apps/treasury/models.py:136 +msgid "Closed" +msgstr "Fermée" + +#: apps/treasury/models.py:159 +msgid "Remittance #{:d}: {}" +msgstr "Remise n°{:d} : {}" + +#: apps/treasury/models.py:178 apps/treasury/tables.py:64 +#: apps/treasury/tables.py:72 templates/treasury/invoice_list.html:13 +#: templates/treasury/remittance_list.html:13 +msgid "Remittance" +msgstr "Remise" + +#: apps/treasury/tables.py:16 +msgid "Invoice #{:d}" +msgstr "Facture n°{:d}" + +#: apps/treasury/tables.py:19 templates/treasury/invoice_list.html:10 +#: templates/treasury/remittance_list.html:10 +msgid "Invoice" +msgstr "Facture" + +#: apps/treasury/tables.py:38 +msgid "Transaction count" +msgstr "Nombre de transactions" + +#: apps/treasury/tables.py:43 apps/treasury/tables.py:45 +msgid "View" +msgstr "Voir" + +#: apps/treasury/tables.py:66 +msgid "Add" +msgstr "Ajouter" + +#: apps/treasury/tables.py:74 +msgid "Remove" +msgstr "supprimer" + +#: note_kfet/settings/__init__.py:63 msgid "" "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 " "again unless your session expires or you logout." msgstr "" -#: note_kfet/settings/base.py:156 +#: note_kfet/settings/base.py:151 msgid "German" msgstr "" -#: note_kfet/settings/base.py:157 +#: note_kfet/settings/base.py:152 msgid "English" msgstr "" -#: note_kfet/settings/base.py:158 +#: note_kfet/settings/base.py:153 msgid "French" msgstr "" @@ -500,18 +655,14 @@ msgstr "" msgid "The ENS Paris-Saclay BDE note." msgstr "La note du BDE de l'ENS Paris-Saclay." -#: templates/base.html:81 +#: templates/base.html:87 msgid "Clubs" msgstr "Clubs" -#: templates/base.html:84 +#: templates/base.html:92 msgid "Activities" msgstr "Activités" -#: templates/base.html:87 -msgid "Buttons" -msgstr "Boutons" - #: templates/cas_server/base.html:7 msgid "Central Authentication Service" msgstr "" @@ -569,11 +720,6 @@ msgstr "" msgid "Field filters" msgstr "" -#: templates/django_filters/rest_framework/form.html:5 -#: templates/member/club_form.html:10 -msgid "Submit" -msgstr "Envoyer" - #: templates/member/club_detail.html:10 msgid "Membership starts on" msgstr "L'adhésion commence le" @@ -620,15 +766,15 @@ msgstr "Ajouter un alias" #: templates/member/profile_detail.html:15 msgid "first name" -msgstr "" +msgstr "prénom" #: templates/member/profile_detail.html:18 msgid "username" -msgstr "" +msgstr "pseudo" #: templates/member/profile_detail.html:21 msgid "password" -msgstr "" +msgstr "mot de passe" #: templates/member/profile_detail.html:24 msgid "Change password" @@ -655,13 +801,13 @@ msgstr "Sauvegarder les changements" msgid "Sign up" msgstr "Inscription" -#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38 +#: templates/note/conso_form.html:28 templates/note/transaction_form.html:50 msgid "Select emitters" msgstr "Sélection des émetteurs" #: templates/note/conso_form.html:45 msgid "Select consumptions" -msgstr "Consommations" +msgstr "Sélection des consommations" #: templates/note/conso_form.html:51 msgid "Consume!" @@ -677,55 +823,59 @@ msgstr "Éditer" #: templates/note/conso_form.html:126 msgid "Single consumptions" -msgstr "Consos simples" +msgstr "Consommations simples" #: templates/note/conso_form.html:130 msgid "Double consumptions" -msgstr "Consos doubles" +msgstr "Consommations doubles" -#: templates/note/conso_form.html:141 +#: templates/note/conso_form.html:141 templates/note/transaction_form.html:152 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:15 +msgid "Gift" +msgstr "Don" -#: templates/note/transaction_form.html:63 +#: templates/note/transaction_form.html:68 +msgid "External payment" +msgstr "Paiement externe" + +#: templates/note/transaction_form.html:76 msgid "Transfer type" msgstr "Type de transfert" -#: templates/note/transaction_form.html:73 +#: templates/note/transaction_form.html:86 msgid "Name" msgstr "Nom" -#: templates/note/transaction_form.html:79 +#: templates/note/transaction_form.html:92 msgid "First name" msgstr "Prénom" -#: templates/note/transaction_form.html:85 +#: templates/note/transaction_form.html:98 msgid "Bank" msgstr "Banque" -#: templates/note/transaction_form.html:97 -#: templates/note/transaction_form.html:179 -#: templates/note/transaction_form.html:186 +#: templates/note/transaction_form.html:111 +#: templates/note/transaction_form.html:169 +#: templates/note/transaction_form.html:176 msgid "Select receivers" msgstr "Sélection des destinataires" -#: templates/note/transaction_form.html:114 +#: templates/note/transaction_form.html:128 msgid "Amount" msgstr "Montant" -#: templates/note/transaction_form.html:119 +#: templates/note/transaction_form.html:138 msgid "Reason" msgstr "Raison" -#: templates/note/transaction_form.html:193 +#: templates/note/transaction_form.html:183 msgid "Credit note" -msgstr "Note à créditer" +msgstr "Note à recharger" -#: templates/note/transaction_form.html:200 +#: templates/note/transaction_form.html:190 msgid "Debit note" msgstr "Note à débiter" @@ -733,6 +883,22 @@ msgstr "Note à débiter" msgid "Buttons list" msgstr "Liste des boutons" +#: templates/note/transactiontemplate_list.html:9 +msgid "search button" +msgstr "Chercher un bouton" + +#: templates/note/transactiontemplate_list.html:20 +msgid "buttons listing " +msgstr "Liste des boutons" + +#: templates/note/transactiontemplate_list.html:71 +msgid "button successfully deleted " +msgstr "Le bouton a bien été supprimé" + +#: templates/note/transactiontemplate_list.html:75 +msgid "Unable to delete button " +msgstr "Impossible de supprimer le bouton " + #: templates/registration/logged_out.html:8 msgid "Thanks for spending some quality time with the Web site today." msgstr "" @@ -742,7 +908,7 @@ msgid "Log in again" msgstr "" #: templates/registration/login.html:7 templates/registration/login.html:8 -#: templates/registration/login.html:26 +#: templates/registration/login.html:28 #: templates/registration/password_reset_complete.html:10 msgid "Log in" msgstr "" @@ -754,7 +920,15 @@ msgid "" "page. Would you like to login to a different account?" msgstr "" -#: templates/registration/login.html:27 +#: templates/registration/login.html:22 +msgid "You can also register via the central authentification server " +msgstr "" + +#: templates/registration/login.html:23 +msgid "using this link " +msgstr "" + +#: templates/registration/login.html:29 msgid "Forgotten your password or username?" msgstr "" @@ -810,3 +984,72 @@ msgstr "" #: templates/registration/password_reset_form.html:11 msgid "Reset my password" msgstr "" + +#: templates/treasury/invoice_form.html:6 +msgid "Invoices list" +msgstr "Liste des factures" + +#: templates/treasury/invoice_form.html:42 +msgid "Add product" +msgstr "Ajouter produit" + +#: templates/treasury/invoice_form.html:43 +msgid "Remove product" +msgstr "Retirer produit" + +#: templates/treasury/invoice_list.html:21 +msgid "New invoice" +msgstr "Nouvelle facture" + +#: templates/treasury/remittance_form.html:7 +msgid "Remittance #" +msgstr "Remise n°" + +#: templates/treasury/remittance_form.html:9 +#: templates/treasury/specialtransactionproxy_form.html:7 +msgid "Remittances list" +msgstr "Liste des remises" + +#: templates/treasury/remittance_form.html:12 +msgid "Count" +msgstr "Nombre" + +#: templates/treasury/remittance_form.html:29 +msgid "Linked transactions" +msgstr "Transactions liées" + +#: templates/treasury/remittance_form.html:34 +msgid "There is no transaction linked with this remittance." +msgstr "Il n'y a pas de transaction liée à cette remise." + +#: templates/treasury/remittance_list.html:19 +msgid "Opened remittances" +msgstr "Remises ouvertes" + +#: templates/treasury/remittance_list.html:24 +msgid "There is no opened remittance." +msgstr "Il n'y a pas de remise ouverte." + +#: templates/treasury/remittance_list.html:28 +msgid "New remittance" +msgstr "Nouvelle remise" + +#: templates/treasury/remittance_list.html:32 +msgid "Transfers without remittances" +msgstr "Transactions sans remise associée" + +#: templates/treasury/remittance_list.html:37 +msgid "There is no transaction without any linked remittance." +msgstr "Il n'y a pas de transactions sans remise associée." + +#: templates/treasury/remittance_list.html:43 +msgid "Transfers with opened remittances" +msgstr "Transactions associées à une remise ouverte" + +#: templates/treasury/remittance_list.html:48 +msgid "There is no transaction with an opened linked remittance." +msgstr "Il n'y a pas de transaction associée à une remise ouverte." + +#: templates/treasury/remittance_list.html:54 +msgid "Closed remittances" +msgstr "Remises fermées" diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 216199de..d49b2542 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -59,6 +59,7 @@ INSTALLED_APPS = [ 'activity', 'member', 'note', + 'treasury', 'permission', 'api', 'logs', diff --git a/note_kfet/urls.py b/note_kfet/urls.py index 9170c62e..40a9a614 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -15,6 +15,7 @@ urlpatterns = [ # Include project routers path('note/', include('note.urls')), + path('treasury/', include('treasury.urls')), # Include Django Contrib and Core routers path('i18n/', include('django.conf.urls.i18n')), diff --git a/requirements/production.txt b/requirements/production.txt index f0b52228..fe939cce 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -1 +1 @@ -psycopg2==2.8.4 +psycopg2-binary==2.8.4 diff --git a/static/img/Finalist.png b/static/img/Finalist.png new file mode 100644 index 00000000..1a3c41f3 Binary files /dev/null and b/static/img/Finalist.png differ diff --git a/static/img/Kataclist.png b/static/img/Kataclist.png new file mode 100644 index 00000000..97fc4115 Binary files /dev/null and b/static/img/Kataclist.png differ diff --git a/static/img/Listorique.png b/static/img/Listorique.png new file mode 100644 index 00000000..c5158324 Binary files /dev/null and b/static/img/Listorique.png differ diff --git a/static/img/Monopolist.png b/static/img/Monopolist.png new file mode 100644 index 00000000..2685b21e Binary files /dev/null and b/static/img/Monopolist.png differ diff --git a/static/img/Satellist.png b/static/img/Satellist.png new file mode 100644 index 00000000..d2377f67 Binary files /dev/null and b/static/img/Satellist.png differ diff --git a/static/js/alias.js b/static/js/alias.js new file mode 100644 index 00000000..267410da --- /dev/null +++ b/static/js/alias.js @@ -0,0 +1,37 @@ + + $("#alias_input").on('keypress',function(e) { + if(e.which == 13) { + $("#alias_submit").click(); + } + }); + + function create_alias(note_id){ + $.post("/api/note/alias/", + { + "csrfmiddlewaretoken": CSRF_TOKEN, + "name": $("#alias_input").val(), + "note": note_id + } + ).done(function(){ + $("#alias_table").load(location.href+ " #alias_table"); + addMsg("Alias ajouté","success"); + }) + .fail(function(xhr, textStatus, error){ + errMsg(xhr.responseJSON); + }); +} + // on click of button "delete" , call the API + function delete_button(button_id){ + $.ajax({ + url:"/api/note/alias/"+button_id+"/", + method:"DELETE", + headers: {"X-CSRFTOKEN": CSRF_TOKEN} + }) + .done(function(){ + addMsg('Alias supprimé','success'); + $("#alias_table").load(location.href + " #alias_table"); + }) + .fail(function(xhr,textStatus, error){ + errMsg(xhr.responseJSON); + }); + } diff --git a/static/js/consos.js b/static/js/consos.js index 896f996c..20859933 100644 --- a/static/js/consos.js +++ b/static/js/consos.js @@ -167,7 +167,7 @@ function reset() { 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, + consume(note_display.id, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount, button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id); }); }); @@ -176,6 +176,7 @@ function consumeAll() { /** * Create a new transaction from a button through the API. * @param source The note that paid the item (type: int) + * @param source_alias The alias used for the source (type: str) * @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) @@ -184,7 +185,7 @@ function consumeAll() { * @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) { +function consume(source, source_alias, dest, quantity, amount, reason, type, category, template) { $.post("/api/note/transaction/transaction/", { "csrfmiddlewaretoken": CSRF_TOKEN, @@ -195,12 +196,32 @@ function consume(source, dest, quantity, amount, reason, type, category, templat "polymorphic_ctype": type, "resourcetype": "RecurrentTransaction", "source": source, + "source_alias": source_alias, "destination": dest, "category": category, "template": template }, reset).fail(function (e) { - reset(); - - addMsg("Une erreur est survenue lors de la transaction : " + e.responseText, "danger"); + $.post("/api/note/transaction/transaction/", + { + "csrfmiddlewaretoken": CSRF_TOKEN, + "quantity": quantity, + "amount": amount, + "reason": reason, + "valid": false, + "invalidity_reason": "Solde insuffisant", + "polymorphic_ctype": type, + "resourcetype": "RecurrentTransaction", + "source": source, + "source_alias": source_alias, + "destination": dest, + "category": category, + "template": template + }).done(function() { + reset(); + addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", "danger"); + }).fail(function () { + reset(); + errMsg(e.responseJSON); + }); }); } diff --git a/static/js/dynamic-formset.js b/static/js/dynamic-formset.js index 87edfaae..c6ff3328 100644 --- a/static/js/dynamic-formset.js +++ b/static/js/dynamic-formset.js @@ -1,5 +1,5 @@ /** - * jQuery Formset 1.3-pre + * jQuery Formset 1.5-pre * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com) * @requires jQuery 1.2.6 or later * @@ -55,19 +55,26 @@ insertDeleteLink = function(row) { var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'), addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.'); - if (row.is('TR')) { + + var delButtonHTML = '' + options.deleteText +''; + if (options.deleteContainerClass) { + // If we have a specific container for the remove button, + // place it as the last child of that container: + row.find('[class*="' + options.deleteContainerClass + '"]').append(delButtonHTML); + } else if (row.is('TR')) { // If the forms are laid out in table rows, insert // the remove button into the last table cell: - row.children(':last').append('' + options.deleteText + ''); + row.children('td:last').append(delButtonHTML); } else if (row.is('UL') || row.is('OL')) { // If they're laid out as an ordered/unordered list, // insert an
  • after the last list item: - row.append('
  • ' + options.deleteText +'
  • '); + row.append('
  • ' + delButtonHTML + '
  • '); } else { // Otherwise, just insert the remove button as the // last child element of the form's container: - row.append('' + options.deleteText +''); + row.append(delButtonHTML); } + // Check if we're under the minimum number of forms - not to display delete link at rendering if (!showDeleteLinks()){ row.find('a.' + delCssSelector).hide(); @@ -156,6 +163,7 @@ } else { // Otherwise, use the last form in the formset; this works much better if you've got // extra (>= 1) forms (thnaks to justhamade for pointing this out): + if (options.hideLastAddForm) $('.' + options.formCssClass + ':last').hide(); template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id'); template.find('input:hidden[id $= "-DELETE"]').remove(); // Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion): @@ -173,21 +181,28 @@ // FIXME: Perhaps using $.data would be a better idea? options.formTemplate = template; - if ($$.is('TR')) { + var addButtonHTML = '' + options.addText + ''; + if (options.addContainerClass) { + // If we have a specific container for the "add" button, + // place it as the last child of that container: + var addContainer = $('[class*="' + options.addContainerClass + '"'); + addContainer.append(addButtonHTML); + addButton = addContainer.find('[class="' + options.addCssClass + '"]'); + } else if ($$.is('TR')) { // If forms are laid out as table rows, insert the // "add" button in a new table row: var numCols = $$.eq(0).children().length, // This is a bit of an assumption :| - buttonRow = $('' + options.addText + '') - .addClass(options.formCssClass + '-add'); + buttonRow = $('' + addButtonHTML + '').addClass(options.formCssClass + '-add'); $$.parent().append(buttonRow); - if (hideAddButton) buttonRow.hide(); addButton = buttonRow.find('a'); } else { // Otherwise, insert it immediately after the last form: - $$.filter(':last').after('' + options.addText + ''); + $$.filter(':last').after(addButtonHTML); addButton = $$.filter(':last').next(); - if (hideAddButton) addButton.hide(); } + + if (hideAddButton) addButton.hide(); + addButton.click(function() { var formCount = parseInt(totalForms.val()), row = options.formTemplate.clone(true).removeClass('formset-custom-template'), @@ -220,12 +235,15 @@ formTemplate: null, // The jQuery selection cloned to generate new form instances addText: 'add another', // Text for the add link deleteText: 'remove', // Text for the delete link - addCssClass: '', // CSS class applied to the add link - deleteCssClass: '', // CSS class applied to the delete link + addContainerClass: null, // Container CSS class for the add link + deleteContainerClass: null, // Container CSS class for the delete link + addCssClass: 'add-row', // CSS class applied to the add link + deleteCssClass: 'delete-row', // CSS class applied to the delete link formCssClass: 'dynamic-form', // CSS class applied to each form in a formset extraClasses: [], // Additional CSS classes, which will be applied to each form in turn keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned added: null, // Function called each time a new form is added - removed: null // Function called each time a form is deleted + removed: null, // Function called each time a form is deleted + hideLastAddForm: false // When set to true, hide last empty add form (becomes visible when clicking on add button) }; })(jQuery); diff --git a/static/js/transfer.js b/static/js/transfer.js index c615f932..cf62e453 100644 --- a/static/js/transfer.js +++ b/static/js/transfer.js @@ -39,10 +39,21 @@ $(document).ready(function() { last.quantity = 1; - $.getJSON("/api/user/" + last.note.user + "/", function(user) { - $("#last_name").val(user.last_name); - $("#first_name").val(user.first_name); - }); + if (!last.note.user) { + $.getJSON("/api/note/note/" + last.note.id + "/?format=json", function(note) { + last.note.user = note.user; + $.getJSON("/api/user/" + last.note.user + "/", function(user) { + $("#last_name").val(user.last_name); + $("#first_name").val(user.first_name); + }); + }); + } + else { + $.getJSON("/api/user/" + last.note.user + "/", function(user) { + $("#last_name").val(user.last_name); + $("#first_name").val(user.first_name); + }); + } } return true; @@ -72,19 +83,41 @@ $("#transfer").click(function() { "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, "resourcetype": "Transaction", "source": user_id, - "destination": dest.id - }, function () { + "destination": dest.id, + "destination_alias": dest.name + }).done(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"); + }).fail(function () { + $.post("/api/note/transaction/transaction/", + { + "csrfmiddlewaretoken": CSRF_TOKEN, + "quantity": dest.quantity, + "amount": 100 * $("#amount").val(), + "reason": $("#reason").val(), + "valid": false, + "invalidity_reason": "Solde insuffisant", + "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, + "resourcetype": "Transaction", + "source": user_id, + "destination": dest.id, + "destination_alias": dest.name + }).done(function () { + addMsg("Le transfert de " + + pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note " + + " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger"); - reset(); + 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(); + }); }); }); } @@ -101,19 +134,43 @@ $("#transfer").click(function() { "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, "resourcetype": "Transaction", "source": source.id, - "destination": dest.id - }, function () { + "source_alias": source.name, + "destination": dest.id, + "destination_alias": dest.name + }).done(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"); + $.post("/api/note/transaction/transaction/", + { + "csrfmiddlewaretoken": CSRF_TOKEN, + "quantity": source.quantity * dest.quantity, + "amount": 100 * $("#amount").val(), + "reason": $("#reason").val(), + "valid": false, + "invalidity_reason": "Solde insuffisant", + "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, + "resourcetype": "Transaction", + "source": source.id, + "source_alias": source.name, + "destination": dest.id, + "destination_alias": dest.name + }).done(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 échoué : Solde insuffisant", "danger"); - reset(); + 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(); + }); }); }); }); @@ -146,15 +203,17 @@ $("#transfer").click(function() { "polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE, "resourcetype": "SpecialTransaction", "source": source, + "source_alias": source.name, "destination": dest, + "destination_alias": dest.name, "last_name": $("#last_name").val(), "first_name": $("#first_name").val(), "bank": $("#bank").val() - }, function () { + }).done(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"); + addMsg("Le crédit/retrait a échoué : " + err.responseText, "danger"); reset(); }); } diff --git a/templates/base.html b/templates/base.html index fae86443..6a688fc9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -79,6 +79,11 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans 'Consumptions' %} {% endif %} + {% if "note.transaction"|not_empty_model_list %} + + {% endif %} {% if "member.club"|not_empty_model_list %} {% endif %} - {% if "note.transactiontemplate"|not_empty_model_change_list %} + {% if "treasury.invoice"|not_empty_model_change_list %} {% endif %} -