3
0
Fork 0
mirror of https://github.com/nanoy42/coope synced 2024-05-02 23:52:24 +00:00
This commit is contained in:
Yoann Pétri 2019-06-23 20:01:54 +02:00
commit 5838521c67
48 changed files with 1115 additions and 122 deletions

View file

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

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Yoann `Nanoy` Pietri
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

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

View file

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

View file

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

View file

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

View file

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

View 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'),
),
]

View 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',
),
]

View file

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

View 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 %}

View 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 dinstance 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}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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'),
),
]

View 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'),)},
),
]

View 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',
},
),
]

View 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)),
],
),
]

View 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 ?'),
),
]

View file

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

View file

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

View 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 %}

View file

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

View file

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

View file

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

View file

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

View file

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5 KiB

View file

@ -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) &copy; 2018-2019 Yoann Pietri.</p>
<p class="copyright">coope.rez v3.6.0 (release stable) &copy; 2018-2019 Yoann Pietri.</p>

View file

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

View file

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

View file

@ -99,7 +99,7 @@ class addWhiteListHistoryForm(forms.ModelForm):
"""
class Meta:
model = WhiteListHistory
fields = ("duration", )
fields = ("duration", "reason")
class SchoolForm(forms.ModelForm):
"""

View 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)
]

View 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)
]

View 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'),
),
]

View 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'},
),
]

View 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)
]

View file

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

View file

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

View file

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