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/note/fixtures/initial.json b/apps/note/fixtures/initial.json index 63285e34b2c525243d3e851579db0f9ad40b10b3..a0682daef6dcf8816f05e4ff2ec8781770324fd9 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 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 e61efb2a6f2e68626aefb660983d2931fba657ac..b6a8c1202f4818fc342f9423b7bcead9f6770217 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 <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\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" +#: apps/note/models/transactions.py:115 +msgid "reason" +msgstr "" + +#: apps/note/models/transactions.py:119 +msgid "valid" +msgstr "" + +#: apps/note/models/transactions.py:124 +msgid "transaction" +msgstr "" + +#: apps/note/models/transactions.py:125 +msgid "transactions" msgstr "" -#: apps/note/models/transactions.py:118 templates/base.html:90 +#: apps/note/models/transactions.py:168 templates/base.html:98 #: templates/note/transaction_form.html:19 -#: templates/note/transaction_form.html:126 +#: templates/note/transaction_form.html:145 msgid "Transfer" msgstr "" -#: apps/note/models/transactions.py:119 +#: apps/note/models/transactions.py:188 msgid "Template" msgstr "" -#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23 +#: apps/note/models/transactions.py:203 +msgid "first_name" +msgstr "" + +#: 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:121 templates/note/transaction_form.html:27 +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28 msgid "Debit" msgstr "" -#: apps/note/models/transactions.py:122 apps/note/models/transactions.py:230 +#: apps/note/models/transactions.py:230 apps/note/models/transactions.py:235 msgid "membership transaction" msgstr "" -#: apps/note/models/transactions.py:129 -msgid "reason" +#: apps/note/models/transactions.py:231 +msgid "membership transactions" msgstr "" -#: apps/note/models/transactions.py:133 -msgid "valid" +#: apps/note/views.py:39 +msgid "Transfer money" msgstr "" -#: apps/note/models/transactions.py:138 -msgid "transaction" +#: apps/note/views.py:145 templates/base.html:79 +msgid "Consumptions" msgstr "" -#: apps/note/models/transactions.py:139 -msgid "transactions" +#: apps/permission/models.py:69 apps/permission/models.py:262 +#, python-brace-format +msgid "Can {type} {model}.{field} in {query}" msgstr "" -#: apps/note/models/transactions.py:207 -msgid "first_name" +#: apps/permission/models.py:71 apps/permission/models.py:264 +#, python-brace-format +msgid "Can {type} {model} in {query}" msgstr "" -#: apps/note/models/transactions.py:212 -msgid "bank" +#: apps/permission/models.py:84 +msgid "rank" msgstr "" -#: apps/note/models/transactions.py:231 -msgid "membership transactions" +#: apps/permission/models.py:147 +msgid "Specifying field applies only to view and change permission types." msgstr "" -#: apps/note/views.py:31 -msgid "Transfer money" +#: apps/treasury/apps.py:11 templates/base.html:102 +msgid "Treasury" msgstr "" -#: apps/note/views.py:132 templates/base.html:78 -msgid "Consumptions" +#: 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 "" -#: note_kfet/settings/__init__.py:61 +#: 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 -msgid "External payment" -msgstr "" - -#: templates/note/transaction_form.html:63 -msgid "Transfer type" -msgstr "" - -#: templates/note/transaction_form.html:73 -msgid "Name" +#: templates/note/transaction_form.html:15 +msgid "Gift" msgstr "" -#: templates/note/transaction_form.html:79 -msgid "First name" +#: templates/note/transaction_form.html:68 +msgid "External payment" msgstr "" -#: templates/note/transaction_form.html:85 -msgid "Bank" +#: templates/note/transaction_form.html:76 +msgid "Transfer type" 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 5e6e94704655dc49e1fb27ae2552aab43dc21301..67af6beeac1bcf6e5c4cd6f83f6d46db953320ab 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 <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\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:115 +msgid "reason" +msgstr "raison" + +#: apps/note/models/transactions.py:119 +msgid "valid" +msgstr "valide" -#: apps/note/models/transactions.py:118 templates/base.html:90 +#: apps/note/models/transactions.py:124 +msgid "transaction" +msgstr "transaction" + +#: apps/note/models/transactions.py:125 +msgid "transactions" +msgstr "transactions" + +#: apps/note/models/transactions.py:168 templates/base.html:98 #: templates/note/transaction_form.html:19 -#: templates/note/transaction_form.html:126 +#: templates/note/transaction_form.html:145 msgid "Transfer" msgstr "Virement" -#: apps/note/models/transactions.py:119 +#: apps/note/models/transactions.py:188 msgid "Template" msgstr "Bouton" -#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23 +#: apps/note/models/transactions.py:203 +msgid "first_name" +msgstr "prénom" + +#: apps/note/models/transactions.py:208 +msgid "bank" +msgstr "banque" + +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24 msgid "Credit" msgstr "Crédit" -#: apps/note/models/transactions.py:121 templates/note/transaction_form.html:27 +#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28 msgid "Debit" -msgstr "Retrait" +msgstr "Débit" -#: apps/note/models/transactions.py:122 apps/note/models/transactions.py:230 +#: 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:129 -msgid "reason" -msgstr "raison" +#: apps/note/models/transactions.py:231 +msgid "membership transactions" +msgstr "transactions d'adhésion" -#: apps/note/models/transactions.py:133 -msgid "valid" -msgstr "valide" +#: apps/note/views.py:39 +msgid "Transfer money" +msgstr "Transférer de l'argent" -#: apps/note/models/transactions.py:138 -msgid "transaction" -msgstr "transaction" +#: apps/note/views.py:145 templates/base.html:79 +msgid "Consumptions" +msgstr "Consommations" -#: apps/note/models/transactions.py:139 -msgid "transactions" -msgstr "transactions" +#: apps/permission/models.py:69 apps/permission/models.py:262 +#, python-brace-format +msgid "Can {type} {model}.{field} in {query}" +msgstr "" -#: apps/note/models/transactions.py:207 -msgid "first_name" +#: 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/note/models/transactions.py:212 -msgid "bank" +#: apps/treasury/forms.py:88 templates/note/transaction_form.html:98 +msgid "Bank" msgstr "Banque" -#: apps/note/models/transactions.py:231 -msgid "membership transactions" -msgstr "transactions d'adhésion" +#: 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/note/views.py:31 -msgid "Transfer money" -msgstr "Transferts d'argent" +#: apps/treasury/models.py:18 +msgid "Invoice identifier" +msgstr "Numéro de facture" -#: apps/note/views.py:132 templates/base.html:78 -msgid "Consumptions" -msgstr "Consommations" +#: 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}" -#: note_kfet/settings/__init__.py:61 +#: 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 +#: templates/note/transaction_form.html:15 +msgid "Gift" +msgstr "Don" + +#: templates/note/transaction_form.html:68 msgid "External payment" -msgstr "Paiement extérieur" +msgstr "Paiement externe" -#: templates/note/transaction_form.html:63 +#: 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 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 f70858500cf97aa61d45fe019429999df51c9c35..b22df07710b59ec1fe25d881b3cb3def98984ee8 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 c615f932bb7f41091fa8d79dc5a720b0cf1d3858..a417191082ccbffbd1a1db8036e4ad040a7af5ba 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/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