1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-11-26 18:37:12 +00:00

Merge branch 'master' into front_club

This commit is contained in:
Pierre-antoine Comby 2020-03-27 14:07:22 +01:00
commit 09fb0e1470
53 changed files with 2434 additions and 305 deletions

View File

@ -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

View File

@ -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 :

View File

@ -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')

View File

@ -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')

View File

@ -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',
]

View File

@ -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)

View File

@ -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 += "<input type='text' class='form-control' id='invalidity_reason_" + str(record.id) \
+ "' value='" + (html.escape(record.invalidity_reason)
if record.invalidity_reason else ("" if value else str(_("No reason specified")))) \
+ "'" + ("" if value else " disabled") \
+ " placeholder='" + html.escape(_("invalidity reason").capitalize()) + "'" \
+ " style='position: absolute; width: 15em; margin-left: -15.5em; margin-top: -2em; display: none;'>"
return format_html(val)
# function delete_button(id) provided in template file

View File

@ -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)

View File

@ -28,4 +28,3 @@ class RolePermissionsAdmin(admin.ModelAdmin):
Admin customisation for RolePermissions
"""
list_display = ('role', )

View File

@ -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

View File

@ -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": "",

View File

@ -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)

View File

@ -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', )

View File

@ -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 = [

View File

@ -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

View File

@ -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'

27
apps/treasury/admin.py Normal file
View File

@ -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)

View File

View File

@ -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)

14
apps/treasury/api/urls.py Normal file
View File

@ -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)

View File

@ -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

33
apps/treasury/apps.py Normal file
View File

@ -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)

View File

@ -0,0 +1,9 @@
[
{
"model": "treasury.remittancetype",
"pk": 1,
"fields": {
"note": 3
}
}
]

156
apps/treasury/forms.py Normal file
View File

@ -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', )

View File

189
apps/treasury/models.py Normal file
View File

@ -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"),
)

12
apps/treasury/signals.py Normal file
View File

@ -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()

103
apps/treasury/tables.py Normal file
View File

@ -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',)

24
apps/treasury/urls.py Normal file
View File

@ -0,0 +1,24 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
from .views import InvoiceCreateView, InvoiceListView, InvoiceUpdateView, InvoiceRenderView, RemittanceListView,\
RemittanceCreateView, RemittanceUpdateView, LinkTransactionToRemittanceView, UnlinkTransactionToRemittanceView
app_name = 'treasury'
urlpatterns = [
# Invoice app paths
path('invoice/', InvoiceListView.as_view(), name='invoice_list'),
path('invoice/create/', InvoiceCreateView.as_view(), name='invoice_create'),
path('invoice/<int:pk>/', InvoiceUpdateView.as_view(), name='invoice_update'),
path('invoice/render/<int:pk>/', InvoiceRenderView.as_view(), name='invoice_render'),
# Remittance app paths
path('remittance/', RemittanceListView.as_view(), name='remittance_list'),
path('remittance/create/', RemittanceCreateView.as_view(), name='remittance_create'),
path('remittance/<int:pk>/', RemittanceUpdateView.as_view(), name='remittance_update'),
path('remittance/link_transaction/<int:pk>/', LinkTransactionToRemittanceView.as_view(), name='link_transaction'),
path('remittance/unlink_transaction/<int:pk>/', UnlinkTransactionToRemittanceView.as_view(),
name='unlink_transaction'),
]

316
apps/treasury/views.py Normal file
View File

@ -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')

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -23,9 +23,9 @@ msgid "activity"
msgstr ""
#: apps/activity/models.py:19 apps/activity/models.py:44
#: apps/member/models.py:61 apps/member/models.py:112
#: apps/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 ""

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,9 +18,9 @@ msgid "activity"
msgstr "activité"
#: apps/activity/models.py:19 apps/activity/models.py:44
#: apps/member/models.py:61 apps/member/models.py:112
#: apps/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"

View File

@ -59,6 +59,7 @@ INSTALLED_APPS = [
'activity',
'member',
'note',
'treasury',
'permission',
'api',
'logs',

View File

@ -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')),

View File

@ -1 +1 @@
psycopg2==2.8.4
psycopg2-binary==2.8.4

BIN
static/img/Finalist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

BIN
static/img/Kataclist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

BIN
static/img/Listorique.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

BIN
static/img/Monopolist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

BIN
static/img/Satellist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

View File

@ -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("<strong style=\"font-size: 16pt;\">⟳ ...</strong>");
// 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.

View File

@ -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);
});
});
}

View File

@ -1,5 +1,5 @@
/**
* jQuery Formset 1.3-pre
* jQuery Formset 1.5-pre
* @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
* @requires jQuery 1.2.6 or later
*
@ -55,19 +55,26 @@
insertDeleteLink = function(row) {
var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'),
addCssSelector = $.trim(options.addCssClass).replace(/\s+/g, '.');
if (row.is('TR')) {
var delButtonHTML = '<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>';
if (options.deleteContainerClass) {
// If we have a specific container for the remove button,
// place it as the last child of that container:
row.find('[class*="' + options.deleteContainerClass + '"]').append(delButtonHTML);
} else if (row.is('TR')) {
// If the forms are laid out in table rows, insert
// the remove button into the last table cell:
row.children(':last').append('<a class="' + options.deleteCssClass +'" href="javascript:void(0)">' + options.deleteText + '</a>');
row.children('td:last').append(delButtonHTML);
} else if (row.is('UL') || row.is('OL')) {
// If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item:
row.append('<li><a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a></li>');
row.append('<li>' + delButtonHTML + '</li>');
} else {
// Otherwise, just insert the remove button as the
// last child element of the form's container:
row.append('<a class="' + options.deleteCssClass + '" href="javascript:void(0)">' + options.deleteText +'</a>');
row.append(delButtonHTML);
}
// Check if we're under the minimum number of forms - not to display delete link at rendering
if (!showDeleteLinks()){
row.find('a.' + delCssSelector).hide();
@ -156,6 +163,7 @@
} else {
// Otherwise, use the last form in the formset; this works much better if you've got
// extra (>= 1) forms (thnaks to justhamade for pointing this out):
if (options.hideLastAddForm) $('.' + options.formCssClass + ':last').hide();
template = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id');
template.find('input:hidden[id $= "-DELETE"]').remove();
// Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion):
@ -173,21 +181,28 @@
// FIXME: Perhaps using $.data would be a better idea?
options.formTemplate = template;
if ($$.is('TR')) {
var addButtonHTML = '<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>';
if (options.addContainerClass) {
// If we have a specific container for the "add" button,
// place it as the last child of that container:
var addContainer = $('[class*="' + options.addContainerClass + '"');
addContainer.append(addButtonHTML);
addButton = addContainer.find('[class="' + options.addCssClass + '"]');
} else if ($$.is('TR')) {
// If forms are laid out as table rows, insert the
// "add" button in a new table row:
var numCols = $$.eq(0).children().length, // This is a bit of an assumption :|
buttonRow = $('<tr><td colspan="' + numCols + '"><a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a></tr>')
.addClass(options.formCssClass + '-add');
buttonRow = $('<tr><td colspan="' + numCols + '">' + addButtonHTML + '</tr>').addClass(options.formCssClass + '-add');
$$.parent().append(buttonRow);
if (hideAddButton) buttonRow.hide();
addButton = buttonRow.find('a');
} else {
// Otherwise, insert it immediately after the last form:
$$.filter(':last').after('<a class="' + options.addCssClass + '" href="javascript:void(0)">' + options.addText + '</a>');
$$.filter(':last').after(addButtonHTML);
addButton = $$.filter(':last').next();
if (hideAddButton) addButton.hide();
}
if (hideAddButton) addButton.hide();
addButton.click(function() {
var formCount = parseInt(totalForms.val()),
row = options.formTemplate.clone(true).removeClass('formset-custom-template'),
@ -220,12 +235,15 @@
formTemplate: null, // The jQuery selection cloned to generate new form instances
addText: 'add another', // Text for the add link
deleteText: 'remove', // Text for the delete link
addCssClass: '', // CSS class applied to the add link
deleteCssClass: '', // CSS class applied to the delete link
addContainerClass: null, // Container CSS class for the add link
deleteContainerClass: null, // Container CSS class for the delete link
addCssClass: 'add-row', // CSS class applied to the add link
deleteCssClass: 'delete-row', // CSS class applied to the delete link
formCssClass: 'dynamic-form', // CSS class applied to each form in a formset
extraClasses: [], // Additional CSS classes, which will be applied to each form in turn
keepFieldValues: '', // jQuery selector for fields whose values should be kept when the form is cloned
added: null, // Function called each time a new form is added
removed: null // Function called each time a form is deleted
removed: null, // Function called each time a form is deleted
hideLastAddForm: false // When set to true, hide last empty add form (becomes visible when clicking on add button)
};
})(jQuery);

View File

@ -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();
});
}

View File

@ -94,6 +94,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a>
</li>
{% endif %}
{% if "treasury.invoice"|not_empty_model_change_list %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'treasury:invoice_list' %}"><i class="fa fa-money"></i>{% trans 'Treasury' %} </a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav ml-auto">
{% if user.is_authenticated %}

View File

@ -0,0 +1,107 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load crispy_forms_tags pretty_money %}
{% block content %}
<p><a class="btn btn-default" href="{% url 'treasury:invoice_list' %}">{% trans "Invoices list" %}</a></p>
<form method="post" action="">
{% csrf_token %}
{# Render the invoice form #}
{% crispy form %}
{# The next part concerns the product formset #}
{# Generate some hidden fields that manage the number of products, and make easier the parsing #}
{{ formset.management_form }}
<table class="table table-condensed table-striped">
{# Fill initial data #}
{% for form in formset %}
{% if forloop.first %}
<thead>
<tr>
<th>{{ form.designation.label }}<span class="asteriskField">*</span></th>
<th>{{ form.quantity.label }}<span class="asteriskField">*</span></th>
<th>{{ form.amount.label }}<span class="asteriskField">*</span></th>
</tr>
</thead>
<tbody id="form_body">
{% endif %}
<tr class="row-formset">
<td>{{ form.designation }}</td>
<td>{{ form.quantity }} </td>
<td>
{# Use custom input for amount, with the € symbol #}
<div class="input-group">
<input type="number" name="product_set-{{ forloop.counter0 }}-amount" step="0.01"
id="id_product_set-{{ forloop.counter0 }}-amount"
value="{{ form.instance.amount|cents_to_euros }}">
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
</div>
</td>
{# These fields are hidden but handled by the formset to link the id and the invoice id #}
{{ form.invoice }}
{{ form.id }}
</tr>
{% endfor %}
</tbody>
</table>
{# Display buttons to add and remove products #}
<div class="btn-group btn-block" role="group">
<button type="button" id="add_more" class="btn btn-primary">{% trans "Add product" %}</button>
<button type="button" id="remove_one" class="btn btn-danger">{% trans "Remove product" %}</button>
</div>
<div class="btn-block">
<button type="submit" class="btn btn-block btn-primary">{% trans "Submit" %}</button>
</div>
</form>
<div id="empty_form" style="display: none;">
{# Hidden div that store an empty product form, to be copied into new forms #}
<table class='no_error'>
<tbody id="for_real">
<tr class="row-formset">
<td>{{ formset.empty_form.designation }}</td>
<td>{{ formset.empty_form.quantity }} </td>
<td>
<div class="input-group">
<input type="number" name="product_set-__prefix__-amount" step="0.01"
id="id_product_set-__prefix__-amount">
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
</div>
</td>
{{ formset.empty_form.invoice }}
{{ formset.empty_form.id }}
</tr>
</tbody>
</table>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
{# Script that handles add and remove lines #}
IDS = {};
$("#id_product_set-TOTAL_FORMS").val($(".row-formset").length - 1);
$('#add_more').click(function () {
var form_idx = $('#id_product_set-TOTAL_FORMS').val();
$('#form_body').append($('#for_real').html().replace(/__prefix__/g, form_idx));
$('#id_product_set-TOTAL_FORMS').val(parseInt(form_idx) + 1);
$('#id_product_set-' + parseInt(form_idx) + '-id').val(IDS[parseInt(form_idx)]);
});
$('#remove_one').click(function () {
let form_idx = $('#id_product_set-TOTAL_FORMS').val();
if (form_idx > 0) {
IDS[parseInt(form_idx) - 1] = $('#id_product_set-' + (parseInt(form_idx) - 1) + '-id').val();
$('#form_body tr:last-child').remove();
$('#id_product_set-TOTAL_FORMS').val(parseInt(form_idx) - 1);
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
<a href="#" class="btn btn-sm btn-outline-primary active">
{% trans "Invoice" %}s
</a>
<a href="{% url "treasury:remittance_list" %}" class="btn btn-sm btn-outline-primary">
{% trans "Remittance" %}s
</a>
</div>
</div>
</div>
{% render_table table %}
<a class="btn btn-primary" href="{% url 'treasury:invoice_create' %}">{% trans "New invoice" %}</a>
{% endblock %}

View File

@ -0,0 +1,186 @@
\nonstopmode
\documentclass[11pt]{article}
\usepackage[french]{babel}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage[a4paper]{geometry}
\usepackage{units}
\usepackage{bera}
\usepackage{graphicx}
\usepackage{fancyhdr}
\usepackage{fp}
\usepackage{transparent}
\usepackage{eso-pic}
\def\TVA{0} % Taux de la TVA
\def\TotalHT{0}
\def\TotalTVA{0}
\newcommand{\AjouterProduit}[4]{% Arguments : Désignation, quantité, prix unitaire HT, prix total HT
\FPround{\prix}{#3}{2}
\FPround{\montant}{#4}{2}
\FPadd{\TotalHT}{\TotalHT}{\montant}
\eaddto\ListeProduits{#1 & \prix & #2 & \montant \cr}
}
\newcommand{\AfficheResultat}{%
\ListeProduits
\FPeval{\TotalTVA}{\TotalHT * \TVA / 100}
\FPadd{\TotalTTC}{\TotalHT}{\TotalTVA}
\FPround{\TotalHT}{\TotalHT}{2}
\FPround{\TotalTVA}{\TotalTVA}{2}
\FPround{\TotalTTC}{\TotalTTC}{2}
\global\let\TotalHT\TotalHT
\global\let\TotalTVA\TotalTVA
\global\let\TotalTTC\TotalTTC
\cr \hline
Total HT & & & \TotalHT \cr
TVA \TVA~\% & & & \TotalTVA \cr
\hline \hline
\textbf{Total TTC} & & & \TotalTTC
}
\newcommand*\eaddto[2]{% version développée de \addto
\edef\tmp{#2}%
\expandafter\addto
\expandafter#1%
\expandafter{\tmp}%
}
\newcommand {\ListeProduits}{}
% Logo du BDE
\AddToShipoutPicture*{
\put(0,0){
\parbox[b][\paperheight]{\paperwidth}{%
\vfill
\centering
{\transparent{0.1}\includegraphics[width=\textwidth]{../../static/img/{{ obj.bde }}}}%
\vfill
}
}
}
%%%%%%%%%%%%%%%%%%%%% A MODIFIER DANS LA FACTURE %%%%%%%%%%%%%%%%%%%%%
% Infos Association
\def\MonNom{{"{"}}{{ obj.my_name }}} % Nom de l'association
\def\MonAdresseRue{{"{"}}{{ obj.my_address_street }}} % Adresse de l'association
\def\MonAdresseVille{{"{"}}{{ obj.my_city }}}
% Informations bancaires de l'association
\def\CodeBanque{{"{"}}{{ obj.bank_code|stringformat:".05d" }}}
\def\CodeGuichet{{"{"}}{{ obj.desk_code|stringformat:".05d" }}}
\def\NCompte{{"{"}}{{ obj.account_number|stringformat:".011d" }}}
\def\CleRib{{"{"}}{{ obj.rib_key|stringformat:".02d" }}}
\def\IBAN{FR76\CodeBanque\CodeGuichet\NCompte\CleRib}
\def\CodeBic{{"{"}}{{ obj.bic }}}
\def\FactureNum {{"{"}}{{obj.id}}} % Numéro de facture
\def\FactureAcquittee {% if obj.acquitted %} {oui} {% else %} {non} {% endif %} % Facture acquittée : oui/non
\def\FactureLieu {{"{"}}{{ obj.place }}} % Lieu de l'édition de la facture
\def\FactureDate {{"{"}}{{ obj.date }}} % Date de l'édition de la facture
\def\FactureObjet {{"{"}}{{ obj.object|safe }} } % Objet du document
% Description de la facture
\def\FactureDescr {{"{"}}{{ obj.description|safe }}}
% Infos Client
\def\ClientNom{{"{"}}{{obj.name|safe}}} % Nom du client
\def\ClientAdresse{{"{"}}{{ obj.address|safe }}} % Adresse du client
% Liste des produits facturés : Désignation, quantité, prix unitaire HT
{% for product in products %}
\AjouterProduit{ {{product.designation|safe}}} { {{product.quantity|safe}}} { {{product.amount_euros|safe}}} { {{product.total_euros|safe}}}
{% endfor %}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\geometry{verbose,tmargin=4em,bmargin=8em,lmargin=6em,rmargin=6em}
\setlength{\parindent}{1pt}
\setlength{\parskip}{1ex plus 0.5ex minus 0.2ex}
\thispagestyle{fancy}
\pagestyle{fancy}
\setlength{\parindent}{0pt}
\renewcommand{\headrulewidth}{0pt}
\cfoot{
\small{\MonNom ~--~ \MonAdresseRue ~ \MonAdresseVille ~--~ Téléphone : +33(0)6 89 88 56 50\newline
Site web : bde.ens-cachan.fr ~--~ E-mail : tresorerie.bde@lists.crans.org \newline Numéro SIRET : 399 485 838 00011
}
}
\begin{document}
% Logo de la société
% \includegraphics{logo.jpg}
% Nom et adresse de la société
\MonNom \\
\MonAdresseRue \\
\MonAdresseVille
Facture n°\FactureNum
{\addtolength{\leftskip}{10.5cm} %in ERT
\ClientNom \\
\ClientAdresse \\
} %in ERT
\hspace*{10.5cm}
\FactureLieu, le \FactureDate
~\\~\\
\textbf{Objet : \FactureObjet \\}
\textnormal{\FactureDescr}
~\\
\begin{center}
\begin{tabular}{lrrr}
\textbf{Désignation ~~~~~~} & \textbf{Prix unitaire} & \textbf{Quantité} & \textbf{Montant (EUR)} \\
\hline
\AfficheResultat{}
\end{tabular}
\end{center}
~\\
\ifthenelse{\equal{\FactureAcquittee}{oui}}{
Facture acquittée.
}{
À régler par chèque ou par virement bancaire :
\begin{center}
\begin{tabular}{|c c c c|}
\hline
\textbf{Code banque} & \textbf{Code guichet} & \textbf{N° de Compte} & \textbf{Clé RIB}\\
\CodeBanque & \CodeGuichet & \NCompte & \CleRib \\
\hline
\textbf{IBAN N°} & \multicolumn{3}{|l|} \IBAN \\
\hline
\textbf{Code BIC} & \multicolumn{3}{|l|}\CodeBic \\
\hline
\end{tabular}
\end{center}
}
\begin{center}
TVA non applicable, article 293 B du CGI.
\end{center}
\end{document}

View File

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load crispy_forms_tags pretty_money %}
{% load render_table from django_tables2 %}
{% block content %}
<h1>{% trans "Remittance #" %}{{ object.pk }}</h1>
<p><a class="btn btn-default" href="{% url 'treasury:remittance_list' %}">{% trans "Remittances list" %}</a></p>
{% if object.pk %}
<div id="div_id_type" class="form-group"><label for="id_count" class="col-form-label">{% trans "Count" %}</label>
<div class="">
<input type="text" name="count" value="{{ object.count }}" class="textinput textInput form-control" id="id_count" disabled>
</div>
</div>
<div id="div_id_type" class="form-group"><label for="id_amount" class="col-form-label">{% trans "Amount" %}</label>
<div class="">
<input class="textinput textInput form-control" type="text" value="{{ object.amount|pretty_money }}" id="id_amount" disabled>
</div>
</div>
{% endif %}
{% crispy form %}
<hr>
<h2>{% trans "Linked transactions" %}</h2>
{% if special_transactions.data %}
{% render_table special_transactions %}
{% else %}
<div class="alert alert-warning">
{% trans "There is no transaction linked with this remittance." %}
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,56 @@
{% extends "base.html" %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons">
<a href="{% url "treasury:invoice_list" %}" class="btn btn-sm btn-outline-primary">
{% trans "Invoice" %}s
</a>
<a href="#" class="btn btn-sm btn-outline-primary active">
{% trans "Remittance" %}s
</a>
</div>
</div>
</div>
<h2>{% trans "Opened remittances" %}</h2>
{% if opened_remittances.data %}
{% render_table opened_remittances %}
{% else %}
<div class="alert alert-warning">
{% trans "There is no opened remittance." %}
</div>
{% endif %}
<a class="btn btn-primary" href="{% url 'treasury:remittance_create' %}">{% trans "New remittance" %}</a>
<hr>
<h2>{% trans "Transfers without remittances" %}</h2>
{% if special_transactions_no_remittance.data %}
{% render_table special_transactions_no_remittance %}
{% else %}
<div class="alert alert-warning">
{% trans "There is no transaction without any linked remittance." %}
</div>
{% endif %}
<hr>
<h2>{% trans "Transfers with opened remittances" %}</h2>
{% if special_transactions_with_remittance.data %}
{% render_table special_transactions_with_remittance %}
{% else %}
<div class="alert alert-warning">
{% trans "There is no transaction with an opened linked remittance." %}
</div>
{% endif %}
<hr>
<h2>{% trans "Closed remittances" %}</h2>
{% render_table closed_remittances %}
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load crispy_forms_tags pretty_money %}
{% load render_table from django_tables2 %}
{% block content %}
<p><a class="btn btn-default" href="{% url 'treasury:remittance_list' %}">{% trans "Remittances list" %}</a></p>
{% crispy form %}
{% endblock %}

View File

@ -30,7 +30,7 @@ deps =
pep8-naming
pyflakes
commands =
flake8 apps/activity apps/api apps/logs apps/member apps/note
flake8 apps/activity apps/api apps/logs apps/member apps/note apps/permission apps/treasury
[flake8]
# Ignore too many errors, should be reduced in the future