Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • bde/nk20
  • mcngnt/nk20
2 results
Show changes
Showing
with 1625 additions and 224 deletions
# 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)
# 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
# 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)
[
{
"model": "treasury.remittancetype",
"pk": 1,
"fields": {
"note": 3
}
}
]
# 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', )
# 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"),
)
# 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()
# 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',)
# 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'),
]
# 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')
......@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-16 11:53+0100\n"
"POT-Creation-Date: 2020-03-26 14:40+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -23,9 +23,9 @@ msgid "activity"
msgstr ""
#: apps/activity/models.py:19 apps/activity/models.py:44
#: apps/member/models.py:61 apps/member/models.py:112
#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202
#: apps/member/models.py:63 apps/member/models.py:114
#: apps/note/models/notes.py:188 apps/note/models/transactions.py:25
#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:232
#: templates/member/profile_detail.html:15
msgid "name"
msgstr ""
......@@ -46,12 +46,13 @@ msgstr ""
msgid "activity types"
msgstr ""
#: apps/activity/models.py:48 apps/note/models/transactions.py:69
#: apps/activity/models.py:48 apps/note/models/transactions.py:70
#: apps/permission/models.py:91
msgid "description"
msgstr ""
#: apps/activity/models.py:54 apps/note/models/notes.py:164
#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115
#: apps/note/models/transactions.py:63
msgid "type"
msgstr ""
......@@ -119,11 +120,11 @@ msgstr ""
msgid "create"
msgstr ""
#: apps/logs/models.py:61
#: apps/logs/models.py:61 apps/note/tables.py:147
msgid "edit"
msgstr ""
#: apps/logs/models.py:62
#: apps/logs/models.py:62 apps/note/tables.py:151
msgid "delete"
msgstr ""
......@@ -143,123 +144,123 @@ msgstr ""
msgid "member"
msgstr ""
#: apps/member/models.py:23
#: apps/member/models.py:25
msgid "phone number"
msgstr ""
#: apps/member/models.py:29 templates/member/profile_detail.html:28
#: apps/member/models.py:31 templates/member/profile_detail.html:28
msgid "section"
msgstr ""
#: apps/member/models.py:30
#: apps/member/models.py:32
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
msgstr ""
#: apps/member/models.py:36 templates/member/profile_detail.html:31
#: apps/member/models.py:38 templates/member/profile_detail.html:31
msgid "address"
msgstr ""
#: apps/member/models.py:42
#: apps/member/models.py:44
msgid "paid"
msgstr ""
#: apps/member/models.py:47 apps/member/models.py:48
#: apps/member/models.py:49 apps/member/models.py:50
msgid "user profile"
msgstr ""
#: apps/member/models.py:66
#: apps/member/models.py:68
msgid "email"
msgstr ""
#: apps/member/models.py:71
#: apps/member/models.py:73
msgid "membership fee"
msgstr ""
#: apps/member/models.py:75
#: apps/member/models.py:77
msgid "membership duration"
msgstr ""
#: apps/member/models.py:76
#: apps/member/models.py:78
msgid "The longest time a membership can last (NULL = infinite)."
msgstr ""
#: apps/member/models.py:81
#: apps/member/models.py:83
msgid "membership start"
msgstr ""
#: apps/member/models.py:82
#: apps/member/models.py:84
msgid "How long after January 1st the members can renew their membership."
msgstr ""
#: apps/member/models.py:87
#: apps/member/models.py:89
msgid "membership end"
msgstr ""
#: apps/member/models.py:88
#: apps/member/models.py:90
msgid ""
"How long the membership can last after January 1st of the next year after "
"members can renew their membership."
msgstr ""
#: apps/member/models.py:94 apps/note/models/notes.py:139
#: apps/member/models.py:96 apps/note/models/notes.py:139
msgid "club"
msgstr ""
#: apps/member/models.py:95
#: apps/member/models.py:97
msgid "clubs"
msgstr ""
#: apps/member/models.py:118
#: apps/member/models.py:120 apps/permission/models.py:276
msgid "role"
msgstr ""
#: apps/member/models.py:119
#: apps/member/models.py:121
msgid "roles"
msgstr ""
#: apps/member/models.py:143
#: apps/member/models.py:145
msgid "membership starts on"
msgstr ""
#: apps/member/models.py:146
#: apps/member/models.py:148
msgid "membership ends on"
msgstr ""
#: apps/member/models.py:150
#: apps/member/models.py:152
msgid "fee"
msgstr ""
#: apps/member/models.py:154
#: apps/member/models.py:162
msgid "membership"
msgstr ""
#: apps/member/models.py:155
#: apps/member/models.py:163
msgid "memberships"
msgstr ""
#: apps/member/views.py:69 templates/member/profile_detail.html:46
#: apps/member/views.py:80 templates/member/profile_detail.html:46
msgid "Update Profile"
msgstr ""
#: apps/member/views.py:82
#: apps/member/views.py:93
msgid "An alias with a similar name already exists."
msgstr ""
#: apps/member/views.py:132
#: apps/member/views.py:146
#, python-format
msgid "Account #%(id)s: %(username)s"
msgstr ""
#: apps/member/views.py:202
#: apps/member/views.py:216
msgid "Alias successfully deleted"
msgstr ""
#: apps/note/admin.py:120 apps/note/models/transactions.py:94
#: apps/note/admin.py:120 apps/note/models/transactions.py:95
msgid "source"
msgstr ""
#: apps/note/admin.py:128 apps/note/admin.py:156
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:100
#: apps/note/models/transactions.py:54 apps/note/models/transactions.py:108
msgid "destination"
msgstr ""
......@@ -309,7 +310,7 @@ msgstr ""
msgid "display image"
msgstr ""
#: apps/note/models/notes.py:53 apps/note/models/transactions.py:103
#: apps/note/models/notes.py:53 apps/note/models/transactions.py:118
msgid "created at"
msgstr ""
......@@ -383,116 +384,274 @@ msgstr ""
msgid "You can't delete your main alias."
msgstr ""
#: apps/note/models/transactions.py:30
#: apps/note/models/transactions.py:31
msgid "transaction category"
msgstr ""
#: apps/note/models/transactions.py:31
#: apps/note/models/transactions.py:32
msgid "transaction categories"
msgstr ""
#: apps/note/models/transactions.py:47
#: apps/note/models/transactions.py:48
msgid "A template with this name already exist"
msgstr ""
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:111
#: apps/note/models/transactions.py:57 apps/note/models/transactions.py:126
msgid "amount"
msgstr ""
#: apps/note/models/transactions.py:57
#: apps/note/models/transactions.py:58
msgid "in centimes"
msgstr ""
#: apps/note/models/transactions.py:75
#: apps/note/models/transactions.py:76
msgid "transaction template"
msgstr ""
#: apps/note/models/transactions.py:76
#: apps/note/models/transactions.py:77
msgid "transaction templates"
msgstr ""
#: apps/note/models/transactions.py:107
#: apps/note/models/transactions.py:101 apps/note/models/transactions.py:114
#: apps/note/tables.py:33 apps/note/tables.py:42
msgid "used alias"
msgstr ""
#: apps/note/models/transactions.py:122
msgid "quantity"
msgstr ""
#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15
msgid "Gift"
#: apps/note/models/transactions.py:130
msgid "reason"
msgstr ""
#: apps/note/models/transactions.py:135
msgid "valid"
msgstr ""
#: apps/note/models/transactions.py:118 templates/base.html:90
#: apps/note/models/transactions.py:140 apps/note/tables.py:95
msgid "invalidity reason"
msgstr ""
#: apps/note/models/transactions.py:147
msgid "transaction"
msgstr ""
#: apps/note/models/transactions.py:148
msgid "transactions"
msgstr ""
#: apps/note/models/transactions.py:202 templates/base.html:83
#: templates/note/transaction_form.html:19
#: templates/note/transaction_form.html:126
#: templates/note/transaction_form.html:145
msgid "Transfer"
msgstr ""
#: apps/note/models/transactions.py:119
#: apps/note/models/transactions.py:188
msgid "Template"
msgstr ""
#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23
#: apps/note/models/transactions.py:203
msgid "first_name"
msgstr ""
#: apps/note/models/transactions.py:208
msgid "bank"
msgstr ""
#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24
msgid "Credit"
msgstr ""
#: apps/note/models/transactions.py:121 templates/note/transaction_form.html:27
#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28
msgid "Debit"
msgstr ""
#: apps/note/models/transactions.py:122 apps/note/models/transactions.py:230
#: apps/note/models/transactions.py:230 apps/note/models/transactions.py:235
msgid "membership transaction"
msgstr ""
#: apps/note/models/transactions.py:129
msgid "reason"
#: apps/note/models/transactions.py:231
msgid "membership transactions"
msgstr ""
#: apps/note/models/transactions.py:133
msgid "valid"
#: apps/note/views.py:39
msgid "Transfer money"
msgstr ""
#: apps/note/models/transactions.py:138
msgid "transaction"
#: apps/note/views.py:145 templates/base.html:79
msgid "Consumptions"
msgstr ""
#: apps/note/models/transactions.py:139
msgid "transactions"
#: apps/permission/models.py:69 apps/permission/models.py:262
#, python-brace-format
msgid "Can {type} {model}.{field} in {query}"
msgstr ""
#: apps/note/models/transactions.py:207
msgid "first_name"
#: apps/permission/models.py:71 apps/permission/models.py:264
#, python-brace-format
msgid "Can {type} {model} in {query}"
msgstr ""
#: apps/note/models/transactions.py:212
msgid "bank"
#: apps/permission/models.py:84
msgid "rank"
msgstr ""
#: apps/note/models/transactions.py:231
msgid "membership transactions"
#: apps/permission/models.py:147
msgid "Specifying field applies only to view and change permission types."
msgstr ""
#: apps/note/views.py:31
msgid "Transfer money"
#: apps/treasury/apps.py:11 templates/base.html:102
msgid "Treasury"
msgstr ""
#: apps/note/views.py:132 templates/base.html:78
msgid "Consumptions"
#: apps/treasury/forms.py:56 apps/treasury/forms.py:95
#: templates/django_filters/rest_framework/form.html:5
#: templates/member/club_form.html:10 templates/treasury/invoice_form.html:47
msgid "Submit"
msgstr ""
#: apps/treasury/forms.py:58
msgid "Close"
msgstr ""
#: apps/treasury/forms.py:65
msgid "Remittance is already closed."
msgstr ""
#: apps/treasury/forms.py:70
msgid "You can't change the type of the remittance."
msgstr ""
#: apps/treasury/forms.py:84
msgid "Last name"
msgstr ""
#: apps/treasury/forms.py:86 templates/note/transaction_form.html:92
msgid "First name"
msgstr ""
#: apps/treasury/forms.py:88 templates/note/transaction_form.html:98
msgid "Bank"
msgstr ""
#: apps/treasury/forms.py:90 apps/treasury/tables.py:40
#: templates/note/transaction_form.html:128
#: templates/treasury/remittance_form.html:18
msgid "Amount"
msgstr ""
#: apps/treasury/models.py:18
msgid "Invoice identifier"
msgstr ""
#: apps/treasury/models.py:32
msgid "BDE"
msgstr ""
#: apps/treasury/models.py:37
msgid "Object"
msgstr ""
#: apps/treasury/models.py:41
msgid "Description"
msgstr ""
#: apps/treasury/models.py:46 templates/note/transaction_form.html:86
msgid "Name"
msgstr ""
#: apps/treasury/models.py:50
msgid "Address"
msgstr ""
#: apps/treasury/models.py:55
msgid "Place"
msgstr ""
#: apps/treasury/models.py:59
msgid "Acquitted"
msgstr ""
#: apps/treasury/models.py:75
msgid "Designation"
msgstr ""
#: apps/treasury/models.py:79
msgid "Quantity"
msgstr ""
#: apps/treasury/models.py:83
msgid "Unit price"
msgstr ""
#: note_kfet/settings/__init__.py:61
#: apps/treasury/models.py:120
msgid "Date"
msgstr ""
#: apps/treasury/models.py:126
msgid "Type"
msgstr ""
#: apps/treasury/models.py:131
msgid "Comment"
msgstr ""
#: apps/treasury/models.py:136
msgid "Closed"
msgstr ""
#: apps/treasury/models.py:159
msgid "Remittance #{:d}: {}"
msgstr ""
#: apps/treasury/models.py:178 apps/treasury/tables.py:64
#: apps/treasury/tables.py:72 templates/treasury/invoice_list.html:13
#: templates/treasury/remittance_list.html:13
msgid "Remittance"
msgstr ""
#: apps/treasury/tables.py:16
msgid "Invoice #{:d}"
msgstr ""
#: apps/treasury/tables.py:19 templates/treasury/invoice_list.html:10
#: templates/treasury/remittance_list.html:10
msgid "Invoice"
msgstr ""
#: apps/treasury/tables.py:38
msgid "Transaction count"
msgstr ""
#: apps/treasury/tables.py:43 apps/treasury/tables.py:45
msgid "View"
msgstr ""
#: apps/treasury/tables.py:66
msgid "Add"
msgstr ""
#: apps/treasury/tables.py:74
msgid "Remove"
msgstr ""
#: note_kfet/settings/__init__.py:63
msgid ""
"The Central Authentication Service grants you access to most of our websites "
"by authenticating only once, so you don't need to type your credentials "
"again unless your session expires or you logout."
msgstr ""
#: note_kfet/settings/base.py:156
#: note_kfet/settings/base.py:151
msgid "German"
msgstr ""
#: note_kfet/settings/base.py:157
#: note_kfet/settings/base.py:152
msgid "English"
msgstr ""
#: note_kfet/settings/base.py:158
#: note_kfet/settings/base.py:153
msgid "French"
msgstr ""
......@@ -500,18 +659,14 @@ msgstr ""
msgid "The ENS Paris-Saclay BDE note."
msgstr ""
#: templates/base.html:81
#: templates/base.html:87
msgid "Clubs"
msgstr ""
#: templates/base.html:84
#: templates/base.html:92
msgid "Activities"
msgstr ""
#: templates/base.html:87
msgid "Buttons"
msgstr ""
#: templates/cas_server/base.html:7
msgid "Central Authentication Service"
msgstr ""
......@@ -567,11 +722,6 @@ msgstr ""
msgid "Field filters"
msgstr ""
#: templates/django_filters/rest_framework/form.html:5
#: templates/member/club_form.html:10
msgid "Submit"
msgstr ""
#: templates/member/club_detail.html:10
msgid "Membership starts on"
msgstr ""
......@@ -653,7 +803,7 @@ msgstr ""
msgid "Sign up"
msgstr ""
#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38
#: templates/note/conso_form.html:28 templates/note/transaction_form.html:50
msgid "Select emitters"
msgstr ""
......@@ -681,49 +831,53 @@ msgstr ""
msgid "Double consumptions"
msgstr ""
#: templates/note/conso_form.html:141
#: templates/note/conso_form.html:141 templates/note/transaction_form.html:152
msgid "Recent transactions history"
msgstr ""
#: templates/note/transaction_form.html:55
#: templates/note/transaction_form.html:15
msgid "Gift"
msgstr ""
#: templates/note/transaction_form.html:68
msgid "External payment"
msgstr ""
#: templates/note/transaction_form.html:63
#: templates/note/transaction_form.html:76
msgid "Transfer type"
msgstr ""
#: templates/note/transaction_form.html:73
#: templates/note/transaction_form.html:86
msgid "Name"
msgstr ""
#: templates/note/transaction_form.html:79
#: templates/note/transaction_form.html:92
msgid "First name"
msgstr ""
#: templates/note/transaction_form.html:85
#: templates/note/transaction_form.html:98
msgid "Bank"
msgstr ""
#: templates/note/transaction_form.html:97
#: templates/note/transaction_form.html:179
#: templates/note/transaction_form.html:186
#: templates/note/transaction_form.html:111
#: templates/note/transaction_form.html:169
#: templates/note/transaction_form.html:176
msgid "Select receivers"
msgstr ""
#: templates/note/transaction_form.html:114
#: templates/note/transaction_form.html:128
msgid "Amount"
msgstr ""
#: templates/note/transaction_form.html:119
#: templates/note/transaction_form.html:138
msgid "Reason"
msgstr ""
#: templates/note/transaction_form.html:193
#: templates/note/transaction_form.html:183
msgid "Credit note"
msgstr ""
#: templates/note/transaction_form.html:200
#: templates/note/transaction_form.html:190
msgid "Debit note"
msgstr ""
......@@ -731,6 +885,22 @@ msgstr ""
msgid "Buttons list"
msgstr ""
#: templates/note/transactiontemplate_list.html:9
msgid "search button"
msgstr ""
#: templates/note/transactiontemplate_list.html:20
msgid "buttons listing "
msgstr ""
#: templates/note/transactiontemplate_list.html:71
msgid "button successfully deleted "
msgstr ""
#: templates/note/transactiontemplate_list.html:75
msgid "Unable to delete button "
msgstr ""
#: templates/registration/logged_out.html:8
msgid "Thanks for spending some quality time with the Web site today."
msgstr ""
......@@ -740,7 +910,7 @@ msgid "Log in again"
msgstr ""
#: templates/registration/login.html:7 templates/registration/login.html:8
#: templates/registration/login.html:26
#: templates/registration/login.html:28
#: templates/registration/password_reset_complete.html:10
msgid "Log in"
msgstr ""
......@@ -752,7 +922,15 @@ msgid ""
"page. Would you like to login to a different account?"
msgstr ""
#: templates/registration/login.html:27
#: templates/registration/login.html:22
msgid "You can also register via the central authentification server "
msgstr ""
#: templates/registration/login.html:23
msgid "using this link "
msgstr ""
#: templates/registration/login.html:29
msgid "Forgotten your password or username?"
msgstr ""
......@@ -808,3 +986,72 @@ msgstr ""
#: templates/registration/password_reset_form.html:11
msgid "Reset my password"
msgstr ""
#: templates/treasury/invoice_form.html:6
msgid "Invoices list"
msgstr ""
#: templates/treasury/invoice_form.html:42
msgid "Add product"
msgstr ""
#: templates/treasury/invoice_form.html:43
msgid "Remove product"
msgstr ""
#: templates/treasury/invoice_list.html:21
msgid "New invoice"
msgstr ""
#: templates/treasury/remittance_form.html:7
msgid "Remittance #"
msgstr ""
#: templates/treasury/remittance_form.html:9
#: templates/treasury/specialtransactionproxy_form.html:7
msgid "Remittances list"
msgstr ""
#: templates/treasury/remittance_form.html:12
msgid "Count"
msgstr ""
#: templates/treasury/remittance_form.html:29
msgid "Linked transactions"
msgstr ""
#: templates/treasury/remittance_form.html:34
msgid "There is no transaction linked with this remittance."
msgstr ""
#: templates/treasury/remittance_list.html:19
msgid "Opened remittances"
msgstr ""
#: templates/treasury/remittance_list.html:24
msgid "There is no opened remittance."
msgstr ""
#: templates/treasury/remittance_list.html:28
msgid "New remittance"
msgstr ""
#: templates/treasury/remittance_list.html:32
msgid "Transfers without remittances"
msgstr ""
#: templates/treasury/remittance_list.html:37
msgid "There is no transaction without any linked remittance."
msgstr ""
#: templates/treasury/remittance_list.html:43
msgid "Transfers with opened remittances"
msgstr ""
#: templates/treasury/remittance_list.html:48
msgid "There is no transaction with an opened linked remittance."
msgstr ""
#: templates/treasury/remittance_list.html:54
msgid "Closed remittances"
msgstr ""
This diff is collapsed.
......@@ -59,6 +59,7 @@ INSTALLED_APPS = [
'activity',
'member',
'note',
'treasury',
'permission',
'api',
'logs',
......
......@@ -15,6 +15,7 @@ urlpatterns = [
# Include project routers
path('note/', include('note.urls')),
path('treasury/', include('treasury.urls')),
# Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')),
......
psycopg2==2.8.4
psycopg2-binary==2.8.4
static/img/Finalist.png

752 KiB

static/img/Kataclist.png

664 KiB

static/img/Listorique.png

414 KiB

static/img/Monopolist.png

375 KiB