diff --git a/Dockerfile b/Dockerfile index d42bdd1f..dfc49d04 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,11 @@ RUN apt update && \ apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \ rm -rf /var/lib/apt/lists/* +# Install LaTeX requirements +RUN apt update && \ + apt install -y texlive-latex-extra texlive-fonts-extra texlive-lang-french && \ + rm -rf /var/lib/apt/lists/* + COPY . /code/ # Comment what is not needed diff --git a/README.md b/README.md index 1ffe8793..9b0c927e 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,17 @@ ## Installation sur un serveur -On supposera pour la suite que vous utiliser debian/ubuntu sur un serveur tout nu ou bien configuré. +On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout nu ou bien configuré. 1. Paquets nécessaires $ sudo apt install nginx python3 python3-pip python3-dev uwsgi $ sudo apt install uwsgi-plugin-python3 python3-venv git acl + La génération des factures de l'application trésorerie nécessite une installation de LaTeX suffisante : + + $ sudo apt install texlive-latex-extra texlive-fonts-extra texlive-lang-french + 2. Clonage du dépot on se met au bon endroit : diff --git a/apps/api/urls.py b/apps/api/urls.py index b275a0b8..67fdba30 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -12,6 +12,7 @@ from activity.api.urls import register_activity_urls from api.viewsets import ReadProtectedModelViewSet from member.api.urls import register_members_urls from note.api.urls import register_note_urls +from treasury.api.urls import register_treasury_urls from logs.api.urls import register_logs_urls from permission.api.urls import register_permission_urls @@ -74,6 +75,7 @@ router.register('user', UserViewSet) register_members_urls(router, 'members') register_activity_urls(router, 'activity') register_note_urls(router, 'note') +register_treasury_urls(router, 'treasury') register_permission_urls(router, 'permission') register_logs_urls(router, 'logs') diff --git a/apps/note/fixtures/initial.json b/apps/note/fixtures/initial.json index 63285e34..a0682dae 100644 --- a/apps/note/fixtures/initial.json +++ b/apps/note/fixtures/initial.json @@ -256,4 +256,4 @@ "name": "Alcool" } } -] +] \ No newline at end of file diff --git a/apps/note/models/__init__.py b/apps/note/models/__init__.py index 8f1921f9..e9c8a0a9 100644 --- a/apps/note/models/__init__.py +++ b/apps/note/models/__init__.py @@ -3,12 +3,12 @@ from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser from .transactions import MembershipTransaction, Transaction, \ - TemplateCategory, TransactionTemplate, RecurrentTransaction + TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction __all__ = [ # Notes 'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser', # Transactions 'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate', - 'RecurrentTransaction', + 'RecurrentTransaction', 'SpecialTransaction', ] diff --git a/apps/note/templatetags/pretty_money.py b/apps/note/templatetags/pretty_money.py index 265870a8..ba527f9b 100644 --- a/apps/note/templatetags/pretty_money.py +++ b/apps/note/templatetags/pretty_money.py @@ -18,5 +18,10 @@ def pretty_money(value): ) +def cents_to_euros(value): + return "{:.02f}".format(value / 100) if value else "" + + register = template.Library() register.filter('pretty_money', pretty_money) +register.filter('cents_to_euros', cents_to_euros) diff --git a/apps/permission/admin.py b/apps/permission/admin.py index aaa6f661..4312f4b0 100644 --- a/apps/permission/admin.py +++ b/apps/permission/admin.py @@ -28,4 +28,3 @@ class RolePermissionsAdmin(admin.ModelAdmin): Admin customisation for RolePermissions """ list_display = ('role', ) - diff --git a/apps/permission/api/views.py b/apps/permission/api/views.py index 6087c83e..965e82c9 100644 --- a/apps/permission/api/views.py +++ b/apps/permission/api/views.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django_filters.rest_framework import DjangoFilterBackend - from api.viewsets import ReadOnlyProtectedModelViewSet + from .serializers import PermissionSerializer from ..models import Permission diff --git a/apps/permission/models.py b/apps/permission/models.py index 109c1875..205f5b41 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -10,7 +10,6 @@ from django.core.exceptions import ValidationError from django.db import models from django.db.models import F, Q, Model from django.utils.translation import gettext_lazy as _ - from member.models import Role @@ -281,4 +280,3 @@ class RolePermissions(models.Model): def __str__(self): return str(self.role) - diff --git a/apps/permission/permissions.py b/apps/permission/permissions.py index 9f6d8cd2..7097085f 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 aebca39d..1e30f56f 100644 --- a/apps/permission/signals.py +++ b/apps/permission/signals.py @@ -3,10 +3,9 @@ from django.core.exceptions import PermissionDenied from django.db.models.signals import pre_save, pre_delete, post_save, post_delete - from logs import signals as logs_signals -from permission.backends import PermissionBackend from note_kfet.middlewares import get_current_authenticated_user +from permission.backends import PermissionBackend EXCLUDED = [ diff --git a/apps/permission/templatetags/perms.py b/apps/permission/templatetags/perms.py index 8f2a0006..8bcd3597 100644 --- a/apps/permission/templatetags/perms.py +++ b/apps/permission/templatetags/perms.py @@ -3,10 +3,8 @@ from django.contrib.contenttypes.models import ContentType from django.template.defaultfilters import stringfilter - -from note_kfet.middlewares import get_current_authenticated_user, get_current_session from django import template - +from note_kfet.middlewares import get_current_authenticated_user, get_current_session from permission.backends import PermissionBackend diff --git a/apps/treasury/__init__.py b/apps/treasury/__init__.py new file mode 100644 index 00000000..c9c6150e --- /dev/null +++ b/apps/treasury/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +default_app_config = 'treasury.apps.TreasuryConfig' diff --git a/apps/treasury/admin.py b/apps/treasury/admin.py new file mode 100644 index 00000000..abeec3e3 --- /dev/null +++ b/apps/treasury/admin.py @@ -0,0 +1,27 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-lateré + +from django.contrib import admin + +from .models import RemittanceType, Remittance + + +@admin.register(RemittanceType) +class RemittanceTypeAdmin(admin.ModelAdmin): + """ + Admin customisation for RemiitanceType + """ + list_display = ('note', ) + + +@admin.register(Remittance) +class RemittanceAdmin(admin.ModelAdmin): + """ + Admin customisation for Remittance + """ + list_display = ('remittance_type', 'date', 'comment', 'count', 'amount', 'closed', ) + + def has_change_permission(self, request, obj=None): + if not obj: + return True + return not obj.closed and super().has_change_permission(request, obj) diff --git a/apps/treasury/api/__init__.py b/apps/treasury/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/treasury/api/serializers.py b/apps/treasury/api/serializers.py new file mode 100644 index 00000000..f1bbef75 --- /dev/null +++ b/apps/treasury/api/serializers.py @@ -0,0 +1,62 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from rest_framework import serializers +from note.api.serializers import SpecialTransactionSerializer + +from ..models import Invoice, Product, RemittanceType, Remittance + + +class ProductSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Product types. + The djangorestframework plugin will analyse the model `Product` and parse all fields in the API. + """ + + class Meta: + model = Product + fields = '__all__' + + +class InvoiceSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Invoice types. + The djangorestframework plugin will analyse the model `Invoice` and parse all fields in the API. + """ + class Meta: + model = Invoice + fields = '__all__' + read_only_fields = ('bde',) + + products = serializers.SerializerMethodField() + + def get_products(self, obj): + return serializers.ListSerializer(child=ProductSerializer())\ + .to_representation(Product.objects.filter(invoice=obj).all()) + + +class RemittanceTypeSerializer(serializers.ModelSerializer): + """ + REST API Serializer for RemittanceType types. + The djangorestframework plugin will analyse the model `RemittanceType` and parse all fields in the API. + """ + + class Meta: + model = RemittanceType + fields = '__all__' + + +class RemittanceSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Remittance types. + The djangorestframework plugin will analyse the model `Remittance` and parse all fields in the API. + """ + + transactions = serializers.SerializerMethodField() + + class Meta: + model = Remittance + fields = '__all__' + + def get_transactions(self, obj): + return serializers.ListSerializer(child=SpecialTransactionSerializer()).to_representation(obj.transactions) diff --git a/apps/treasury/api/urls.py b/apps/treasury/api/urls.py new file mode 100644 index 00000000..30ac00e1 --- /dev/null +++ b/apps/treasury/api/urls.py @@ -0,0 +1,14 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from .views import InvoiceViewSet, ProductViewSet, RemittanceViewSet, RemittanceTypeViewSet + + +def register_treasury_urls(router, path): + """ + Configure router for treasury REST API. + """ + router.register(path + '/invoice', InvoiceViewSet) + router.register(path + '/product', ProductViewSet) + router.register(path + '/remittance_type', RemittanceTypeViewSet) + router.register(path + '/remittance', RemittanceViewSet) diff --git a/apps/treasury/api/views.py b/apps/treasury/api/views.py new file mode 100644 index 00000000..7a70fd24 --- /dev/null +++ b/apps/treasury/api/views.py @@ -0,0 +1,53 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.filters import SearchFilter +from api.viewsets import ReadProtectedModelViewSet + +from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer +from ..models import Invoice, Product, RemittanceType, Remittance + + +class InvoiceViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Invoice` objects, serialize it to JSON with the given serializer, + then render it on /api/treasury/invoice/ + """ + queryset = Invoice.objects.all() + serializer_class = InvoiceSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['bde', ] + + +class ProductViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Product` objects, serialize it to JSON with the given serializer, + then render it on /api/treasury/product/ + """ + queryset = Product.objects.all() + serializer_class = ProductSerializer + filter_backends = [SearchFilter] + search_fields = ['$designation', ] + + +class RemittanceTypeViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer + then render it on /api/treasury/remittance_type/ + """ + queryset = RemittanceType.objects.all() + serializer_class = RemittanceTypeSerializer + + +class RemittanceViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer, + then render it on /api/treasury/remittance/ + """ + queryset = Remittance.objects.all() + serializer_class = RemittanceSerializer diff --git a/apps/treasury/apps.py b/apps/treasury/apps.py new file mode 100644 index 00000000..e2873ea2 --- /dev/null +++ b/apps/treasury/apps.py @@ -0,0 +1,33 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.apps import AppConfig +from django.db.models import Q +from django.db.models.signals import post_save, post_migrate +from django.utils.translation import gettext_lazy as _ + + +class TreasuryConfig(AppConfig): + name = 'treasury' + verbose_name = _('Treasury') + + def ready(self): + """ + Define app internal signals to interact with other apps + """ + + from . import signals + from note.models import SpecialTransaction, NoteSpecial + from treasury.models import SpecialTransactionProxy + post_save.connect(signals.save_special_transaction, sender=SpecialTransaction) + + def setup_specialtransactions_proxies(**kwargs): + # If the treasury app was disabled for any reason during a certain amount of time, + # we ensure that each special transaction is linked to a proxy + for transaction in SpecialTransaction.objects.filter( + source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), + specialtransactionproxy=None, + ): + SpecialTransactionProxy.objects.create(transaction=transaction, remittance=None) + + post_migrate.connect(setup_specialtransactions_proxies, sender=SpecialTransactionProxy) diff --git a/apps/treasury/fixtures/initial.json b/apps/treasury/fixtures/initial.json new file mode 100644 index 00000000..143d2101 --- /dev/null +++ b/apps/treasury/fixtures/initial.json @@ -0,0 +1,9 @@ +[ + { + "model": "treasury.remittancetype", + "pk": 1, + "fields": { + "note": 3 + } + } +] diff --git a/apps/treasury/forms.py b/apps/treasury/forms.py new file mode 100644 index 00000000..caaa365f --- /dev/null +++ b/apps/treasury/forms.py @@ -0,0 +1,156 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import datetime + +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Submit +from django import forms +from django.utils.translation import gettext_lazy as _ + +from .models import Invoice, Product, Remittance, SpecialTransactionProxy + + +class InvoiceForm(forms.ModelForm): + """ + Create and generate invoices. + """ + + # Django forms don't support date fields. We have to add it manually + date = forms.DateField( + initial=datetime.date.today, + widget=forms.TextInput(attrs={'type': 'date'}) + ) + + def clean_date(self): + self.instance.date = self.data.get("date") + + class Meta: + model = Invoice + exclude = ('bde', ) + + +# Add a subform per product in the invoice form, and manage correctly the link between the invoice and +# its products. The FormSet will search automatically the ForeignKey in the Product model. +ProductFormSet = forms.inlineformset_factory( + Invoice, + Product, + fields='__all__', + extra=1, +) + + +class ProductFormSetHelper(FormHelper): + """ + Specify some template informations for the product form. + """ + + def __init__(self, form=None): + super().__init__(form) + self.form_tag = False + self.form_method = 'POST' + self.form_class = 'form-inline' + self.template = 'bootstrap4/table_inline_formset.html' + + +class RemittanceForm(forms.ModelForm): + """ + Create remittances. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.helper = FormHelper() + + # We can't update the type of the remittance once created. + if self.instance.pk: + self.fields["remittance_type"].disabled = True + self.fields["remittance_type"].required = False + + # We display the submit button iff the remittance is open, + # the close button iff it is open and has a linked transaction + if not self.instance.closed: + self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'})) + if self.instance.transactions: + self.helper.add_input(Submit("close", _("Close"), css_class='btn btn-success')) + else: + # If the remittance is closed, we can't change anything + self.fields["comment"].disabled = True + self.fields["comment"].required = False + + def clean(self): + # We can't update anything if the remittance is already closed. + if self.instance.closed: + self.add_error("comment", _("Remittance is already closed.")) + + cleaned_data = super().clean() + + if self.instance.pk and cleaned_data.get("remittance_type") != self.instance.remittance_type: + self.add_error("remittance_type", _("You can't change the type of the remittance.")) + + # The close button is manually handled + if "close" in self.data: + self.instance.closed = True + self.cleaned_data["closed"] = True + + return cleaned_data + + class Meta: + model = Remittance + fields = ('remittance_type', 'comment',) + + +class LinkTransactionToRemittanceForm(forms.ModelForm): + """ + Attach a special transaction to a remittance. + """ + + # Since we use a proxy model for special transactions, we add manually the fields related to the transaction + last_name = forms.CharField(label=_("Last name")) + + first_name = forms.Field(label=_("First name")) + + bank = forms.Field(label=_("Bank")) + + amount = forms.IntegerField(label=_("Amount"), min_value=0) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + # Add submit button + self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'})) + + self.fields["remittance"].queryset = Remittance.objects.filter(closed=False) + + def clean_last_name(self): + """ + Replace the first name in the information of the transaction. + """ + self.instance.transaction.last_name = self.data.get("last_name") + self.instance.transaction.clean() + + def clean_first_name(self): + """ + Replace the last name in the information of the transaction. + """ + self.instance.transaction.first_name = self.data.get("first_name") + self.instance.transaction.clean() + + def clean_bank(self): + """ + Replace the bank in the information of the transaction. + """ + self.instance.transaction.bank = self.data.get("bank") + self.instance.transaction.clean() + + def clean_amount(self): + """ + Replace the amount of the transaction. + """ + self.instance.transaction.amount = self.data.get("amount") + self.instance.transaction.clean() + + class Meta: + model = SpecialTransactionProxy + fields = ('remittance', ) diff --git a/apps/treasury/migrations/__init__.py b/apps/treasury/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/treasury/models.py b/apps/treasury/models.py new file mode 100644 index 00000000..bcd89db9 --- /dev/null +++ b/apps/treasury/models.py @@ -0,0 +1,189 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.core.exceptions import ValidationError +from django.db import models +from django.db.models import Q +from django.utils.translation import gettext_lazy as _ +from note.models import NoteSpecial, SpecialTransaction + + +class Invoice(models.Model): + """ + An invoice model that can generates a true invoice. + """ + + id = models.PositiveIntegerField( + primary_key=True, + verbose_name=_("Invoice identifier"), + ) + + bde = models.CharField( + max_length=32, + default='Saperlistpopette.png', + choices=( + ('Saperlistpopette.png', 'Saper[list]popette'), + ('Finalist.png', 'Fina[list]'), + ('Listorique.png', '[List]orique'), + ('Satellist.png', 'Satel[list]'), + ('Monopolist.png', 'Monopo[list]'), + ('Kataclist.png', 'Katac[list]'), + ), + verbose_name=_("BDE"), + ) + + object = models.CharField( + max_length=255, + verbose_name=_("Object"), + ) + + description = models.TextField( + verbose_name=_("Description") + ) + + name = models.CharField( + max_length=255, + verbose_name=_("Name"), + ) + + address = models.TextField( + verbose_name=_("Address"), + ) + + date = models.DateField( + auto_now_add=True, + verbose_name=_("Place"), + ) + + acquitted = models.BooleanField( + verbose_name=_("Acquitted"), + ) + + +class Product(models.Model): + """ + Product that appears on an invoice. + """ + + invoice = models.ForeignKey( + Invoice, + on_delete=models.PROTECT, + ) + + designation = models.CharField( + max_length=255, + verbose_name=_("Designation"), + ) + + quantity = models.PositiveIntegerField( + verbose_name=_("Quantity") + ) + + amount = models.IntegerField( + verbose_name=_("Unit price") + ) + + @property + def amount_euros(self): + return self.amount / 100 + + @property + def total(self): + return self.quantity * self.amount + + @property + def total_euros(self): + return self.total / 100 + + +class RemittanceType(models.Model): + """ + Store what kind of remittances can be stored. + """ + + note = models.OneToOneField( + NoteSpecial, + on_delete=models.CASCADE, + ) + + def __str__(self): + return str(self.note) + + +class Remittance(models.Model): + """ + Treasurers want to regroup checks or bank transfers in bank remittances. + """ + + date = models.DateTimeField( + auto_now_add=True, + verbose_name=_("Date"), + ) + + remittance_type = models.ForeignKey( + RemittanceType, + on_delete=models.PROTECT, + verbose_name=_("Type"), + ) + + comment = models.CharField( + max_length=255, + verbose_name=_("Comment"), + ) + + closed = models.BooleanField( + default=False, + verbose_name=_("Closed"), + ) + + @property + def transactions(self): + """ + :return: Transactions linked to this remittance. + """ + if not self.pk: + return SpecialTransaction.objects.none() + return SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self) + + def count(self): + """ + Linked transactions count. + """ + return self.transactions.count() + + @property + def amount(self): + """ + Total amount of the remittance. + """ + return sum(transaction.total for transaction in self.transactions.all()) + + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + # Check if all transactions have the right type. + if self.transactions.filter(~Q(source=self.remittance_type.note)).exists(): + raise ValidationError("All transactions in a remittance must have the same type") + + return super().save(force_insert, force_update, using, update_fields) + + def __str__(self): + return _("Remittance #{:d}: {}").format(self.id, self.comment, ) + + +class SpecialTransactionProxy(models.Model): + """ + In order to keep modularity, we don't that the Note app depends on the treasury app. + That's why we create a proxy in this app, to link special transactions and remittances. + If it isn't very clean, that makes what we want. + """ + + transaction = models.OneToOneField( + SpecialTransaction, + on_delete=models.CASCADE, + ) + + remittance = models.ForeignKey( + Remittance, + on_delete=models.PROTECT, + null=True, + verbose_name=_("Remittance"), + ) diff --git a/apps/treasury/signals.py b/apps/treasury/signals.py new file mode 100644 index 00000000..54c19c09 --- /dev/null +++ b/apps/treasury/signals.py @@ -0,0 +1,12 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from treasury.models import SpecialTransactionProxy, RemittanceType + + +def save_special_transaction(instance, created, **kwargs): + """ + When a special transaction is created, we create its linked proxy + """ + if created and RemittanceType.objects.filter(note=instance.source).exists(): + SpecialTransactionProxy.objects.create(transaction=instance, remittance=None).save() diff --git a/apps/treasury/tables.py b/apps/treasury/tables.py new file mode 100644 index 00000000..1ecc04db --- /dev/null +++ b/apps/treasury/tables.py @@ -0,0 +1,103 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import django_tables2 as tables +from django.utils.translation import gettext_lazy as _ +from django_tables2 import A +from note.models import SpecialTransaction +from note.templatetags.pretty_money import pretty_money + +from .models import Invoice, Remittance + + +class InvoiceTable(tables.Table): + """ + List all invoices. + """ + id = tables.LinkColumn("treasury:invoice_update", + args=[A("pk")], + text=lambda record: _("Invoice #{:d}").format(record.id), ) + + invoice = tables.LinkColumn("treasury:invoice_render", + verbose_name=_("Invoice"), + args=[A("pk")], + accessor="pk", + text="", + attrs={ + 'a': {'class': 'fa fa-file-pdf-o'}, + 'td': {'data-turbolinks': 'false'} + }) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = Invoice + template_name = 'django_tables2/bootstrap4.html' + fields = ('id', 'name', 'object', 'acquitted', 'invoice',) + + +class RemittanceTable(tables.Table): + """ + List all remittances. + """ + + count = tables.Column(verbose_name=_("Transaction count")) + + amount = tables.Column(verbose_name=_("Amount")) + + view = tables.LinkColumn("treasury:remittance_update", + verbose_name=_("View"), + args=[A("pk")], + text=_("View"), + attrs={ + 'a': {'class': 'btn btn-primary'} + }, ) + + def render_amount(self, value): + return pretty_money(value) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = Remittance + template_name = 'django_tables2/bootstrap4.html' + fields = ('id', 'date', 'remittance_type', 'comment', 'count', 'amount', 'view',) + + +class SpecialTransactionTable(tables.Table): + """ + List special credit transactions that are (or not, following the queryset) attached to a remittance. + """ + + # Display add and remove buttons. Use the `exclude` field to select what is needed. + remittance_add = tables.LinkColumn("treasury:link_transaction", + verbose_name=_("Remittance"), + args=[A("specialtransactionproxy.pk")], + text=_("Add"), + attrs={ + 'a': {'class': 'btn btn-primary'} + }, ) + + remittance_remove = tables.LinkColumn("treasury:unlink_transaction", + verbose_name=_("Remittance"), + args=[A("specialtransactionproxy.pk")], + text=_("Remove"), + attrs={ + 'a': {'class': 'btn btn-primary btn-danger'} + }, ) + + def render_id(self, record): + return record.specialtransactionproxy.pk + + def render_amount(self, value): + return pretty_money(value) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = SpecialTransaction + template_name = 'django_tables2/bootstrap4.html' + fields = ('id', 'source', 'destination', 'last_name', 'first_name', 'bank', 'amount', 'reason',) diff --git a/apps/treasury/urls.py b/apps/treasury/urls.py new file mode 100644 index 00000000..d44cc414 --- /dev/null +++ b/apps/treasury/urls.py @@ -0,0 +1,24 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.urls import path + +from .views import InvoiceCreateView, InvoiceListView, InvoiceUpdateView, InvoiceRenderView, RemittanceListView,\ + RemittanceCreateView, RemittanceUpdateView, LinkTransactionToRemittanceView, UnlinkTransactionToRemittanceView + +app_name = 'treasury' +urlpatterns = [ + # Invoice app paths + path('invoice/', InvoiceListView.as_view(), name='invoice_list'), + path('invoice/create/', InvoiceCreateView.as_view(), name='invoice_create'), + path('invoice//', InvoiceUpdateView.as_view(), name='invoice_update'), + path('invoice/render//', InvoiceRenderView.as_view(), name='invoice_render'), + + # Remittance app paths + path('remittance/', RemittanceListView.as_view(), name='remittance_list'), + path('remittance/create/', RemittanceCreateView.as_view(), name='remittance_create'), + path('remittance//', RemittanceUpdateView.as_view(), name='remittance_update'), + path('remittance/link_transaction//', LinkTransactionToRemittanceView.as_view(), name='link_transaction'), + path('remittance/unlink_transaction//', UnlinkTransactionToRemittanceView.as_view(), + name='unlink_transaction'), +] diff --git a/apps/treasury/views.py b/apps/treasury/views.py new file mode 100644 index 00000000..90440566 --- /dev/null +++ b/apps/treasury/views.py @@ -0,0 +1,316 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import os +import shutil +import subprocess +from tempfile import mkdtemp + +from crispy_forms.helper import FormHelper +from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.exceptions import ValidationError +from django.db.models import Q +from django.http import HttpResponse +from django.shortcuts import redirect +from django.template.loader import render_to_string +from django.urls import reverse_lazy +from django.views.generic import CreateView, UpdateView +from django.views.generic.base import View, TemplateView +from django_tables2 import SingleTableView +from note.models import SpecialTransaction, NoteSpecial +from note_kfet.settings.base import BASE_DIR + +from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm +from .models import Invoice, Product, Remittance, SpecialTransactionProxy +from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable + + +class InvoiceCreateView(LoginRequiredMixin, CreateView): + """ + Create Invoice + """ + model = Invoice + form_class = InvoiceForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + form = context['form'] + form.helper = FormHelper() + # Remove form tag on the generation of the form in the template (already present on the template) + form.helper.form_tag = False + # The formset handles the set of the products + form_set = ProductFormSet(instance=form.instance) + context['formset'] = form_set + context['helper'] = ProductFormSetHelper() + context['no_cache'] = True + + return context + + def form_valid(self, form): + ret = super().form_valid(form) + + kwargs = {} + + # The user type amounts in cents. We convert it in euros. + for key in self.request.POST: + value = self.request.POST[key] + if key.endswith("amount") and value: + kwargs[key] = str(int(100 * float(value))) + elif value: + kwargs[key] = value + + # For each product, we save it + formset = ProductFormSet(kwargs, instance=form.instance) + if formset.is_valid(): + for f in formset: + # We don't save the product if the designation is not entered, ie. if the line is empty + if f.is_valid() and f.instance.designation: + f.save() + f.instance.save() + else: + f.instance = None + + return ret + + def get_success_url(self): + return reverse_lazy('treasury:invoice_list') + + +class InvoiceListView(LoginRequiredMixin, SingleTableView): + """ + List existing Invoices + """ + model = Invoice + table_class = InvoiceTable + + +class InvoiceUpdateView(LoginRequiredMixin, UpdateView): + """ + Create Invoice + """ + model = Invoice + form_class = InvoiceForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + form = context['form'] + form.helper = FormHelper() + # Remove form tag on the generation of the form in the template (already present on the template) + form.helper.form_tag = False + # Fill the intial value for the date field, with the initial date of the model instance + form.fields['date'].initial = form.instance.date + # The formset handles the set of the products + form_set = ProductFormSet(instance=form.instance) + context['formset'] = form_set + context['helper'] = ProductFormSetHelper() + context['no_cache'] = True + + return context + + def form_valid(self, form): + ret = super().form_valid(form) + + kwargs = {} + # The user type amounts in cents. We convert it in euros. + for key in self.request.POST: + value = self.request.POST[key] + if key.endswith("amount") and value: + kwargs[key] = str(int(100 * float(value))) + elif value: + kwargs[key] = value + + formset = ProductFormSet(kwargs, instance=form.instance) + saved = [] + # For each product, we save it + if formset.is_valid(): + for f in formset: + # We don't save the product if the designation is not entered, ie. if the line is empty + if f.is_valid() and f.instance.designation: + f.save() + f.instance.save() + saved.append(f.instance.pk) + else: + f.instance = None + # Remove old products that weren't given in the form + Product.objects.filter(~Q(pk__in=saved), invoice=form.instance).delete() + + return ret + + def get_success_url(self): + return reverse_lazy('treasury:invoice_list') + + +class InvoiceRenderView(LoginRequiredMixin, View): + """ + Render Invoice as a generated PDF with the given information and a LaTeX template + """ + + def get(self, request, **kwargs): + pk = kwargs["pk"] + invoice = Invoice.objects.get(pk=pk) + products = Product.objects.filter(invoice=invoice).all() + + # Informations of the BDE. Should be updated when the school will move. + invoice.place = "Cachan" + invoice.my_name = "BDE ENS Cachan" + invoice.my_address_street = "61 avenue du Président Wilson" + invoice.my_city = "94230 Cachan" + invoice.bank_code = 30003 + invoice.desk_code = 3894 + invoice.account_number = 37280662 + invoice.rib_key = 14 + invoice.bic = "SOGEFRPP" + + # Replace line breaks with the LaTeX equivalent + invoice.description = invoice.description.replace("\r", "").replace("\n", "\\\\ ") + invoice.address = invoice.address.replace("\r", "").replace("\n", "\\\\ ") + # Fill the template with the information + tex = render_to_string("treasury/invoice_sample.tex", dict(obj=invoice, products=products)) + + try: + os.mkdir(BASE_DIR + "/tmp") + except FileExistsError: + pass + # We render the file in a temporary directory + tmp_dir = mkdtemp(prefix=BASE_DIR + "/tmp/") + + try: + with open("{}/invoice-{:d}.tex".format(tmp_dir, pk), "wb") as f: + f.write(tex.encode("UTF-8")) + del tex + + # The file has to be rendered twice + for _ in range(2): + error = subprocess.Popen( + ["pdflatex", "invoice-{}.tex".format(pk)], + cwd=tmp_dir, + stdin=open(os.devnull, "r"), + stderr=open(os.devnull, "wb"), + stdout=open(os.devnull, "wb"), + ).wait() + + if error: + raise IOError("An error attempted while generating a invoice (code=" + str(error) + ")") + + # Display the generated pdf as a HTTP Response + pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read() + response = HttpResponse(pdf, content_type="application/pdf") + response['Content-Disposition'] = "inline;filename=invoice-{:d}.pdf".format(pk) + except IOError as e: + raise e + finally: + # Delete all temporary files + shutil.rmtree(tmp_dir) + + return response + + +class RemittanceCreateView(LoginRequiredMixin, CreateView): + """ + Create Remittance + """ + model = Remittance + form_class = RemittanceForm + + def get_success_url(self): + return reverse_lazy('treasury:remittance_list') + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + ctx["table"] = RemittanceTable(data=Remittance.objects.all()) + ctx["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none()) + + return ctx + + +class RemittanceListView(LoginRequiredMixin, TemplateView): + """ + List existing Remittances + """ + template_name = "treasury/remittance_list.html" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + ctx["opened_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=False).all()) + ctx["closed_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=True).reverse().all()) + + ctx["special_transactions_no_remittance"] = SpecialTransactionTable( + data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), + specialtransactionproxy__remittance=None).all(), + exclude=('remittance_remove', )) + ctx["special_transactions_with_remittance"] = SpecialTransactionTable( + data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), + specialtransactionproxy__remittance__closed=False).all(), + exclude=('remittance_add', )) + + return ctx + + +class RemittanceUpdateView(LoginRequiredMixin, UpdateView): + """ + Update Remittance + """ + model = Remittance + form_class = RemittanceForm + + def get_success_url(self): + return reverse_lazy('treasury:remittance_list') + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + ctx["table"] = RemittanceTable(data=Remittance.objects.all()) + data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).all() + ctx["special_transactions"] = SpecialTransactionTable( + data=data, + exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', )) + + return ctx + + +class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView): + """ + Attach a special transaction to a remittance + """ + + model = SpecialTransactionProxy + form_class = LinkTransactionToRemittanceForm + + def get_success_url(self): + return reverse_lazy('treasury:remittance_list') + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + form = ctx["form"] + form.fields["last_name"].initial = self.object.transaction.last_name + form.fields["first_name"].initial = self.object.transaction.first_name + form.fields["bank"].initial = self.object.transaction.bank + form.fields["amount"].initial = self.object.transaction.amount + form.fields["remittance"].queryset = form.fields["remittance"] \ + .queryset.filter(remittance_type__note=self.object.transaction.source) + + return ctx + + +class UnlinkTransactionToRemittanceView(LoginRequiredMixin, View): + """ + Unlink a special transaction and its remittance + """ + + def get(self, *args, **kwargs): + pk = kwargs["pk"] + transaction = SpecialTransactionProxy.objects.get(pk=pk) + + # The remittance must be open (or inexistant) + if transaction.remittance and transaction.remittance.closed: + raise ValidationError("Remittance is already closed.") + + transaction.remittance = None + transaction.save() + + return redirect('treasury:remittance_list') diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index e61efb2a..b6a8c120 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-03-16 11:53+0100\n" +"POT-Creation-Date: 2020-03-24 15:49+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -23,9 +23,9 @@ msgid "activity" msgstr "" #: apps/activity/models.py:19 apps/activity/models.py:44 -#: apps/member/models.py:61 apps/member/models.py:112 +#: apps/member/models.py:63 apps/member/models.py:114 #: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 -#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202 +#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:198 #: templates/member/profile_detail.html:15 msgid "name" msgstr "" @@ -47,11 +47,12 @@ msgid "activity types" msgstr "" #: apps/activity/models.py:48 apps/note/models/transactions.py:69 +#: apps/permission/models.py:90 msgid "description" msgstr "" #: apps/activity/models.py:54 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115 +#: apps/note/models/transactions.py:62 msgid "type" msgstr "" @@ -143,114 +144,114 @@ msgstr "" msgid "member" msgstr "" -#: apps/member/models.py:23 +#: apps/member/models.py:25 msgid "phone number" msgstr "" -#: apps/member/models.py:29 templates/member/profile_detail.html:28 +#: apps/member/models.py:31 templates/member/profile_detail.html:28 msgid "section" msgstr "" -#: apps/member/models.py:30 +#: apps/member/models.py:32 msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "" -#: apps/member/models.py:36 templates/member/profile_detail.html:31 +#: apps/member/models.py:38 templates/member/profile_detail.html:31 msgid "address" msgstr "" -#: apps/member/models.py:42 +#: apps/member/models.py:44 msgid "paid" msgstr "" -#: apps/member/models.py:47 apps/member/models.py:48 +#: apps/member/models.py:49 apps/member/models.py:50 msgid "user profile" msgstr "" -#: apps/member/models.py:66 +#: apps/member/models.py:68 msgid "email" msgstr "" -#: apps/member/models.py:71 +#: apps/member/models.py:73 msgid "membership fee" msgstr "" -#: apps/member/models.py:75 +#: apps/member/models.py:77 msgid "membership duration" msgstr "" -#: apps/member/models.py:76 +#: apps/member/models.py:78 msgid "The longest time a membership can last (NULL = infinite)." msgstr "" -#: apps/member/models.py:81 +#: apps/member/models.py:83 msgid "membership start" msgstr "" -#: apps/member/models.py:82 +#: apps/member/models.py:84 msgid "How long after January 1st the members can renew their membership." msgstr "" -#: apps/member/models.py:87 +#: apps/member/models.py:89 msgid "membership end" msgstr "" -#: apps/member/models.py:88 +#: apps/member/models.py:90 msgid "" "How long the membership can last after January 1st of the next year after " "members can renew their membership." msgstr "" -#: apps/member/models.py:94 apps/note/models/notes.py:139 +#: apps/member/models.py:96 apps/note/models/notes.py:139 msgid "club" msgstr "" -#: apps/member/models.py:95 +#: apps/member/models.py:97 msgid "clubs" msgstr "" -#: apps/member/models.py:118 +#: apps/member/models.py:120 apps/permission/models.py:275 msgid "role" msgstr "" -#: apps/member/models.py:119 +#: apps/member/models.py:121 msgid "roles" msgstr "" -#: apps/member/models.py:143 +#: apps/member/models.py:145 msgid "membership starts on" msgstr "" -#: apps/member/models.py:146 +#: apps/member/models.py:148 msgid "membership ends on" msgstr "" -#: apps/member/models.py:150 +#: apps/member/models.py:152 msgid "fee" msgstr "" -#: apps/member/models.py:154 +#: apps/member/models.py:162 msgid "membership" msgstr "" -#: apps/member/models.py:155 +#: apps/member/models.py:163 msgid "memberships" msgstr "" -#: apps/member/views.py:69 templates/member/profile_detail.html:46 +#: apps/member/views.py:80 templates/member/profile_detail.html:46 msgid "Update Profile" msgstr "" -#: apps/member/views.py:82 +#: apps/member/views.py:93 msgid "An alias with a similar name already exists." msgstr "" -#: apps/member/views.py:132 +#: apps/member/views.py:146 #, python-format msgid "Account #%(id)s: %(username)s" msgstr "" -#: apps/member/views.py:202 +#: apps/member/views.py:216 msgid "Alias successfully deleted" msgstr "" @@ -415,84 +416,233 @@ msgstr "" msgid "quantity" msgstr "" -#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15 -msgid "Gift" -msgstr "" - -#: apps/note/models/transactions.py:118 templates/base.html:90 -#: templates/note/transaction_form.html:19 -#: templates/note/transaction_form.html:126 -msgid "Transfer" -msgstr "" - -#: apps/note/models/transactions.py:119 -msgid "Template" -msgstr "" - -#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23 -msgid "Credit" -msgstr "" - -#: apps/note/models/transactions.py:121 templates/note/transaction_form.html:27 -msgid "Debit" -msgstr "" - -#: apps/note/models/transactions.py:122 apps/note/models/transactions.py:230 -msgid "membership transaction" -msgstr "" - -#: apps/note/models/transactions.py:129 +#: apps/note/models/transactions.py:115 msgid "reason" msgstr "" -#: apps/note/models/transactions.py:133 +#: apps/note/models/transactions.py:119 msgid "valid" msgstr "" -#: apps/note/models/transactions.py:138 +#: apps/note/models/transactions.py:124 msgid "transaction" msgstr "" -#: apps/note/models/transactions.py:139 +#: apps/note/models/transactions.py:125 msgid "transactions" msgstr "" -#: apps/note/models/transactions.py:207 +#: 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 "" + +#: apps/note/models/transactions.py:188 +msgid "Template" +msgstr "" + +#: apps/note/models/transactions.py:203 msgid "first_name" msgstr "" -#: apps/note/models/transactions.py:212 +#: apps/note/models/transactions.py:208 msgid "bank" msgstr "" +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24 +msgid "Credit" +msgstr "" + +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28 +msgid "Debit" +msgstr "" + +#: apps/note/models/transactions.py:230 apps/note/models/transactions.py:235 +msgid "membership transaction" +msgstr "" + #: apps/note/models/transactions.py:231 msgid "membership transactions" msgstr "" -#: apps/note/views.py:31 +#: apps/note/views.py:39 msgid "Transfer money" msgstr "" -#: apps/note/views.py:132 templates/base.html:78 +#: apps/note/views.py:145 templates/base.html:79 msgid "Consumptions" msgstr "" -#: note_kfet/settings/__init__.py:61 +#: apps/permission/models.py:69 apps/permission/models.py:262 +#, python-brace-format +msgid "Can {type} {model}.{field} in {query}" +msgstr "" + +#: apps/permission/models.py:71 apps/permission/models.py:264 +#, python-brace-format +msgid "Can {type} {model} in {query}" +msgstr "" + +#: apps/permission/models.py:84 +msgid "rank" +msgstr "" + +#: apps/permission/models.py:147 +msgid "Specifying field applies only to view and change permission types." +msgstr "" + +#: apps/treasury/apps.py:11 templates/base.html:102 +msgid "Treasury" +msgstr "" + +#: apps/treasury/forms.py:56 apps/treasury/forms.py:95 +#: templates/django_filters/rest_framework/form.html:5 +#: templates/member/club_form.html:10 templates/treasury/invoice_form.html:47 +msgid "Submit" +msgstr "" + +#: apps/treasury/forms.py:58 +msgid "Close" +msgstr "" + +#: apps/treasury/forms.py:65 +msgid "Remittance is already closed." +msgstr "" + +#: apps/treasury/forms.py:70 +msgid "You can't change the type of the remittance." +msgstr "" + +#: apps/treasury/forms.py:84 +msgid "Last name" +msgstr "" + +#: apps/treasury/forms.py:86 templates/note/transaction_form.html:92 +msgid "First name" +msgstr "" + +#: apps/treasury/forms.py:88 templates/note/transaction_form.html:98 +msgid "Bank" +msgstr "" + +#: apps/treasury/forms.py:90 apps/treasury/tables.py:40 +#: templates/note/transaction_form.html:128 +#: templates/treasury/remittance_form.html:18 +msgid "Amount" +msgstr "" + +#: apps/treasury/models.py:18 +msgid "Invoice identifier" +msgstr "" + +#: apps/treasury/models.py:32 +msgid "BDE" +msgstr "" + +#: apps/treasury/models.py:37 +msgid "Object" +msgstr "" + +#: apps/treasury/models.py:41 +msgid "Description" +msgstr "" + +#: apps/treasury/models.py:46 templates/note/transaction_form.html:86 +msgid "Name" +msgstr "" + +#: apps/treasury/models.py:50 +msgid "Address" +msgstr "" + +#: apps/treasury/models.py:55 +msgid "Place" +msgstr "" + +#: apps/treasury/models.py:59 +msgid "Acquitted" +msgstr "" + +#: apps/treasury/models.py:75 +msgid "Designation" +msgstr "" + +#: apps/treasury/models.py:79 +msgid "Quantity" +msgstr "" + +#: apps/treasury/models.py:83 +msgid "Unit price" +msgstr "" + +#: apps/treasury/models.py:120 +msgid "Date" +msgstr "" + +#: apps/treasury/models.py:126 +msgid "Type" +msgstr "" + +#: apps/treasury/models.py:131 +msgid "Comment" +msgstr "" + +#: apps/treasury/models.py:136 +msgid "Closed" +msgstr "" + +#: apps/treasury/models.py:159 +msgid "Remittance #{:d}: {}" +msgstr "" + +#: apps/treasury/models.py:178 apps/treasury/tables.py:64 +#: apps/treasury/tables.py:72 templates/treasury/invoice_list.html:13 +#: templates/treasury/remittance_list.html:13 +msgid "Remittance" +msgstr "" + +#: apps/treasury/tables.py:16 +msgid "Invoice #{:d}" +msgstr "" + +#: apps/treasury/tables.py:19 templates/treasury/invoice_list.html:10 +#: templates/treasury/remittance_list.html:10 +msgid "Invoice" +msgstr "" + +#: apps/treasury/tables.py:38 +msgid "Transaction count" +msgstr "" + +#: apps/treasury/tables.py:43 apps/treasury/tables.py:45 +msgid "View" +msgstr "" + +#: apps/treasury/tables.py:66 +msgid "Add" +msgstr "" + +#: apps/treasury/tables.py:74 +msgid "Remove" +msgstr "" + +#: note_kfet/settings/__init__.py:63 msgid "" "The Central Authentication Service grants you access to most of our websites " "by authenticating only once, so you don't need to type your credentials " "again unless your session expires or you logout." msgstr "" -#: note_kfet/settings/base.py:156 +#: note_kfet/settings/base.py:153 msgid "German" msgstr "" -#: note_kfet/settings/base.py:157 +#: note_kfet/settings/base.py:154 msgid "English" msgstr "" -#: note_kfet/settings/base.py:158 +#: note_kfet/settings/base.py:155 msgid "French" msgstr "" @@ -500,15 +650,15 @@ msgstr "" msgid "The ENS Paris-Saclay BDE note." msgstr "" -#: templates/base.html:81 +#: templates/base.html:84 msgid "Clubs" msgstr "" -#: templates/base.html:84 +#: templates/base.html:89 msgid "Activities" msgstr "" -#: templates/base.html:87 +#: templates/base.html:94 msgid "Buttons" msgstr "" @@ -567,11 +717,6 @@ msgstr "" msgid "Field filters" msgstr "" -#: templates/django_filters/rest_framework/form.html:5 -#: templates/member/club_form.html:10 -msgid "Submit" -msgstr "" - #: templates/member/club_detail.html:10 msgid "Membership starts on" msgstr "" @@ -653,7 +798,7 @@ msgstr "" msgid "Sign up" msgstr "" -#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38 +#: templates/note/conso_form.html:28 templates/note/transaction_form.html:40 msgid "Select emitters" msgstr "" @@ -681,49 +826,37 @@ msgstr "" msgid "Double consumptions" msgstr "" -#: templates/note/conso_form.html:141 +#: templates/note/conso_form.html:141 templates/note/transaction_form.html:152 msgid "Recent transactions history" msgstr "" -#: templates/note/transaction_form.html:55 +#: templates/note/transaction_form.html:15 +msgid "Gift" +msgstr "" + +#: templates/note/transaction_form.html:68 msgid "External payment" msgstr "" -#: templates/note/transaction_form.html:63 +#: templates/note/transaction_form.html:76 msgid "Transfer type" msgstr "" -#: templates/note/transaction_form.html:73 -msgid "Name" -msgstr "" - -#: templates/note/transaction_form.html:79 -msgid "First name" -msgstr "" - -#: templates/note/transaction_form.html:85 -msgid "Bank" -msgstr "" - -#: templates/note/transaction_form.html:97 -#: templates/note/transaction_form.html:179 -#: templates/note/transaction_form.html:186 +#: templates/note/transaction_form.html:111 +#: templates/note/transaction_form.html:169 +#: templates/note/transaction_form.html:176 msgid "Select receivers" msgstr "" -#: templates/note/transaction_form.html:114 -msgid "Amount" -msgstr "" - -#: templates/note/transaction_form.html:119 +#: templates/note/transaction_form.html:138 msgid "Reason" msgstr "" -#: templates/note/transaction_form.html:193 +#: templates/note/transaction_form.html:183 msgid "Credit note" msgstr "" -#: templates/note/transaction_form.html:200 +#: templates/note/transaction_form.html:190 msgid "Debit note" msgstr "" @@ -808,3 +941,72 @@ msgstr "" #: templates/registration/password_reset_form.html:11 msgid "Reset my password" msgstr "" + +#: templates/treasury/invoice_form.html:6 +msgid "Invoices list" +msgstr "" + +#: templates/treasury/invoice_form.html:42 +msgid "Add product" +msgstr "" + +#: templates/treasury/invoice_form.html:43 +msgid "Remove product" +msgstr "" + +#: templates/treasury/invoice_list.html:21 +msgid "New invoice" +msgstr "" + +#: templates/treasury/remittance_form.html:7 +msgid "Remittance #" +msgstr "" + +#: templates/treasury/remittance_form.html:9 +#: templates/treasury/specialtransactionproxy_form.html:7 +msgid "Remittances list" +msgstr "" + +#: templates/treasury/remittance_form.html:12 +msgid "Count" +msgstr "" + +#: templates/treasury/remittance_form.html:29 +msgid "Linked transactions" +msgstr "" + +#: templates/treasury/remittance_form.html:34 +msgid "There is no transaction linked with this remittance." +msgstr "" + +#: templates/treasury/remittance_list.html:19 +msgid "Opened remittances" +msgstr "" + +#: templates/treasury/remittance_list.html:24 +msgid "There is no opened remittance." +msgstr "" + +#: templates/treasury/remittance_list.html:28 +msgid "New remittance" +msgstr "" + +#: templates/treasury/remittance_list.html:32 +msgid "Transfers without remittances" +msgstr "" + +#: templates/treasury/remittance_list.html:37 +msgid "There is no transaction without any linked remittance." +msgstr "" + +#: templates/treasury/remittance_list.html:43 +msgid "Transfers with opened remittances" +msgstr "" + +#: templates/treasury/remittance_list.html:48 +msgid "There is no transaction with an opened linked remittance." +msgstr "" + +#: templates/treasury/remittance_list.html:54 +msgid "Closed remittances" +msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 5e6e9470..67af6bee 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-03-16 11:53+0100\n" +"POT-Creation-Date: 2020-03-24 15:49+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,9 +18,9 @@ msgid "activity" msgstr "activité" #: apps/activity/models.py:19 apps/activity/models.py:44 -#: apps/member/models.py:61 apps/member/models.py:112 +#: apps/member/models.py:63 apps/member/models.py:114 #: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 -#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202 +#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:198 #: templates/member/profile_detail.html:15 msgid "name" msgstr "nom" @@ -42,11 +42,12 @@ msgid "activity types" msgstr "types d'activité" #: apps/activity/models.py:48 apps/note/models/transactions.py:69 +#: apps/permission/models.py:90 msgid "description" msgstr "description" #: apps/activity/models.py:54 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115 +#: apps/note/models/transactions.py:62 msgid "type" msgstr "type" @@ -138,61 +139,61 @@ msgstr "Les logs ne peuvent pas être détruits." msgid "member" msgstr "adhérent" -#: apps/member/models.py:23 +#: apps/member/models.py:25 msgid "phone number" msgstr "numéro de téléphone" -#: apps/member/models.py:29 templates/member/profile_detail.html:28 +#: apps/member/models.py:31 templates/member/profile_detail.html:28 msgid "section" msgstr "section" -#: apps/member/models.py:30 +#: apps/member/models.py:32 msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" -#: apps/member/models.py:36 templates/member/profile_detail.html:31 +#: apps/member/models.py:38 templates/member/profile_detail.html:31 msgid "address" msgstr "adresse" -#: apps/member/models.py:42 +#: apps/member/models.py:44 msgid "paid" msgstr "payé" -#: apps/member/models.py:47 apps/member/models.py:48 +#: apps/member/models.py:49 apps/member/models.py:50 msgid "user profile" msgstr "profil utilisateur" -#: apps/member/models.py:66 +#: apps/member/models.py:68 msgid "email" msgstr "courriel" -#: apps/member/models.py:71 +#: apps/member/models.py:73 msgid "membership fee" msgstr "cotisation pour adhérer" -#: apps/member/models.py:75 +#: apps/member/models.py:77 msgid "membership duration" msgstr "durée de l'adhésion" -#: apps/member/models.py:76 +#: apps/member/models.py:78 msgid "The longest time a membership can last (NULL = infinite)." msgstr "La durée maximale d'une adhésion (NULL = infinie)." -#: apps/member/models.py:81 +#: apps/member/models.py:83 msgid "membership start" msgstr "début de l'adhésion" -#: apps/member/models.py:82 +#: apps/member/models.py:84 msgid "How long after January 1st the members can renew their membership." msgstr "" "Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur " "adhésion." -#: apps/member/models.py:87 +#: apps/member/models.py:89 msgid "membership end" msgstr "fin de l'adhésion" -#: apps/member/models.py:88 +#: apps/member/models.py:90 msgid "" "How long the membership can last after January 1st of the next year after " "members can renew their membership." @@ -200,56 +201,56 @@ msgstr "" "Combien de temps l'adhésion peut durer après le 1er Janvier de l'année " "suivante avant que les adhérents peuvent renouveler leur adhésion." -#: apps/member/models.py:94 apps/note/models/notes.py:139 +#: apps/member/models.py:96 apps/note/models/notes.py:139 msgid "club" msgstr "club" -#: apps/member/models.py:95 +#: apps/member/models.py:97 msgid "clubs" msgstr "clubs" -#: apps/member/models.py:118 +#: apps/member/models.py:120 apps/permission/models.py:275 msgid "role" msgstr "rôle" -#: apps/member/models.py:119 +#: apps/member/models.py:121 msgid "roles" msgstr "rôles" -#: apps/member/models.py:143 +#: apps/member/models.py:145 msgid "membership starts on" msgstr "l'adhésion commence le" -#: apps/member/models.py:146 +#: apps/member/models.py:148 msgid "membership ends on" msgstr "l'adhésion finie le" -#: apps/member/models.py:150 +#: apps/member/models.py:152 msgid "fee" msgstr "cotisation" -#: apps/member/models.py:154 +#: apps/member/models.py:162 msgid "membership" msgstr "adhésion" -#: apps/member/models.py:155 +#: apps/member/models.py:163 msgid "memberships" msgstr "adhésions" -#: apps/member/views.py:69 templates/member/profile_detail.html:46 +#: apps/member/views.py:80 templates/member/profile_detail.html:46 msgid "Update Profile" msgstr "Modifier le profil" -#: apps/member/views.py:82 +#: apps/member/views.py:93 msgid "An alias with a similar name already exists." msgstr "Un alias avec un nom similaire existe déjà." -#: apps/member/views.py:132 +#: apps/member/views.py:146 #, python-format msgid "Account #%(id)s: %(username)s" msgstr "Compte n°%(id)s : %(username)s" -#: apps/member/views.py:202 +#: apps/member/views.py:216 msgid "Alias successfully deleted" msgstr "L'alias a bien été supprimé" @@ -415,84 +416,233 @@ msgstr "modèles de transaction" msgid "quantity" msgstr "quantité" -#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15 -msgid "Gift" -msgstr "Don" - -#: apps/note/models/transactions.py:118 templates/base.html:90 -#: templates/note/transaction_form.html:19 -#: templates/note/transaction_form.html:126 -msgid "Transfer" -msgstr "Virement" - -#: apps/note/models/transactions.py:119 -msgid "Template" -msgstr "Bouton" - -#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23 -msgid "Credit" -msgstr "Crédit" - -#: apps/note/models/transactions.py:121 templates/note/transaction_form.html:27 -msgid "Debit" -msgstr "Retrait" - -#: apps/note/models/transactions.py:122 apps/note/models/transactions.py:230 -msgid "membership transaction" -msgstr "transaction d'adhésion" - -#: apps/note/models/transactions.py:129 +#: apps/note/models/transactions.py:115 msgid "reason" msgstr "raison" -#: apps/note/models/transactions.py:133 +#: apps/note/models/transactions.py:119 msgid "valid" msgstr "valide" -#: apps/note/models/transactions.py:138 +#: apps/note/models/transactions.py:124 msgid "transaction" msgstr "transaction" -#: apps/note/models/transactions.py:139 +#: apps/note/models/transactions.py:125 msgid "transactions" msgstr "transactions" -#: apps/note/models/transactions.py:207 -msgid "first_name" -msgstr "Prénom" +#: apps/note/models/transactions.py:168 templates/base.html:98 +#: templates/note/transaction_form.html:19 +#: templates/note/transaction_form.html:145 +msgid "Transfer" +msgstr "Virement" -#: apps/note/models/transactions.py:212 +#: apps/note/models/transactions.py:188 +msgid "Template" +msgstr "Bouton" + +#: apps/note/models/transactions.py:203 +msgid "first_name" +msgstr "prénom" + +#: apps/note/models/transactions.py:208 msgid "bank" -msgstr "Banque" +msgstr "banque" + +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24 +msgid "Credit" +msgstr "Crédit" + +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28 +msgid "Debit" +msgstr "Débit" + +#: apps/note/models/transactions.py:230 apps/note/models/transactions.py:235 +msgid "membership transaction" +msgstr "transaction d'adhésion" #: apps/note/models/transactions.py:231 msgid "membership transactions" msgstr "transactions d'adhésion" -#: apps/note/views.py:31 +#: apps/note/views.py:39 msgid "Transfer money" -msgstr "Transferts d'argent" +msgstr "Transférer de l'argent" -#: apps/note/views.py:132 templates/base.html:78 +#: apps/note/views.py:145 templates/base.html:79 msgid "Consumptions" msgstr "Consommations" -#: note_kfet/settings/__init__.py:61 +#: apps/permission/models.py:69 apps/permission/models.py:262 +#, python-brace-format +msgid "Can {type} {model}.{field} in {query}" +msgstr "" + +#: apps/permission/models.py:71 apps/permission/models.py:264 +#, python-brace-format +msgid "Can {type} {model} in {query}" +msgstr "" + +#: apps/permission/models.py:84 +msgid "rank" +msgstr "Rang" + +#: apps/permission/models.py:147 +msgid "Specifying field applies only to view and change permission types." +msgstr "" + +#: apps/treasury/apps.py:11 templates/base.html:102 +msgid "Treasury" +msgstr "Trésorerie" + +#: apps/treasury/forms.py:56 apps/treasury/forms.py:95 +#: templates/django_filters/rest_framework/form.html:5 +#: templates/member/club_form.html:10 templates/treasury/invoice_form.html:47 +msgid "Submit" +msgstr "Envoyer" + +#: apps/treasury/forms.py:58 +msgid "Close" +msgstr "Fermer" + +#: apps/treasury/forms.py:65 +msgid "Remittance is already closed." +msgstr "La remise est déjà fermée." + +#: apps/treasury/forms.py:70 +msgid "You can't change the type of the remittance." +msgstr "Vous ne pouvez pas changer le type de la remise." + +#: apps/treasury/forms.py:84 +msgid "Last name" +msgstr "Nom de famille" + +#: apps/treasury/forms.py:86 templates/note/transaction_form.html:92 +msgid "First name" +msgstr "Prénom" + +#: apps/treasury/forms.py:88 templates/note/transaction_form.html:98 +msgid "Bank" +msgstr "Banque" + +#: apps/treasury/forms.py:90 apps/treasury/tables.py:40 +#: templates/note/transaction_form.html:128 +#: templates/treasury/remittance_form.html:18 +msgid "Amount" +msgstr "Montant" + +#: apps/treasury/models.py:18 +msgid "Invoice identifier" +msgstr "Numéro de facture" + +#: apps/treasury/models.py:32 +msgid "BDE" +msgstr "BDE" + +#: apps/treasury/models.py:37 +msgid "Object" +msgstr "Objet" + +#: apps/treasury/models.py:41 +msgid "Description" +msgstr "Description" + +#: apps/treasury/models.py:46 templates/note/transaction_form.html:86 +msgid "Name" +msgstr "Nom" + +#: apps/treasury/models.py:50 +msgid "Address" +msgstr "Adresse" + +#: apps/treasury/models.py:55 +msgid "Place" +msgstr "Lieu" + +#: apps/treasury/models.py:59 +msgid "Acquitted" +msgstr "Acquittée" + +#: apps/treasury/models.py:75 +msgid "Designation" +msgstr "Désignation" + +#: apps/treasury/models.py:79 +msgid "Quantity" +msgstr "Quantité" + +#: apps/treasury/models.py:83 +msgid "Unit price" +msgstr "Prix unitaire" + +#: apps/treasury/models.py:120 +msgid "Date" +msgstr "Date" + +#: apps/treasury/models.py:126 +msgid "Type" +msgstr "Type" + +#: apps/treasury/models.py:131 +msgid "Comment" +msgstr "Commentaire" + +#: apps/treasury/models.py:136 +msgid "Closed" +msgstr "Fermée" + +#: apps/treasury/models.py:159 +msgid "Remittance #{:d}: {}" +msgstr "Remise n°{:d} : {}" + +#: apps/treasury/models.py:178 apps/treasury/tables.py:64 +#: apps/treasury/tables.py:72 templates/treasury/invoice_list.html:13 +#: templates/treasury/remittance_list.html:13 +msgid "Remittance" +msgstr "Remise" + +#: apps/treasury/tables.py:16 +msgid "Invoice #{:d}" +msgstr "Facture n°{:d}" + +#: apps/treasury/tables.py:19 templates/treasury/invoice_list.html:10 +#: templates/treasury/remittance_list.html:10 +msgid "Invoice" +msgstr "Facture" + +#: apps/treasury/tables.py:38 +msgid "Transaction count" +msgstr "Nombre de transactions" + +#: apps/treasury/tables.py:43 apps/treasury/tables.py:45 +msgid "View" +msgstr "Voir" + +#: apps/treasury/tables.py:66 +msgid "Add" +msgstr "Ajouter" + +#: apps/treasury/tables.py:74 +msgid "Remove" +msgstr "supprimer" + +#: note_kfet/settings/__init__.py:63 msgid "" "The Central Authentication Service grants you access to most of our websites " "by authenticating only once, so you don't need to type your credentials " "again unless your session expires or you logout." msgstr "" -#: note_kfet/settings/base.py:156 +#: note_kfet/settings/base.py:153 msgid "German" msgstr "" -#: note_kfet/settings/base.py:157 +#: note_kfet/settings/base.py:154 msgid "English" msgstr "" -#: note_kfet/settings/base.py:158 +#: note_kfet/settings/base.py:155 msgid "French" msgstr "" @@ -500,15 +650,15 @@ msgstr "" msgid "The ENS Paris-Saclay BDE note." msgstr "La note du BDE de l'ENS Paris-Saclay." -#: templates/base.html:81 +#: templates/base.html:84 msgid "Clubs" msgstr "Clubs" -#: templates/base.html:84 +#: templates/base.html:89 msgid "Activities" msgstr "Activités" -#: templates/base.html:87 +#: templates/base.html:94 msgid "Buttons" msgstr "Boutons" @@ -569,11 +719,6 @@ msgstr "" msgid "Field filters" msgstr "" -#: templates/django_filters/rest_framework/form.html:5 -#: templates/member/club_form.html:10 -msgid "Submit" -msgstr "Envoyer" - #: templates/member/club_detail.html:10 msgid "Membership starts on" msgstr "L'adhésion commence le" @@ -620,15 +765,15 @@ msgstr "Ajouter un alias" #: templates/member/profile_detail.html:15 msgid "first name" -msgstr "" +msgstr "prénom" #: templates/member/profile_detail.html:18 msgid "username" -msgstr "" +msgstr "pseudo" #: templates/member/profile_detail.html:21 msgid "password" -msgstr "" +msgstr "mot de passe" #: templates/member/profile_detail.html:24 msgid "Change password" @@ -655,13 +800,13 @@ msgstr "Sauvegarder les changements" msgid "Sign up" msgstr "Inscription" -#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38 +#: templates/note/conso_form.html:28 templates/note/transaction_form.html:40 msgid "Select emitters" msgstr "Sélection des émetteurs" #: templates/note/conso_form.html:45 msgid "Select consumptions" -msgstr "Consommations" +msgstr "Sélection des consommations" #: templates/note/conso_form.html:51 msgid "Consume!" @@ -677,55 +822,43 @@ msgstr "Éditer" #: templates/note/conso_form.html:126 msgid "Single consumptions" -msgstr "Consos simples" +msgstr "Consommations simples" #: templates/note/conso_form.html:130 msgid "Double consumptions" -msgstr "Consos doubles" +msgstr "Consommations doubles" -#: templates/note/conso_form.html:141 +#: templates/note/conso_form.html:141 templates/note/transaction_form.html:152 msgid "Recent transactions history" msgstr "Historique des transactions récentes" -#: templates/note/transaction_form.html:55 -msgid "External payment" -msgstr "Paiement extérieur" +#: templates/note/transaction_form.html:15 +msgid "Gift" +msgstr "Don" -#: templates/note/transaction_form.html:63 +#: templates/note/transaction_form.html:68 +msgid "External payment" +msgstr "Paiement externe" + +#: templates/note/transaction_form.html:76 msgid "Transfer type" msgstr "Type de transfert" -#: templates/note/transaction_form.html:73 -msgid "Name" -msgstr "Nom" - -#: templates/note/transaction_form.html:79 -msgid "First name" -msgstr "Prénom" - -#: templates/note/transaction_form.html:85 -msgid "Bank" -msgstr "Banque" - -#: templates/note/transaction_form.html:97 -#: templates/note/transaction_form.html:179 -#: templates/note/transaction_form.html:186 +#: templates/note/transaction_form.html:111 +#: templates/note/transaction_form.html:169 +#: templates/note/transaction_form.html:176 msgid "Select receivers" msgstr "Sélection des destinataires" -#: templates/note/transaction_form.html:114 -msgid "Amount" -msgstr "Montant" - -#: templates/note/transaction_form.html:119 +#: templates/note/transaction_form.html:138 msgid "Reason" msgstr "Raison" -#: templates/note/transaction_form.html:193 +#: templates/note/transaction_form.html:183 msgid "Credit note" -msgstr "Note à créditer" +msgstr "Note à recharger" -#: templates/note/transaction_form.html:200 +#: templates/note/transaction_form.html:190 msgid "Debit note" msgstr "Note à débiter" @@ -810,3 +943,72 @@ msgstr "" #: templates/registration/password_reset_form.html:11 msgid "Reset my password" msgstr "" + +#: templates/treasury/invoice_form.html:6 +msgid "Invoices list" +msgstr "Liste des factures" + +#: templates/treasury/invoice_form.html:42 +msgid "Add product" +msgstr "Ajouter produit" + +#: templates/treasury/invoice_form.html:43 +msgid "Remove product" +msgstr "Retirer produit" + +#: templates/treasury/invoice_list.html:21 +msgid "New invoice" +msgstr "Nouvelle facture" + +#: templates/treasury/remittance_form.html:7 +msgid "Remittance #" +msgstr "Remise n°" + +#: templates/treasury/remittance_form.html:9 +#: templates/treasury/specialtransactionproxy_form.html:7 +msgid "Remittances list" +msgstr "Liste des remises" + +#: templates/treasury/remittance_form.html:12 +msgid "Count" +msgstr "Nombre" + +#: templates/treasury/remittance_form.html:29 +msgid "Linked transactions" +msgstr "Transactions liées" + +#: templates/treasury/remittance_form.html:34 +msgid "There is no transaction linked with this remittance." +msgstr "Il n'y a pas de transaction liée à cette remise." + +#: templates/treasury/remittance_list.html:19 +msgid "Opened remittances" +msgstr "Remises ouvertes" + +#: templates/treasury/remittance_list.html:24 +msgid "There is no opened remittance." +msgstr "Il n'y a pas de remise ouverte." + +#: templates/treasury/remittance_list.html:28 +msgid "New remittance" +msgstr "Nouvelle remise" + +#: templates/treasury/remittance_list.html:32 +msgid "Transfers without remittances" +msgstr "Transactions sans remise associée" + +#: templates/treasury/remittance_list.html:37 +msgid "There is no transaction without any linked remittance." +msgstr "Il n'y a pas de transactions sans remise associée." + +#: templates/treasury/remittance_list.html:43 +msgid "Transfers with opened remittances" +msgstr "Transactions associées à une remise ouverte" + +#: templates/treasury/remittance_list.html:48 +msgid "There is no transaction with an opened linked remittance." +msgstr "Il n'y a pas de transaction associée à une remise ouverte." + +#: templates/treasury/remittance_list.html:54 +msgid "Closed remittances" +msgstr "Remises fermées" diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 216199de..d49b2542 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -59,6 +59,7 @@ INSTALLED_APPS = [ 'activity', 'member', 'note', + 'treasury', 'permission', 'api', 'logs', diff --git a/note_kfet/urls.py b/note_kfet/urls.py index 9170c62e..40a9a614 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -15,6 +15,7 @@ urlpatterns = [ # Include project routers path('note/', include('note.urls')), + path('treasury/', include('treasury.urls')), # Include Django Contrib and Core routers path('i18n/', include('django.conf.urls.i18n')), diff --git a/requirements/production.txt b/requirements/production.txt index f0b52228..fe939cce 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -1 +1 @@ -psycopg2==2.8.4 +psycopg2-binary==2.8.4 diff --git a/static/img/Finalist.png b/static/img/Finalist.png new file mode 100644 index 00000000..1a3c41f3 Binary files /dev/null and b/static/img/Finalist.png differ diff --git a/static/img/Kataclist.png b/static/img/Kataclist.png new file mode 100644 index 00000000..97fc4115 Binary files /dev/null and b/static/img/Kataclist.png differ diff --git a/static/img/Listorique.png b/static/img/Listorique.png new file mode 100644 index 00000000..c5158324 Binary files /dev/null and b/static/img/Listorique.png differ diff --git a/static/img/Monopolist.png b/static/img/Monopolist.png new file mode 100644 index 00000000..2685b21e Binary files /dev/null and b/static/img/Monopolist.png differ diff --git a/static/img/Satellist.png b/static/img/Satellist.png new file mode 100644 index 00000000..d2377f67 Binary files /dev/null and b/static/img/Satellist.png differ diff --git a/static/js/base.js b/static/js/base.js index f7085850..b22df077 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 87edfaae..c6ff3328 100644 --- a/static/js/dynamic-formset.js +++ b/static/js/dynamic-formset.js @@ -1,5 +1,5 @@ /** - * jQuery Formset 1.3-pre + * jQuery Formset 1.5-pre * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com) * @requires jQuery 1.2.6 or later * @@ -55,19 +55,26 @@ insertDeleteLink = function(row) { var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'), addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.'); - if (row.is('TR')) { + + var delButtonHTML = '' + options.deleteText +''; + if (options.deleteContainerClass) { + // If we have a specific container for the remove button, + // place it as the last child of that container: + row.find('[class*="' + options.deleteContainerClass + '"]').append(delButtonHTML); + } else if (row.is('TR')) { // If the forms are laid out in table rows, insert // the remove button into the last table cell: - row.children(':last').append('' + options.deleteText + ''); + row.children('td:last').append(delButtonHTML); } else if (row.is('UL') || row.is('OL')) { // If they're laid out as an ordered/unordered list, // insert an
  • after the last list item: - row.append('
  • ' + options.deleteText +'
  • '); + row.append('
  • ' + delButtonHTML + '
  • '); } else { // Otherwise, just insert the remove button as the // last child element of the form's container: - row.append('' + options.deleteText +''); + row.append(delButtonHTML); } + // Check if we're under the minimum number of forms - not to display delete link at rendering if (!showDeleteLinks()){ row.find('a.' + delCssSelector).hide(); @@ -156,6 +163,7 @@ } else { // Otherwise, use the last form in the formset; this works much better if you've got // extra (>= 1) forms (thnaks to justhamade for pointing this out): + if (options.hideLastAddForm) $('.' + options.formCssClass + ':last').hide(); template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id'); template.find('input:hidden[id $= "-DELETE"]').remove(); // Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion): @@ -173,21 +181,28 @@ // FIXME: Perhaps using $.data would be a better idea? options.formTemplate = template; - if ($$.is('TR')) { + var addButtonHTML = '' + options.addText + ''; + if (options.addContainerClass) { + // If we have a specific container for the "add" button, + // place it as the last child of that container: + var addContainer = $('[class*="' + options.addContainerClass + '"'); + addContainer.append(addButtonHTML); + addButton = addContainer.find('[class="' + options.addCssClass + '"]'); + } else if ($$.is('TR')) { // If forms are laid out as table rows, insert the // "add" button in a new table row: var numCols = $$.eq(0).children().length, // This is a bit of an assumption :| - buttonRow = $('' + options.addText + '') - .addClass(options.formCssClass + '-add'); + buttonRow = $('' + addButtonHTML + '').addClass(options.formCssClass + '-add'); $$.parent().append(buttonRow); - if (hideAddButton) buttonRow.hide(); addButton = buttonRow.find('a'); } else { // Otherwise, insert it immediately after the last form: - $$.filter(':last').after('' + options.addText + ''); + $$.filter(':last').after(addButtonHTML); addButton = $$.filter(':last').next(); - if (hideAddButton) addButton.hide(); } + + if (hideAddButton) addButton.hide(); + addButton.click(function() { var formCount = parseInt(totalForms.val()), row = options.formTemplate.clone(true).removeClass('formset-custom-template'), @@ -220,12 +235,15 @@ formTemplate: null, // The jQuery selection cloned to generate new form instances addText: 'add another', // Text for the add link deleteText: 'remove', // Text for the delete link - addCssClass: '', // CSS class applied to the add link - deleteCssClass: '', // CSS class applied to the delete link + addContainerClass: null, // Container CSS class for the add link + deleteContainerClass: null, // Container CSS class for the delete link + addCssClass: 'add-row', // CSS class applied to the add link + deleteCssClass: 'delete-row', // CSS class applied to the delete link formCssClass: 'dynamic-form', // CSS class applied to each form in a formset extraClasses: [], // Additional CSS classes, which will be applied to each form in turn keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned added: null, // Function called each time a new form is added - removed: null // Function called each time a form is deleted + removed: null, // Function called each time a form is deleted + hideLastAddForm: false // When set to true, hide last empty add form (becomes visible when clicking on add button) }; })(jQuery); diff --git a/static/js/transfer.js b/static/js/transfer.js index c615f932..a4171910 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 2f07a6cc..62fc9c58 100644 --- a/templates/base.html +++ b/templates/base.html @@ -92,6 +92,11 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans 'Activities' %} {% endif %} + {% if "treasury.invoice"|not_empty_model_change_list %} + + {% endif %}