Merge branch 'tresorerie' into 'master'

Tresorerie

See merge request bde/nk20!61
This commit is contained in:
Pierre-antoine Comby 2020-03-25 16:59:51 +01:00
commit 02ba4b6db6
48 changed files with 2131 additions and 256 deletions

View File

@ -9,6 +9,11 @@ RUN apt update && \
apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \ apt install -y gettext nginx uwsgi uwsgi-plugin-python3 && \
rm -rf /var/lib/apt/lists/* 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/ COPY . /code/
# Comment what is not needed # Comment what is not needed

View File

@ -6,13 +6,17 @@
## Installation sur un serveur ## 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 1. Paquets nécessaires
$ sudo apt install nginx python3 python3-pip python3-dev uwsgi $ sudo apt install nginx python3 python3-pip python3-dev uwsgi
$ sudo apt install uwsgi-plugin-python3 python3-venv git acl $ 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 2. Clonage du dépot
on se met au bon endroit : 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 api.viewsets import ReadProtectedModelViewSet
from member.api.urls import register_members_urls from member.api.urls import register_members_urls
from note.api.urls import register_note_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 logs.api.urls import register_logs_urls
from permission.api.urls import register_permission_urls from permission.api.urls import register_permission_urls
@ -74,6 +75,7 @@ router.register('user', UserViewSet)
register_members_urls(router, 'members') register_members_urls(router, 'members')
register_activity_urls(router, 'activity') register_activity_urls(router, 'activity')
register_note_urls(router, 'note') register_note_urls(router, 'note')
register_treasury_urls(router, 'treasury')
register_permission_urls(router, 'permission') register_permission_urls(router, 'permission')
register_logs_urls(router, 'logs') register_logs_urls(router, 'logs')

View File

@ -256,4 +256,4 @@
"name": "Alcool" "name": "Alcool"
} }
} }
] ]

View File

@ -3,12 +3,12 @@
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .transactions import MembershipTransaction, Transaction, \ from .transactions import MembershipTransaction, Transaction, \
TemplateCategory, TransactionTemplate, RecurrentTransaction TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction
__all__ = [ __all__ = [
# Notes # Notes
'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser', 'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
# Transactions # Transactions
'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate', 'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
'RecurrentTransaction', 'RecurrentTransaction', 'SpecialTransaction',
] ]

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 = template.Library()
register.filter('pretty_money', pretty_money) 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 Admin customisation for RolePermissions
""" """
list_display = ('role', ) list_display = ('role', )

View File

@ -2,8 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from api.viewsets import ReadOnlyProtectedModelViewSet from api.viewsets import ReadOnlyProtectedModelViewSet
from .serializers import PermissionSerializer from .serializers import PermissionSerializer
from ..models import Permission from ..models import Permission

View File

@ -10,7 +10,6 @@ from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import F, Q, Model from django.db.models import F, Q, Model
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from member.models import Role from member.models import Role
@ -281,4 +280,3 @@ class RolePermissions(models.Model):
def __str__(self): def __str__(self):
return str(self.role) return str(self.role)

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework.permissions import DjangoObjectPermissions from rest_framework.permissions import DjangoObjectPermissions
from .backends import PermissionBackend from .backends import PermissionBackend
SAFE_METHODS = ('HEAD', 'OPTIONS', ) SAFE_METHODS = ('HEAD', 'OPTIONS', )

View File

@ -3,10 +3,9 @@
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db.models.signals import pre_save, pre_delete, post_save, post_delete from django.db.models.signals import pre_save, pre_delete, post_save, post_delete
from logs import signals as logs_signals from logs import signals as logs_signals
from permission.backends import PermissionBackend
from note_kfet.middlewares import get_current_authenticated_user from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
EXCLUDED = [ EXCLUDED = [

View File

@ -3,10 +3,8 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import stringfilter from django.template.defaultfilters import stringfilter
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
from django import template from django import template
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
from permission.backends import PermissionBackend 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 "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-16 11:53+0100\n" "POT-Creation-Date: 2020-03-24 15:49+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -23,9 +23,9 @@ msgid "activity"
msgstr "" msgstr ""
#: apps/activity/models.py:19 apps/activity/models.py:44 #: apps/activity/models.py:19 apps/activity/models.py:44
#: apps/member/models.py:61 apps/member/models.py:112 #: apps/member/models.py:63 apps/member/models.py:114
#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 #: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202 #: apps/note/models/transactions.py:44 apps/note/models/transactions.py:198
#: templates/member/profile_detail.html:15 #: templates/member/profile_detail.html:15
msgid "name" msgid "name"
msgstr "" msgstr ""
@ -47,11 +47,12 @@ msgid "activity types"
msgstr "" msgstr ""
#: apps/activity/models.py:48 apps/note/models/transactions.py:69 #: apps/activity/models.py:48 apps/note/models/transactions.py:69
#: apps/permission/models.py:90
msgid "description" msgid "description"
msgstr "" msgstr ""
#: apps/activity/models.py:54 apps/note/models/notes.py:164 #: apps/activity/models.py:54 apps/note/models/notes.py:164
#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115 #: apps/note/models/transactions.py:62
msgid "type" msgid "type"
msgstr "" msgstr ""
@ -143,114 +144,114 @@ msgstr ""
msgid "member" msgid "member"
msgstr "" msgstr ""
#: apps/member/models.py:23 #: apps/member/models.py:25
msgid "phone number" msgid "phone number"
msgstr "" 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" msgid "section"
msgstr "" msgstr ""
#: apps/member/models.py:30 #: apps/member/models.py:32
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
msgstr "" 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" msgid "address"
msgstr "" msgstr ""
#: apps/member/models.py:42 #: apps/member/models.py:44
msgid "paid" msgid "paid"
msgstr "" msgstr ""
#: apps/member/models.py:47 apps/member/models.py:48 #: apps/member/models.py:49 apps/member/models.py:50
msgid "user profile" msgid "user profile"
msgstr "" msgstr ""
#: apps/member/models.py:66 #: apps/member/models.py:68
msgid "email" msgid "email"
msgstr "" msgstr ""
#: apps/member/models.py:71 #: apps/member/models.py:73
msgid "membership fee" msgid "membership fee"
msgstr "" msgstr ""
#: apps/member/models.py:75 #: apps/member/models.py:77
msgid "membership duration" msgid "membership duration"
msgstr "" msgstr ""
#: apps/member/models.py:76 #: apps/member/models.py:78
msgid "The longest time a membership can last (NULL = infinite)." msgid "The longest time a membership can last (NULL = infinite)."
msgstr "" msgstr ""
#: apps/member/models.py:81 #: apps/member/models.py:83
msgid "membership start" msgid "membership start"
msgstr "" msgstr ""
#: apps/member/models.py:82 #: apps/member/models.py:84
msgid "How long after January 1st the members can renew their membership." msgid "How long after January 1st the members can renew their membership."
msgstr "" msgstr ""
#: apps/member/models.py:87 #: apps/member/models.py:89
msgid "membership end" msgid "membership end"
msgstr "" msgstr ""
#: apps/member/models.py:88 #: apps/member/models.py:90
msgid "" msgid ""
"How long the membership can last after January 1st of the next year after " "How long the membership can last after January 1st of the next year after "
"members can renew their membership." "members can renew their membership."
msgstr "" 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" msgid "club"
msgstr "" msgstr ""
#: apps/member/models.py:95 #: apps/member/models.py:97
msgid "clubs" msgid "clubs"
msgstr "" msgstr ""
#: apps/member/models.py:118 #: apps/member/models.py:120 apps/permission/models.py:275
msgid "role" msgid "role"
msgstr "" msgstr ""
#: apps/member/models.py:119 #: apps/member/models.py:121
msgid "roles" msgid "roles"
msgstr "" msgstr ""
#: apps/member/models.py:143 #: apps/member/models.py:145
msgid "membership starts on" msgid "membership starts on"
msgstr "" msgstr ""
#: apps/member/models.py:146 #: apps/member/models.py:148
msgid "membership ends on" msgid "membership ends on"
msgstr "" msgstr ""
#: apps/member/models.py:150 #: apps/member/models.py:152
msgid "fee" msgid "fee"
msgstr "" msgstr ""
#: apps/member/models.py:154 #: apps/member/models.py:162
msgid "membership" msgid "membership"
msgstr "" msgstr ""
#: apps/member/models.py:155 #: apps/member/models.py:163
msgid "memberships" msgid "memberships"
msgstr "" 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" msgid "Update Profile"
msgstr "" msgstr ""
#: apps/member/views.py:82 #: apps/member/views.py:93
msgid "An alias with a similar name already exists." msgid "An alias with a similar name already exists."
msgstr "" msgstr ""
#: apps/member/views.py:132 #: apps/member/views.py:146
#, python-format #, python-format
msgid "Account #%(id)s: %(username)s" msgid "Account #%(id)s: %(username)s"
msgstr "" msgstr ""
#: apps/member/views.py:202 #: apps/member/views.py:216
msgid "Alias successfully deleted" msgid "Alias successfully deleted"
msgstr "" msgstr ""
@ -415,84 +416,233 @@ msgstr ""
msgid "quantity" msgid "quantity"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15 #: apps/note/models/transactions.py:115
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
msgid "reason" msgid "reason"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:133 #: apps/note/models/transactions.py:119
msgid "valid" msgid "valid"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:138 #: apps/note/models/transactions.py:124
msgid "transaction" msgid "transaction"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:139 #: apps/note/models/transactions.py:125
msgid "transactions" msgid "transactions"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:207 #: apps/note/models/transactions.py:168 templates/base.html:98
#: templates/note/transaction_form.html:19
#: templates/note/transaction_form.html:145
msgid "Transfer"
msgstr ""
#: apps/note/models/transactions.py:188
msgid "Template"
msgstr ""
#: apps/note/models/transactions.py:203
msgid "first_name" msgid "first_name"
msgstr "" msgstr ""
#: apps/note/models/transactions.py:212 #: apps/note/models/transactions.py:208
msgid "bank" msgid "bank"
msgstr "" 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 #: apps/note/models/transactions.py:231
msgid "membership transactions" msgid "membership transactions"
msgstr "" msgstr ""
#: apps/note/views.py:31 #: apps/note/views.py:39
msgid "Transfer money" msgid "Transfer money"
msgstr "" msgstr ""
#: apps/note/views.py:132 templates/base.html:78 #: apps/note/views.py:145 templates/base.html:79
msgid "Consumptions" msgid "Consumptions"
msgstr "" 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 "" msgid ""
"The Central Authentication Service grants you access to most of our websites " "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 " "by authenticating only once, so you don't need to type your credentials "
"again unless your session expires or you logout." "again unless your session expires or you logout."
msgstr "" msgstr ""
#: note_kfet/settings/base.py:156 #: note_kfet/settings/base.py:153
msgid "German" msgid "German"
msgstr "" msgstr ""
#: note_kfet/settings/base.py:157 #: note_kfet/settings/base.py:154
msgid "English" msgid "English"
msgstr "" msgstr ""
#: note_kfet/settings/base.py:158 #: note_kfet/settings/base.py:155
msgid "French" msgid "French"
msgstr "" msgstr ""
@ -500,15 +650,15 @@ msgstr ""
msgid "The ENS Paris-Saclay BDE note." msgid "The ENS Paris-Saclay BDE note."
msgstr "" msgstr ""
#: templates/base.html:81 #: templates/base.html:84
msgid "Clubs" msgid "Clubs"
msgstr "" msgstr ""
#: templates/base.html:84 #: templates/base.html:89
msgid "Activities" msgid "Activities"
msgstr "" msgstr ""
#: templates/base.html:87 #: templates/base.html:94
msgid "Buttons" msgid "Buttons"
msgstr "" msgstr ""
@ -567,11 +717,6 @@ msgstr ""
msgid "Field filters" msgid "Field filters"
msgstr "" msgstr ""
#: templates/django_filters/rest_framework/form.html:5
#: templates/member/club_form.html:10
msgid "Submit"
msgstr ""
#: templates/member/club_detail.html:10 #: templates/member/club_detail.html:10
msgid "Membership starts on" msgid "Membership starts on"
msgstr "" msgstr ""
@ -653,7 +798,7 @@ msgstr ""
msgid "Sign up" msgid "Sign up"
msgstr "" msgstr ""
#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38 #: templates/note/conso_form.html:28 templates/note/transaction_form.html:40
msgid "Select emitters" msgid "Select emitters"
msgstr "" msgstr ""
@ -681,49 +826,37 @@ msgstr ""
msgid "Double consumptions" msgid "Double consumptions"
msgstr "" msgstr ""
#: templates/note/conso_form.html:141 #: templates/note/conso_form.html:141 templates/note/transaction_form.html:152
msgid "Recent transactions history" msgid "Recent transactions history"
msgstr "" 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" msgid "External payment"
msgstr "" msgstr ""
#: templates/note/transaction_form.html:63 #: templates/note/transaction_form.html:76
msgid "Transfer type" msgid "Transfer type"
msgstr "" msgstr ""
#: templates/note/transaction_form.html:73 #: templates/note/transaction_form.html:111
msgid "Name" #: templates/note/transaction_form.html:169
msgstr "" #: templates/note/transaction_form.html:176
#: templates/note/transaction_form.html:79
msgid "First name"
msgstr ""
#: templates/note/transaction_form.html:85
msgid "Bank"
msgstr ""
#: templates/note/transaction_form.html:97
#: templates/note/transaction_form.html:179
#: templates/note/transaction_form.html:186
msgid "Select receivers" msgid "Select receivers"
msgstr "" msgstr ""
#: templates/note/transaction_form.html:114 #: templates/note/transaction_form.html:138
msgid "Amount"
msgstr ""
#: templates/note/transaction_form.html:119
msgid "Reason" msgid "Reason"
msgstr "" msgstr ""
#: templates/note/transaction_form.html:193 #: templates/note/transaction_form.html:183
msgid "Credit note" msgid "Credit note"
msgstr "" msgstr ""
#: templates/note/transaction_form.html:200 #: templates/note/transaction_form.html:190
msgid "Debit note" msgid "Debit note"
msgstr "" msgstr ""
@ -808,3 +941,72 @@ msgstr ""
#: templates/registration/password_reset_form.html:11 #: templates/registration/password_reset_form.html:11
msgid "Reset my password" msgid "Reset my password"
msgstr "" 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 "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-16 11:53+0100\n" "POT-Creation-Date: 2020-03-24 15:49+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,9 +18,9 @@ msgid "activity"
msgstr "activité" msgstr "activité"
#: apps/activity/models.py:19 apps/activity/models.py:44 #: apps/activity/models.py:19 apps/activity/models.py:44
#: apps/member/models.py:61 apps/member/models.py:112 #: apps/member/models.py:63 apps/member/models.py:114
#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 #: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202 #: apps/note/models/transactions.py:44 apps/note/models/transactions.py:198
#: templates/member/profile_detail.html:15 #: templates/member/profile_detail.html:15
msgid "name" msgid "name"
msgstr "nom" msgstr "nom"
@ -42,11 +42,12 @@ msgid "activity types"
msgstr "types d'activité" 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:69
#: apps/permission/models.py:90
msgid "description" msgid "description"
msgstr "description" msgstr "description"
#: apps/activity/models.py:54 apps/note/models/notes.py:164 #: apps/activity/models.py:54 apps/note/models/notes.py:164
#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115 #: apps/note/models/transactions.py:62
msgid "type" msgid "type"
msgstr "type" msgstr "type"
@ -138,61 +139,61 @@ msgstr "Les logs ne peuvent pas être détruits."
msgid "member" msgid "member"
msgstr "adhérent" msgstr "adhérent"
#: apps/member/models.py:23 #: apps/member/models.py:25
msgid "phone number" msgid "phone number"
msgstr "numéro de téléphone" 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" msgid "section"
msgstr "section" msgstr "section"
#: apps/member/models.py:30 #: apps/member/models.py:32
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
msgstr "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" msgid "address"
msgstr "adresse" msgstr "adresse"
#: apps/member/models.py:42 #: apps/member/models.py:44
msgid "paid" msgid "paid"
msgstr "payé" 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" msgid "user profile"
msgstr "profil utilisateur" msgstr "profil utilisateur"
#: apps/member/models.py:66 #: apps/member/models.py:68
msgid "email" msgid "email"
msgstr "courriel" msgstr "courriel"
#: apps/member/models.py:71 #: apps/member/models.py:73
msgid "membership fee" msgid "membership fee"
msgstr "cotisation pour adhérer" msgstr "cotisation pour adhérer"
#: apps/member/models.py:75 #: apps/member/models.py:77
msgid "membership duration" msgid "membership duration"
msgstr "durée de l'adhésion" 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)." msgid "The longest time a membership can last (NULL = infinite)."
msgstr "La durée maximale d'une adhésion (NULL = infinie)." msgstr "La durée maximale d'une adhésion (NULL = infinie)."
#: apps/member/models.py:81 #: apps/member/models.py:83
msgid "membership start" msgid "membership start"
msgstr "début de l'adhésion" 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." msgid "How long after January 1st the members can renew their membership."
msgstr "" msgstr ""
"Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur " "Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur "
"adhésion." "adhésion."
#: apps/member/models.py:87 #: apps/member/models.py:89
msgid "membership end" msgid "membership end"
msgstr "fin de l'adhésion" msgstr "fin de l'adhésion"
#: apps/member/models.py:88 #: apps/member/models.py:90
msgid "" msgid ""
"How long the membership can last after January 1st of the next year after " "How long the membership can last after January 1st of the next year after "
"members can renew their membership." "members can renew their membership."
@ -200,56 +201,56 @@ msgstr ""
"Combien de temps l'adhésion peut durer après le 1er Janvier de l'année " "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." "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" msgid "club"
msgstr "club" msgstr "club"
#: apps/member/models.py:95 #: apps/member/models.py:97
msgid "clubs" msgid "clubs"
msgstr "clubs" msgstr "clubs"
#: apps/member/models.py:118 #: apps/member/models.py:120 apps/permission/models.py:275
msgid "role" msgid "role"
msgstr "rôle" msgstr "rôle"
#: apps/member/models.py:119 #: apps/member/models.py:121
msgid "roles" msgid "roles"
msgstr "rôles" msgstr "rôles"
#: apps/member/models.py:143 #: apps/member/models.py:145
msgid "membership starts on" msgid "membership starts on"
msgstr "l'adhésion commence le" msgstr "l'adhésion commence le"
#: apps/member/models.py:146 #: apps/member/models.py:148
msgid "membership ends on" msgid "membership ends on"
msgstr "l'adhésion finie le" msgstr "l'adhésion finie le"
#: apps/member/models.py:150 #: apps/member/models.py:152
msgid "fee" msgid "fee"
msgstr "cotisation" msgstr "cotisation"
#: apps/member/models.py:154 #: apps/member/models.py:162
msgid "membership" msgid "membership"
msgstr "adhésion" msgstr "adhésion"
#: apps/member/models.py:155 #: apps/member/models.py:163
msgid "memberships" msgid "memberships"
msgstr "adhésions" 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" msgid "Update Profile"
msgstr "Modifier le profil" msgstr "Modifier le profil"
#: apps/member/views.py:82 #: apps/member/views.py:93
msgid "An alias with a similar name already exists." msgid "An alias with a similar name already exists."
msgstr "Un alias avec un nom similaire existe déjà." msgstr "Un alias avec un nom similaire existe déjà."
#: apps/member/views.py:132 #: apps/member/views.py:146
#, python-format #, python-format
msgid "Account #%(id)s: %(username)s" msgid "Account #%(id)s: %(username)s"
msgstr "Compte n°%(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" msgid "Alias successfully deleted"
msgstr "L'alias a bien été supprimé" msgstr "L'alias a bien été supprimé"
@ -415,84 +416,233 @@ msgstr "modèles de transaction"
msgid "quantity" msgid "quantity"
msgstr "quantité" msgstr "quantité"
#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15 #: apps/note/models/transactions.py:115
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
msgid "reason" msgid "reason"
msgstr "raison" msgstr "raison"
#: apps/note/models/transactions.py:133 #: apps/note/models/transactions.py:119
msgid "valid" msgid "valid"
msgstr "valide" msgstr "valide"
#: apps/note/models/transactions.py:138 #: apps/note/models/transactions.py:124
msgid "transaction" msgid "transaction"
msgstr "transaction" msgstr "transaction"
#: apps/note/models/transactions.py:139 #: apps/note/models/transactions.py:125
msgid "transactions" msgid "transactions"
msgstr "transactions" msgstr "transactions"
#: apps/note/models/transactions.py:207 #: apps/note/models/transactions.py:168 templates/base.html:98
msgid "first_name" #: templates/note/transaction_form.html:19
msgstr "Prénom" #: 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" 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 #: apps/note/models/transactions.py:231
msgid "membership transactions" msgid "membership transactions"
msgstr "transactions d'adhésion" msgstr "transactions d'adhésion"
#: apps/note/views.py:31 #: apps/note/views.py:39
msgid "Transfer money" 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" msgid "Consumptions"
msgstr "Consommations" 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 "" msgid ""
"The Central Authentication Service grants you access to most of our websites " "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 " "by authenticating only once, so you don't need to type your credentials "
"again unless your session expires or you logout." "again unless your session expires or you logout."
msgstr "" msgstr ""
#: note_kfet/settings/base.py:156 #: note_kfet/settings/base.py:153
msgid "German" msgid "German"
msgstr "" msgstr ""
#: note_kfet/settings/base.py:157 #: note_kfet/settings/base.py:154
msgid "English" msgid "English"
msgstr "" msgstr ""
#: note_kfet/settings/base.py:158 #: note_kfet/settings/base.py:155
msgid "French" msgid "French"
msgstr "" msgstr ""
@ -500,15 +650,15 @@ msgstr ""
msgid "The ENS Paris-Saclay BDE note." msgid "The ENS Paris-Saclay BDE note."
msgstr "La note du BDE de l'ENS Paris-Saclay." msgstr "La note du BDE de l'ENS Paris-Saclay."
#: templates/base.html:81 #: templates/base.html:84
msgid "Clubs" msgid "Clubs"
msgstr "Clubs" msgstr "Clubs"
#: templates/base.html:84 #: templates/base.html:89
msgid "Activities" msgid "Activities"
msgstr "Activités" msgstr "Activités"
#: templates/base.html:87 #: templates/base.html:94
msgid "Buttons" msgid "Buttons"
msgstr "Boutons" msgstr "Boutons"
@ -569,11 +719,6 @@ msgstr ""
msgid "Field filters" msgid "Field filters"
msgstr "" 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 #: templates/member/club_detail.html:10
msgid "Membership starts on" msgid "Membership starts on"
msgstr "L'adhésion commence le" msgstr "L'adhésion commence le"
@ -620,15 +765,15 @@ msgstr "Ajouter un alias"
#: templates/member/profile_detail.html:15 #: templates/member/profile_detail.html:15
msgid "first name" msgid "first name"
msgstr "" msgstr "prénom"
#: templates/member/profile_detail.html:18 #: templates/member/profile_detail.html:18
msgid "username" msgid "username"
msgstr "" msgstr "pseudo"
#: templates/member/profile_detail.html:21 #: templates/member/profile_detail.html:21
msgid "password" msgid "password"
msgstr "" msgstr "mot de passe"
#: templates/member/profile_detail.html:24 #: templates/member/profile_detail.html:24
msgid "Change password" msgid "Change password"
@ -655,13 +800,13 @@ msgstr "Sauvegarder les changements"
msgid "Sign up" msgid "Sign up"
msgstr "Inscription" msgstr "Inscription"
#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38 #: templates/note/conso_form.html:28 templates/note/transaction_form.html:40
msgid "Select emitters" msgid "Select emitters"
msgstr "Sélection des émetteurs" msgstr "Sélection des émetteurs"
#: templates/note/conso_form.html:45 #: templates/note/conso_form.html:45
msgid "Select consumptions" msgid "Select consumptions"
msgstr "Consommations" msgstr "Sélection des consommations"
#: templates/note/conso_form.html:51 #: templates/note/conso_form.html:51
msgid "Consume!" msgid "Consume!"
@ -677,55 +822,43 @@ msgstr "Éditer"
#: templates/note/conso_form.html:126 #: templates/note/conso_form.html:126
msgid "Single consumptions" msgid "Single consumptions"
msgstr "Consos simples" msgstr "Consommations simples"
#: templates/note/conso_form.html:130 #: templates/note/conso_form.html:130
msgid "Double consumptions" 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" msgid "Recent transactions history"
msgstr "Historique des transactions récentes" msgstr "Historique des transactions récentes"
#: templates/note/transaction_form.html:55 #: templates/note/transaction_form.html:15
msgid "External payment" msgid "Gift"
msgstr "Paiement extérieur" 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" msgid "Transfer type"
msgstr "Type de transfert" msgstr "Type de transfert"
#: templates/note/transaction_form.html:73 #: templates/note/transaction_form.html:111
msgid "Name" #: templates/note/transaction_form.html:169
msgstr "Nom" #: templates/note/transaction_form.html:176
#: templates/note/transaction_form.html:79
msgid "First name"
msgstr "Prénom"
#: templates/note/transaction_form.html:85
msgid "Bank"
msgstr "Banque"
#: templates/note/transaction_form.html:97
#: templates/note/transaction_form.html:179
#: templates/note/transaction_form.html:186
msgid "Select receivers" msgid "Select receivers"
msgstr "Sélection des destinataires" msgstr "Sélection des destinataires"
#: templates/note/transaction_form.html:114 #: templates/note/transaction_form.html:138
msgid "Amount"
msgstr "Montant"
#: templates/note/transaction_form.html:119
msgid "Reason" msgid "Reason"
msgstr "Raison" msgstr "Raison"
#: templates/note/transaction_form.html:193 #: templates/note/transaction_form.html:183
msgid "Credit note" 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" msgid "Debit note"
msgstr "Note à débiter" msgstr "Note à débiter"
@ -810,3 +943,72 @@ msgstr ""
#: templates/registration/password_reset_form.html:11 #: templates/registration/password_reset_form.html:11
msgid "Reset my password" msgid "Reset my password"
msgstr "" 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', 'activity',
'member', 'member',
'note', 'note',
'treasury',
'permission', 'permission',
'api', 'api',
'logs', 'logs',

View File

@ -15,6 +15,7 @@ urlpatterns = [
# Include project routers # Include project routers
path('note/', include('note.urls')), path('note/', include('note.urls')),
path('treasury/', include('treasury.urls')),
# Include Django Contrib and Core routers # Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')), 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

@ -62,11 +62,12 @@ function li(id, text) {
*/ */
function displayNote(note, alias, user_note_field=null, profile_pic_field=null) { function displayNote(note, alias, user_note_field=null, profile_pic_field=null) {
if (!note.display_image) { 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) { $.getJSON("/api/note/note/" + note.id + "/?format=json", function(new_note) {
note.display_image = new_note.display_image.replace("http:", "https:"); note.display_image = new_note.display_image.replace("http:", "https:");
note.name = new_note.name; note.name = new_note.name;
note.balance = new_note.balance; note.balance = new_note.balance;
note.user = new_note.user;
displayNote(note, alias, user_note_field, profile_pic_field); displayNote(note, alias, user_note_field, profile_pic_field);
}); });
@ -151,10 +152,13 @@ function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes
let old_pattern = null; 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) { field.keypress(function(event) {
if (event.originalEvent.charCode === 13) if (event.originalEvent.charCode === 13) {
$("#" + alias_matched_id + " li").first().trigger("click"); 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 // When the user type something, the matched aliases are refreshed

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) * @author Stanislaus Madueke (stan DOT madueke AT gmail DOT com)
* @requires jQuery 1.2.6 or later * @requires jQuery 1.2.6 or later
* *
@ -55,19 +55,26 @@
insertDeleteLink = function(row) { insertDeleteLink = function(row) {
var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'), var delCssSelector = $.trim(options.deleteCssClass).replace(/\s+/g, '.'),
addCssSelector = $.trim(options.addCssClass).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 // If the forms are laid out in table rows, insert
// the remove button into the last table cell: // 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')) { } else if (row.is('UL') || row.is('OL')) {
// If they're laid out as an ordered/unordered list, // If they're laid out as an ordered/unordered list,
// insert an <li> after the last list item: // 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 { } else {
// Otherwise, just insert the remove button as the // Otherwise, just insert the remove button as the
// last child element of the form's container: // 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 // Check if we're under the minimum number of forms - not to display delete link at rendering
if (!showDeleteLinks()){ if (!showDeleteLinks()){
row.find('a.' + delCssSelector).hide(); row.find('a.' + delCssSelector).hide();
@ -156,6 +163,7 @@
} else { } else {
// Otherwise, use the last form in the formset; this works much better if you've got // 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): // 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 = $('.' + options.formCssClass + ':last').clone(true).removeAttr('id');
template.find('input:hidden[id $= "-DELETE"]').remove(); template.find('input:hidden[id $= "-DELETE"]').remove();
// Clear all cloned fields, except those the user wants to keep (thanks to brunogola for the suggestion): // 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? // FIXME: Perhaps using $.data would be a better idea?
options.formTemplate = template; 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 // If forms are laid out as table rows, insert the
// "add" button in a new table row: // "add" button in a new table row:
var numCols = $$.eq(0).children().length, // This is a bit of an assumption :| 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>') buttonRow = $('<tr><td colspan="' + numCols + '">' + addButtonHTML + '</tr>').addClass(options.formCssClass + '-add');
.addClass(options.formCssClass + '-add');
$$.parent().append(buttonRow); $$.parent().append(buttonRow);
if (hideAddButton) buttonRow.hide();
addButton = buttonRow.find('a'); addButton = buttonRow.find('a');
} else { } else {
// Otherwise, insert it immediately after the last form: // 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(); addButton = $$.filter(':last').next();
if (hideAddButton) addButton.hide();
} }
if (hideAddButton) addButton.hide();
addButton.click(function() { addButton.click(function() {
var formCount = parseInt(totalForms.val()), var formCount = parseInt(totalForms.val()),
row = options.formTemplate.clone(true).removeClass('formset-custom-template'), row = options.formTemplate.clone(true).removeClass('formset-custom-template'),
@ -220,12 +235,15 @@
formTemplate: null, // The jQuery selection cloned to generate new form instances formTemplate: null, // The jQuery selection cloned to generate new form instances
addText: 'add another', // Text for the add link addText: 'add another', // Text for the add link
deleteText: 'remove', // Text for the delete link deleteText: 'remove', // Text for the delete link
addCssClass: '', // CSS class applied to the add link addContainerClass: null, // Container CSS class for the add link
deleteCssClass: '', // CSS class applied to the delete 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 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 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 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 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); })(jQuery);

View File

@ -39,10 +39,21 @@ $(document).ready(function() {
last.quantity = 1; last.quantity = 1;
$.getJSON("/api/user/" + last.note.user + "/", function(user) { if (!last.note.user) {
$("#last_name").val(user.last_name); $.getJSON("/api/note/note/" + last.note.id + "/?format=json", function(note) {
$("#first_name").val(user.first_name); 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; return true;

View File

@ -92,6 +92,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a> <a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a>
</li> </li>
{% endif %} {% 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>
<ul class="navbar-nav ml-auto"> <ul class="navbar-nav ml-auto">
{% if user.is_authenticated %} {% 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 pep8-naming
pyflakes pyflakes
commands = 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] [flake8]
# Ignore too many errors, should be reduced in the future # Ignore too many errors, should be reduced in the future