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