mirror of
https://github.com/nanoy42/coope
synced 2024-11-21 19:03:12 +00:00
commit
cc1fb654d9
47 changed files with 1094 additions and 122 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,3 +1,17 @@
|
|||
## v3.6.0
|
||||
* AJout d'un débit direct comme champ du profil
|
||||
* Suppression des codes bare
|
||||
* Création plus simple (création automatiques des produits avec les bons prix)
|
||||
* Calcul des prix des produits depuis le site
|
||||
* Génération de factures depuis le site
|
||||
* Ajouter un champ "raison" dans les accès gracieux
|
||||
* Fix de la recherche dans l'admin
|
||||
* Onglet de répartition des cotisations
|
||||
* Ajout d'un champ alcool pour optimiser le classement
|
||||
* Amélioration et fix de la redirection après connexion
|
||||
* Amélioration de l'affichage du nombre de jour dans une cotisation
|
||||
* Amélioration de l'affichage des pressions
|
||||
* TM (trademarks) enlevés et remplacés
|
||||
## v3.5.3
|
||||
* Fix le profil (division par 0 lorsque toutes les transactions d'un produit avaient été annulées)
|
||||
## v3.5.2
|
||||
|
|
|
@ -38,6 +38,7 @@ INSTALLED_APPS = [
|
|||
'dal_select2',
|
||||
'simple_history',
|
||||
'django_tex',
|
||||
'debug_toolbar'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -50,6 +51,7 @@ MIDDLEWARE = [
|
|||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'simple_history.middleware.HistoryRequestMiddleware',
|
||||
'django.contrib.admindocs.middleware.XViewMiddleware',
|
||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'coopeV3.urls'
|
||||
|
@ -127,3 +129,4 @@ LOGIN_URL = '/users/login'
|
|||
MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
INTERNAL_IPS = ["127.0.0.1"]
|
|
@ -30,3 +30,10 @@ urlpatterns = [
|
|||
path('gestion/', include('gestion.urls')),
|
||||
path('preferences/', include('preferences.urls')),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
|
||||
if settings.DEBUG:
|
||||
import debug_toolbar
|
||||
urlpatterns = [
|
||||
path('__debug__/', include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
|
|
7
coopeV3/utils.py
Normal file
7
coopeV3/utils.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
import math
|
||||
|
||||
def compute_price(price, a, b, c, alpha):
|
||||
if price < alpha:
|
||||
return float(price) * (1 + float(a) + float(b) * math.exp(-c/(price-alpha)**2))
|
||||
else:
|
||||
return price * (1 + a)
|
|
@ -18,6 +18,7 @@ def run_tex(source):
|
|||
filename = os.path.join(tempdir, 'texput.tex')
|
||||
with open(filename, 'x', encoding='utf-8') as f:
|
||||
f.write(source)
|
||||
print(source)
|
||||
latex_interpreter = getattr(settings, 'LATEX_INTERPRETER', DEFAULT_INTERPRETER)
|
||||
latex_command = 'cd "{tempdir}" && {latex_interpreter} -interaction=batchmode {path}'.format(tempdir=tempdir, latex_interpreter=latex_interpreter, path=os.path.basename(filename))
|
||||
process = run(latex_command, shell=True, stdout=PIPE, stderr=PIPE)
|
||||
|
|
|
@ -17,7 +17,7 @@ class ConsumptionHistoryAdmin(SimpleHistoryAdmin):
|
|||
"""
|
||||
list_display = ('customer', 'product', 'quantity', 'paymentMethod', 'date', 'amount')
|
||||
ordering = ('-date', )
|
||||
search_fields = ('customer', 'product')
|
||||
search_fields = ('customer__username', 'customer__first_name', 'customer__last_name', 'product__name')
|
||||
list_filter = ('paymentMethod',)
|
||||
|
||||
class KegAdmin(SimpleHistoryAdmin):
|
||||
|
@ -35,7 +35,7 @@ class KegHistoryAdmin(SimpleHistoryAdmin):
|
|||
"""
|
||||
list_display = ('keg', 'openingDate', 'closingDate', 'isCurrentKegHistory', 'quantitySold')
|
||||
ordering = ('-openingDate', 'quantitySold')
|
||||
search_fields = ('keg',)
|
||||
search_fields = ('keg__name',)
|
||||
list_filter = ('isCurrentKegHistory', 'keg')
|
||||
|
||||
class MenuHistoryAdmin(SimpleHistoryAdmin):
|
||||
|
@ -70,7 +70,7 @@ class ReloadAdmin(SimpleHistoryAdmin):
|
|||
"""
|
||||
list_display = ('customer', 'amount', 'date', 'PaymentMethod')
|
||||
ordering = ('-date', 'amount', 'customer')
|
||||
search_fields = ('customer',)
|
||||
search_fields = ('customer__username', 'customer__first_name', 'customer__last_name')
|
||||
list_filter = ('PaymentMethod', )
|
||||
|
||||
class RefundAdmin(SimpleHistoryAdmin):
|
||||
|
@ -79,7 +79,7 @@ class RefundAdmin(SimpleHistoryAdmin):
|
|||
"""
|
||||
list_display = ('customer', 'amount', 'date')
|
||||
ordering = ('-date', 'amount', 'customer')
|
||||
search_fields = ('customer',)
|
||||
search_fields = ('customer__username', 'customer__first_name', 'customer__last_name')
|
||||
|
||||
class CategoryAdmin(SimpleHistoryAdmin):
|
||||
"""
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import MinValueValidator
|
||||
|
||||
|
||||
from dal import autocomplete
|
||||
|
||||
from .models import Reload, Refund, Product, Keg, Menu, Category
|
||||
from preferences.models import PaymentMethod
|
||||
from preferences.models import PaymentMethod, PriceProfile
|
||||
|
||||
class ReloadForm(forms.ModelForm):
|
||||
"""
|
||||
|
@ -44,17 +46,21 @@ class KegForm(forms.ModelForm):
|
|||
"""
|
||||
A form to create and edit a :class:`~gestion.models.Keg`.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(KegForm, self).__init__(*args, **kwargs)
|
||||
self.fields['pinte'].queryset = Product.objects.filter(draft_category=Product.DRAFT_PINTE)
|
||||
self.fields['demi'].queryset = Product.objects.filter(draft_category=Product.DRAFT_DEMI)
|
||||
self.fields['galopin'].queryset = Product.objects.filter(draft_category=Product.DRAFT_GALOPIN)
|
||||
|
||||
class Meta:
|
||||
model = Keg
|
||||
fields = "__all__"
|
||||
fields = ["name", "stockHold", "amount", "capacity"]
|
||||
widgets = {'amount': forms.TextInput}
|
||||
|
||||
category = forms.ModelChoiceField(queryset=Category.objects.all(), label="Catégorie")
|
||||
deg = forms.DecimalField(max_digits=5, decimal_places=2, label="Degré", validators=[MinValueValidator(0)])
|
||||
create_galopin = forms.BooleanField(label="Créer le produit galopin ?")
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
if cleaned_data.get("name")[0:4] != "Fût ":
|
||||
raise ValidationError("Le nom du fût doit être sous la forme 'Fût nom de la bière'")
|
||||
|
||||
class MenuForm(forms.ModelForm):
|
||||
"""
|
||||
A form to create and edit a :class:`~gestion.models.Menu`.
|
||||
|
@ -122,4 +128,25 @@ class SearchCategoryForm(forms.Form):
|
|||
"""
|
||||
A form to search a :class:`~gestion.models.Category`.
|
||||
"""
|
||||
category = forms.ModelChoiceField(queryset=Category.objects.all(), required=True, label="Catégorie", widget=autocomplete.ModelSelect2(url='gestion:categories-autocomplete', attrs={'data-minimum-input-length':2}))
|
||||
category = forms.ModelChoiceField(queryset=Category.objects.all(), required=True, label="Catégorie", widget=autocomplete.ModelSelect2(url='gestion:categories-autocomplete', attrs={'data-minimum-input-length':2}))
|
||||
|
||||
class GenerateInvoiceForm(forms.Form):
|
||||
"""
|
||||
A form to generate an invoice
|
||||
"""
|
||||
invoice_date = forms.CharField(label="Date")
|
||||
invoice_number = forms.CharField(label="Numéro", help_text="Au format 19018, sans le FE")
|
||||
invoice_place = forms.CharField(label="Lieu")
|
||||
invoice_object = forms.CharField(label="Objet")
|
||||
invoice_description = forms.CharField(label="Description", required=False)
|
||||
client_name = forms.CharField(label="Nom du client")
|
||||
client_address_fisrt_line = forms.CharField(label="Première ligne d'adresse")
|
||||
client_address_second_line = forms.CharField(label="Deuxième ligne d'adresse")
|
||||
products = forms.CharField(widget=forms.Textarea, label="Produits", help_text="Au format nom;prix;quantité avec saut de ligne")
|
||||
|
||||
class ComputePriceForm(forms.Form):
|
||||
"""
|
||||
A form to compute price
|
||||
"""
|
||||
price_profile = forms.ModelChoiceField(queryset=PriceProfile.objects.all(), label="Profil de prix")
|
||||
price = forms.DecimalField(max_digits=10, decimal_places=5, label="Prix")
|
43
gestion/migrations/0010_auto_20190623_1623.py
Normal file
43
gestion/migrations/0010_auto_20190623_1623.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
# Generated by Django 2.1 on 2019-06-23 14:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gestion', '0009_auto_20190506_0939'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='historicalkeg',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, max_length=255, verbose_name='Nom'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalproduct',
|
||||
name='barcode',
|
||||
field=models.CharField(db_index=True, max_length=255, verbose_name='Code barre'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='historicalproduct',
|
||||
name='name',
|
||||
field=models.CharField(db_index=True, max_length=255, verbose_name='Nom'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='keg',
|
||||
name='name',
|
||||
field=models.CharField(max_length=255, unique=True, verbose_name='Nom'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='barcode',
|
||||
field=models.CharField(max_length=255, unique=True, verbose_name='Code barre'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='name',
|
||||
field=models.CharField(max_length=255, unique=True, verbose_name='Nom'),
|
||||
),
|
||||
]
|
37
gestion/migrations/0011_auto_20190623_1640.py
Normal file
37
gestion/migrations/0011_auto_20190623_1640.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Generated by Django 2.1 on 2019-06-23 14:40
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('gestion', '0010_auto_20190623_1623'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='historicalkeg',
|
||||
name='barcode',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='historicalmenu',
|
||||
name='barcode',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='historicalproduct',
|
||||
name='barcode',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='keg',
|
||||
name='barcode',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='menu',
|
||||
name='barcode',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='product',
|
||||
name='barcode',
|
||||
),
|
||||
]
|
|
@ -46,7 +46,7 @@ class Product(models.Model):
|
|||
class Meta:
|
||||
verbose_name = "Produit"
|
||||
|
||||
name = models.CharField(max_length=40, verbose_name="Nom", unique=True)
|
||||
name = models.CharField(max_length=255, verbose_name="Nom", unique=True)
|
||||
"""
|
||||
The name of the product.
|
||||
"""
|
||||
|
@ -62,10 +62,6 @@ class Product(models.Model):
|
|||
"""
|
||||
Number of product at the bar.
|
||||
"""
|
||||
barcode = models.CharField(max_length=20, unique=True, verbose_name="Code barre")
|
||||
"""
|
||||
The barcode of the product.
|
||||
"""
|
||||
category = models.ForeignKey('Category', on_delete=models.PROTECT, verbose_name="Catégorie")
|
||||
"""
|
||||
The category of the product
|
||||
|
@ -98,7 +94,11 @@ class Product(models.Model):
|
|||
history = HistoricalRecords()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
if self.draft_category == self.DRAFT_NONE:
|
||||
return self.name + "(" + str(self.amount) + " €)"
|
||||
else:
|
||||
return self.name + " (" + str(self.amount) + " €, " + str(self.deg) + "°)"
|
||||
|
||||
|
||||
def user_ranking(self, pk):
|
||||
"""
|
||||
|
@ -158,7 +158,7 @@ class Keg(models.Model):
|
|||
("close_keg", "Peut fermer les fûts")
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=20, unique=True, verbose_name="Nom")
|
||||
name = models.CharField(max_length=255, unique=True, verbose_name="Nom")
|
||||
"""
|
||||
The name of the keg.
|
||||
"""
|
||||
|
@ -166,10 +166,6 @@ class Keg(models.Model):
|
|||
"""
|
||||
The number of this keg in the hold.
|
||||
"""
|
||||
barcode = models.CharField(max_length=20, unique=True, verbose_name="Code barre")
|
||||
"""
|
||||
The barcode of the keg.
|
||||
"""
|
||||
amount = models.DecimalField(max_digits=7, decimal_places=2, verbose_name="Prix du fût", validators=[MinValueValidator(0)])
|
||||
"""
|
||||
The price of the keg.
|
||||
|
@ -313,10 +309,6 @@ class Menu(models.Model):
|
|||
"""
|
||||
Price of the menu.
|
||||
"""
|
||||
barcode = models.CharField(max_length=20, unique=True, verbose_name="Code barre")
|
||||
"""
|
||||
Barcode of the menu.
|
||||
"""
|
||||
articles = models.ManyToManyField(Product, verbose_name="Produits")
|
||||
"""
|
||||
Stores :class:`Products <gestion.models.Product>` contained in the menu
|
||||
|
|
77
gestion/templates/gestion/divide.html
Normal file
77
gestion/templates/gestion/divide.html
Normal file
|
@ -0,0 +1,77 @@
|
|||
{% extends 'base.html' %}
|
||||
{% block entete %}Répartition des cotisations{% endblock %}
|
||||
{% block navbar %}
|
||||
<ul>
|
||||
<li><a href="#first">Répartition des cotisations</a></li>
|
||||
<li><a href="#second">Historique des répartitions</a></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<section id="first" class="main">
|
||||
<header class="major">
|
||||
<h2>Répartition des cotisations</h2>
|
||||
</header>
|
||||
<section>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Champ</th>
|
||||
<th>Valeur</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Nombre de cotisations non réparties</td>
|
||||
<td>{{total_cotisations}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Valeur totale des cotisations non réparties</td>
|
||||
<td>{{total_amount}} €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Valeur à donner au Club Phœnix Technopôle Metz</td>
|
||||
<td>{{total_amount_ptm}} €</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit"><i class="fa fa-hand-holding-usd"></i> Répartir</button>
|
||||
</form>
|
||||
<p>Attention, cliquer sur ce bouton marquera toutes les cotisations actuellement non réparties comme réparties. L'historique de cette action n'est pas simple à obtenir et l'action peut être considérée comme irreversible.</p>
|
||||
</section>
|
||||
</section>
|
||||
<section id="second" class="main">
|
||||
<header class="major">
|
||||
<h2>Historique des répartitions</h2>
|
||||
</header>
|
||||
<section>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Nombre de cotisations</th>
|
||||
<th>Montant des cotisations</th>
|
||||
<th>Montant des cotisations pourle Phœnix</th>
|
||||
<th>Coopeman</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for divide_history in divide_histories %}
|
||||
<tr>
|
||||
<td>{{ divide_history.date }}</td>
|
||||
<td>{{ divide_history.total_cotisations }}</td>
|
||||
<td>{{ divide_history.total_cotisations_amount }} €</td>
|
||||
<td>{{ divide_history.total_ptm_amount }} €</td>
|
||||
<td>{{ divide_history.coopeman }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
{% endblock %}
|
100
gestion/templates/gestion/invoice.tex
Normal file
100
gestion/templates/gestion/invoice.tex
Normal file
|
@ -0,0 +1,100 @@
|
|||
\documentclass[french,11pt]{article}
|
||||
\usepackage{babel}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[a4paper]{geometry}
|
||||
\usepackage{units}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{fancyhdr}
|
||||
\usepackage{fp}
|
||||
\usepackage{float}
|
||||
\usepackage{eurosym}
|
||||
\def\FactureDate { {{- invoice_date -}} }
|
||||
\def\FactureNum { {{- invoice_number -}} }
|
||||
\def\FactureAcquittee {non}
|
||||
\def\FactureLieu { {{- invoice_place -}} }
|
||||
\def\FactureObjet { {{- invoice_object -}} }
|
||||
\def\FactureDescr {
|
||||
{{- invoice_description -}}
|
||||
}
|
||||
|
||||
\def\ClientNom{ {{- client_name -}} }
|
||||
\def\ClientAdresse{
|
||||
{{- client_address_first_line -}}\newline
|
||||
{{ client_address_second_line }}
|
||||
}
|
||||
|
||||
\geometry{verbose,tmargin=4em,bmargin=8em,lmargin=6em,rmargin=6em}
|
||||
\setlength{\parindent}{0pt}
|
||||
\setlength{\parskip}{1ex plus 0.5ex minus 0.2ex}
|
||||
|
||||
\thispagestyle{fancy}
|
||||
\pagestyle{fancy}
|
||||
\setlength{\parindent}{0pt}
|
||||
|
||||
\renewcommand{\headrulewidth}{0pt}
|
||||
\cfoot{
|
||||
\small{
|
||||
Coopé Technopôle Metz (CTM)\\
|
||||
Adresse mail : coopemetz@gmail.com\\}
|
||||
\tiny{
|
||||
Inscrite au registre des associations du tribunal d’instance de Metz
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
\begin{document}
|
||||
|
||||
\begin{figure}[H]
|
||||
\includegraphics[scale=0.3]{ {{- path -}} }
|
||||
\end{figure}
|
||||
Coopé Technopôle Metz\\
|
||||
4 place Édouard Branly\\
|
||||
57070 Metz
|
||||
|
||||
Facture FE\FactureNum
|
||||
|
||||
{\addtolength{\leftskip}{10.5cm}
|
||||
\textbf{\ClientNom} \\
|
||||
\ClientAdresse \\
|
||||
|
||||
}
|
||||
|
||||
\hspace*{10.5cm}
|
||||
\FactureLieu, le \FactureDate
|
||||
|
||||
~\\~\\
|
||||
|
||||
\textbf{Objet : \FactureObjet \\}
|
||||
|
||||
\textnormal{\FactureDescr}
|
||||
|
||||
\vspace{10mm}
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{lrrr}
|
||||
\textbf{Désignation ~~~~~~} & \textbf{Prix unitaire} & \textbf{Quantité} & \textbf{Montant (EUR)} \\
|
||||
\hline
|
||||
{% for product in products %}
|
||||
{{- product.0 -}} & {{- product.1 -}} \euro{} & {{- product.2 -}} & {{- product.3 -}} \euro{}\\
|
||||
{% endfor %}
|
||||
\hline
|
||||
\textbf{Total HT} & & & {{- total -}} \euro{}
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\vfill
|
||||
À régler par chèque, espèces 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} \\
|
||||
20041 & 01010 & 1074350Z031 & 48 \\
|
||||
\hline \textbf{IBAN Nº} & \multicolumn{3}{|l|}{ FR82 2004 1010 1010 7435 0Z03 148 } \\
|
||||
\hline \textbf{BIC} & \multicolumn{3}{|l|}{ PSSTFRPPNCY }\\
|
||||
\hline \textbf{Domiciliation} & \multicolumn{3}{|l|}{La Banque Postale - Centre Financier - 54900 Nancy CEDEX 9}\\
|
||||
\hline \textbf{Titulaire} & \multicolumn{3}{|l|}{ASSO COOPE TECHNOPOLE METZ}\\
|
||||
\hline
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
\end{document}
|
|
@ -28,7 +28,6 @@
|
|||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Stock en soute</th>
|
||||
<th>Code barre</th>
|
||||
<th>Capacité</th>
|
||||
<th>Quantité vendue</th>
|
||||
<th>Montant vendu</th>
|
||||
|
@ -43,7 +42,6 @@
|
|||
<tr>
|
||||
<td>{{ kegH.keg.name }}</td>
|
||||
<td>{{ kegH.keg.stockHold}}</td>
|
||||
<td>{{ kegH.keg.barcode }}</td>
|
||||
<td>{{ kegH.keg.capacity }} L</td>
|
||||
<td>{{ kegH.quantitySold }} L</td>
|
||||
<td>{{ kegH.amountSold }} €</td>
|
||||
|
@ -77,7 +75,6 @@
|
|||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Stock en soute</th>
|
||||
<th>Code barre</th>
|
||||
<th>Capacité</th>
|
||||
<th>Prix du fût</th>
|
||||
<th>Degré</th>
|
||||
|
@ -90,7 +87,6 @@
|
|||
<tr>
|
||||
<td>{{ keg.name }}</td>
|
||||
<td>{{ keg.stockHold}}</td>
|
||||
<td>{{ keg.barcode }}</td>
|
||||
<td>{{ keg.capacity }} L</td>
|
||||
<td>{{ keg.amount }} €</td>
|
||||
<td>{{ keg.pinte.deg }}°</td>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{%block entete%}Gestion de la Coopé™{%endblock%}
|
||||
{%block entete%}Gestion de la Coopé Technopôle Metz{%endblock%}
|
||||
|
||||
{% block navbar %}
|
||||
<ul>
|
||||
|
@ -98,7 +98,6 @@
|
|||
<table id="productTable" type="input" name="tableau" class="alt">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>CodeBarre</th>
|
||||
<th>Nom Produit</th>
|
||||
<th>Prix Unitaire</th>
|
||||
<th>Quantité</th>
|
||||
|
@ -117,43 +116,43 @@
|
|||
<tbody class="actions" id="bouton Produit">
|
||||
<tr class="cotisation-hidden" style="text-align:center; font-weight:bold;"><td colspan="4">Cotisations</td></tr>
|
||||
{% for cotisation in cotisations %}
|
||||
{% if forloop.counter0|divisibleby:4 %}
|
||||
{% if forloop.counter0|divisibleby:3 %}
|
||||
<tr class="cotisation-hidden" style="text-align:center">
|
||||
{% endif %}
|
||||
<td class="cotisation-hidden"><button class="cotisation" target="{{cotisation.pk}}">Cotisation {{cotisation.duration}} jours ({{cotisation.amount}} €)</button></td>
|
||||
{% if forloop.counter|divisibleby:4 %}
|
||||
{% if forloop.counter|divisibleby:3 %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not cotisations|divisibleby:4 %}
|
||||
{% if not cotisations|divisibleby:3 %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr style="text-align:center; font-weight:bold;"><td colspan="4">Bières pression</td></tr>
|
||||
{% for product in bieresPression %}
|
||||
{% if forloop.counter0|divisibleby:4 %}
|
||||
{% if forloop.counter0|divisibleby:3 %}
|
||||
<tr style="text-align:center">
|
||||
{% endif %}
|
||||
<td><button class="product {% if product.adherentRequired %}special{% endif%}" target="{{product.pk}}">{{product.name}}</button></td>
|
||||
{% if forloop.counter|divisibleby:4 %}
|
||||
<td><button class="product {% if product.adherentRequired %}special{% endif%}" target="{{product.pk}}">{{product}}</button></td>
|
||||
{% if forloop.counter|divisibleby:3 %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not bieresPression|divisibleby:4 %}
|
||||
{% if not bieresPression|divisibleby:3 %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for category in categories %}
|
||||
{% if category.active_products.count > 0 %}
|
||||
<tr style="text-align:center; font-weight:bold;"><td colspan="4">{{category}}</td></tr>
|
||||
{% for product in category.active_products %}
|
||||
{% if forloop.counter0|divisibleby:4 %}
|
||||
{% if forloop.counter0|divisibleby:3 %}
|
||||
<tr style="text-align:center">
|
||||
{% endif %}
|
||||
<td><button class="product {% if product.adherentRequired %}special{% endif%}" target="{{product.pk}}">{{product.name}}</button></td>
|
||||
{% if forloop.counter|divisibleby:4 %}
|
||||
<td><button class="product {% if product.adherentRequired %}special{% endif%}" target="{{product.pk}}">{{product}}</button></td>
|
||||
{% if forloop.counter|divisibleby:3 %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not category.active_products|divisibleby:4 %}
|
||||
{% if not category.active_products|divisibleby:3 %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@ -161,15 +160,15 @@
|
|||
{% if menus %}
|
||||
<tr style="text-align:center; font-weight:bold;"><td colspan="4">Menus</td></tr>
|
||||
{% for product in menus %}
|
||||
{% if forloop.counter0|divisibleby:4 %}
|
||||
{% if forloop.counter0|divisibleby:3 %}
|
||||
<tr style="text-align:center">
|
||||
{% endif %}
|
||||
<td><button class="menu {% if product.adherent_required %}special{% endif%}" target="{{product.pk}}">{{product.name}}</button></td>
|
||||
{% if forloop.counter|divisibleby:4 %}
|
||||
<td><button class="menu {% if product.adherent_required %}special{% endif%}" target="{{product.pk}}">{{product}}</button></td>
|
||||
{% if forloop.counter|divisibleby:3 %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not menus|divisibleby:4 %}
|
||||
{% if not menus|divisibleby:3 %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Prix</th>
|
||||
<th>Code barre</th>
|
||||
<th>Produits</th>
|
||||
<th>Actif</th>
|
||||
<th>Administrer</th>
|
||||
|
@ -28,7 +27,6 @@
|
|||
<tr>
|
||||
<td>{{ menu.name }}</td>
|
||||
<td>{{ menu.amount}} €</td>
|
||||
<td>{{ menu.barcode }}</td>
|
||||
<td>{% for art in menu.articles.all %}<a href="{% url 'gestion:productProfile' art.pk %}">{{art}}</a>,{% endfor %}</td>
|
||||
<td>{{ menu.is_active | yesno:"Oui, Non"}}</td>
|
||||
<td>{% if perms.gestion.change_menu %}<a href="{% url 'gestion:switchActivateMenu' menu.pk %}" class="button small">{% if menu.is_active %}<i class="fa fa-times-cirlce"></i> Désa{% else %}<i class="fa fa-check-circle"></i> A{% endif %}ctiver</a> <a href="{% url 'gestion:editMenu' menu.pk %}" class="button small"><i class="fa fa-pencil-alt"></i> Modifier</a>{% endif %}</td>
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
<strong>Prix de vente</strong> : {{ product.amount }}€<br>
|
||||
<strong>Stock en soute</strong> : {{ product.stockHold }}<br>
|
||||
<strong>Stock au bar</strong> : {{ product.stockBar }}<br>
|
||||
<strong>Code Barre</strong> : {{ product.barcode }}<br>
|
||||
<strong>Catégorie</strong> : <a href="{% url 'gestion:categoryProfile' product.category.pk %}">{{ product.category }}</a><br>
|
||||
<strong>Actif</strong> : {{ product.is_active | yesno:"Oui, Non"}}<br>
|
||||
<strong>Dégré</strong> : {{ product.deg }}<br>
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
<th>Prix</th>
|
||||
<th>Stock (soute)</th>
|
||||
<th>Stock (bar)</th>
|
||||
<th>Code barre</th>
|
||||
<th>Catégorie</th>
|
||||
<th>Actif</th>
|
||||
<th>Degré</th>
|
||||
|
@ -36,7 +35,6 @@
|
|||
<td>{{ product.amount}}</td>
|
||||
<td>{{ product.stockHold }}</td>
|
||||
<td>{{ product.stockBar }}</td>
|
||||
<td>{{ product.barcode }}</td>
|
||||
<td>{{ product.category }}</td>
|
||||
<td>{{ product.is_active | yesno:"Oui, Non"}}</td>
|
||||
<td>{{ product.deg }}</td>
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
<th>Place</th>
|
||||
<th>Pseudo</th>
|
||||
<th>Debit</th>
|
||||
<th>Débit direct (non pris en compte pour le classement)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -30,7 +31,8 @@
|
|||
<tr>
|
||||
<th>{{ forloop.counter }}</th>
|
||||
<th><a href="{% url 'users:profile' customer.pk %}">{{ customer.username }}</a></th>
|
||||
<th>{{ customer.profile.debit }}€</th>
|
||||
<th>{{ customer.profile.debit }} €</th>
|
||||
<th>{{ customer.profile.direct_debit }} €</th>
|
||||
</tr>
|
||||
{%endfor%}
|
||||
</tbody>
|
||||
|
@ -60,10 +62,10 @@
|
|||
{% for customer in bestDrinkers %}
|
||||
<tr>
|
||||
<th>{{ forloop.counter }}</th>
|
||||
<th><a href="{% url 'users:profile' customer.0.pk %}">{{ customer.0.username }}</a></th>
|
||||
<th>{{ customer.1 }}</th>
|
||||
<th><a href="{% url 'users:profile' customer.pk %}">{{ customer.username }}</a></th>
|
||||
<th>{{ customer.profile.alcohol }}</th>
|
||||
</tr>
|
||||
{%endfor%}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
@ -53,4 +53,7 @@ urlpatterns = [
|
|||
path('categoriesList', views.categoriesList, name="categoriesList"),
|
||||
path('categories-autocomplete', views.CategoriesAutocomplete.as_view(), name="categories-autocomplete"),
|
||||
path('stats', views.stats, name="stats"),
|
||||
path('divide', views.divide, name="divide"),
|
||||
path('gen_invoice', views.gen_invoice, name="gen_invoice"),
|
||||
path('compute-price', views.compute_price_view, name="compute-price"),
|
||||
]
|
181
gestion/views.py
181
gestion/views.py
|
@ -8,20 +8,24 @@ from django.contrib.auth.decorators import login_required, permission_required
|
|||
from django.utils import timezone
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.db import transaction
|
||||
from django.conf import settings
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django_tex.views import render_to_pdf
|
||||
from coopeV3.acl import active_required, acl_or, admin_required
|
||||
from coopeV3.utils import compute_price
|
||||
|
||||
import simplejson as json
|
||||
from dal import autocomplete
|
||||
from decimal import *
|
||||
import os
|
||||
from math import floor, ceil
|
||||
|
||||
from .forms import ReloadForm, RefundForm, ProductForm, KegForm, MenuForm, GestionForm, SearchMenuForm, SearchProductForm, SelectPositiveKegForm, SelectActiveKegForm, PinteForm, GenerateReleveForm, CategoryForm, SearchCategoryForm
|
||||
from .forms import ReloadForm, RefundForm, ProductForm, KegForm, MenuForm, GestionForm, SearchMenuForm, SearchProductForm, SelectPositiveKegForm, SelectActiveKegForm, PinteForm, GenerateReleveForm, CategoryForm, SearchCategoryForm, GenerateInvoiceForm, ComputePriceForm
|
||||
from .models import Product, Menu, Keg, ConsumptionHistory, KegHistory, Consumption, MenuHistory, Pinte, Reload, Refund, Category
|
||||
from users.models import School
|
||||
from preferences.models import PaymentMethod, GeneralPreferences, Cotisation
|
||||
from preferences.models import PaymentMethod, GeneralPreferences, Cotisation, DivideHistory, PriceProfile
|
||||
from users.models import CotisationHistory
|
||||
|
||||
@active_required
|
||||
|
@ -96,6 +100,8 @@ def order(request):
|
|||
else:
|
||||
error_message = "Solde insuffisant"
|
||||
raise Exception(error_message)
|
||||
else:
|
||||
user.profile.direct_debit += cotisation_history.cotisation.amount
|
||||
cotisation_history.user = user
|
||||
cotisation_history.coopeman = request.user
|
||||
cotisation_history.amount = cotisation.amount
|
||||
|
@ -163,12 +169,15 @@ def order(request):
|
|||
consumption.save()
|
||||
ch = ConsumptionHistory(customer=user, quantity=quantity, paymentMethod=paymentMethod, product=product, amount=Decimal(quantity*product.amount), coopeman=request.user)
|
||||
ch.save()
|
||||
user.profile.alcohol += Decimal(quantity * float(product.deg) * product.volume * 0.79 /10 /1000)
|
||||
if(paymentMethod.affect_balance):
|
||||
if(user.profile.balance >= Decimal(product.amount*quantity)):
|
||||
user.profile.debit += Decimal(product.amount*quantity)
|
||||
else:
|
||||
error_message = "Solde insuffisant"
|
||||
raise Exception(error_message)
|
||||
else:
|
||||
user.profile.direct_debit += Decimal(product.amount*quantity)
|
||||
for m in menus:
|
||||
menu = get_object_or_404(Menu, pk=m["pk"])
|
||||
quantity = int(m["quantity"])
|
||||
|
@ -180,6 +189,8 @@ def order(request):
|
|||
else:
|
||||
error_message = "Solde insuffisant"
|
||||
raise Exception(error_message)
|
||||
else:
|
||||
user.profile.direct_debit += Decimal(product.amount*quantity)
|
||||
for article in menu.articles.all():
|
||||
consumption, _ = Consumption.objects.get_or_create(customer=user, product=article)
|
||||
consumption.quantity += quantity
|
||||
|
@ -187,11 +198,10 @@ def order(request):
|
|||
if(article.stockHold > 0):
|
||||
article.stockHold -= 1
|
||||
article.save()
|
||||
user.profile.alcohol += Decimal(quantity * float(product.deg) * product.volume * 0.79 /10 /1000)
|
||||
user.save()
|
||||
return HttpResponse("La commande a bien été effectuée")
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("test")
|
||||
return HttpResponse(error_message)
|
||||
|
||||
@active_required
|
||||
|
@ -274,7 +284,10 @@ def cancel_consumption(request, pk):
|
|||
user = consumption.customer
|
||||
if consumption.paymentMethod.affect_balance:
|
||||
user.profile.debit -= consumption.amount
|
||||
user.save()
|
||||
else:
|
||||
user.profile.direct_debit -= consumption.amount
|
||||
user.profile.alcohol -= Decimal(consumption.quantity * float(consumption.product.deg) * consumption.product.volume * 0.79 /10 /1000)
|
||||
user.save()
|
||||
consumptionT = Consumption.objects.get(customer=user, product=consumption.product)
|
||||
consumptionT.quantity -= consumption.quantity
|
||||
consumptionT.save()
|
||||
|
@ -296,11 +309,14 @@ def cancel_menu(request, pk):
|
|||
user = menu_history.customer
|
||||
if menu_history.paymentMethod.affect_balance:
|
||||
user.profile.debit -= menu_history.amount
|
||||
user.save()
|
||||
else:
|
||||
user.profile.direct_debit -= menu_history.amount
|
||||
for product in manu_history.menu.articles:
|
||||
consumptionT = Consumption.objects.get(customer=user, product=product)
|
||||
consumptionT -= menu_history.quantity
|
||||
consumptionT.save()
|
||||
user.profile.alcohol -= Decimal(menu_history.quantity * float(menu_history.product.deg) * menu_history.product.volume * 0.79 /10 /1000)
|
||||
user.save()
|
||||
menu_history.delete()
|
||||
messages.success(request, "La consommation du menu a bien été annulée")
|
||||
return redirect(reverse('users:profile', kwargs={'pk': user.pk}))
|
||||
|
@ -386,7 +402,7 @@ def productProfile(request, pk):
|
|||
@login_required
|
||||
def getProduct(request, pk):
|
||||
"""
|
||||
Get a :class:`gestion.models.Product` by barcode and return it in JSON format.
|
||||
Get a :class:`gestion.models.Product` by pk and return it in JSON format.
|
||||
|
||||
pk
|
||||
The primary key of the :class:`gestion.models.Product` to get infos.
|
||||
|
@ -396,7 +412,7 @@ def getProduct(request, pk):
|
|||
nb_pintes = 1
|
||||
else:
|
||||
nb_pintes = 0
|
||||
data = json.dumps({"pk": product.pk, "barcode" : product.barcode, "name": product.name, "amount": product.amount, "needQuantityButton": product.needQuantityButton, "nb_pintes": nb_pintes})
|
||||
data = json.dumps({"pk": product.pk, "name": product.name, "amount": product.amount, "needQuantityButton": product.needQuantityButton, "nb_pintes": nb_pintes})
|
||||
return HttpResponse(data, content_type='application/json')
|
||||
|
||||
@active_required
|
||||
|
@ -445,7 +461,62 @@ def addKeg(request):
|
|||
Displays a :class:`gestion.forms.KegForm` to add a :class:`gestion.models.Keg`.
|
||||
"""
|
||||
form = KegForm(request.POST or None)
|
||||
if(form.is_valid()):
|
||||
if form.is_valid():
|
||||
keg = form.save(commit=False)
|
||||
price_profile = get_object_or_404(PriceProfile, use_for_draft=True)
|
||||
pinte_price = compute_price(form.cleaned_data["amount"]/(2*form.cleaned_data["capacity"]), price_profile.a, price_profile.b, price_profile.c, price_profile.alpha)
|
||||
pinte_price = ceil(10*pinte_price)/10
|
||||
name = form.cleaned_data["name"][4:]
|
||||
create_galopin = form.cleaned_data["create_galopin"]
|
||||
pinte = Product(
|
||||
name = "Pinte " + name,
|
||||
amount = pinte_price,
|
||||
stockHold = 0,
|
||||
stockBar = 0,
|
||||
category = form.cleaned_data["category"],
|
||||
needQuantityButton = False,
|
||||
is_active = True,
|
||||
volume = 50,
|
||||
deg = form.cleaned_data["deg"],
|
||||
adherentRequired = True,
|
||||
showingMultiplier = 1,
|
||||
draft_category = Product.DRAFT_PINTE
|
||||
)
|
||||
pinte.save()
|
||||
keg.pinte = pinte
|
||||
demi = Product(
|
||||
name = "Demi " + name,
|
||||
amount = ceil(5*pinte_price)/10,
|
||||
stockHold = 0,
|
||||
stockBar = 0,
|
||||
category = form.cleaned_data["category"],
|
||||
needQuantityButton = False,
|
||||
is_active = True,
|
||||
volume = 25,
|
||||
deg = form.cleaned_data["deg"],
|
||||
adherentRequired = True,
|
||||
showingMultiplier = 1,
|
||||
draft_category = Product.DRAFT_DEMI
|
||||
)
|
||||
demi.save()
|
||||
keg.demi = demi
|
||||
if create_galopin:
|
||||
galopin = Product(
|
||||
name = "Galopin " + name,
|
||||
amount = ceil(2.5 * pinte_price)/10,
|
||||
stockHold = 0,
|
||||
stockBar = 0,
|
||||
category = form.cleaned_data["category"],
|
||||
needQuantityButton = False,
|
||||
is_active = True,
|
||||
volume = 13,
|
||||
deg = form.cleaned_data["deg"],
|
||||
adherentRequired = True,
|
||||
showingMultiplier = 1,
|
||||
draft_category = Product.DRAFT_DEMI
|
||||
)
|
||||
galopin.save()
|
||||
keg.galopin = galopin
|
||||
keg = form.save()
|
||||
messages.success(request, "Le fût " + keg.name + " a bien été ajouté")
|
||||
return redirect(reverse('gestion:kegsList'))
|
||||
|
@ -693,7 +764,7 @@ def get_menu(request, pk):
|
|||
for article in menu.articles:
|
||||
if article.category == Product.DRAFT_PINTE:
|
||||
nb_pintes +=1
|
||||
data = json.dumps({"pk": menu.pk, "barcode" : menu.barcode, "name": menu.name, "amount" : menu.amount, "needQuantityButton": False, "nb_pintes": nb_pintes})
|
||||
data = json.dumps({"pk": menu.pk, "name": menu.name, "amount" : menu.amount, "needQuantityButton": False, "nb_pintes": nb_pintes})
|
||||
return HttpResponse(data, content_type='application/json')
|
||||
|
||||
class MenusAutocomplete(autocomplete.Select2QuerySetView):
|
||||
|
@ -715,12 +786,7 @@ def ranking(request):
|
|||
Displays the ranking page.
|
||||
"""
|
||||
bestBuyers = User.objects.order_by('-profile__debit')[:25]
|
||||
customers = User.objects.all()
|
||||
list = []
|
||||
for customer in customers:
|
||||
alcohol = customer.profile.alcohol
|
||||
list.append([customer, alcohol])
|
||||
bestDrinkers = sorted(list, key=lambda x: x[1], reverse=True)[:25]
|
||||
bestDrinkers = User.objects.order_by('-profile__alcohol')[:25]
|
||||
form = SearchProductForm(request.POST or None)
|
||||
if(form.is_valid()):
|
||||
product_ranking = form.cleaned_data['product'].ranking
|
||||
|
@ -828,6 +894,41 @@ def pintes_user_list(request):
|
|||
users = User.objects.filter(pk__in=pks)
|
||||
return render(request, "gestion/pintes_user_list.html", {"users": users})
|
||||
|
||||
@active_required
|
||||
@login_required
|
||||
@permission_required('users.can_generate_invoices')
|
||||
def gen_invoice(request):
|
||||
"""
|
||||
Displays a form to generate an invoice.
|
||||
"""
|
||||
form = GenerateInvoiceForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
products = [x.split(";") for x in form.cleaned_data["products"].split("\n")]
|
||||
total = 0
|
||||
for product in products:
|
||||
sub_total = Decimal(product[1]) * Decimal(product[2])
|
||||
product.append(sub_total)
|
||||
total += sub_total
|
||||
return render_to_pdf(
|
||||
request,
|
||||
'gestion/invoice.tex',
|
||||
{
|
||||
"invoice_date": form.cleaned_data["invoice_date"],
|
||||
"invoice_number": form.cleaned_data["invoice_number"],
|
||||
"invoice_place": form.cleaned_data["invoice_place"],
|
||||
"invoice_object": form.cleaned_data["invoice_object"],
|
||||
"invoice_description": form.cleaned_data["invoice_description"],
|
||||
"client_name": form.cleaned_data["client_name"],
|
||||
"client_address_first_line": form.cleaned_data["client_address_fisrt_line"],
|
||||
"client_address_second_line": form.cleaned_data["client_address_second_line"],
|
||||
"products" : products,
|
||||
"total": total,
|
||||
"path" : os.path.join(settings.BASE_DIR, "templates/coope.png"),
|
||||
},
|
||||
filename="FE" + form.cleaned_data["invoice_number"] + ".pdf")
|
||||
else:
|
||||
return render(request, "form.html", {"form": form, "form_title": "Génération d'une facture", "form_button": "Générer", "form_button_icon": "file-pdf"})
|
||||
|
||||
@active_required
|
||||
@login_required
|
||||
@admin_required
|
||||
|
@ -879,7 +980,39 @@ def gen_releve(request):
|
|||
else:
|
||||
return render(request, "form.html", {"form": form, "form_title": "Génération d'un relevé", "form_button": "Générer", "form_button_icon": "file-pdf"})
|
||||
|
||||
|
||||
@active_required
|
||||
@login_required
|
||||
@permission_required('preferences.can_divide')
|
||||
def divide(request):
|
||||
"""
|
||||
Divide all non-divided cotisation
|
||||
"""
|
||||
if request.POST:
|
||||
non_divided_cotisations = CotisationHistory.objects.filter(divided=False)
|
||||
for cotisation_history in non_divided_cotisations:
|
||||
cotisation_history.divided = True
|
||||
cotisation_history.save()
|
||||
divide_history = DivideHistory(
|
||||
total_cotisations = non_divided_cotisations.count(),
|
||||
total_cotisations_amount = sum([x.amount for x in non_divided_cotisations]),
|
||||
total_ptm_amount = sum([x.amount_ptm for x in non_divided_cotisations]),
|
||||
coopeman = request.user
|
||||
)
|
||||
divide_history.save()
|
||||
non_divided_cotisations = CotisationHistory.objects.filter(divided=False)
|
||||
total_amount = sum([x.amount for x in non_divided_cotisations])
|
||||
total_amount_ptm = sum([x.amount_ptm for x in non_divided_cotisations])
|
||||
divide_histories = DivideHistory.objects.all().order_by('-date')
|
||||
return render(
|
||||
request,
|
||||
"gestion/divide.html",
|
||||
{
|
||||
"total_cotisations": non_divided_cotisations.count(),
|
||||
"total_amount": total_amount,
|
||||
"total_amount_ptm": total_amount_ptm,
|
||||
"divide_histories": divide_histories,
|
||||
}
|
||||
)
|
||||
########## categories ##########
|
||||
@active_required
|
||||
@login_required
|
||||
|
@ -997,4 +1130,16 @@ def stats(request):
|
|||
"menus": menus,
|
||||
"payment_methods": payment_methods,
|
||||
"cotisations": cotisations,
|
||||
})
|
||||
})
|
||||
|
||||
########## Compute price ##########
|
||||
|
||||
def compute_price_view(request):
|
||||
form = ComputePriceForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
price_profile = form.cleaned_data["price_profile"]
|
||||
price = compute_price(form.cleaned_data["price"], price_profile.a, price_profile.b, price_profile.c, price_profile.alpha)
|
||||
form_p = "Le prix est " + str(ceil(100*price)/100) + " € (arrondi au centième) ou " + str(ceil(10*price)/10) + " € (arrondi au dixième)."
|
||||
else:
|
||||
form_p = ""
|
||||
return render(request, "form.html", {"form": form, "form_title": "Calcul d'un prix", "form_button": "Calculer", "form_icon": "search_dollar", "form_p": form_p})
|
|
@ -1,6 +1,6 @@
|
|||
from django.contrib import admin
|
||||
from simple_history.admin import SimpleHistoryAdmin
|
||||
from .models import PaymentMethod, GeneralPreferences, Cotisation
|
||||
from .models import PaymentMethod, GeneralPreferences, Cotisation, DivideHistory, PriceProfile
|
||||
|
||||
class CotisationAdmin(SimpleHistoryAdmin):
|
||||
"""
|
||||
|
@ -24,6 +24,24 @@ class PaymentMethodAdmin(SimpleHistoryAdmin):
|
|||
search_fields = ('name',)
|
||||
list_filter = ('is_active', 'is_usable_in_cotisation', 'is_usable_in_reload', 'affect_balance')
|
||||
|
||||
class PriceProfileAdmin(SimpleHistoryAdmin):
|
||||
"""
|
||||
The admin class for :class:`Consumptions <preferences.models.PriceProfile>`.
|
||||
"""
|
||||
list_display = ('name', 'a', 'b', 'c', 'alpha', 'use_for_draft')
|
||||
ordering = ('name',)
|
||||
search_fields = ('name',)
|
||||
list_filter = ('use_for_draft',)
|
||||
|
||||
class DivideHistoryAdmin(SimpleHistoryAdmin):
|
||||
"""
|
||||
The admin class for Divide histories
|
||||
"""
|
||||
list_display = ('date', 'total_cotisations', 'total_cotisations_amount', 'total_ptm_amount', 'coopeman')
|
||||
ordering = ('-date',)
|
||||
|
||||
admin.site.register(PaymentMethod, PaymentMethodAdmin)
|
||||
admin.site.register(GeneralPreferences, GeneralPreferencesAdmin)
|
||||
admin.site.register(Cotisation, CotisationAdmin)
|
||||
admin.site.register(Cotisation, CotisationAdmin)
|
||||
admin.site.register(PriceProfile, PriceProfileAdmin)
|
||||
admin.site.register(DivideHistory, DivideHistoryAdmin)
|
|
@ -1,7 +1,7 @@
|
|||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from .models import Cotisation, PaymentMethod, GeneralPreferences
|
||||
from .models import Cotisation, PaymentMethod, GeneralPreferences, PriceProfile
|
||||
|
||||
class CotisationForm(forms.ModelForm):
|
||||
"""
|
||||
|
@ -11,6 +11,12 @@ class CotisationForm(forms.ModelForm):
|
|||
model = Cotisation
|
||||
fields = "__all__"
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
if cleaned_data.get("amount_ptm") > cleaned_data.get("amount"):
|
||||
raise ValidationError("La quantité d'argent donnée au club doit être inférieure à\
|
||||
la quantité d'argent totale")
|
||||
|
||||
class PaymentMethodForm(forms.ModelForm):
|
||||
"""
|
||||
Form to add and edit :class:`~preferences.models.PaymentMethod`.
|
||||
|
@ -19,6 +25,13 @@ class PaymentMethodForm(forms.ModelForm):
|
|||
model = PaymentMethod
|
||||
fields = "__all__"
|
||||
|
||||
class PriceProfileForm(forms.ModelForm):
|
||||
"""
|
||||
Form to add and edit :class:`~preferences.models.PriceProfile`.
|
||||
"""
|
||||
class Meta:
|
||||
model = PriceProfile
|
||||
fields = "__all__"
|
||||
|
||||
class GeneralPreferencesForm(forms.ModelForm):
|
||||
"""
|
||||
|
|
23
preferences/migrations/0013_auto_20190622_2334.py
Normal file
23
preferences/migrations/0013_auto_20190622_2334.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.1 on 2019-06-22 21:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('preferences', '0012_auto_20190428_1327'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cotisation',
|
||||
name='amount_ptm',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=5, null=True, verbose_name='Montant pour le club Phœnix Technopôle Metz'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalcotisation',
|
||||
name='amount_ptm',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=5, null=True, verbose_name='Montant pour le club Phœnix Technopôle Metz'),
|
||||
),
|
||||
]
|
17
preferences/migrations/0014_auto_20190623_0957.py
Normal file
17
preferences/migrations/0014_auto_20190623_0957.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 2.1 on 2019-06-23 07:57
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('preferences', '0013_auto_20190622_2334'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='cotisation',
|
||||
options={'permissions': (('can_divide', 'Can divide money for cotisation'),)},
|
||||
),
|
||||
]
|
30
preferences/migrations/0015_dividehistory.py
Normal file
30
preferences/migrations/0015_dividehistory.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Generated by Django 2.1 on 2019-06-23 08:49
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('preferences', '0014_auto_20190623_0957'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DivideHistory',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('date', models.DateTimeField(auto_now_add=True)),
|
||||
('total_cotisations', models.IntegerField(verbose_name='Nombre de cotisations')),
|
||||
('total_cotisations_amount', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Montant total des cotisations')),
|
||||
('total_ptm_amount', models.DecimalField(decimal_places=2, max_digits=5, verbose_name='Montant donné au Phœnix Technopôle Metz')),
|
||||
('coopeman', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='divide_realized', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Historique répartition',
|
||||
},
|
||||
),
|
||||
]
|
25
preferences/migrations/0016_priceprofile.py
Normal file
25
preferences/migrations/0016_priceprofile.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 2.1 on 2019-06-23 12:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('preferences', '0015_dividehistory'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PriceProfile',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('a', models.DecimalField(decimal_places=2, max_digits=3, verbose_name='Marge constante')),
|
||||
('b', models.DecimalField(decimal_places=2, max_digits=3, verbose_name='Marge constante')),
|
||||
('c', models.DecimalField(decimal_places=2, max_digits=4, verbose_name='Marge constante')),
|
||||
('alpha', models.DecimalField(decimal_places=2, max_digits=4, verbose_name='Marge constante')),
|
||||
('use_for_draft', models.BooleanField(default=False)),
|
||||
],
|
||||
),
|
||||
]
|
38
preferences/migrations/0017_auto_20190623_1453.py
Normal file
38
preferences/migrations/0017_auto_20190623_1453.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 2.1 on 2019-06-23 12:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('preferences', '0016_priceprofile'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='priceprofile',
|
||||
name='alpha',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=4, verbose_name='Étendue'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='priceprofile',
|
||||
name='b',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=3, verbose_name='Marge variable'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='priceprofile',
|
||||
name='c',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=4, verbose_name='Paramètre de forme'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='priceprofile',
|
||||
name='name',
|
||||
field=models.CharField(max_length=255, verbose_name='Nom'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='priceprofile',
|
||||
name='use_for_draft',
|
||||
field=models.BooleanField(default=False, verbose_name='Utiliser pour les pressions ?'),
|
||||
),
|
||||
]
|
|
@ -1,6 +1,7 @@
|
|||
from django.db import models
|
||||
from simple_history.models import HistoricalRecords
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class PaymentMethod(models.Model):
|
||||
|
@ -118,6 +119,8 @@ class Cotisation(models.Model):
|
|||
"""
|
||||
Stores cotisations.
|
||||
"""
|
||||
class Meta:
|
||||
permissions = (("can_divide", "Can divide money for cotisation"),)
|
||||
amount = models.DecimalField(max_digits=5, decimal_places=2, null=True, verbose_name="Montant", validators=[MinValueValidator(0)])
|
||||
"""
|
||||
Price of the cotisation.
|
||||
|
@ -126,7 +129,72 @@ class Cotisation(models.Model):
|
|||
"""
|
||||
Duration (in days) of the cotisation
|
||||
"""
|
||||
amount_ptm = models.DecimalField(max_digits=5, decimal_places=2, null=True, verbose_name="Montant pour le club Phœnix Technopôle Metz")
|
||||
"""
|
||||
Amount of money given to the PTM club
|
||||
"""
|
||||
history = HistoricalRecords()
|
||||
|
||||
def __str__(self):
|
||||
return "Cotisation de " + str(self.duration) + " jours pour le prix de " + str(self.amount) + "€"
|
||||
if self.duration == 1:
|
||||
jour = "jour"
|
||||
else:
|
||||
jour = "jours"
|
||||
return "Cotisation de " + str(self.duration) + " " + jour + " pour le prix de " + str(self.amount) + "€"
|
||||
|
||||
class DivideHistory(models.Model):
|
||||
"""
|
||||
Stores divide history
|
||||
"""
|
||||
class Meta:
|
||||
verbose_name = "Historique répartition"
|
||||
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
"""
|
||||
Date of the divide
|
||||
"""
|
||||
total_cotisations = models.IntegerField(verbose_name="Nombre de cotisations")
|
||||
"""
|
||||
Number of non-divided cotisations (before the divide)
|
||||
"""
|
||||
total_cotisations_amount = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Montant total des cotisations")
|
||||
"""
|
||||
Amount of non-divided cotisations (before the divide)
|
||||
"""
|
||||
total_ptm_amount = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="Montant donné au Phœnix Technopôle Metz")
|
||||
"""
|
||||
Amount given to the PTM
|
||||
"""
|
||||
coopeman = models.ForeignKey(User, on_delete=models.PROTECT, related_name="divide_realized")
|
||||
"""
|
||||
Coopeman (:class:`django.contrib.auth.models.User`) who collected the reload.
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return "Répartition du " + str(self.date)
|
||||
|
||||
|
||||
class PriceProfile(models.Model):
|
||||
"""
|
||||
Stores parameters to compute price
|
||||
"""
|
||||
name = models.CharField(max_length=255, verbose_name="Nom")
|
||||
a = models.DecimalField(verbose_name="Marge constante", max_digits=3, decimal_places=2)
|
||||
b = models.DecimalField(verbose_name="Marge variable", max_digits=3, decimal_places=2)
|
||||
c = models.DecimalField(verbose_name="Paramètre de forme", max_digits=4, decimal_places=2)
|
||||
alpha = models.DecimalField(verbose_name="Étendue", max_digits=4, decimal_places=2)
|
||||
use_for_draft = models.BooleanField(default=False, verbose_name="Utiliser pour les pressions ?")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.use_for_draft:
|
||||
try:
|
||||
temp = PriceProfile.objects.get(use_for_draft=True)
|
||||
if self != temp:
|
||||
temp.use_for_draft = False
|
||||
temp.save()
|
||||
except PriceProfile.DoesNotExist:
|
||||
pass
|
||||
super(PriceProfile, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
|
|
@ -19,14 +19,16 @@
|
|||
<tr>
|
||||
<th>Durée de cotisation</th>
|
||||
<th>Prix</th>
|
||||
<th>Pour PhœnixTM</th>
|
||||
<th>Administration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cotisation in cotisations %}
|
||||
<tr>
|
||||
<td>{{ cotisation.duration }} jours</td>
|
||||
<td>{{ cotisation.duration }} jour{{ cotisation.duration|pluralize }}</td>
|
||||
<td>{{ cotisation.amount }} €</td>
|
||||
<td>{{ cotisation.amount_ptm | default:0}} €</td>
|
||||
<td>{% if perms.preferences.change_cotisation %}<a class="button small" href="{% url 'preferences:editCotisation' cotisation.pk %}"><i class="fa fa-pencil-alt"></i> Modifier</a> {% endif %}{% if perms.preferences.delete_cotisation %}<a class="button small" href="{% url 'preferences:deleteCotisation' cotisation.pk %}"><i class="fa fa-trash"></i> Supprimer</a>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
45
preferences/templates/preferences/price_profiles_index.html
Normal file
45
preferences/templates/preferences/price_profiles_index.html
Normal file
|
@ -0,0 +1,45 @@
|
|||
{% extends "base.html" %}
|
||||
{% block entete %}Gestion des profils de prix{% endblock %}
|
||||
{% block navbar %}
|
||||
<ul>
|
||||
<li><a href="#first">Liste des profils de prix</a></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<section id="first" class="main">
|
||||
<header class="major">
|
||||
<h2>Liste des profils de prix</h2>
|
||||
</header>
|
||||
{% if perms.preferences.add_priceprofile %}
|
||||
<a class="button" href="{% url 'preferences:addPriceProfile' %}"><i class="fa fa-plus-square"></i> Créer un profil de prix</a><br><br>
|
||||
{% endif %}
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>a (marge constante)</th>
|
||||
<th>b (marge variable)</th>
|
||||
<th>c (paramètre de forme)</th>
|
||||
<th>alpha (étendue)</th>
|
||||
<th>Pression ?</th>
|
||||
<th>Administration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for pp in price_profiles %}
|
||||
<tr>
|
||||
<td>{{ pp.name }} </td>
|
||||
<td>{{ pp.a }}</td>
|
||||
<td>{{ pp.b }}</td>
|
||||
<td>{{ pp.c }}</td>
|
||||
<td>{{ pp.alpha }}</td>
|
||||
<td>{{ pp.use_for_draft | yesno:"Oui,Non"}}</td>
|
||||
<td>{% if perms.preferences.change_priceprofile %}<a class="button small" href="{% url 'preferences:editPriceProfile' pp.pk %}"><i class="fa fa-pencil-alt"></i> Modifier</a> {% endif %}{% if perms.preferences.delete_priceprofile %}<a class="button small" href="{% url 'preferences:deletePriceProfile' pp.pk %}"><i class="fa fa-trash"></i> Supprimer</a>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -13,6 +13,10 @@ urlpatterns = [
|
|||
path('addPaymentMethod', views.addPaymentMethod, name="addPaymentMethod"),
|
||||
path('editPaymentMethod/<int:pk>', views.editPaymentMethod, name="editPaymentMethod"),
|
||||
path('deletePaymentMethod/<int:pk>', views.deletePaymentMethod, name="deletePaymentMethod"),
|
||||
path('priceProfilesIndex', views.price_profiles_index, name="priceProfilesIndex"),
|
||||
path('addPriceProfile', views.add_price_profile, name="addPriceProfile"),
|
||||
path('editPriceProfile/<int:pk>', views.edit_price_profile, name="editPriceProfile"),
|
||||
path('deletePriceProfile/<int:pk>', views.delete_price_profile, name="deletePriceProfile"),
|
||||
path('inactive', views.inactive, name="inactive"),
|
||||
path('getConfig', views.get_config, name="getConfig"),
|
||||
path('getCotisation/<int:pk>', views.get_cotisation, name="getCotisation")
|
||||
|
|
|
@ -10,9 +10,9 @@ from django.http import Http404
|
|||
|
||||
from coopeV3.acl import active_required
|
||||
|
||||
from .models import GeneralPreferences, Cotisation, PaymentMethod
|
||||
from .models import GeneralPreferences, Cotisation, PaymentMethod, PriceProfile
|
||||
|
||||
from .forms import CotisationForm, PaymentMethodForm, GeneralPreferencesForm
|
||||
from .forms import CotisationForm, PaymentMethodForm, GeneralPreferencesForm, PriceProfileForm
|
||||
|
||||
@active_required
|
||||
@login_required
|
||||
|
@ -185,4 +185,63 @@ def get_config(request):
|
|||
del gp_dict["alcohol_charter"]
|
||||
data = json.dumps(gp_dict)
|
||||
return HttpResponse(data, content_type='application/json')
|
||||
|
||||
|
||||
########## Price Profiles ##########
|
||||
|
||||
@active_required
|
||||
@login_required
|
||||
@permission_required('preferences.view_priceprofile')
|
||||
def price_profiles_index(request):
|
||||
"""
|
||||
View which lists all the :class:`~preferences.models.PriceProfile`.
|
||||
"""
|
||||
price_profiles = PriceProfile.objects.all()
|
||||
return render(request, "preferences/price_profiles_index.html", {"price_profiles": price_profiles})
|
||||
|
||||
@active_required
|
||||
@login_required
|
||||
@permission_required('preferences.add_priceprofile')
|
||||
def add_price_profile(request):
|
||||
"""
|
||||
View which displays a :class:`~preferences.forms.PriceProfileForm` to create a :class:`~preferences.models.PriceProfile`.
|
||||
"""
|
||||
form = PriceProfileForm(request.POST or None)
|
||||
if form.is_valid():
|
||||
price_profile = form.save()
|
||||
messages.success(request, "Le profil de prix " + price_profile.name + " a bien été crée")
|
||||
return redirect(reverse('preferences:priceProfilesIndex'))
|
||||
return render(request, "form.html", {"form": form, "form_title": "Création d'un profil de prix", "form_button": "Créer", "form_button_icon": "plus-square"})
|
||||
|
||||
@active_required
|
||||
@login_required
|
||||
@permission_required('preferences.change_priceprofile')
|
||||
def edit_price_profile(request, pk):
|
||||
"""
|
||||
View which displays a :class:`~preferences.forms.PriceProfile` to edit a :class:`~preferences.models.PriceProfile`.
|
||||
|
||||
pk
|
||||
The primary key of the :class:`~preferences.models.PriceProfile` to edit.
|
||||
"""
|
||||
price_profile = get_object_or_404(PriceProfile, pk=pk)
|
||||
form = PriceProfileForm(request.POST or None, instance=price_profile)
|
||||
if form.is_valid():
|
||||
price_profile = form.save()
|
||||
messages.success(request, "Le profil de prix " + price_profile.name + " a bien été modifié")
|
||||
return redirect(reverse('preferences:priceProfilesIndex'))
|
||||
return render(request, "form.html", {"form": form, "form_title": "Modification d'un profil de prix", "form_button": "Modifier", "form_button_icon": "pencil-alt"})
|
||||
|
||||
@active_required
|
||||
@login_required
|
||||
@permission_required('preferences.delete_priceprofile')
|
||||
def delete_price_profile(request,pk):
|
||||
"""
|
||||
Delete a :class:`~preferences.models.PriceProfile`.
|
||||
|
||||
pk
|
||||
The primary key of the :class:`~preferences.models.PriceProfile` to delete.
|
||||
"""
|
||||
price_profile = get_object_or_404(PriceProfile, pk=pk)
|
||||
message = "Le profil de prix " + price_profile.name + " a bien été supprimé"
|
||||
price_pofile.delete()
|
||||
messages.success(request, message)
|
||||
return redirect(reverse('preferences:priceProfilesIndex'))
|
||||
|
|
|
@ -19,14 +19,14 @@ function get_config(){
|
|||
function get_product(id){
|
||||
res = $.get("getProduct/" + id, function(data){
|
||||
nbPintes += data.nb_pintes;
|
||||
add_product(data.pk, data.barcode, data.name, data.amount, data.needQuantityButton);
|
||||
add_product(data.pk, data.name, data.amount, data.needQuantityButton);
|
||||
});
|
||||
}
|
||||
|
||||
function get_menu(id){
|
||||
res = $.get("getMenu/" + id, function(data){
|
||||
nbPintes += data.nb_pintes;
|
||||
add_menu(data.pk, data.barcode, data.name, data.amount, data.needQuantityButton);
|
||||
add_menu(data.pk, data.name, data.amount, data.needQuantityButton);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ function get_cotisation(id){
|
|||
});
|
||||
}
|
||||
|
||||
function add_product(pk, barcode, name, amount, needQuantityButton){
|
||||
function add_product(pk, name, amount, needQuantityButton){
|
||||
exist = false
|
||||
index = -1;
|
||||
for(k=0;k < products.length; k++){
|
||||
|
@ -56,12 +56,12 @@ function add_product(pk, barcode, name, amount, needQuantityButton){
|
|||
if(exist){
|
||||
products[index].quantity += quantity;
|
||||
}else{
|
||||
products.push({"pk": pk, "barcode": barcode, "name": name, "amount": amount, "quantity": quantity});
|
||||
products.push({"pk": pk, "name": name, "amount": amount, "quantity": quantity});
|
||||
}
|
||||
generate_html()
|
||||
}
|
||||
|
||||
function add_menu(pk, barcode, name, amount){
|
||||
function add_menu(pk, name, amount){
|
||||
exist = false;
|
||||
index = -1;
|
||||
for(k=0; k < menus.length; k++){
|
||||
|
@ -73,12 +73,12 @@ function add_menu(pk, barcode, name, amount){
|
|||
if(exist){
|
||||
menus[index].quantity += 1;
|
||||
}else{
|
||||
menus.push({"pk": pk, "barcode": barcode, "name": name, "amount": amount, "quantity":1});
|
||||
menus.push({"pk": pk, "name": name, "amount": amount, "quantity":1});
|
||||
}
|
||||
generate_html();
|
||||
}
|
||||
|
||||
function add_cotisation(pk, barcode, duration, amount){
|
||||
function add_cotisation(pk, duration, amount){
|
||||
exist = false;
|
||||
index = -1;
|
||||
for(k=0; k < cotisations.length; k++){
|
||||
|
@ -90,7 +90,7 @@ function add_cotisation(pk, barcode, duration, amount){
|
|||
if(exist){
|
||||
cotisations[index].quantity += 1;
|
||||
}else{
|
||||
cotisations.push({"pk": pk, "barcode": barcode, "duration": duration, "amount": amount, "quantity":1});
|
||||
cotisations.push({"pk": pk, "duration": duration, "amount": amount, "quantity":1});
|
||||
}
|
||||
generate_html();
|
||||
}
|
||||
|
@ -103,11 +103,11 @@ function generate_html(){
|
|||
}
|
||||
for(k=0;k<products.length;k++){
|
||||
product = products[k]
|
||||
html += '<tr><td>' + product.barcode + '</td><td>' + product.name + '</td><td>' + String(product.amount) + ' €</td><td><input type="number" data-target="' + String(k) + '" onChange="updateInput(this)" value="' + String(product.quantity) + '"/></td><td>' + String(Number((product.quantity * product.amount).toFixed(2))) + ' €</td></tr>';
|
||||
html += '<tr><td>' + product.name + '</td><td>' + String(product.amount) + ' €</td><td><input type="number" data-target="' + String(k) + '" onChange="updateInput(this)" value="' + String(product.quantity) + '"/></td><td>' + String(Number((product.quantity * product.amount).toFixed(2))) + ' €</td></tr>';
|
||||
}
|
||||
for(k=0; k<menus.length;k++){
|
||||
menu = menus[k]
|
||||
html += '<tr><td>' + menu.barcode + '</td><td>' + menu.name + '</td><td>' + String(menu.amount) + ' €</td><td><input type="number" data-target="' + String(k) + '" onChange="updateMenuInput(this)" value="' + String(menu.quantity) + '"/></td><td>' + String(Number((menu.quantity * menu.amount).toFixed(2))) + ' €</td></tr>';
|
||||
html += '<tr><td>' + menu.name + '</td><td>' + String(menu.amount) + ' €</td><td><input type="number" data-target="' + String(k) + '" onChange="updateMenuInput(this)" value="' + String(menu.quantity) + '"/></td><td>' + String(Number((menu.quantity * menu.amount).toFixed(2))) + ' €</td></tr>';
|
||||
}
|
||||
$("#items").html(html)
|
||||
updateTotal();
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %}Coopé™ Metz{% endblock %}</title>
|
||||
<title>{% block title %}Coopé Technopôle Metz{% endblock %}</title>
|
||||
<link rel="shortcut icon" href="{% static 'favicon16.ico' %}" type="image/x-icon">
|
||||
<link rel="icon" sizes="16x16" href="{% static 'favicon16.ico' %}" type="image/x-icon">
|
||||
<link rel="icon" sizes="32x32" href="{% static 'favicon32.ico' %}" type="image/x-icon">
|
||||
|
|
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 5 KiB |
|
@ -42,6 +42,6 @@
|
|||
<li><a href="https://www.facebook.com/coopesmetz/" class="icon fa-facebook alt"><span class="label">Facebook</span></a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<p class="copyright">coope.rez v3.5.3 (release stable) © 2018-2019 Yoann Pietri.</p>
|
||||
<p class="copyright">coope.rez v3.6.0 (release stable) © 2018-2019 Yoann Pietri.</p>
|
||||
|
||||
|
||||
|
|
|
@ -32,9 +32,19 @@
|
|||
{% if request.user.is_staff %}
|
||||
<span class="tabulation2">
|
||||
<i class="fa fa-chart-bar"></i> <a href="{% url 'gestion:stats' %}">Stats</a>
|
||||
</span>
|
||||
</span>
|
||||
<span class="tabulation2">
|
||||
<i class="fa fa-business-time"></i> <a href="{% url 'gestion:gen_releve' %}">Comptabilité</a>
|
||||
<i class="fa fa-business-time"></i> <a href="{% url 'gestion:gen_releve' %}">Relevé</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if perms.preferences.can_divide %}
|
||||
<span class="tabulation2">
|
||||
<i class="fa fa-hand-holding-usd"></i> <a href="{% url 'gestion:divide' %}">Répartition</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if perms.users.can_generate_invoices %}
|
||||
<span class="tabulation2">
|
||||
<i class="fa fa-file-invoice-dollar"></i> <a href="{% url 'gestion:gen_invoice' %}">Facture</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if perms.preferences.view_cotisation %}
|
||||
|
@ -42,11 +52,19 @@
|
|||
<i class="fa fa-calendar-check"></i> <a href="{% url 'preferences:cotisationsIndex' %}">Cotisations</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if perms.preferences.view_cotisation %}
|
||||
{% if perms.preferences.view_paymentmethod %}
|
||||
<span class="tabulation2">
|
||||
<i class="fa fa-comments-dollar"></i> <a href="{% url 'preferences:paymentMethodsIndex' %}">Moyens de paiement</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if perms.preferences.view_priceprofile %}
|
||||
<span class="tabulation2">
|
||||
<i class="fa fa-search-dollar"></i> <a href="{% url 'preferences:priceProfilesIndex' %}">Profils de prix</a>
|
||||
</span>
|
||||
<span class="tabulation2">
|
||||
<i class="fa fa-search-dollar"></i> <a href="{% url 'gestion:compute-price' %}">Calcul de prix</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="tabulation2">
|
||||
<i class="fa fa-bed"></i> <a href="{% url 'users:logout' %}">Deconnexion</a>
|
||||
</span>
|
||||
|
|
|
@ -11,7 +11,7 @@ class CotisationHistoryAdmin(SimpleHistoryAdmin):
|
|||
"""
|
||||
list_display = ('user', 'amount', 'duration', 'paymentDate', 'endDate', 'paymentMethod')
|
||||
ordering = ('user', 'amount', 'duration', 'paymentDate', 'endDate')
|
||||
search_fields = ('user',)
|
||||
search_fields = ('user__username', 'user__first_name', 'user__last_name')
|
||||
list_filter = ('paymentMethod', )
|
||||
|
||||
class BalanceFilter(admin.SimpleListFilter):
|
||||
|
@ -43,16 +43,16 @@ class ProfileAdmin(SimpleHistoryAdmin):
|
|||
"""
|
||||
list_display = ('user', 'credit', 'debit', 'balance', 'school', 'cotisationEnd', 'is_adherent')
|
||||
ordering = ('user', '-credit', '-debit')
|
||||
search_fields = ('user',)
|
||||
search_fields = ('user__username', 'user__first_name', 'user__last_name')
|
||||
list_filter = ('school', BalanceFilter)
|
||||
|
||||
class WhiteListHistoryAdmin(SimpleHistoryAdmin):
|
||||
"""
|
||||
The admin class for :class:`Consumptions <users.models.WhiteListHistory>`.
|
||||
"""
|
||||
list_display = ('user', 'paymentDate', 'endDate', 'duration')
|
||||
list_display = ('user', 'paymentDate', 'endDate', 'duration', 'reason')
|
||||
ordering = ('user', 'duration', 'paymentDate', 'endDate')
|
||||
search_fields = ('user',)
|
||||
search_fields = ('user__username', 'user__first_name', 'user__last_name', 'reason')
|
||||
|
||||
admin.site.register(Permission, SimpleHistoryAdmin)
|
||||
admin.site.register(School, SimpleHistoryAdmin)
|
||||
|
|
|
@ -99,7 +99,7 @@ class addWhiteListHistoryForm(forms.ModelForm):
|
|||
"""
|
||||
class Meta:
|
||||
model = WhiteListHistory
|
||||
fields = ("duration", )
|
||||
fields = ("duration", "reason")
|
||||
|
||||
class SchoolForm(forms.ModelForm):
|
||||
"""
|
||||
|
|
38
users/migrations/0006_auto_20190611_0105.py
Normal file
38
users/migrations/0006_auto_20190611_0105.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 2.1 on 2019-06-10 23:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
def update(apps, schema_editor):
|
||||
db_alias = schema_editor.connection.alias
|
||||
users = apps.get_model('auth', 'User').objects.using(db_alias).all()
|
||||
for user in users:
|
||||
consumptions = apps.get_model('gestion', 'ConsumptionHistory').objects.using(db_alias).filter(customer=user).select_related('product')
|
||||
alcohol = 0
|
||||
for consumption in consumptions:
|
||||
product = consumption.product
|
||||
alcohol += consumption.quantity * float(product.deg) * product.volume * 0.79 /10 /1000
|
||||
user.profile.alcohol = alcohol
|
||||
user.profile.save()
|
||||
|
||||
def reverse_update(apps, schema_editor):
|
||||
pass
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0005_auto_20190227_0859'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='historicalprofile',
|
||||
name='alcohol',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='alcohol',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=5, null=True),
|
||||
),
|
||||
migrations.RunPython(update, reverse_update)
|
||||
]
|
42
users/migrations/0007_auto_20190623_0957.py
Normal file
42
users/migrations/0007_auto_20190623_0957.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Generated by Django 2.1 on 2019-06-23 07:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
def update(apps, schema_editor):
|
||||
CotisationHistory = apps.get_model('users', 'CotisationHistory')
|
||||
for cotisation_history in CotisationHistory.objects.all():
|
||||
cotisation_history.amount_ptm = cotisation_history.cotisation.amount_ptm
|
||||
cotisation_history.save()
|
||||
|
||||
def reverse_update(apps, schema_editor):
|
||||
pass
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0006_auto_20190611_0105'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cotisationhistory',
|
||||
name='amount_ptm',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=5, null=True, verbose_name='Montant pour le club Phœnix Technopôle Metz'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cotisationhistory',
|
||||
name='divided',
|
||||
field=models.BooleanField(default=False, verbose_name='Répartition'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalcotisationhistory',
|
||||
name='amount_ptm',
|
||||
field=models.DecimalField(decimal_places=2, max_digits=5, null=True, verbose_name='Montant pour le club Phœnix Technopôle Metz'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalcotisationhistory',
|
||||
name='divided',
|
||||
field=models.BooleanField(default=False, verbose_name='Répartition'),
|
||||
),
|
||||
migrations.RunPython(update, reverse_update)
|
||||
]
|
23
users/migrations/0008_auto_20190623_1105.py
Normal file
23
users/migrations/0008_auto_20190623_1105.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.1 on 2019-06-23 09:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0007_auto_20190623_0957'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='historicalwhitelisthistory',
|
||||
name='reason',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='Raison'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='whitelisthistory',
|
||||
name='reason',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='Raison'),
|
||||
),
|
||||
]
|
17
users/migrations/0009_auto_20190623_1437.py
Normal file
17
users/migrations/0009_auto_20190623_1437.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Generated by Django 2.1 on 2019-06-23 12:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0008_auto_20190623_1105'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='profile',
|
||||
options={'permissions': (('can_generate_invoices', 'Can generate invocies'),), 'verbose_name': 'Profil'},
|
||||
),
|
||||
]
|
34
users/migrations/0010_auto_20190623_1656.py
Normal file
34
users/migrations/0010_auto_20190623_1656.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
# Generated by Django 2.1 on 2019-06-23 14:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
def update(apps, schema_editor):
|
||||
User = apps.get_model('auth', 'User')
|
||||
ConsumptionHistory = apps.get_model('gestion', 'ConsumptionHistory')
|
||||
for u in User.objects.all():
|
||||
chs = ConsumptionHistory.objects.filter(customer=u).filter(paymentMethod__affect_balance=False)
|
||||
u.profile.direct_debit = sum([x.amount for x in chs])
|
||||
u.profile.save()
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
pass
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0009_auto_20190623_1437'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='historicalprofile',
|
||||
name='direct_debit',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=7, verbose_name='Débit (non compte)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='direct_debit',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=7, verbose_name='Débit (non compte)'),
|
||||
),
|
||||
migrations.RunPython(update, reverse)
|
||||
]
|
|
@ -61,6 +61,14 @@ class CotisationHistory(models.Model):
|
|||
"""
|
||||
User (:class:`django.contrib.auth.models.User`) who registered the cotisation.
|
||||
"""
|
||||
divided = models.BooleanField(default=False, verbose_name="Répartition")
|
||||
"""
|
||||
True if money of cotisation have been divided between CTM and PTM
|
||||
"""
|
||||
amount_ptm = models.DecimalField(max_digits=5, decimal_places=2, null=True, verbose_name="Montant pour le club Phœnix Technopôle Metz")
|
||||
"""
|
||||
Amount of money given to the PTM club
|
||||
"""
|
||||
history = HistoricalRecords()
|
||||
|
||||
class WhiteListHistory(models.Model):
|
||||
|
@ -91,6 +99,10 @@ class WhiteListHistory(models.Model):
|
|||
"""
|
||||
User (:class:`django.contrib.auth.models.User`) who registered the cotisation.
|
||||
"""
|
||||
reason = models.CharField(max_length=255, verbose_name="Raison", blank=True)
|
||||
"""
|
||||
Reason of the whitelist
|
||||
"""
|
||||
history = HistoricalRecords()
|
||||
|
||||
class Profile(models.Model):
|
||||
|
@ -99,6 +111,7 @@ class Profile(models.Model):
|
|||
"""
|
||||
class Meta:
|
||||
verbose_name = "Profil"
|
||||
permissions = (('can_generate_invoices', 'Can generate invocies'),)
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name="Utilisateur")
|
||||
"""
|
||||
|
@ -110,7 +123,11 @@ class Profile(models.Model):
|
|||
"""
|
||||
debit = models.DecimalField(max_digits=7, decimal_places=2, default=0, verbose_name="Débit")
|
||||
"""
|
||||
Amount of money, in euros, spent form the account
|
||||
Amount of money, in euros, spent from the account
|
||||
"""
|
||||
direct_debit = models.DecimalField(max_digits=7, decimal_places=2, default=0, verbose_name="Débit (non compte)")
|
||||
"""
|
||||
Amount of money, in euro, spent with other mean than the account
|
||||
"""
|
||||
school = models.ForeignKey(School, on_delete=models.PROTECT, blank=True, null=True, verbose_name="École")
|
||||
"""
|
||||
|
@ -120,6 +137,10 @@ class Profile(models.Model):
|
|||
"""
|
||||
Date of end of cotisation for the client
|
||||
"""
|
||||
alcohol = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True)
|
||||
"""
|
||||
Ingerated alcohol
|
||||
"""
|
||||
history = HistoricalRecords()
|
||||
|
||||
@property
|
||||
|
@ -152,18 +173,6 @@ class Profile(models.Model):
|
|||
"""
|
||||
return Profile.objects.filter(debit__gte=self.debit).count()
|
||||
|
||||
@property
|
||||
def alcohol(self):
|
||||
"""
|
||||
Computes ingerated alcohol.
|
||||
"""
|
||||
consumptions = ConsumptionHistory.objects.filter(customer=self.user).select_related('product')
|
||||
alcohol = 0
|
||||
for consumption in consumptions:
|
||||
product = consumption.product
|
||||
alcohol += consumption.quantity * float(product.deg) * product.volume * 0.79 /10 /1000
|
||||
return alcohol
|
||||
|
||||
@property
|
||||
def nb_pintes(self):
|
||||
"""
|
||||
|
|
|
@ -38,7 +38,8 @@
|
|||
</li>
|
||||
<li><b>Solde : </b>{{user.profile.balance}} €<span class="tabulation">
|
||||
<b>Crédit : </b>{{user.profile.credit}} €</span><span class="tabulation">
|
||||
<b>Débit : </b>{{user.profile.debit}} €</span>
|
||||
<b>Débit : </b>{{user.profile.debit}} €</span><span class="tabulation">
|
||||
<b>Débit direct : </b>{{user.profile.direct_debit}}</b></span>
|
||||
</li>
|
||||
<li><b>Groupe(s) : </b>{{user.groups.all|join:", "}}</li>
|
||||
<li>
|
||||
|
@ -263,6 +264,7 @@
|
|||
<th>Date de l'ajout</th>
|
||||
<th>Date de fin</th>
|
||||
<th>Durée</th>
|
||||
<th>Raison</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -271,6 +273,7 @@
|
|||
<td>{{whitelist.paymentDate}}</td>
|
||||
<td>{{whitelist.endDate}}</td>
|
||||
<td>{{whitelist.duration}} jours</td>
|
||||
<td>{{ whitelist.reason }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -35,10 +35,7 @@ def loginView(request):
|
|||
if user is not None:
|
||||
login(request, user)
|
||||
messages.success(request, "Vous êtes à présent connecté sous le compte " + str(user))
|
||||
if(request.user.has_perm('gestion.can_manage')):
|
||||
return redirect(reverse('gestion:manage'))
|
||||
else:
|
||||
return redirect(reverse('users:profile', kwargs={'pk':request.user.pk}))
|
||||
return redirect(reverse('home'))
|
||||
else:
|
||||
messages.error(request, "Nom d'utilisateur et/ou mot de passe invalide")
|
||||
return render(request, "form.html", {"form_entete": "Connexion", "form": form, "form_title": "Connexion", "form_button": "Se connecter", "form_button_icon": "sign-in-alt"})
|
||||
|
@ -349,7 +346,7 @@ def gen_user_infos(request, pk):
|
|||
user= get_object_or_404(User, pk=pk)
|
||||
cotisations = CotisationHistory.objects.filter(user=user).order_by('-paymentDate')
|
||||
now = datetime.now()
|
||||
path = os.path.join(settings.BASE_DIR, "users/templates/users/coope.png")
|
||||
path = os.path.join(settings.BASE_DIR, "templates/coope.png")
|
||||
return render_to_pdf(request, 'users/bulletin.tex', {"user": user, "now": now, "cotisations": cotisations, "path":path}, filename="bulletin_" + user.first_name + "_" + user.last_name + ".pdf")
|
||||
|
||||
########## Groups ##########
|
||||
|
@ -586,6 +583,7 @@ def addCotisationHistory(request, pk):
|
|||
cotisation.coopeman = request.user
|
||||
cotisation.amount = cotisation.cotisation.amount
|
||||
cotisation.duration = cotisation.cotisation.duration
|
||||
cotisation.amount_ptm = cotisation.cotisation.amount_ptm
|
||||
if(user.profile.cotisationEnd and user.profile.cotisationEnd > timezone.now()):
|
||||
cotisation.endDate = user.profile.cotisationEnd + timedelta(days=cotisation.cotisation.duration)
|
||||
else:
|
||||
|
|
Loading…
Reference in a new issue