diff --git a/Dockerfile b/Dockerfile index d42bdd1f479c7155f33160d75271f54c1bd5ee6c..dfc49d04c604229621d73de77b553bb1fbc5a1ac 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 1ffe8793a27cdf3d1082a88a19df51e0662ac133..9b0c927ecb5768e50e953b4be206801c000c2b91 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 b275a0b87665464550f57ae7ea1dcdb943370183..67fdba309267bca584d7869b15d92341cf1ba2db 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 769650a0af14da5e3e42f955d2da0f0afa135173..bba1e7ac8de61ef1028e2eb99d306bc3032f62f2 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 cdbb933274e7cdccd378d956bef1480ffbf750f7..d0051e59d612019df3f4cb104857ba743c09e713 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 a6de17d2f3a0cc3406f928ee8eb4376cd5f13e66..d0c37a6e2e47baaa3e4b2f3eef1eebe152fdc5d1 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 d9dfd18153a14fecc9c9fc0c397350ac77050412..bc536f604834ce94f61d7c7def8743887d82f69a 100644 --- a/apps/member/urls.py +++ b/apps/member/urls.py @@ -12,6 +12,8 @@ urlpatterns = [ path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"), path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"), path('club/create/', views.ClubCreateView.as_view(), name="club_create"), + path('club/<int:pk>/update', views.ClubUpdateView.as_view(), name="club_update"), + path('club/<int:pk>/update_pic', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), path('user/', views.UserListView.as_view(), name="user_list"), path('user/<int:pk>', views.UserDetailView.as_view(), name="user_detail"), path('user/<int:pk>/update', views.UserUpdateView.as_view(), name="user_update_profile"), diff --git a/apps/member/views.py b/apps/member/views.py index 0ba76d6ac30cc077712846dc4faefeba976d0ba4..7d3ed748c1a34e3d1d0186f5543ef57566a7131c 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -223,10 +223,7 @@ class DeleteAliasView(LoginRequiredMixin, DeleteView): 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 +270,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 +332,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): """ @@ -365,6 +369,23 @@ class ClubDetailView(LoginRequiredMixin, DetailView): 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 form_class = MembershipForm diff --git a/apps/note/fixtures/initial.json b/apps/note/fixtures/initial.json index 63285e34b2c525243d3e851579db0f9ad40b10b3..efe37afa4244caf78f775be776150ca518fc461d 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" } }, { @@ -256,4 +241,4 @@ "name": "Alcool" } } -] +] \ No newline at end of file diff --git a/apps/note/models/__init__.py b/apps/note/models/__init__.py index 8f1921f9e8da3f04f775d59cab20d71de4eab25d..e9c8a0a9f28d15f586e4398a0f29406bdf1aec01 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/templatetags/pretty_money.py b/apps/note/templatetags/pretty_money.py index 265870a85aadd70d949f13cdeb990db5c0955a18..ba527f9be24c170cd0260f604e588d4f57fb8382 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/permission/admin.py b/apps/permission/admin.py index aaa6f66142b7c1b878312029fb2a64bf291d5e5c..4312f4b0158e2bf7c2fb22d9a0894b51e161838e 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 6087c83e774b1afca9f73fd39d24abd2d8738de4..965e82c928aa676555f4f8f47356284cbac6b0ff 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/models.py b/apps/permission/models.py index 109c1875adedcaa970a876da6298993badbd7851..205f5b418c7baf09254087c939a504ee919c7f0f 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 9f6d8cd22c6cc85f60b4c0dd005406d919283bcd..7097085f04670a33fbb65431cfdc046e7e00d832 100644 --- a/apps/permission/permissions.py +++ b/apps/permission/permissions.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from rest_framework.permissions import DjangoObjectPermissions + from .backends import PermissionBackend SAFE_METHODS = ('HEAD', 'OPTIONS', ) diff --git a/apps/permission/signals.py b/apps/permission/signals.py index aebca39db243d953d611c057da8a84505c133701..1e30f56f5793296e929b195231bb4a4eb4660d26 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 8f2a0006f097601b0959cb745f7ee5d2dcc398e9..8bcd359794f1721f84b133c9a3a2402ac788bcb9 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 0000000000000000000000000000000000000000..c9c6150edaa9526ab1476be7dc0e5c144ceabe1f --- /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 0000000000000000000000000000000000000000..abeec3e37681997023ffbfa2b7e7a857531d9420 --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/treasury/api/serializers.py b/apps/treasury/api/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..f1bbef75cfac7e2d714e7a3d83b073a5f39342a9 --- /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 0000000000000000000000000000000000000000..30ac00e1ae3a9f598634099c217d2f5b5bafee0a --- /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 0000000000000000000000000000000000000000..7a70fd2466379461d2cff980a882b5107d39ae0e --- /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 0000000000000000000000000000000000000000..e2873ea2d8cba8cefcb88feb19750790e639d425 --- /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 0000000000000000000000000000000000000000..143d2101bc00195201105623ce7d5d1528b7df2d --- /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 0000000000000000000000000000000000000000..caaa365fea3dfde0f7dda776f5872c34428333f7 --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/treasury/models.py b/apps/treasury/models.py new file mode 100644 index 0000000000000000000000000000000000000000..bcd89db964dbcdf9da9cdee7e30be23fd4c597eb --- /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 0000000000000000000000000000000000000000..54c19c09e0c18820435deef0505f2cf0b63be030 --- /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 0000000000000000000000000000000000000000..1ecc04db196ff8e94f45f60bbd5996a49c03e9e5 --- /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 0000000000000000000000000000000000000000..d44cc4145921fd63d3c645d67c39612e2103a54c --- /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/<int:pk>/', InvoiceUpdateView.as_view(), name='invoice_update'), + path('invoice/render/<int:pk>/', 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/<int:pk>/', RemittanceUpdateView.as_view(), name='remittance_update'), + path('remittance/link_transaction/<int:pk>/', LinkTransactionToRemittanceView.as_view(), name='link_transaction'), + path('remittance/unlink_transaction/<int:pk>/', UnlinkTransactionToRemittanceView.as_view(), + name='unlink_transaction'), +] diff --git a/apps/treasury/views.py b/apps/treasury/views.py new file mode 100644 index 0000000000000000000000000000000000000000..904405661ed5ceb87c25737d40624a87401817a4 --- /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 bc1a0d0335850ae5ca1246f1d1dfed2978679275..c9eda5aaff8f81f6fc84e4d7c4f9d96730fefa9f 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -447,72 +447,195 @@ msgstr "" msgid "Transfer" msgstr "" -#: apps/note/models/transactions.py:222 +#: apps/note/models/transactions.py:188 msgid "Template" msgstr "" -#: apps/note/models/transactions.py:237 +#: apps/note/models/transactions.py:203 msgid "first_name" msgstr "" -#: apps/note/models/transactions.py:242 +#: apps/note/models/transactions.py:208 msgid "bank" msgstr "" -#: apps/note/models/transactions.py:248 templates/note/transaction_form.html:24 +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24 msgid "Credit" msgstr "" -#: apps/note/models/transactions.py:248 templates/note/transaction_form.html:28 +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28 msgid "Debit" msgstr "" -#: apps/note/models/transactions.py:264 apps/note/models/transactions.py:269 +#: apps/note/models/transactions.py:230 apps/note/models/transactions.py:235 msgid "membership transaction" msgstr "" -#: apps/note/models/transactions.py:265 +#: apps/note/models/transactions.py:231 msgid "membership transactions" msgstr "" -#: apps/note/tables.py:57 -msgid "Click to invalidate" -msgstr "" - -#: apps/note/tables.py:57 -msgid "Click to validate" -msgstr "" - -#: apps/note/tables.py:93 -msgid "No reason specified" -msgstr "" - -#: apps/note/views.py:42 +#: apps/note/views.py:39 msgid "Transfer money" msgstr "" -#: apps/note/views.py:158 templates/base.html:79 +#: apps/note/views.py:145 templates/base.html:79 msgid "Consumptions" msgstr "" -#: apps/permission/models.py:70 apps/permission/models.py:263 +#: 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:72 apps/permission/models.py:265 +#: apps/permission/models.py:71 apps/permission/models.py:264 #, python-brace-format msgid "Can {type} {model} in {query}" msgstr "" -#: apps/permission/models.py:85 +#: apps/permission/models.py:84 msgid "rank" msgstr "" -#: apps/permission/models.py:148 +#: 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 " @@ -599,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 "" @@ -868,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 f7f1d1a4a9a2b9c0142d0640672bcf2528db8414..ca43d5a4ccfcddcc42fdf81678c08499904d693f 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -421,98 +421,217 @@ msgstr "alias utilisé" msgid "quantity" msgstr "quantité" -#: apps/note/models/transactions.py:130 +#: apps/note/models/transactions.py:115 msgid "reason" msgstr "raison" -#: apps/note/models/transactions.py:135 +#: apps/note/models/transactions.py:119 msgid "valid" msgstr "valide" -#: apps/note/models/transactions.py:140 apps/note/tables.py:95 -msgid "invalidity reason" -msgstr "Motif d'invalidité" - -#: apps/note/models/transactions.py:147 +#: apps/note/models/transactions.py:124 msgid "transaction" msgstr "transaction" -#: apps/note/models/transactions.py:148 +#: apps/note/models/transactions.py:125 msgid "transactions" msgstr "transactions" -#: apps/note/models/transactions.py:202 templates/base.html:83 +#: 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:222 +#: apps/note/models/transactions.py:188 msgid "Template" msgstr "Bouton" -#: apps/note/models/transactions.py:237 +#: apps/note/models/transactions.py:203 msgid "first_name" -msgstr "Prénom" +msgstr "prénom" -#: apps/note/models/transactions.py:242 +#: apps/note/models/transactions.py:208 msgid "bank" -msgstr "Banque" +msgstr "banque" -#: apps/note/models/transactions.py:248 templates/note/transaction_form.html:24 +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24 msgid "Credit" msgstr "Crédit" -#: apps/note/models/transactions.py:248 templates/note/transaction_form.html:28 +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28 msgid "Debit" -msgstr "Retrait" +msgstr "Débit" -#: apps/note/models/transactions.py:264 apps/note/models/transactions.py:269 +#: 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:265 +#: apps/note/models/transactions.py:231 msgid "membership transactions" msgstr "transactions d'adhésion" -#: apps/note/tables.py:57 -msgid "Click to invalidate" -msgstr "Cliquez pour dévalider" - -#: apps/note/tables.py:57 -msgid "Click to validate" -msgstr "Cliquez pour valider" - -#: apps/note/tables.py:93 -msgid "No reason specified" -msgstr "Pas de motif spécifié" - -#: apps/note/views.py:42 +#: apps/note/views.py:39 msgid "Transfer money" -msgstr "Transferts d'argent" +msgstr "Transférer de l'argent" -#: apps/note/views.py:158 templates/base.html:79 +#: apps/note/views.py:145 templates/base.html:79 msgid "Consumptions" msgstr "Consommations" -#: apps/permission/models.py:70 apps/permission/models.py:263 +#: 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:72 apps/permission/models.py:265 +#: apps/permission/models.py:71 apps/permission/models.py:264 #, python-brace-format msgid "Can {type} {model} in {query}" msgstr "" -#: apps/permission/models.py:85 +#: apps/permission/models.py:84 msgid "rank" -msgstr "rang" +msgstr "Rang" -#: apps/permission/models.py:148 +#: 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 " @@ -601,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" @@ -652,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" @@ -693,7 +807,7 @@ 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!" @@ -709,11 +823,11 @@ 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/transaction_form.html:152 msgid "Recent transactions history" @@ -725,7 +839,7 @@ msgstr "Don" #: templates/note/transaction_form.html:68 msgid "External payment" -msgstr "Paiement extérieur" +msgstr "Paiement externe" #: templates/note/transaction_form.html:76 msgid "Transfer type" @@ -759,7 +873,7 @@ msgstr "Raison" #: templates/note/transaction_form.html:183 msgid "Credit note" -msgstr "Note à créditer" +msgstr "Note à recharger" #: templates/note/transaction_form.html:190 msgid "Debit note" @@ -870,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 216199deab73fe40e70ca3b065d494e18434f67d..d49b25424258dc73b5e2ae336b88727c0f50f3b9 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 9170c62ed6c21eab048e8db366f240f194ff880f..40a9a614dee79fa0282ce43a2e2e30a3cd58027d 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 f0b5222826649e71d629934444582238919500d9..fe939cce41bb95dda07273e102a31bbca595af80 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 0000000000000000000000000000000000000000..1a3c41f3462ec962dd3fa9105db657f53f5052a3 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 0000000000000000000000000000000000000000..97fc411506c37a288eaef8c627ee26f06c8c2706 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 0000000000000000000000000000000000000000..c515832478403494c63c030eb199f581dec56da4 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 0000000000000000000000000000000000000000..2685b21e23e44a7782d3f7c4a652a9a84f1dc86c 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 0000000000000000000000000000000000000000..d2377f670bf5b18e1e2c428394c9958d2e9379f5 Binary files /dev/null and b/static/img/Satellist.png differ diff --git a/static/js/base.js b/static/js/base.js index 81f9f323e0d73c19e6cee034871c7394e8fad38b..422c2b6f68947c66b8be6002be396597c22ec7ce 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -62,11 +62,12 @@ function li(id, text) { */ function displayNote(note, alias, user_note_field=null, profile_pic_field=null) { if (!note.display_image) { - note.display_image = 'https://nk20.ynerant.fr/media/pic/default.png'; + note.display_image = '/media/pic/default.png'; $.getJSON("/api/note/note/" + note.id + "/?format=json", function(new_note) { note.display_image = new_note.display_image.replace("http:", "https:"); note.name = new_note.name; note.balance = new_note.balance; + note.user = new_note.user; displayNote(note, alias, user_note_field, profile_pic_field); }); @@ -151,10 +152,13 @@ function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes let old_pattern = null; - // When the user type "Enter", the first alias is clicked + // When the user type "Enter", the first alias is clicked, and the informations are displayed field.keypress(function(event) { - if (event.originalEvent.charCode === 13) - $("#" + alias_matched_id + " li").first().trigger("click"); + if (event.originalEvent.charCode === 13) { + let li_obj = $("#" + alias_matched_id + " li").first(); + displayNote(notes[0], li_obj.text(), user_note_field, profile_pic_field); + li_obj.trigger("click"); + } }); // When the user type something, the matched aliases are refreshed diff --git a/static/js/dynamic-formset.js b/static/js/dynamic-formset.js index 87edfaaeb38e7ed4cd2f06fe0e9f48939b706b86..c6ff3328d5aa0923d732f8599fe929c6ccb2895c 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 = '<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>'; + 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('<a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + '</a>'); + 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 <li> after the last list item: - row.append('<li><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a></li>'); + row.append('<li>' + delButtonHTML + '</li>'); } else { // Otherwise, just insert the remove button as the // last child element of the form's container: - row.append('<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>'); + 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 = '<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>'; + 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 = $('<tr><td colspan="' + numCols + '"><a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a></tr>') - .addClass(options.formCssClass + '-add'); + buttonRow = $('<tr><td colspan="' + numCols + '">' + addButtonHTML + '</tr>').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('<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>'); + $$.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 a23c6e3880af112f74aea7f8f6f92ec92acf07d5..cf62e45376ec9f201964cf71332055f29008b01a 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; diff --git a/templates/base.html b/templates/base.html index 2f07a6cc209cb330efc13b796814fd559490ce62..62fc9c58c8732a692cfa201cc2145b0eece7940c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -92,6 +92,11 @@ SPDX-License-Identifier: GPL-3.0-or-later <a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a> </li> {% endif %} + {% if "treasury.invoice"|not_empty_model_change_list %} + <li class="nav-item active"> + <a class="nav-link" href="{% url 'treasury:invoice_list' %}"><i class="fa fa-money"></i>{% trans 'Treasury' %} </a> + </li> + {% endif %} </ul> <ul class="navbar-nav ml-auto"> {% if user.is_authenticated %} diff --git a/templates/member/club_detail.html b/templates/member/club_detail.html index 38f8781259be7c99291fc6434220dc816f2adb26..979c08971448746e22dccff52ddc2313d1d48315 100644 --- a/templates/member/club_detail.html +++ b/templates/member/club_detail.html @@ -1,62 +1,9 @@ -{% extends "base.html" %} -{% load static %} -{% load i18n %} -{% load render_table from django_tables2 %} -{% load pretty_money %} -{% block content %} -<p><a class="btn btn-primary" href="{% url 'member:club_list' %}">Clubs</a></p> -<h3 class="text-center"> Club {{ object.name }}</h3> -<dl> - <dt>{% trans 'Membership starts on' %}</dt> - <dd>{{ club.membership_start }}</dd> - <dt>{% trans 'Membership ends on' %}</dt> - <dd>{{ club.membership_end }}</dd> - <dt>{% trans 'Membership duration' %}</dt> - <dd>{{ club.membership_duration }}</dd> - <dt> Aliases </dt> - <dd>{{ club.note.aliases_set.all }}</dd> - <dt>{% trans 'balance' %}</dt> - <dd>{{ club.note.balance | pretty_money }}</dd> +{% extends "member/noteowner_detail.html" %} -</dl> +{% block profile_info %} +{% include "member/club_info.html" %} +{% endblock %} - -<div class="btn-group" role="group"> - <a class="btn btn-primary" href="{% url 'member:club_add_member' pk=object.pk %}"> Ajouter des membres </a> - <a class="btn btn-primary" href="{% url 'member:club_add_member' pk=object.pk %}"> Modifier les informations </a> - <a class="btn btn-primary" href="{% url 'member:club_add_member' pk=object.pk %}"> Ajouter des roles </a> -</div> - -<div class="accordion" id="accordionExample"> - <div class="card"> - <div class="card-header" id="headingOne"> - <h5 class="mb-0"> - <button class="btn btn-link" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne"> - <i class="fa fa-users"></i> Membres du club - </button> - </h5> - </div> - - <div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordionExample"> - <div class="card-body"> - - {% render_table member_list %} - </div> - </div> - </div> - <div class="card"> - <div class="card-header" id="headingTwo"> - <h5 class="mb-0"> - <button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"> - <i class="fa fa-euro"></i> {% trans "Transaction history" %} - </button> - </h5> - </div> - <div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionExample"> - <div class="card-body"> - {% render_table history_list %} - </div> - </div> - </div> -</div> - {% endblock %} +{% block profile_content %} +{% include "member/club_tables.html" %} +{% endblock %} diff --git a/templates/member/club_form.html b/templates/member/club_form.html index 577297bbc12eb39b0f0457ae8c65945ffc71750f..99c254e3a2d550f39208a5f070fb7f7a8a328735 100644 --- a/templates/member/club_form.html +++ b/templates/member/club_form.html @@ -3,7 +3,6 @@ {% load i18n %} {% load crispy_forms_tags %} {% block content %} -<p><a class="btn btn-default" href="{% url 'note:template_list' %}">{% trans "Clubs list" %}</a></p> <form method="post"> {% csrf_token %} {{form|crispy}} diff --git a/templates/member/club_info.html b/templates/member/club_info.html new file mode 100644 index 0000000000000000000000000000000000000000..a88527fcac1af3580d87462f27a277c9b2f75473 --- /dev/null +++ b/templates/member/club_info.html @@ -0,0 +1,32 @@ +{% load i18n static pretty_money %} +<div class="card bg-light shadow"> + <div class="card-top text-center"> + <a href="{% url 'member:club_update_pic' club.pk %}"> + <img src="{{ club.note.display_image.url }}" class="img-thumbnail mt-2" > + </a> + </div> + <div class="card-body" id="profile_infos"> + <dl class="row"> + <dt class="col-xl-6">{% trans 'name'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.name}}</dd> + + <dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.membership_start }}</dd> + + <dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.membership_end }}</dd> + + <dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.membership_duration }}</dd> + + <dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.membership_fee|pretty_money }}</dd> + + <dt class="col-xl-6"><a href="{% url 'member:user_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt> + <dd class="col-xl-6 text-truncate">{{ object.note.alias_set.all|join:", " }}</dd> + + <dt class="col-xl-3">{% trans 'email'|capfirst %}</dt> + <dd class="col-xl-9"><a href="mailto:{{ club.email }}">{{ club.email}}</a></dd> + </dl> + </div> +</div> diff --git a/templates/member/club_list.html b/templates/member/club_list.html index 165711136329c1a8ccbdf12880c33499541c0436..7f0b02a10f035c4c3ad3aef2089415f163028594 100644 --- a/templates/member/club_list.html +++ b/templates/member/club_list.html @@ -2,15 +2,65 @@ {% load render_table from django_tables2 %} {% load i18n %} {% block content %} - -{% render_table table %} - -<a class="btn btn-primary" href="{% url 'member:club_create' %}">{% trans "New club" %}</a> +<div class="row justify-content-center mb-4"> + <div class="col-md-10 text-center"> + <h4> + {% trans "search clubs" %} + </h4> + <input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved();return(false);" id="search_field"/> + <hr> + <a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Créer un club" %}</a> + </div> +</div> +<div class="row justify-content-center"> + <div class="col-md-10"> + <div class="card card-border shadow"> + <div class="card-header text-center"> + <h5> {% trans "club listing "%}</h5> + </div> + <div class="card-body px-0 py-0" id="club_table"> + {% render_table table %} + </div> + </div> + </div> +</div> {% endblock %} {% block extrajavascript %} <script type="text/javascript"> +function getInfo() { + var asked = $("#search_field").val(); + /* on ne fait la requête que si on a au moins un caractère pour chercher */ + var sel = $(".table-row"); + if (asked.length >= 1) { + $.getJSON("/api/members/club/?format=json&search="+asked, function(buttons){ + let selected_id = buttons.results.map((a => "#row-"+a.id)); + console.log(selected_id.join()); + $(".table-row,"+selected_id.join()).show(); + $(".table-row").not(selected_id.join()).hide(); + + }); + }else{ + // show everything + $('table tr').show(); + } +} +var timer; +var timer_on; +/* Fontion appelée quand le texte change (délenche le timer) */ +function search_field_moved(secondfield) { + if (timer_on) { // Si le timer a déjà été lancé, on réinitialise le compteur. + clearTimeout(timer); + timer = setTimeout("getInfo(" + secondfield + ")", 300); + } + else { // Sinon, on le lance et on enregistre le fait qu'il tourne. + timer = setTimeout("getInfo(" + secondfield + ")", 300); + timer_on = true; + } +} + +// clickable row $(document).ready(function($) { $(".table-row").click(function() { window.document.location = $(this).data("href"); diff --git a/templates/member/club_picture_update.html b/templates/member/club_picture_update.html new file mode 100644 index 0000000000000000000000000000000000000000..70f5cf4ac0f99c1f729fde535090fbda67821984 --- /dev/null +++ b/templates/member/club_picture_update.html @@ -0,0 +1,10 @@ +{% extends "member/club_detail.html" %} +{% load i18n static pretty_money django_tables2 crispy_forms_tags %} + +{% block profile_info %} +{% include "member/club_info.html" %} +{% endblock%} + +{% block profile_content%} +{% include "member/picture_update.html" %} +{% endblock%} diff --git a/templates/member/club_tables.html b/templates/member/club_tables.html new file mode 100644 index 0000000000000000000000000000000000000000..fbded9c3b194088e74b178ba20f220af127a0f43 --- /dev/null +++ b/templates/member/club_tables.html @@ -0,0 +1,31 @@ +{% load render_table from django_tables2 %} +{% load i18n %} +<div class="accordion shadow" id="accordionProfile"> + <div class="card"> + <div class="card-header position-relative" id="clubListHeading"> + <a class="btn btn-link stretched-link font-weight-bold" + data-toggle="collapse" data-target="#clubListCollapse" + aria-expanded="true" aria-controls="clubListCollapse"> + <i class="fa fa-users"></i> {% trans "Member of the Club" %} + </a> + </div> + <div id="clubListCollapse" class="collapse show" style="overflow:auto hidden" aria-labelledby="clubListHeading" data-parent="#accordionProfile"> + {% render_table member_list %} + </div> + </div> + + <div class="card"> + <div class="card-header position-relative" id="historyListHeading"> + <a class="btn btn-link stretched-link collapsed font-weight-bold" + data-toggle="collapse" data-target="#historyListCollapse" + aria-expanded="false" aria-controls="historyListCollapse"> + <i class="fa fa-euro"></i> {% trans "Transaction history" %} + </a> + </div> + <div id="historyListCollapse" class="collapse" style="overflow:auto hidden" aria-labelledby="historyListHeading" data-parent="#accordionProfile"> + <div id="history_list"> + {% render_table history_list %} + </div> + </div> + </div> +</div> diff --git a/templates/member/noteowner_detail.html b/templates/member/noteowner_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..ad329aee5e48b3fb66a9bd0c80cdf59403f609a4 --- /dev/null +++ b/templates/member/noteowner_detail.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load render_table from django_tables2 %} +{% load pretty_money %} + +{% block content %} +<div class="row mt-4"> + <div class="col-md-3 mb-4"> + {% block profile_info %} + {% endblock %} + </div> + <div class="col-md-9"> + {% block profile_content %} + {% endblock %} + </div> +</div> +{% endblock %} + +{% block extrajavascript %} + <script> + function refreshhistory() { + $("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list"); + $("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos"); + } + </script> +{% endblock %} diff --git a/templates/member/picture_update.html b/templates/member/picture_update.html new file mode 100644 index 0000000000000000000000000000000000000000..f0c43e47c52c659e2d308a48647486a0b7db26fc --- /dev/null +++ b/templates/member/picture_update.html @@ -0,0 +1,95 @@ +{% load i18n crispy_forms_tags %} +{% block profile_content %} +<div class="text-center"> +<form method="post" enctype="multipart/form-data" id="formUpload"> + {% csrf_token %} + {{ form |crispy }} +</form> +</div> +<!-- MODAL TO CROP THE IMAGE --> +<div class="modal fade" id="modalCrop"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-body"> + <img src="" id="modal-image" style="max-width: 100%;"> + </div> + <div class="modal-footer"> + <div class="btn-group pull-left" role="group"> + <button type="button" class="btn btn-default" id="js-zoom-in"> + <span class="glyphicon glyphicon-zoom-in"></span> + </button> + <button type="button" class="btn btn-default js-zoom-out"> + <span class="glyphicon glyphicon-zoom-out"></span> + </button> + </div> + <button type="button" class="btn btn-default" data-dismiss="modal">Nevermind</button> + <button type="button" class="btn btn-primary js-crop-and-upload">Crop and upload</button> + </div> + </div> + </div> +</div> +{% endblock %} +{% block extracss %} + <link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.min.css" rel="stylesheet"> +{% endblock %} + +{% block extrajavascript%} + <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.min.js"></script> + <script src="https://cdn.jsdelivr.net/npm/jquery-cropper@1.0.1/dist/jquery-cropper.min.js"></script> + <script> + $(function () { + + /* SCRIPT TO OPEN THE MODAL WITH THE PREVIEW */ + $("#id_image").change(function (e) { + if (this.files && this.files[0]) { + var reader = new FileReader(); + reader.onload = function (e) { + $("#modal-image").attr("src", e.target.result); + $("#modalCrop").modal("show"); + } + reader.readAsDataURL(this.files[0]); + } + }); + + /* SCRIPTS TO HANDLE THE CROPPER BOX */ + var $image = $("#modal-image"); + var cropBoxData; + var canvasData; + $("#modalCrop").on("shown.bs.modal", function () { + $image.cropper({ + viewMode: 1, + aspectRatio: 1/1, + minCropBoxWidth: 200, + minCropBoxHeight: 200, + ready: function () { + $image.cropper("setCanvasData", canvasData); + $image.cropper("setCropBoxData", cropBoxData); + } + }); + }).on("hidden.bs.modal", function () { + cropBoxData = $image.cropper("getCropBoxData"); + canvasData = $image.cropper("getCanvasData"); + $image.cropper("destroy"); + }); + + $(".js-zoom-in").click(function () { + $image.cropper("zoom", 0.1); + }); + + $(".js-zoom-out").click(function () { + $image.cropper("zoom", -0.1); + }); + + /* SCRIPT TO COLLECT THE DATA AND POST TO THE SERVER */ + $(".js-crop-and-upload").click(function () { + var cropData = $image.cropper("getData"); + $("#id_x").val(cropData["x"]); + $("#id_y").val(cropData["y"]); + $("#id_height").val(cropData["height"]); + $("#id_width").val(cropData["width"]); + $("#formUpload").submit(); + }); + + }); + </script> +{% endblock %} diff --git a/templates/member/profile_detail.html b/templates/member/profile_detail.html index 31510acfb8f8d71511880fa3126cb62f0665dcf5..42d03d8b2c26a1ca859b6acd4cb16944721d51e2 100644 --- a/templates/member/profile_detail.html +++ b/templates/member/profile_detail.html @@ -1,97 +1,9 @@ -{% extends "base.html" %} -{% load i18n static pretty_money django_tables2 %} +{% extends "member/noteowner_detail.html" %} -{% block content %} -<div class="row mt-4"> - <div class="col-md-3 mb-4"> - <div class="card bg-light shadow"> - <div class="card-top text-center"> - <a href="{% url 'member:user_update_pic' object.pk %}"> - <img src="{{ object.note.display_image.url }}" class="img-thumbnail mt-2" > - </a> - </div> - <div class="card-body" id="profile_infos"> - <dl class="row"> - <dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt> - <dd class="col-xl-6">{{ object.last_name }} {{ object.first_name }}</dd> - - <dt class="col-xl-6">{% trans 'username'|capfirst %}</dt> - <dd class="col-xl-6">{{ object.username }}</dd> - - <dt class="col-xl-6">{% trans 'password'|capfirst %}</dt> - <dd class="col-xl-6"> - <a class="small" href="{% url 'password_change' %}"> - {% trans 'Change password' %} - </a> - </dd> - - <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> - <dd class="col-xl-6">{{ object.profile.section }}</dd> - - <dt class="col-xl-6">{% trans 'address'|capfirst %}</dt> - <dd class="col-xl-6">{{ object.profile.address }}</dd> - - <dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt> - <dd class="col-xl-6">{{ object.note.balance | pretty_money }}</dd> - - <dt class="col-xl-6"> <a href="{% url 'member:user_alias' object.pk %}">{% trans 'aliases'|capfirst %}</a></dt> - <dd class="col-xl-6 text-truncate">{{ object.note.alias_set.all|join:", " }}</dd> - </dl> - - {% if object.pk == user.pk %} - <a class="small" href="{% url 'member:auth_token' %}">{% trans 'Manage auth token' %}</a> - {% endif %} - </div> - <div class="card-footer text-center"> - <a class="btn btn-primary btn-sm" href="{% url 'member:user_update_profile' object.pk %}">{% trans 'Update Profile' %}</a> - {% url 'member:user_detail' object.pk as user_profile_url %} - {%if request.get_full_path != user_profile_url %} - <a class="btn btn-primary btn-sm" href="{{ user_profile_url }}">{% trans 'View Profile' %}</a> - {% endif %} - </div> - </div> - </div> - <div class="col-md-9"> - {% block profile_content %} - <div class="accordion shadow" id="accordionProfile"> - <div class="card"> - <div class="card-header position-relative" id="clubListHeading"> - <a class="btn btn-link stretched-link font-weight-bold" - data-toggle="collapse" data-target="#clubListCollapse" - aria-expanded="true" aria-controls="clubListCollapse"> - <i class="fa fa-users"></i> {% trans "View my memberships" %} - </a> - </div> - <div id="clubListCollapse" class="collapse show" style="overflow:auto hidden" aria-labelledby="clubListHeading" data-parent="#accordionProfile"> - {% render_table club_list %} - </div> - </div> - - <div class="card"> - <div class="card-header position-relative" id="historyListHeading"> - <a class="btn btn-link stretched-link collapsed font-weight-bold" - data-toggle="collapse" data-target="#historyListCollapse" - aria-expanded="false" aria-controls="historyListCollapse"> - <i class="fa fa-euro"></i> {% trans "Transaction history" %} - </a> - </div> - <div id="historyListCollapse" class="collapse" style="overflow:auto hidden" aria-labelledby="historyListHeading" data-parent="#accordionProfile"> - <div id="history_list"> - {% render_table history_list %} - </div> - </div> - </div> - </div> - {% endblock %} - </div> -</div> +{% block profile_info %} +{% include "member/profile_info.html" %} {% endblock %} -{% block extrajavascript %} - <script> - function refreshHistory() { - $("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list"); - $("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos"); - } - </script> +{% block profile_content %} +{% include "member/profile_tables.html" %} {% endblock %} diff --git a/templates/member/profile_info.html b/templates/member/profile_info.html new file mode 100644 index 0000000000000000000000000000000000000000..3038386650422cd85f05333a01b7f5950a7bd41a --- /dev/null +++ b/templates/member/profile_info.html @@ -0,0 +1,48 @@ +{% load i18n static pretty_money %} + +<div class="card bg-light shadow"> + <div class="card-top text-center"> + <a href="{% url 'member:user_update_pic' object.pk %}"> + <img src="{{ object.note.display_image.url }}" class="img-thumbnail mt-2" > + </a> + </div> + <div class="card-body" id="profile_infos"> + <dl class="row"> + <dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt> + <dd class="col-xl-6">{{ object.last_name }} {{ object.first_name }}</dd> + + <dt class="col-xl-6">{% trans 'username'|capfirst %}</dt> + <dd class="col-xl-6">{{ object.username }}</dd> + + <dt class="col-xl-6">{% trans 'password'|capfirst %}</dt> + <dd class="col-xl-6"> + <a class="small" href="{% url 'password_change' %}"> + {% trans 'Change password' %} + </a> + </dd> + + <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> + <dd class="col-xl-6">{{ object.profile.section }}</dd> + + <dt class="col-xl-6">{% trans 'address'|capfirst %}</dt> + <dd class="col-xl-6">{{ object.profile.address }}</dd> + + <dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt> + <dd class="col-xl-6">{{ object.note.balance | pretty_money }}</dd> + + <dt class="col-xl-6"> <a href="{% url 'member:user_alias' object.pk %}">{% trans 'aliases'|capfirst %}</a></dt> + <dd class="col-xl-6 text-truncate">{{ object.note.alias_set.all|join:", " }}</dd> + </dl> + + {% if object.pk == user.pk %} + <a class="small" href="{% url 'member:auth_token' %}">{% trans 'Manage auth token' %}</a> + {% endif %} + </div> + <div class="card-footer text-center"> + <a class="btn btn-primary btn-sm" href="{% url 'member:user_update_profile' object.pk %}">{% trans 'Update Profile' %}</a> + {% url 'member:user_detail' object.pk as user_profile_url %} + {%if request.get_full_path != user_profile_url %} + <a class="btn btn-primary btn-sm" href="{{ user_profile_url }}">{% trans 'View Profile' %}</a> + {% endif %} + </div> +</div> diff --git a/templates/member/profile_picture_update.html b/templates/member/profile_picture_update.html index 36e53dcd3444f40407b56200a92a12f761220902..db7c5767970a285c0a703240254b967090c1d073 100644 --- a/templates/member/profile_picture_update.html +++ b/templates/member/profile_picture_update.html @@ -1,97 +1,10 @@ -{% extends "member/profile_detail.html" %} +{% extends "member/noteowner_detail.html" %} {% load i18n static pretty_money django_tables2 crispy_forms_tags %} -{% block profile_content %} -<div class="text-center"> -<form method="post" enctype="multipart/form-data" id="formUpload"> - {% csrf_token %} - {{ form |crispy }} -</form> -</div> -<!-- MODAL TO CROP THE IMAGE --> -<div class="modal fade" id="modalCrop"> - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-body"> - <img src="" id="modal-image" style="max-width: 100%;"> - </div> - <div class="modal-footer"> - <div class="btn-group pull-left" role="group"> - <button type="button" class="btn btn-default" id="js-zoom-in"> - <span class="glyphicon glyphicon-zoom-in"></span> - </button> - <button type="button" class="btn btn-default js-zoom-out"> - <span class="glyphicon glyphicon-zoom-out"></span> - </button> - </div> - <button type="button" class="btn btn-default" data-dismiss="modal">Nevermind</button> - <button type="button" class="btn btn-primary js-crop-and-upload">Crop and upload</button> - </div> - </div> - </div> -</div> -{% endblock %} -{% block extracss %} - <link href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.min.css" rel="stylesheet"> -{% endblock %} +{% block profile_info %} +{% include "member/profile_info.html" %} +{% endblock%} -{% block extrajavascript%} - <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.min.js"></script> - <script src="https://cdn.jsdelivr.net/npm/jquery-cropper@1.0.1/dist/jquery-cropper.min.js"></script> - <script> - $(function () { - - /* SCRIPT TO OPEN THE MODAL WITH THE PREVIEW */ - $("#id_image").change(function (e) { - if (this.files && this.files[0]) { - var reader = new FileReader(); - reader.onload = function (e) { - $("#modal-image").attr("src", e.target.result); - $("#modalCrop").modal("show"); - } - reader.readAsDataURL(this.files[0]); - } - }); - - /* SCRIPTS TO HANDLE THE CROPPER BOX */ - var $image = $("#modal-image"); - var cropBoxData; - var canvasData; - $("#modalCrop").on("shown.bs.modal", function () { - $image.cropper({ - viewMode: 1, - aspectRatio: 1/1, - minCropBoxWidth: 200, - minCropBoxHeight: 200, - ready: function () { - $image.cropper("setCanvasData", canvasData); - $image.cropper("setCropBoxData", cropBoxData); - } - }); - }).on("hidden.bs.modal", function () { - cropBoxData = $image.cropper("getCropBoxData"); - canvasData = $image.cropper("getCanvasData"); - $image.cropper("destroy"); - }); - - $(".js-zoom-in").click(function () { - $image.cropper("zoom", 0.1); - }); - - $(".js-zoom-out").click(function () { - $image.cropper("zoom", -0.1); - }); - - /* SCRIPT TO COLLECT THE DATA AND POST TO THE SERVER */ - $(".js-crop-and-upload").click(function () { - var cropData = $image.cropper("getData"); - $("#id_x").val(cropData["x"]); - $("#id_y").val(cropData["y"]); - $("#id_height").val(cropData["height"]); - $("#id_width").val(cropData["width"]); - $("#formUpload").submit(); - }); - - }); - </script> -{% endblock %} +{% block profile_content%} +{% include "member/picture_update.html" %} +{% endblock%} diff --git a/templates/member/profile_tables.html b/templates/member/profile_tables.html new file mode 100644 index 0000000000000000000000000000000000000000..9d2c687f23274092472660dcbdbe0adb01654738 --- /dev/null +++ b/templates/member/profile_tables.html @@ -0,0 +1,31 @@ +{% load render_table from django_tables2 %} +{% load i18n %} +<div class="accordion shadow" id="accordionProfile"> + <div class="card"> + <div class="card-header position-relative" id="clubListHeading"> + <a class="btn btn-link stretched-link font-weight-bold" + data-toggle="collapse" data-target="#clubListCollapse" + aria-expanded="true" aria-controls="clubListCollapse"> + <i class="fa fa-users"></i> {% trans "View my memberships" %} + </a> + </div> + <div id="clubListCollapse" class="collapse show" style="overflow:auto hidden" aria-labelledby="clubListHeading" data-parent="#accordionProfile"> + {% render_table club_list %} + </div> + </div> + + <div class="card"> + <div class="card-header position-relative" id="historyListHeading"> + <a class="btn btn-link stretched-link collapsed font-weight-bold" + data-toggle="collapse" data-target="#historyListCollapse" + aria-expanded="false" aria-controls="historyListCollapse"> + <i class="fa fa-euro"></i> {% trans "Transaction history" %} + </a> + </div> + <div id="historyListCollapse" class="collapse" style="overflow:auto hidden" aria-labelledby="historyListHeading" data-parent="#accordionProfile"> + <div id="history_list"> + {% render_table history_list %} + </div> + </div> + </div> +</div> diff --git a/templates/treasury/invoice_form.html b/templates/treasury/invoice_form.html new file mode 100644 index 0000000000000000000000000000000000000000..0edcbdcdcbd8e55d66cc66c2a58e825664d0fd77 --- /dev/null +++ b/templates/treasury/invoice_form.html @@ -0,0 +1,107 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load crispy_forms_tags pretty_money %} +{% block content %} + <p><a class="btn btn-default" href="{% url 'treasury:invoice_list' %}">{% trans "Invoices list" %}</a></p> + <form method="post" action=""> + {% csrf_token %} + {# Render the invoice form #} + {% crispy form %} + {# The next part concerns the product formset #} + {# Generate some hidden fields that manage the number of products, and make easier the parsing #} + {{ formset.management_form }} + <table class="table table-condensed table-striped"> + {# Fill initial data #} + {% for form in formset %} + {% if forloop.first %} + <thead> + <tr> + <th>{{ form.designation.label }}<span class="asteriskField">*</span></th> + <th>{{ form.quantity.label }}<span class="asteriskField">*</span></th> + <th>{{ form.amount.label }}<span class="asteriskField">*</span></th> + </tr> + </thead> + <tbody id="form_body"> + {% endif %} + <tr class="row-formset"> + <td>{{ form.designation }}</td> + <td>{{ form.quantity }} </td> + <td> + {# Use custom input for amount, with the € symbol #} + <div class="input-group"> + <input type="number" name="product_set-{{ forloop.counter0 }}-amount" step="0.01" + id="id_product_set-{{ forloop.counter0 }}-amount" + value="{{ form.instance.amount|cents_to_euros }}"> + <div class="input-group-append"> + <span class="input-group-text">€</span> + </div> + </div> + </td> + {# These fields are hidden but handled by the formset to link the id and the invoice id #} + {{ form.invoice }} + {{ form.id }} + </tr> + {% endfor %} + </tbody> + </table> + + {# Display buttons to add and remove products #} + <div class="btn-group btn-block" role="group"> + <button type="button" id="add_more" class="btn btn-primary">{% trans "Add product" %}</button> + <button type="button" id="remove_one" class="btn btn-danger">{% trans "Remove product" %}</button> + </div> + + <div class="btn-block"> + <button type="submit" class="btn btn-block btn-primary">{% trans "Submit" %}</button> + </div> + </form> + + <div id="empty_form" style="display: none;"> + {# Hidden div that store an empty product form, to be copied into new forms #} + <table class='no_error'> + <tbody id="for_real"> + <tr class="row-formset"> + <td>{{ formset.empty_form.designation }}</td> + <td>{{ formset.empty_form.quantity }} </td> + <td> + <div class="input-group"> + <input type="number" name="product_set-__prefix__-amount" step="0.01" + id="id_product_set-__prefix__-amount"> + <div class="input-group-append"> + <span class="input-group-text">€</span> + </div> + </div> + </td> + {{ formset.empty_form.invoice }} + {{ formset.empty_form.id }} + </tr> + </tbody> + </table> + </div> +{% endblock %} + +{% block extrajavascript %} + <script> + {# Script that handles add and remove lines #} + IDS = {}; + + $("#id_product_set-TOTAL_FORMS").val($(".row-formset").length - 1); + + $('#add_more').click(function () { + var form_idx = $('#id_product_set-TOTAL_FORMS').val(); + $('#form_body').append($('#for_real').html().replace(/__prefix__/g, form_idx)); + $('#id_product_set-TOTAL_FORMS').val(parseInt(form_idx) + 1); + $('#id_product_set-' + parseInt(form_idx) + '-id').val(IDS[parseInt(form_idx)]); + }); + + $('#remove_one').click(function () { + let form_idx = $('#id_product_set-TOTAL_FORMS').val(); + if (form_idx > 0) { + IDS[parseInt(form_idx) - 1] = $('#id_product_set-' + (parseInt(form_idx) - 1) + '-id').val(); + $('#form_body tr:last-child').remove(); + $('#id_product_set-TOTAL_FORMS').val(parseInt(form_idx) - 1); + } + }); + </script> +{% endblock %} diff --git a/templates/treasury/invoice_list.html b/templates/treasury/invoice_list.html new file mode 100644 index 0000000000000000000000000000000000000000..f14d278dd0375528512d84fb5d0e00c8dfbff214 --- /dev/null +++ b/templates/treasury/invoice_list.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% load render_table from django_tables2 %} +{% load i18n %} +{% block content %} + + <div class="row"> + <div class="col-xl-12"> + <div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons"> + <a href="#" class="btn btn-sm btn-outline-primary active"> + {% trans "Invoice" %}s + </a> + <a href="{% url "treasury:remittance_list" %}" class="btn btn-sm btn-outline-primary"> + {% trans "Remittance" %}s + </a> + </div> + </div> + </div> + +{% render_table table %} + +<a class="btn btn-primary" href="{% url 'treasury:invoice_create' %}">{% trans "New invoice" %}</a> + +{% endblock %} diff --git a/templates/treasury/invoice_sample.tex b/templates/treasury/invoice_sample.tex new file mode 100644 index 0000000000000000000000000000000000000000..3c76403e97d466f23771172a78f755ddf4c3658a --- /dev/null +++ b/templates/treasury/invoice_sample.tex @@ -0,0 +1,186 @@ +\nonstopmode +\documentclass[11pt]{article} + +\usepackage[french]{babel} +\usepackage[T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[a4paper]{geometry} +\usepackage{units} +\usepackage{bera} +\usepackage{graphicx} +\usepackage{fancyhdr} +\usepackage{fp} +\usepackage{transparent} +\usepackage{eso-pic} + +\def\TVA{0} % Taux de la TVA + +\def\TotalHT{0} +\def\TotalTVA{0} + +\newcommand{\AjouterProduit}[4]{% Arguments : Désignation, quantité, prix unitaire HT, prix total HT + \FPround{\prix}{#3}{2} + \FPround{\montant}{#4}{2} + \FPadd{\TotalHT}{\TotalHT}{\montant} + + \eaddto\ListeProduits{#1 & \prix & #2 & \montant \cr} +} + +\newcommand{\AfficheResultat}{% + \ListeProduits + + \FPeval{\TotalTVA}{\TotalHT * \TVA / 100} + \FPadd{\TotalTTC}{\TotalHT}{\TotalTVA} + \FPround{\TotalHT}{\TotalHT}{2} + \FPround{\TotalTVA}{\TotalTVA}{2} + \FPround{\TotalTTC}{\TotalTTC}{2} + \global\let\TotalHT\TotalHT + \global\let\TotalTVA\TotalTVA + \global\let\TotalTTC\TotalTTC + + \cr \hline + Total HT & & & \TotalHT \cr + TVA \TVA~\% & & & \TotalTVA \cr + \hline \hline + \textbf{Total TTC} & & & \TotalTTC +} + +\newcommand*\eaddto[2]{% version développée de \addto + \edef\tmp{#2}% + \expandafter\addto + \expandafter#1% + \expandafter{\tmp}% +} + +\newcommand {\ListeProduits}{} + +% Logo du BDE +\AddToShipoutPicture*{ + \put(0,0){ + \parbox[b][\paperheight]{\paperwidth}{% + \vfill + \centering + {\transparent{0.1}\includegraphics[width=\textwidth]{../../static/img/{{ obj.bde }}}}% + \vfill + } + } +} + + +%%%%%%%%%%%%%%%%%%%%% A MODIFIER DANS LA FACTURE %%%%%%%%%%%%%%%%%%%%% +% Infos Association +\def\MonNom{{"{"}}{{ obj.my_name }}} % Nom de l'association +\def\MonAdresseRue{{"{"}}{{ obj.my_address_street }}} % Adresse de l'association +\def\MonAdresseVille{{"{"}}{{ obj.my_city }}} + +% Informations bancaires de l'association +\def\CodeBanque{{"{"}}{{ obj.bank_code|stringformat:".05d" }}} +\def\CodeGuichet{{"{"}}{{ obj.desk_code|stringformat:".05d" }}} +\def\NCompte{{"{"}}{{ obj.account_number|stringformat:".011d" }}} +\def\CleRib{{"{"}}{{ obj.rib_key|stringformat:".02d" }}} +\def\IBAN{FR76\CodeBanque\CodeGuichet\NCompte\CleRib} +\def\CodeBic{{"{"}}{{ obj.bic }}} + +\def\FactureNum {{"{"}}{{obj.id}}} % Numéro de facture +\def\FactureAcquittee {% if obj.acquitted %} {oui} {% else %} {non} {% endif %} % Facture acquittée : oui/non +\def\FactureLieu {{"{"}}{{ obj.place }}} % Lieu de l'édition de la facture +\def\FactureDate {{"{"}}{{ obj.date }}} % Date de l'édition de la facture +\def\FactureObjet {{"{"}}{{ obj.object|safe }} } % Objet du document +% Description de la facture +\def\FactureDescr {{"{"}}{{ obj.description|safe }}} + +% Infos Client +\def\ClientNom{{"{"}}{{obj.name|safe}}} % Nom du client +\def\ClientAdresse{{"{"}}{{ obj.address|safe }}} % Adresse du client + +% Liste des produits facturés : Désignation, quantité, prix unitaire HT + +{% for product in products %} +\AjouterProduit{ {{product.designation|safe}}} { {{product.quantity|safe}}} { {{product.amount_euros|safe}}} { {{product.total_euros|safe}}} +{% endfor %} + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + +\geometry{verbose,tmargin=4em,bmargin=8em,lmargin=6em,rmargin=6em} +\setlength{\parindent}{1pt} +\setlength{\parskip}{1ex plus 0.5ex minus 0.2ex} + +\thispagestyle{fancy} +\pagestyle{fancy} +\setlength{\parindent}{0pt} + +\renewcommand{\headrulewidth}{0pt} +\cfoot{ + \small{\MonNom ~--~ \MonAdresseRue ~ \MonAdresseVille ~--~ Téléphone : +33(0)6 89 88 56 50\newline + Site web : bde.ens-cachan.fr ~--~ E-mail : tresorerie.bde@lists.crans.org \newline Numéro SIRET : 399 485 838 00011 + } +} + +\begin{document} + +% Logo de la société +% \includegraphics{logo.jpg} + +% Nom et adresse de la société +\MonNom \\ +\MonAdresseRue \\ +\MonAdresseVille + +Facture n°\FactureNum + + +{\addtolength{\leftskip}{10.5cm} %in ERT + \ClientNom \\ + \ClientAdresse \\ + +} %in ERT + + +\hspace*{10.5cm} +\FactureLieu, le \FactureDate + +~\\~\\ + +\textbf{Objet : \FactureObjet \\} + +\textnormal{\FactureDescr} + +~\\ + +\begin{center} + \begin{tabular}{lrrr} + \textbf{Désignation ~~~~~~} & \textbf{Prix unitaire} & \textbf{Quantité} & \textbf{Montant (EUR)} \\ + \hline + \AfficheResultat{} + \end{tabular} +\end{center} + +~\\ + +\ifthenelse{\equal{\FactureAcquittee}{oui}}{ + Facture acquittée. +}{ + + À régler par chèque ou par virement bancaire : + + \begin{center} + \begin{tabular}{|c c c c|} + \hline + \textbf{Code banque} & \textbf{Code guichet} & \textbf{N° de Compte} & \textbf{Clé RIB}\\ + \CodeBanque & \CodeGuichet & \NCompte & \CleRib \\ + \hline + \textbf{IBAN N°} & \multicolumn{3}{|l|} \IBAN \\ + \hline + \textbf{Code BIC} & \multicolumn{3}{|l|}\CodeBic \\ + \hline + \end{tabular} + \end{center} + +} + +\begin{center} +TVA non applicable, article 293 B du CGI. +\end{center} + +\end{document} diff --git a/templates/treasury/remittance_form.html b/templates/treasury/remittance_form.html new file mode 100644 index 0000000000000000000000000000000000000000..af4170f48dae1247cc137484a6e0efe06faac7b6 --- /dev/null +++ b/templates/treasury/remittance_form.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load crispy_forms_tags pretty_money %} +{% load render_table from django_tables2 %} +{% block content %} + <h1>{% trans "Remittance #" %}{{ object.pk }}</h1> + + <p><a class="btn btn-default" href="{% url 'treasury:remittance_list' %}">{% trans "Remittances list" %}</a></p> + + {% if object.pk %} + <div id="div_id_type" class="form-group"><label for="id_count" class="col-form-label">{% trans "Count" %}</label> + <div class=""> + <input type="text" name="count" value="{{ object.count }}" class="textinput textInput form-control" id="id_count" disabled> + </div> + </div> + + <div id="div_id_type" class="form-group"><label for="id_amount" class="col-form-label">{% trans "Amount" %}</label> + <div class=""> + <input class="textinput textInput form-control" type="text" value="{{ object.amount|pretty_money }}" id="id_amount" disabled> + </div> + </div> + {% endif %} + + {% crispy form %} + + <hr> + + <h2>{% trans "Linked transactions" %}</h2> + {% if special_transactions.data %} + {% render_table special_transactions %} + {% else %} + <div class="alert alert-warning"> + {% trans "There is no transaction linked with this remittance." %} + </div> + {% endif %} +{% endblock %} diff --git a/templates/treasury/remittance_list.html b/templates/treasury/remittance_list.html new file mode 100644 index 0000000000000000000000000000000000000000..8bc634e4b35e029a95d57369dd24d0c50c0aeb5e --- /dev/null +++ b/templates/treasury/remittance_list.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} +{% load render_table from django_tables2 %} +{% load i18n %} +{% block content %} + + <div class="row"> + <div class="col-xl-12"> + <div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons"> + <a href="{% url "treasury:invoice_list" %}" class="btn btn-sm btn-outline-primary"> + {% trans "Invoice" %}s + </a> + <a href="#" class="btn btn-sm btn-outline-primary active"> + {% trans "Remittance" %}s + </a> + </div> + </div> + </div> + + <h2>{% trans "Opened remittances" %}</h2> + {% if opened_remittances.data %} + {% render_table opened_remittances %} + {% else %} + <div class="alert alert-warning"> + {% trans "There is no opened remittance." %} + </div> + {% endif %} + + <a class="btn btn-primary" href="{% url 'treasury:remittance_create' %}">{% trans "New remittance" %}</a> + + <hr> + + <h2>{% trans "Transfers without remittances" %}</h2> + {% if special_transactions_no_remittance.data %} + {% render_table special_transactions_no_remittance %} + {% else %} + <div class="alert alert-warning"> + {% trans "There is no transaction without any linked remittance." %} + </div> + {% endif %} + + <hr> + + <h2>{% trans "Transfers with opened remittances" %}</h2> + {% if special_transactions_with_remittance.data %} + {% render_table special_transactions_with_remittance %} + {% else %} + <div class="alert alert-warning"> + {% trans "There is no transaction with an opened linked remittance." %} + </div> + {% endif %} + + <hr> + + <h2>{% trans "Closed remittances" %}</h2> + {% render_table closed_remittances %} +{% endblock %} diff --git a/templates/treasury/specialtransactionproxy_form.html b/templates/treasury/specialtransactionproxy_form.html new file mode 100644 index 0000000000000000000000000000000000000000..4e7758ae4fb966b08be32046af19aaff9a04a1df --- /dev/null +++ b/templates/treasury/specialtransactionproxy_form.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load crispy_forms_tags pretty_money %} +{% load render_table from django_tables2 %} +{% block content %} + <p><a class="btn btn-default" href="{% url 'treasury:remittance_list' %}">{% trans "Remittances list" %}</a></p> + {% crispy form %} +{% endblock %} diff --git a/tox.ini b/tox.ini index 0b5c20c98b24a30131e48697742b212595fcd11a..01bf4edbd0f9f6e21622bb04b4778c30ac4add57 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,7 @@ deps = pep8-naming pyflakes commands = - flake8 apps/activity apps/api apps/logs apps/member apps/note + flake8 apps/activity apps/api apps/logs apps/member apps/note apps/permission apps/treasury [flake8] # Ignore too many errors, should be reduced in the future