mirror of https://github.com/nanoy42/coope
54 changed files with 2199 additions and 188 deletions
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"python.pythonPath": "/home/nanoy/.virtualenvs/coopeV3/bin/python" |
||||
|
} |
||||
@ -0,0 +1,40 @@ |
|||||
|
from django import template |
||||
|
|
||||
|
from preferences.models import GeneralPreferences |
||||
|
|
||||
|
register = template.Library() |
||||
|
|
||||
|
@register.simple_tag |
||||
|
def president(): |
||||
|
gp,_ = GeneralPreferences.objects.get_or_create(pk=1) |
||||
|
return gp.president |
||||
|
|
||||
|
@register.simple_tag |
||||
|
def vice_president(): |
||||
|
gp,_ = GeneralPreferences.objects.get_or_create(pk=1) |
||||
|
return gp.vice_president |
||||
|
|
||||
|
@register.simple_tag |
||||
|
def treasurer(): |
||||
|
gp,_ = GeneralPreferences.objects.get_or_create(pk=1) |
||||
|
return gp.treasurer |
||||
|
|
||||
|
@register.simple_tag |
||||
|
def secretary(): |
||||
|
gp,_ = GeneralPreferences.objects.get_or_create(pk=1) |
||||
|
return gp.secretary |
||||
|
|
||||
|
@register.simple_tag |
||||
|
def brewer(): |
||||
|
gp,_ = GeneralPreferences.objects.get_or_create(pk=1) |
||||
|
return gp.brewer |
||||
|
|
||||
|
@register.simple_tag |
||||
|
def grocer(): |
||||
|
gp,_ = GeneralPreferences.objects.get_or_create(pk=1) |
||||
|
return gp.grocer |
||||
|
|
||||
|
@register.simple_tag |
||||
|
def global_message(): |
||||
|
gp,_ = GeneralPreferences.objects.get_or_create(pk=1) |
||||
|
return gp.global_message |
||||
@ -1,13 +1,11 @@ |
|||||
from django.forms.widgets import Input |
from django.forms.widgets import Select, Input |
||||
from django.template import Context, Template |
from django.template import Context, Template |
||||
from django.template.loader import get_template |
from django.template.loader import get_template |
||||
|
|
||||
class SearchField: |
class SearchField(Input): |
||||
def __init__(self, url): |
|
||||
self.url = url |
|
||||
|
|
||||
def render(self, name, value, attrs=None): |
def render(self, name, value, attrs=None): |
||||
super().render(name, value, attrs) |
#super().render(name, value, attrs) |
||||
template = get_template('search_field.html') |
template = get_template('search_field.html') |
||||
context = Context({}) |
context = Context({}) |
||||
return template.render(context) |
return template.render(context) |
||||
|
|||||
@ -1,3 +1,8 @@ |
|||||
from django.contrib import admin |
from django.contrib import admin |
||||
|
|
||||
# Register your models here. |
from .models import Reload, Refund, Product, Keg |
||||
|
|
||||
|
admin.site.register(Reload) |
||||
|
admin.site.register(Refund) |
||||
|
admin.site.register(Product) |
||||
|
admin.site.register(Keg) |
||||
|
|||||
@ -0,0 +1,37 @@ |
|||||
|
from django import forms |
||||
|
from django.contrib.auth.models import User |
||||
|
|
||||
|
from dal import autocomplete |
||||
|
|
||||
|
from .models import Reload, Refund, Product, Keg, Menu |
||||
|
from preferences.models import PaymentMethod |
||||
|
from coopeV3.widgets import SearchField |
||||
|
|
||||
|
class ReloadForm(forms.ModelForm): |
||||
|
class Meta: |
||||
|
model = Reload |
||||
|
fields = ("customer", "amount", "PaymentMethod") |
||||
|
|
||||
|
class RefundForm(forms.ModelForm): |
||||
|
class Meta: |
||||
|
model = Refund |
||||
|
fields = ("customer", "amount") |
||||
|
|
||||
|
class ProductForm(forms.ModelForm): |
||||
|
class Meta: |
||||
|
model = Product |
||||
|
fields = "__all__" |
||||
|
|
||||
|
class KegForm(forms.ModelForm): |
||||
|
class Meta: |
||||
|
model = Keg |
||||
|
fields = "__all__" |
||||
|
|
||||
|
class MenuForm(forms.ModelForm): |
||||
|
class Meta: |
||||
|
model = Menu |
||||
|
fields = "__all__" |
||||
|
|
||||
|
class GestionForm(forms.Form): |
||||
|
client = forms.ModelChoiceField(queryset=User.objects.filter(is_active=True), required=True, label="Client", widget=autocomplete.ModelSelect2(url='users:active-users-autocomplete', attrs={'data-minimum-input-length':2})) |
||||
|
paymentMethod = forms.ModelChoiceField(queryset=PaymentMethod.objects.all(), required=True, label="Moyen de paiement") |
||||
@ -0,0 +1,212 @@ |
|||||
|
{% extends "base.html" %} |
||||
|
{% load static %} |
||||
|
|
||||
|
{%block entete%}<h1>Gestion de la Coopé™</h1>{%endblock%} |
||||
|
|
||||
|
{% block navbar %} |
||||
|
<ul> |
||||
|
{% if perms.gestion.can_add_consumption_history %}<li><a href="#first">Commande</a></li>{% endif %} |
||||
|
{% if perms.gestion.can_add_reload %}<li><a href="#second">Rechargement Client</a></li>{% endif %} |
||||
|
{% if perms.gestion.can_add_refund %}<li><a href="#third">Remboursement client</a><li>{% endif %} |
||||
|
</ul> |
||||
|
{% endblock %} |
||||
|
|
||||
|
|
||||
|
{% block content %} |
||||
|
<a class="up_button" href="#intro"> |
||||
|
UP |
||||
|
</a> |
||||
|
<style> |
||||
|
.up_button{ |
||||
|
display:block; |
||||
|
background-color:white; |
||||
|
position:fixed; |
||||
|
border-radius:100%; |
||||
|
width:50px; |
||||
|
height:50px; |
||||
|
color:black; |
||||
|
text-align:center; |
||||
|
line-height:50px; |
||||
|
right:1em; |
||||
|
bottom : 1em; |
||||
|
} |
||||
|
</style> |
||||
|
<section id="intro" class="main"> |
||||
|
<div class="spotlight"> |
||||
|
<div class="content"> |
||||
|
<header class="major"> |
||||
|
<h2>Transaction</h2> |
||||
|
</header> |
||||
|
<div class="row uniform"> |
||||
|
<div class="12u$"> |
||||
|
{{gestion_form}} |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row uniform"> |
||||
|
<h3>Récapitulatif</h3> |
||||
|
</div> |
||||
|
<div class="row uniform"> |
||||
|
<div class="12u$"> |
||||
|
<table id="sumUpTable"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th>Solde</th> |
||||
|
<th>Montant total de la commande</th> |
||||
|
<th>Solde après la commande</th> |
||||
|
<th>Payer</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<tr> |
||||
|
<td id="balance">0€</td> |
||||
|
<td id="totalAmount">0€</td> |
||||
|
<td id="totalAfter">0€</td> |
||||
|
<td><button class="btn small">Payer</button></td> |
||||
|
</tr> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row uniform"> |
||||
|
<h3>Produits</h3> |
||||
|
</div> |
||||
|
<div class="row uniform"> |
||||
|
<div class="12u$"> |
||||
|
<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> |
||||
|
<th>Sous-total</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody id="items"> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row uniform"> |
||||
|
<div class="12u$"> |
||||
|
<div class="boutonProduit"> |
||||
|
<table> |
||||
|
<tbody class="actions" id="bouton Produit"> |
||||
|
<tr style="text-align:center; font-weight:bold;"><td colspan="4">Bières pression</td></tr> |
||||
|
{% for produit in bieresPression %} |
||||
|
{% if forloop.counter0|divisibleby:4 %} |
||||
|
<tr style="text-align:center"> |
||||
|
{% endif %} |
||||
|
<td><button class="product" target="{{produit.barcode}}">{{produit.name}}</button></td> |
||||
|
{% if forloop.counter|divisibleby:4 %} |
||||
|
</tr> |
||||
|
{% endif %} |
||||
|
{% endfor %} |
||||
|
{% if not bieresPression|divisibleby:4 %} |
||||
|
</tr> |
||||
|
{% endif %} |
||||
|
<tr style="text-align:center; font-weight:bold;"><td colspan="4">Bières bouteilles</td></tr> |
||||
|
{% for produit in bieresBouteille %} |
||||
|
{% if forloop.counter0|divisibleby:4 %} |
||||
|
<tr style="text-align:center"> |
||||
|
{% endif %} |
||||
|
<td><button class="boutonsProduit" disabled target="{{produit.codeBarre}}" type="{{produit.typeSaisie}}">{{produit.nom}}</button></td> |
||||
|
{% if forloop.counter|divisibleby:4 %} |
||||
|
</tr> |
||||
|
{% endif %} |
||||
|
{% endfor %} |
||||
|
{% if not bieresBouteille|divisibleby:4 %} |
||||
|
</tr> |
||||
|
{% endif %} |
||||
|
<tr style="text-align:center; font-weight:bold;"><td colspan="4">Paninis</td></tr> |
||||
|
{% for produit in panini %} |
||||
|
{% if forloop.counter0|divisibleby:4 %} |
||||
|
<tr style="text-align:center"> |
||||
|
{% endif %} |
||||
|
<td><button class="boutonsProduit" disabled target="{{produit.codeBarre}}" type="{{produit.typeSaisie}}">{{produit.nom}}</button></td> |
||||
|
{% if forloop.counter|divisibleby:4 %} |
||||
|
</tr> |
||||
|
{% endif %} |
||||
|
{% endfor %} |
||||
|
{% if not panini|divisibleby:4 %} |
||||
|
</tr> |
||||
|
{% endif %} |
||||
|
<tr style="text-align:center; font-weight:bold;"><td colspan="4">Softs</td></tr> |
||||
|
{% for produit in soft %} |
||||
|
{% if forloop.counter0|divisibleby:4 %} |
||||
|
<tr style="text-align:center"> |
||||
|
{% endif %} |
||||
|
<td><button class="boutonsProduit" disabled target="{{produit.codeBarre}}" type="{{produit.typeSaisie}}">{{produit.nom}}</button></td> |
||||
|
{% if forloop.counter|divisibleby:4 %} |
||||
|
</tr> |
||||
|
{% endif %} |
||||
|
{% endfor %} |
||||
|
{% if not soft|divisibleby:4 %} |
||||
|
</tr> |
||||
|
{% endif %} |
||||
|
|
||||
|
<tr style="text-align:center; font-weight:bold;"><td colspan="4">Bouffe</td></tr> |
||||
|
{% for produit in autreBouffe %} |
||||
|
{% if forloop.counter0|divisibleby:4 %} |
||||
|
<tr style="text-align:center"> |
||||
|
{% endif %} |
||||
|
<td><button class="boutonsProduit" disabled target="{{produit.codeBarre}}" type="{{produit.typeSaisie}}">{{produit.nom}}</button></td> |
||||
|
{% if forloop.counter|divisibleby:4 %} |
||||
|
</tr> |
||||
|
{% endif %} |
||||
|
{% endfor %} |
||||
|
{% if not autreBouffe|divisibleby:4 %} |
||||
|
</tr> |
||||
|
{% endif %} |
||||
|
{% if menus %} |
||||
|
<tr style="text-align:center; font-weight:bold;"><td colspan="4">Menus</td></tr> |
||||
|
{% for produit in menus %} |
||||
|
{% if forloop.counter0|divisibleby:4 %} |
||||
|
<tr style="text-align:center"> |
||||
|
{% endif %} |
||||
|
<td><button class="boutonsProduit" disabled target="{{produit.codeBarre}}" type="MN">{{produit.nom}}</button></td> |
||||
|
{% if forloop.counter|divisibleby:4 %} |
||||
|
</tr> |
||||
|
{% endif %} |
||||
|
{% endfor %} |
||||
|
{% if not menus|divisibleby:4 %} |
||||
|
</tr> |
||||
|
{% endif %} |
||||
|
{% endif %} |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
{% if perms.gestion.cand_add_reload %} |
||||
|
<section id="second" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Rechargement client</h2> |
||||
|
</header> |
||||
|
<form method="post" action="{% url 'gestion:reload' %}"> |
||||
|
{% csrf_token %} |
||||
|
{{reload_form}} |
||||
|
<br> |
||||
|
<button type="submit">Recharger</button> |
||||
|
</form> |
||||
|
</section> |
||||
|
{% endif %} |
||||
|
{% if perms.gestion.can_refund %} |
||||
|
<section id="third" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Remboursement client</h2> |
||||
|
</header> |
||||
|
<form method="post" action="{% url 'gestion:refund' %}"> |
||||
|
{% csrf_token %} |
||||
|
{{refund_form}} |
||||
|
<br> |
||||
|
<button type="submit">Rembourser</button> |
||||
|
</form> |
||||
|
</section> |
||||
|
{% endif %} |
||||
|
{{gestion_form.media}} |
||||
|
<script src="{% static 'manage.js' %}"></script> |
||||
|
{%endblock%} |
||||
@ -0,0 +1,55 @@ |
|||||
|
{% extends 'base.html' %} |
||||
|
{% block entete %}<h1>Gestion des produits</h1>{% endblock %} |
||||
|
{% block navbar%} |
||||
|
<ul> |
||||
|
<li><a href="#first">Produits</a></li> |
||||
|
<li><a href="#second">Futs</a></li> |
||||
|
<li><a href="#third">Menus</a></li> |
||||
|
<li><a href="#fourth">Stocks</a></li> |
||||
|
</ul> |
||||
|
{% endblock %} |
||||
|
{% block content %} |
||||
|
<section id="first" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Produits</h2> |
||||
|
</header> |
||||
|
Actions possibles : |
||||
|
<ul> |
||||
|
<li><a href="{% url 'gestion:addProduct' %}">Créer un produit</a></li> |
||||
|
<li><a href="{% url 'users:searchUser' %}">Rechercher un produit</a></li> |
||||
|
<li><a href="{% url 'users:usersIndex' %}">Lister tous les produits</a></li> |
||||
|
</ul> |
||||
|
</section> |
||||
|
<section id="second" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Futs</h2> |
||||
|
</header> |
||||
|
Actions possibles : |
||||
|
<ul> |
||||
|
<li><a href="{% url 'gestion:addBarrel' %}">Créer un fut</a></li> |
||||
|
<li><a href="">Percuter un fut</a></li> |
||||
|
<li><a href="">Lister les futs</a></li> |
||||
|
</ul> |
||||
|
</section> |
||||
|
<section id="third" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Menus</h2> |
||||
|
</header> |
||||
|
Actions possibles : |
||||
|
<ul> |
||||
|
<li><a href="{% url 'gestion:addMenu' %}">Créer un menu</a></li> |
||||
|
<li><a href="">Rechercher un menu</a></li> |
||||
|
<li><a href="{% url 'users:adminsIndex' %}">Lister les menus</a></li> |
||||
|
</ul> |
||||
|
</section> |
||||
|
<section id="fourth" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Stocks</h2> |
||||
|
</header> |
||||
|
Actions possibles : |
||||
|
<ul> |
||||
|
<li><a href="{% url 'users:addSuperuser' %}">Voir les Stocks</a></li> |
||||
|
<li><a href="{% url 'users:superusersIndex' %}">Classement sur un produit</a></li> |
||||
|
</ul> |
||||
|
</section> |
||||
|
{% endblock %} |
||||
@ -0,0 +1,55 @@ |
|||||
|
{% extends 'base.html' %} |
||||
|
{% block entete %}<h1>Gestion des produits</h1>{% endblock %} |
||||
|
{% block navbar%} |
||||
|
<ul> |
||||
|
<li><a href="#first">Produits</a></li> |
||||
|
<li><a href="#second">Futs</a></li> |
||||
|
<li><a href="#third">Menus</a></li> |
||||
|
<li><a href="#fourth">Stocks</a></li> |
||||
|
</ul> |
||||
|
{% endblock %} |
||||
|
{% block content %} |
||||
|
<section id="first" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Liste des produits</h2> |
||||
|
</header> |
||||
|
Actions possibles : |
||||
|
<ul> |
||||
|
<li><a href="{% url 'gestion:addProduct' %}">Créer un produit</a></li> |
||||
|
<li><a href="{% url 'users:searchUser' %}">Rechercher un produit</a></li> |
||||
|
<li><a href="{% url 'users:usersIndex' %}">Lister tous les produits</a></li> |
||||
|
</ul> |
||||
|
</section> |
||||
|
<section id="second" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Futs</h2> |
||||
|
</header> |
||||
|
Actions possibles : |
||||
|
<ul> |
||||
|
<li><a href="{% url 'users:createGroup' %}">Créer un fut</a></li> |
||||
|
<li><a href="">Percuter un fut</a></li> |
||||
|
<li><a href="">Lister les futs</a><li> |
||||
|
</ul> |
||||
|
</section> |
||||
|
<section id="third" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Menus</h2> |
||||
|
</header> |
||||
|
Actions possibles : |
||||
|
<ul> |
||||
|
<li><a href="{% url 'users:addAdmin' %}">Créer un menu</a></li> |
||||
|
<li><a href="">Rechercher un menu</a></li> |
||||
|
<li><a href="{% url 'users:adminsIndex' %}">Lister les menus</a></li> |
||||
|
</ul> |
||||
|
</section> |
||||
|
<section id="fourth" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Stocks</h2> |
||||
|
</header> |
||||
|
Actions possibles : |
||||
|
<ul> |
||||
|
<li><a href="{% url 'users:addSuperuser' %}">Voir les Stocks</a></li> |
||||
|
<li><a href="{% url 'users:superusersIndex' %}">Classement sur un produit</a></li> |
||||
|
</ul> |
||||
|
</section> |
||||
|
{% endblock %} |
||||
@ -1,4 +1,108 @@ |
|||||
from django.shortcuts import render |
from django.shortcuts import render, redirect |
||||
|
from django.contrib import messages |
||||
|
from django.urls import reverse |
||||
|
from django.http import HttpResponse |
||||
|
from django.contrib.auth.models import User |
||||
|
|
||||
|
import json |
||||
|
from dal import autocomplete |
||||
|
|
||||
|
from .forms import ReloadForm, RefundForm, ProductForm, KegForm, MenuForm, GestionForm |
||||
|
from .models import Product, Menu, Keg |
||||
|
|
||||
def manage(request): |
def manage(request): |
||||
return render(request, "base.html") |
gestion_form = GestionForm(request.POST or None) |
||||
|
reload_form = ReloadForm(request.POST or None) |
||||
|
refund_form = RefundForm(request.POST or None) |
||||
|
bieresPression = [] |
||||
|
bieresBouteille = Product.objects.filter(category=Product.BOTTLE).filter(is_active=True) |
||||
|
panini = Product.objects.filter(category=Product.PANINI).filter(is_active=True) |
||||
|
food = Product.objects.filter(category=Product.FOOD).filter(is_active=True) |
||||
|
soft = Product.objects.filter(category=Product.SOFT).filter(is_active=True) |
||||
|
menus = Menu.objects.filter(is_active=True) |
||||
|
kegs = Keg.objects.filter(is_active=True) |
||||
|
for keg in kegs: |
||||
|
if(keg.pinte): |
||||
|
bieresPression.append(keg.pinte) |
||||
|
if(keg.demi): |
||||
|
bieresPression.append(keg.demi) |
||||
|
if(keg.galopin): |
||||
|
bieresPression.append(keg.galopin) |
||||
|
return render(request, "gestion/manage.html", {"gestion_form": gestion_form, "reload_form": reload_form, "refund_form": refund_form, "bieresPression": bieresPression, "bieresBouteille": bieresBouteille, "panini": panini, "food": food, "soft": soft, "menus": menus}) |
||||
|
|
||||
|
def reload(request): |
||||
|
reload_form = ReloadForm(request.POST or None) |
||||
|
if(reload_form.is_valid()): |
||||
|
reloadEntry = reload_form.save(commit=False) |
||||
|
reloadEntry.coopeman = request.user |
||||
|
reloadEntry.save() |
||||
|
user = reload_form.cleaned_data['customer'] |
||||
|
amount = reload_form.cleaned_data['amount'] |
||||
|
user.profile.credit += amount |
||||
|
user.save() |
||||
|
messages.success(request,"Le compte de " + user.username + " a bien été crédité de " + str(amount) + "€") |
||||
|
else: |
||||
|
messages.error(request, "Le rechargement a échoué") |
||||
|
return redirect(reverse('gestion:manage')) |
||||
|
|
||||
|
def refund(request): |
||||
|
refund_form = RefundForm(request.POST or None) |
||||
|
if(refund_form.is_valid()): |
||||
|
user = refund_form.cleaned_data['customer'] |
||||
|
amount = refund_form.cleaned_data['amount'] |
||||
|
if(amount <= user.profile.balance): |
||||
|
refundEntry = refund_form.save(commit = False) |
||||
|
refundEntry.coopeman = request.user |
||||
|
refundEntry.save() |
||||
|
user.profile.credit -= amount |
||||
|
user.save() |
||||
|
messages.success(request, "Le compte de " + user.username + " a bien été remboursé de " + str(amount) + "€") |
||||
|
else: |
||||
|
messages.error(request, "Impossible de rembourser l'utilisateur " + user.username + " de " + str(amount) + "€ : il n'a que " + str(user.profile.balance) + "€ sur son compte.") |
||||
|
else: |
||||
|
messages.error(request, "Le remboursement a échoué") |
||||
|
return redirect(reverse('gestion:manage')) |
||||
|
|
||||
|
def productsIndex(request): |
||||
|
return render(request, "gestion/products_index.html") |
||||
|
|
||||
|
def addProduct(request): |
||||
|
form = ProductForm(request.POST or None) |
||||
|
if(form.is_valid()): |
||||
|
form.save() |
||||
|
messages.success(request, "Le produit a bien été ajouté") |
||||
|
return redirect(reverse('gestion:productsIndex')) |
||||
|
return render(request, "form.html", {"form": form, "form_title": "Ajout d'un produit", "form_button": "Ajouter"}) |
||||
|
|
||||
|
def productsList(request): |
||||
|
products = Product.objects.all() |
||||
|
return render(request, "gestion/products_list.html", {"products": products}) |
||||
|
|
||||
|
def getProduct(request, barcode): |
||||
|
product = Product.objects.get(barcode=barcode) |
||||
|
data = json.dumps({"pk": product.pk, "barcode" : product.barcode, "name": product.name, "amount" : float(product.amount)}) |
||||
|
return HttpResponse(data, content_type='application/json') |
||||
|
|
||||
|
|
||||
|
########## Kegs ########## |
||||
|
|
||||
|
def addKeg(request): |
||||
|
form = KegForm(request.POST or None) |
||||
|
if(form.is_valid()): |
||||
|
keg = form.save() |
||||
|
messages.success(request, "Le fût " + keg.name + " a bien été ajouté") |
||||
|
return redirect(reverse('gestion:productsIndex')) |
||||
|
return render(request, "form.html", {"form":form, "form_title": "Ajout d'un fût", "form_button": "Ajouter"}) |
||||
|
|
||||
|
|
||||
|
########## Menus ########## |
||||
|
|
||||
|
def addMenu(request): |
||||
|
form = MenuForm(request.POST or None) |
||||
|
extra_css = "#id_articles{height:200px;}" |
||||
|
if(form.is_valid()): |
||||
|
menu = form.save() |
||||
|
messages.success(request, "Le menu " + menu.name + " a bien été ajouté") |
||||
|
return redirect(reverse('gestion:productsIndex')) |
||||
|
return render(request, "form.html", {"form":form, "form_title": "Ajout d'un menu", "form_button": "Ajouter", "extra_css": extra_css}) |
||||
|
|
||||
|
|||||
@ -1,3 +1,8 @@ |
|||||
from django.contrib import admin |
from django.contrib import admin |
||||
|
|
||||
|
from .models import PaymentMethod, GeneralPreferences, Cotisation |
||||
|
|
||||
|
admin.site.register(PaymentMethod) |
||||
|
admin.site.register(GeneralPreferences) |
||||
|
admin.site.register(Cotisation) |
||||
# Register your models here. |
# Register your models here. |
||||
|
|||||
@ -0,0 +1,30 @@ |
|||||
|
from django import forms |
||||
|
|
||||
|
from .models import Cotisation, PaymentMethod, GeneralPreferences |
||||
|
|
||||
|
class CotisationForm(forms.ModelForm): |
||||
|
class Meta: |
||||
|
model = Cotisation |
||||
|
fields = "__all__" |
||||
|
|
||||
|
class PaymentMethodForm(forms.ModelForm): |
||||
|
class Meta: |
||||
|
model = PaymentMethod |
||||
|
fields = "__all__" |
||||
|
|
||||
|
|
||||
|
class GeneralPreferencesForm(forms.ModelForm): |
||||
|
class Meta: |
||||
|
model = GeneralPreferences |
||||
|
fields = "__all__" |
||||
|
widgets = { |
||||
|
'global_message': forms.Textarea(attrs={'placeholder': 'Message global à afficher sur le site'}), |
||||
|
'active_message': forms.Textarea(attrs={'placeholder': 'Ce message s\'affichera si le site n\'est pas actif'}), |
||||
|
'president': forms.TextInput(attrs={'placeholder': 'Président'}), |
||||
|
'vice_president': forms.TextInput(attrs={'placeholder': 'Vice-président'}), |
||||
|
'secretary': forms.TextInput(attrs={'placeholder': 'Secrétaire'}), |
||||
|
'treasurer': forms.TextInput(attrs={'placeholder': 'Trésorier'}), |
||||
|
'brewer': forms.TextInput(attrs={'placeholder': 'Maître brasseur'}), |
||||
|
'grocer': forms.TextInput(attrs={'placeholder': 'Epic épicier'}), |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,35 @@ |
|||||
|
{% extends "base.html" %} |
||||
|
{% block entete %}<h1>Gestion des cotisations</h1>{% endblock %} |
||||
|
{% block navbar %} |
||||
|
<ul> |
||||
|
<li><a href="#first">Liste des cotisations</a></li> |
||||
|
</ul> |
||||
|
{% endblock %} |
||||
|
{% block content %} |
||||
|
<section id="first" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Liste des cotisations</h2> |
||||
|
</header> |
||||
|
<a class="button" href="{% url 'preferences:addCotisation' %}">Créer une cotisation</a><br><br> |
||||
|
<div class="table-wrapper"> |
||||
|
<table> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th>Durée de cotisation</th> |
||||
|
<th>Prix</th> |
||||
|
<th>Administration</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
{% for cotisation in cotisations %} |
||||
|
<tr> |
||||
|
<td>{{ cotisation.duration }} jours</td> |
||||
|
<td>{{ cotisation.amount }} €</td> |
||||
|
<td><a class="button small" href="{% url 'preferences:editCotisation' cotisation.pk %}">Modifier</a> <a class="button small" href="{% url 'preferences:deleteCotisation' cotisation.pk %}">Supprimer</a></td> |
||||
|
</tr> |
||||
|
{% endfor %} |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
</section> |
||||
|
{% endblock %} |
||||
@ -0,0 +1,89 @@ |
|||||
|
{% extends 'base.html' %} |
||||
|
{% block entete %} |
||||
|
<h1>Administration</h1> |
||||
|
{% endblock %} |
||||
|
{% block nav %} |
||||
|
<ul> |
||||
|
<li><a href="#first" class="active">Message global</a></li> |
||||
|
<li><a href="#second">Site actif</a></li> |
||||
|
<li><a href="#third">Bureau</a></li> |
||||
|
</ul> |
||||
|
{% endblock %} |
||||
|
|
||||
|
{% block content %} |
||||
|
<form class="main" method="post" action=""> |
||||
|
{% csrf_token %} |
||||
|
<section id="first" class="main"> |
||||
|
<div class="spotlight"> |
||||
|
<div class="content"> |
||||
|
<header class="major"> |
||||
|
<h2>Message global</h2> |
||||
|
</header> |
||||
|
<div class="row"> |
||||
|
<div class="12u"> |
||||
|
{{form.global_message}} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
<section id="second" class="main"> |
||||
|
<div class="spotlight"> |
||||
|
<div class="content"> |
||||
|
<header class="major"> |
||||
|
<h2>Site actif</h2> |
||||
|
</header> |
||||
|
<div class="row uniform"> |
||||
|
<div class="12u"> |
||||
|
{{form.is_active}} |
||||
|
<label for="{{form.is_active.id_for_label}}">Site actif ?</label> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row uniform"> |
||||
|
<div class="12u"> |
||||
|
{{form.active_message}} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
<section id="third" class="main"> |
||||
|
<div class="spotlight"> |
||||
|
<div class="content"> |
||||
|
<header class="major"> |
||||
|
<h2>Bureau</h2> |
||||
|
</header> |
||||
|
<div class="row uniform"> |
||||
|
<div class="6u"> |
||||
|
{{form.president}} |
||||
|
</div> |
||||
|
<div class="6u"> |
||||
|
{{form.vice_president}} |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row uniform"> |
||||
|
<div class="6u"> |
||||
|
{{form.secretary}} |
||||
|
</div> |
||||
|
<div class="6u"> |
||||
|
{{form.treasurer}} |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row uniform"> |
||||
|
<div class="6u"> |
||||
|
{{form.grocer}} |
||||
|
</div> |
||||
|
<div class="6u"> |
||||
|
{{form.brewer}} |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row uniform"> |
||||
|
<div class="12u"> |
||||
|
<button type="submit">Enregistrer</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
</form> |
||||
|
{% endblock %} |
||||
@ -0,0 +1,39 @@ |
|||||
|
{% extends "base.html" %} |
||||
|
{% block entete %}<h1>Gestion des moyens de paiement</h1>{% endblock %} |
||||
|
{% block navbar %} |
||||
|
<ul> |
||||
|
<li><a href="#first">Liste des moyens de paiement</a></li> |
||||
|
</ul> |
||||
|
{% endblock %} |
||||
|
{% block content %} |
||||
|
<section id="first" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Liste des moyens de paiement</h2> |
||||
|
</header> |
||||
|
<a class="button" href="{% url 'preferences:addPaymentMethod' %}">Créer un moyen de paiement</a><br><br> |
||||
|
<div class="table-wrapper"> |
||||
|
<table> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th>Nom</th> |
||||
|
<th>Actif ?</th> |
||||
|
<th>Utilisable dans les cotisations</th> |
||||
|
<th>Affecte le solde</th> |
||||
|
<th>Administration</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
{% for pm in paymentMethods %} |
||||
|
<tr> |
||||
|
<td>{{ pm.name }} </td> |
||||
|
<td>{{ pm.is_active | yesno:"Oui, Non"}}</td> |
||||
|
<td>{{ pm.is_usable_in_cotisation | yesno:"Oui, Non" }}</td> |
||||
|
<td>{{ pm.affect_balance | yesno:"Oui, Non" }}</td> |
||||
|
<td><a class="button small" href="{% url 'preferences:editPaymentMethod' pm.pk %}">Modifier</a> <a class="button small" href="{% url 'preferences:deletePaymentMethod' pm.pk %}">Supprimer</a></td> |
||||
|
</tr> |
||||
|
{% endfor %} |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
</section> |
||||
|
{% endblock %} |
||||
@ -0,0 +1,16 @@ |
|||||
|
from django.urls import path |
||||
|
|
||||
|
from . import views |
||||
|
|
||||
|
app_name="preferences" |
||||
|
urlpatterns = [ |
||||
|
path('generalPreferences', views.generalPreferences, name="generalPreferences"), |
||||
|
path('cotisationsIndex', views.cotisationsIndex, name="cotisationsIndex"), |
||||
|
path('addCotisation', views.addCotisation, name="addCotisation"), |
||||
|
path('editCotisation/<int:pk>', views.editCotisation, name="editCotisation"), |
||||
|
path('deleteCotisation/<int:pk>', views.deleteCotisation, name="deleteCotisation"), |
||||
|
path('paymentMethodsIndex', views.paymentMethodsIndex, name="paymentMethodsIndex"), |
||||
|
path('addPaymentMethod', views.addPaymentMethod, name="addPaymentMethod"), |
||||
|
path('editPaymentMethod/<int:pk>', views.editPaymentMethod, name="editPaymentMethod"), |
||||
|
path('deletePaymentMethod/<int:pk>', views.deletePaymentMethod, name="deletePaymentMethod"), |
||||
|
] |
||||
@ -1,3 +1,75 @@ |
|||||
from django.shortcuts import render |
from django.shortcuts import render, redirect, get_object_or_404 |
||||
|
from django.contrib import messages |
||||
|
from django.urls import reverse |
||||
|
|
||||
# Create your views here. |
from .models import GeneralPreferences, Cotisation, PaymentMethod |
||||
|
|
||||
|
from .forms import CotisationForm, PaymentMethodForm, GeneralPreferencesForm |
||||
|
|
||||
|
def generalPreferences(request): |
||||
|
gp,_ = GeneralPreferences.objects.get_or_create(pk=1) |
||||
|
form = GeneralPreferencesForm(request.POST or None, instance=gp) |
||||
|
if(form.is_valid()): |
||||
|
form.save() |
||||
|
return render(request, "preferences/general_preferences.html", {"form": form}) |
||||
|
|
||||
|
########## Cotisations ########## |
||||
|
|
||||
|
def cotisationsIndex(request): |
||||
|
cotisations = Cotisation.objects.all() |
||||
|
return render(request, "preferences/cotisations_index.html", {"cotisations": cotisations}) |
||||
|
|
||||
|
def addCotisation(request): |
||||
|
form = CotisationForm(request.POST or None) |
||||
|
if(form.is_valid()): |
||||
|
cotisation = form.save() |
||||
|
messages.success(request, "La cotisation (" + str(cotisation.duration) + " jours, " + str(cotisation.amount) + "€) a bien été créée") |
||||
|
return redirect(reverse('preferences:cotisationsIndex')) |
||||
|
return render(request, "form.html", {"form": form, "form_title": "Création d'une cotisation", "form_button": "Créer"}) |
||||
|
|
||||
|
def editCotisation(request, pk): |
||||
|
cotisation = get_object_or_404(Cotisation, pk=pk) |
||||
|
form = CotisationForm(request.POST or None, instance=cotisation) |
||||
|
if(form.is_valid()): |
||||
|
cotisation = form.save() |
||||
|
messages.success(request, "La cotisation (" + str(cotisation.duration) + " jours, " + str(cotisation.amount) + "€) a bien été modifiée") |
||||
|
return redirect(reverse('preferences:cotisationsIndex')) |
||||
|
return render(request, "form.html", {"form": form, "form_title": "Modification d'une cotisation", "form_button": "Modifier"}) |
||||
|
|
||||
|
def deleteCotisation(request,pk): |
||||
|
cotisation = get_object_or_404(Cotisation, pk=pk) |
||||
|
message = "La cotisation (" + str(cotisation.duration) + " jours, " + str(cotisation.amount) + "€) a bien été supprimée" |
||||
|
cotisation.delete() |
||||
|
messages.success(request, message) |
||||
|
return redirect(reverse('preferences:cotisationsIndex')) |
||||
|
|
||||
|
|
||||
|
########## Payment Methods ########## |
||||
|
|
||||
|
def paymentMethodsIndex(request): |
||||
|
paymentMethods = PaymentMethod.objects.all() |
||||
|
return render(request, "preferences/payment_methods_index.html", {"paymentMethods": paymentMethods}) |
||||
|
|
||||
|
def addPaymentMethod(request): |
||||
|
form = PaymentMethodForm(request.POST or None) |
||||
|
if(form.is_valid()): |
||||
|
paymentMethod = form.save() |
||||
|
messages.success(request, "Le moyen de paiement " + paymentMethod.name + " a bien été crée") |
||||
|
return redirect(reverse('preferences:paymentMethodsIndex')) |
||||
|
return render(request, "form.html", {"form": form, "form_title": "Création d'un moyen de paiement", "form_button": "Créer"}) |
||||
|
|
||||
|
def editPaymentMethod(request, pk): |
||||
|
paymentMethod = get_object_or_404(PaymentMethod, pk=pk) |
||||
|
form = PaymentMethodForm(request.POST or None, instance=paymentMethod) |
||||
|
if(form.is_valid()): |
||||
|
paymentMethod = form.save() |
||||
|
messages.success(request, "Le moyen de paiment " + paymentMethod.name + " a bien été modifié") |
||||
|
return redirect(reverse('preferences:paymentMethodsIndex')) |
||||
|
return render(request, "form.html", {"form": form, "form_title": "Modification d'un moyen de paiement", "form_button": "Modifier"}) |
||||
|
|
||||
|
def deletePaymentMethod(request,pk): |
||||
|
paymentMethod = get_object_or_404(PaymentMethod, pk=pk) |
||||
|
message = "Le moyen de paiement " + paymentMethod.name + " a bien été supprimé" |
||||
|
paymentMethod.delete() |
||||
|
messages.success(request, message) |
||||
|
return redirect(reverse('preferences:paymentMethodsIndex')) |
||||
|
|||||
@ -1,2 +1,3 @@ |
|||||
Django==2.1 |
Django==2.1 |
||||
|
django-autocomplete-light==3.3.2 |
||||
pytz==2018.5 |
pytz==2018.5 |
||||
|
|||||
@ -0,0 +1,162 @@ |
|||||
|
/* |
||||
|
This script garantees that this will be called once in django admin. |
||||
|
However, its the callback's responsability to clean up if the |
||||
|
element was cloned with data - which should be the case. |
||||
|
*/ |
||||
|
|
||||
|
;(function ($) { |
||||
|
$.fn.getFormPrefix = function() { |
||||
|
/* Get the form prefix for a field. |
||||
|
* |
||||
|
* For example: |
||||
|
* |
||||
|
* $(':input[name$=owner]').getFormsetPrefix() |
||||
|
* |
||||
|
* Would return an empty string for an input with name 'owner' but would return |
||||
|
* 'inline_model-0-' for an input named 'inline_model-0-owner'. |
||||
|
*/ |
||||
|
var parts = $(this).attr('name').split('-'); |
||||
|
var prefix = ''; |
||||
|
|
||||
|
for (var i in parts) { |
||||
|
var testPrefix = parts.slice(0, -i).join('-'); |
||||
|
if (! testPrefix.length) continue; |
||||
|
testPrefix += '-'; |
||||
|
|
||||
|
var result = $(':input[name^=' + testPrefix + ']') |
||||
|
|
||||
|
if (result.length) { |
||||
|
return testPrefix; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
$.fn.getFormPrefixes = function() { |
||||
|
/* |
||||
|
* Get the form prefixes for a field, from the most specific to the least. |
||||
|
* |
||||
|
* For example: |
||||
|
* |
||||
|
* $(':input[name$=owner]').getFormPrefixes() |
||||
|
* |
||||
|
* Would return: |
||||
|
* - [''] for an input named 'owner'. |
||||
|
* - ['inline_model-0-', ''] for an input named 'inline_model-0-owner' (i.e. nested with a nested inline). |
||||
|
* - ['sections-0-items-0-', 'sections-0-', ''] for an input named 'sections-0-items-0-product' |
||||
|
* (i.e. nested multiple time with django-nested-admin). |
||||
|
*/ |
||||
|
var parts = $(this).attr('name').split('-').slice(0, -1); |
||||
|
var prefixes = []; |
||||
|
|
||||
|
for (i = 0; i < parts.length; i += 2) { |
||||
|
var testPrefix = parts.slice(0, -i || parts.length).join('-'); |
||||
|
if (!testPrefix.length) |
||||
|
continue; |
||||
|
|
||||
|
testPrefix += '-'; |
||||
|
|
||||
|
var result = $(':input[name^=' + testPrefix + ']') |
||||
|
|
||||
|
if (result.length) |
||||
|
prefixes.push(testPrefix); |
||||
|
} |
||||
|
|
||||
|
prefixes.push(''); |
||||
|
|
||||
|
return prefixes; |
||||
|
} |
||||
|
|
||||
|
var initialized = []; |
||||
|
|
||||
|
function initialize(element) { |
||||
|
if (typeof element === 'undefined' || typeof element === 'number') { |
||||
|
element = this; |
||||
|
} |
||||
|
|
||||
|
if (window.__dal__initListenerIsSet !== true || initialized.indexOf(element) >= 0) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
$(element).trigger('autocompleteLightInitialize'); |
||||
|
initialized.push(element); |
||||
|
} |
||||
|
|
||||
|
if (!window.__dal__initialize) { |
||||
|
window.__dal__initialize = initialize; |
||||
|
|
||||
|
$(document).ready(function () { |
||||
|
$('[data-autocomplete-light-function=select2]:not([id*="__prefix__"])').each(initialize); |
||||
|
}); |
||||
|
|
||||
|
$(document).bind('DOMNodeInserted', function (e) { |
||||
|
$(e.target).find('[data-autocomplete-light-function=select2]:not([id*="__prefix__"])').each(initialize); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// using jQuery
|
||||
|
function getCookie(name) { |
||||
|
var cookieValue = null; |
||||
|
if (document.cookie && document.cookie != '') { |
||||
|
var cookies = document.cookie.split(';'); |
||||
|
for (var i = 0; i < cookies.length; i++) { |
||||
|
var cookie = $.trim(cookies[i]); |
||||
|
// Does this cookie string begin with the name we want?
|
||||
|
if (cookie.substring(0, name.length + 1) == (name + '=')) { |
||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return cookieValue; |
||||
|
} |
||||
|
|
||||
|
document.csrftoken = getCookie('csrftoken'); |
||||
|
if (document.csrftoken === null) { |
||||
|
// Try to get CSRF token from DOM when cookie is missing
|
||||
|
var $csrf = $('form :input[name="csrfmiddlewaretoken"]'); |
||||
|
if ($csrf.length > 0) { |
||||
|
document.csrftoken = $csrf[0].value; |
||||
|
} |
||||
|
} |
||||
|
})(yl.jQuery); |
||||
|
|
||||
|
// Does the same thing as django's admin/js/autocomplete.js, but uses yl.jQuery.
|
||||
|
(function($) { |
||||
|
'use strict'; |
||||
|
var init = function($element, options) { |
||||
|
var settings = $.extend({ |
||||
|
ajax: { |
||||
|
data: function(params) { |
||||
|
return { |
||||
|
term: params.term, |
||||
|
page: params.page |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
}, options); |
||||
|
$element.select2(settings); |
||||
|
}; |
||||
|
|
||||
|
$.fn.djangoAdminSelect2 = function(options) { |
||||
|
var settings = $.extend({}, options); |
||||
|
$.each(this, function(i, element) { |
||||
|
var $element = $(element); |
||||
|
init($element, settings); |
||||
|
}); |
||||
|
return this; |
||||
|
}; |
||||
|
|
||||
|
$(function() { |
||||
|
// Initialize all autocomplete widgets except the one in the template
|
||||
|
// form used when a new formset is added.
|
||||
|
$('.admin-autocomplete').not('[name*=__prefix__]').djangoAdminSelect2(); |
||||
|
}); |
||||
|
|
||||
|
$(document).on('formset:added', (function() { |
||||
|
return function(event, $newFormset) { |
||||
|
return $newFormset.find('.admin-autocomplete').djangoAdminSelect2(); |
||||
|
}; |
||||
|
})(this)); |
||||
|
}(yl.jQuery)); |
||||
@ -0,0 +1,183 @@ |
|||||
|
;(function($, yl) { |
||||
|
yl.forwardHandlerRegistry = yl.forwardHandlerRegistry || {}; |
||||
|
|
||||
|
yl.registerForwardHandler = function(name, handler) { |
||||
|
yl.forwardHandlerRegistry[name] = handler; |
||||
|
}; |
||||
|
|
||||
|
yl.getForwardHandler = function(name) { |
||||
|
return yl.forwardHandlerRegistry[name]; |
||||
|
}; |
||||
|
|
||||
|
function getForwardStrategy(element) { |
||||
|
var checkForCheckboxes = function() { |
||||
|
var all = true; |
||||
|
$.each(element, function(ix, e) { |
||||
|
if ($(e).attr("type") !== "checkbox") { |
||||
|
all = false; |
||||
|
} |
||||
|
}); |
||||
|
return all; |
||||
|
}; |
||||
|
|
||||
|
if (element.length === 1 && |
||||
|
element.attr("type") === "checkbox" && |
||||
|
element.attr("value") === undefined) { |
||||
|
// Single checkbox without 'value' attribute
|
||||
|
// Boolean field
|
||||
|
return "exists"; |
||||
|
} else if (element.length === 1 && |
||||
|
element.attr("multiple") !== undefined) { |
||||
|
// Multiple by HTML semantics. E. g. multiple select
|
||||
|
// Multiple choice field
|
||||
|
return "multiple"; |
||||
|
} else if (checkForCheckboxes()) { |
||||
|
// Multiple checkboxes or one checkbox with 'value' attribute.
|
||||
|
// Multiple choice field represented by checkboxes
|
||||
|
return "multiple"; |
||||
|
} else { |
||||
|
// Other cases
|
||||
|
return "single"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get fields with name `name` relative to `element` with considering form |
||||
|
* prefixes. |
||||
|
* @param element the element |
||||
|
* @param name name of the field |
||||
|
* @returns jQuery object with found fields or empty jQuery object if no |
||||
|
* field was found |
||||
|
*/ |
||||
|
yl.getFieldRelativeTo = function(element, name) { |
||||
|
var prefixes = $(element).getFormPrefixes(); |
||||
|
|
||||
|
for (var i = 0; i < prefixes.length; i++) { |
||||
|
var fieldSelector = "[name=" + prefixes[i] + name + "]"; |
||||
|
var field = $(fieldSelector); |
||||
|
|
||||
|
if (field.length) { |
||||
|
return field; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return $(); |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* Get field value which is put to forwarded dictionary |
||||
|
* @param field the field |
||||
|
* @returns forwarded value |
||||
|
*/ |
||||
|
yl.getValueFromField = function(field) { |
||||
|
var strategy = getForwardStrategy(field); |
||||
|
var serializedField = $(field).serializeArray(); |
||||
|
|
||||
|
var getSerializedFieldElementAt = function (index) { |
||||
|
// Return serializedField[index]
|
||||
|
// or null if something went wrong
|
||||
|
if (serializedField.length > index) { |
||||
|
return serializedField[index]; |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var getValueOf = function (elem) { |
||||
|
// Return elem.value
|
||||
|
// or null if something went wrong
|
||||
|
if (elem.hasOwnProperty("value") && |
||||
|
elem.value !== undefined |
||||
|
) { |
||||
|
return elem.value; |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var getSerializedFieldValueAt = function (index) { |
||||
|
// Return serializedField[index].value
|
||||
|
// or null if something went wrong
|
||||
|
var elem = getSerializedFieldElementAt(index); |
||||
|
if (elem !== null) { |
||||
|
return getValueOf(elem); |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
if (strategy === "multiple") { |
||||
|
return serializedField.map( |
||||
|
function (item) { |
||||
|
return getValueOf(item); |
||||
|
} |
||||
|
); |
||||
|
} else if (strategy === "exists") { |
||||
|
return serializedField.length > 0; |
||||
|
} else { |
||||
|
return getSerializedFieldValueAt(0); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
yl.getForwards = function(element) { |
||||
|
var forwardElem, |
||||
|
forwardList, |
||||
|
forwardedData, |
||||
|
divSelector, |
||||
|
form; |
||||
|
divSelector = "div.dal-forward-conf#dal-forward-conf-for-" + |
||||
|
element.attr("id"); |
||||
|
form = element.length > 0 ? $(element[0].form) : $(); |
||||
|
|
||||
|
forwardElem = |
||||
|
form.find(divSelector).find('script'); |
||||
|
if (forwardElem.length === 0) { |
||||
|
return; |
||||
|
} |
||||
|
try { |
||||
|
forwardList = JSON.parse(forwardElem.text()); |
||||
|
} catch (e) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (!Array.isArray(forwardList)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
forwardedData = {}; |
||||
|
|
||||
|
$.each(forwardList, function(ix, field) { |
||||
|
var srcName, dstName; |
||||
|
if (field.type === "const") { |
||||
|
forwardedData[field.dst] = field.val; |
||||
|
} else if (field.type === "self") { |
||||
|
if (field.hasOwnProperty("dst")) { |
||||
|
dstName = field.dst; |
||||
|
} else { |
||||
|
dstName = "self"; |
||||
|
} |
||||
|
forwardedData[dstName] = yl.getValueFromField(element); |
||||
|
} else if (field.type === "field") { |
||||
|
srcName = field.src; |
||||
|
if (field.hasOwnProperty("dst")) { |
||||
|
dstName = field.dst; |
||||
|
} else { |
||||
|
dstName = srcName; |
||||
|
} |
||||
|
var forwardedField = yl.getFieldRelativeTo(element, srcName); |
||||
|
|
||||
|
if (!forwardedField.length) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
forwardedData[dstName] = yl.getValueFromField(forwardedField); |
||||
|
} else if (field.type === "javascript") { |
||||
|
var handler = yl.getForwardHandler(field.handler); |
||||
|
forwardedData[field.dst || field.handler] = handler(element); |
||||
|
} |
||||
|
|
||||
|
}); |
||||
|
return JSON.stringify(forwardedData); |
||||
|
}; |
||||
|
|
||||
|
})(yl.jQuery, yl); |
||||
@ -0,0 +1,36 @@ |
|||||
|
var yl = yl || {}; |
||||
|
if (typeof django !== 'undefined' && typeof django.jQuery !== 'undefined') { |
||||
|
// If django.jQuery is already defined, use it.
|
||||
|
yl.jQuery = django.jQuery; |
||||
|
} |
||||
|
else { |
||||
|
// We include jquery itself in our widget's media, because we need it.
|
||||
|
// Normally, we expect our widget's reference to admin/js/vendor/jquery/jquery.js
|
||||
|
// to be skipped, because django's own code has already included it.
|
||||
|
// However, if django.jQuery is NOT defined, we know that jquery was not
|
||||
|
// included before we did it ourselves. This can happen if we're not being
|
||||
|
// rendered in a django admin form.
|
||||
|
// However, someone ELSE'S jQuery may have been included before ours, in
|
||||
|
// which case we must ensure that our jquery doesn't override theirs, since
|
||||
|
// it might be a newer version that other code on the page relies on.
|
||||
|
// Thus, we must run jQuery.noConflict(true) here to move our jQuery out of
|
||||
|
// the way.
|
||||
|
yl.jQuery = jQuery.noConflict(true); |
||||
|
} |
||||
|
|
||||
|
// In addition to all of this, we must ensure that the global jQuery and $ are
|
||||
|
// defined, because Select2 requires that. jQuery will only be undefined at
|
||||
|
// this point if only we or django included it.
|
||||
|
if (typeof jQuery === 'undefined') { |
||||
|
jQuery = yl.jQuery; |
||||
|
$ = yl.jQuery; |
||||
|
} |
||||
|
else { |
||||
|
// jQuery IS still defined, which means someone else also included jQuery.
|
||||
|
// In this situation, we need to store the old jQuery in a
|
||||
|
// temp variable, set the global jQuery to our yl.jQuery, then let select2
|
||||
|
// set itself up. We restore the global jQuery to its original value in
|
||||
|
// jquery.post-setup.js.
|
||||
|
dal_jquery_backup = jQuery.noConflict(true); |
||||
|
jQuery = yl.jQuery; |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
if (typeof dal_jquery_backup !== 'undefined') { |
||||
|
// We made a backup of the original global jQuery before forcing it to our
|
||||
|
// yl.jQuery value. Now that select2 has been set up, we need to restore
|
||||
|
// our backup to its rightful place.
|
||||
|
jQuery = dal_jquery_backup; |
||||
|
$ = dal_jquery_backup; |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
.select2-container { |
||||
|
min-width: 20em; |
||||
|
} |
||||
|
|
||||
|
ul li.select2-selection__choice, |
||||
|
ul li.select2-search { |
||||
|
/* Cancel out django's style */ |
||||
|
list-style-type: none; |
||||
|
} |
||||
@ -0,0 +1,116 @@ |
|||||
|
;(function ($) { |
||||
|
if (window.__dal__initListenerIsSet) |
||||
|
return; |
||||
|
|
||||
|
$(document).on('autocompleteLightInitialize', '[data-autocomplete-light-function=select2]', function() { |
||||
|
var element = $(this); |
||||
|
|
||||
|
// Templating helper
|
||||
|
function template(text, is_html) { |
||||
|
if (is_html) { |
||||
|
var $result = $('<span>'); |
||||
|
$result.html(text); |
||||
|
return $result; |
||||
|
} else { |
||||
|
return text; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function result_template(item) { |
||||
|
return template(item.text, |
||||
|
element.attr('data-html') !== undefined || element.attr('data-result-html') !== undefined |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
function selected_template(item) { |
||||
|
if (item.selected_text !== undefined) { |
||||
|
return template(item.selected_text, |
||||
|
element.attr('data-html') !== undefined || element.attr('data-selected-html') !== undefined |
||||
|
); |
||||
|
} else { |
||||
|
return result_template(item); |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
var ajax = null; |
||||
|
if ($(this).attr('data-autocomplete-light-url')) { |
||||
|
ajax = { |
||||
|
url: $(this).attr('data-autocomplete-light-url'), |
||||
|
dataType: 'json', |
||||
|
delay: 250, |
||||
|
|
||||
|
data: function (params) { |
||||
|
var data = { |
||||
|
q: params.term, // search term
|
||||
|
page: params.page, |
||||
|
create: element.attr('data-autocomplete-light-create') && !element.attr('data-tags'), |
||||
|
forward: yl.getForwards(element) |
||||
|
}; |
||||
|
|
||||
|
return data; |
||||
|
}, |
||||
|
processResults: function (data, page) { |
||||
|
if (element.attr('data-tags')) { |
||||
|
$.each(data.results, function(index, value) { |
||||
|
value.id = value.text; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return data; |
||||
|
}, |
||||
|
cache: true |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
$(this).select2({ |
||||
|
tokenSeparators: element.attr('data-tags') ? [','] : null, |
||||
|
debug: true, |
||||
|
containerCssClass: ':all:', |
||||
|
placeholder: element.attr('data-placeholder') || '', |
||||
|
language: element.attr('data-autocomplete-light-language'), |
||||
|
minimumInputLength: element.attr('data-minimum-input-length') || 0, |
||||
|
allowClear: ! $(this).is('[required]'), |
||||
|
templateResult: result_template, |
||||
|
templateSelection: selected_template, |
||||
|
ajax: ajax, |
||||
|
tags: Boolean(element.attr('data-tags')), |
||||
|
}); |
||||
|
|
||||
|
$(this).on('select2:selecting', function (e) { |
||||
|
var data = e.params.args.data; |
||||
|
|
||||
|
if (data.create_id !== true) |
||||
|
return; |
||||
|
|
||||
|
e.preventDefault(); |
||||
|
|
||||
|
var select = $(this); |
||||
|
|
||||
|
$.ajax({ |
||||
|
url: $(this).attr('data-autocomplete-light-url'), |
||||
|
type: 'POST', |
||||
|
dataType: 'json', |
||||
|
data: { |
||||
|
text: data.id, |
||||
|
forward: yl.getForwards($(this)) |
||||
|
}, |
||||
|
beforeSend: function(xhr, settings) { |
||||
|
xhr.setRequestHeader("X-CSRFToken", document.csrftoken); |
||||
|
}, |
||||
|
success: function(data, textStatus, jqXHR ) { |
||||
|
select.append( |
||||
|
$('<option>', {value: data.id, text: data.text, selected: true}) |
||||
|
); |
||||
|
select.trigger('change'); |
||||
|
select.select2('close'); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
}); |
||||
|
window.__dal__initListenerIsSet = true; |
||||
|
$('[data-autocomplete-light-function=select2]:not([id*="__prefix__"])').each(function() { |
||||
|
window.__dal__initialize(this); |
||||
|
}); |
||||
|
})(yl.jQuery); |
||||
@ -1,17 +1,3 @@ |
|||||
#listePseudoTransaction { |
.select2-container{ |
||||
} |
color: !black; |
||||
|
|
||||
#listePseudoTransaction .item { |
|
||||
padding: 3px; |
|
||||
font-family: Helvetica; |
|
||||
border: 1px solid #c0c0c0; |
|
||||
} |
|
||||
|
|
||||
#istePseudoTransaction .item:hover { |
|
||||
background-color: #f2f2f2; |
|
||||
cursor: pointer; |
|
||||
} |
|
||||
|
|
||||
.itemSelected { |
|
||||
background-color: #dcdcdc; |
|
||||
} |
} |
||||
|
|||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,58 @@ |
|||||
|
totalAmount = 0 |
||||
|
products = [] |
||||
|
paymentMethod = null |
||||
|
solde = 0 |
||||
|
|
||||
|
function get_product(barcode){ |
||||
|
res = $.get("getProduct/" + barcode, function(data){ |
||||
|
add_product(data.pk, data.barcode, data.name, data.amount); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function add_product(pk, barcode, name, amount){ |
||||
|
exist = false |
||||
|
index = -1 |
||||
|
for(k=0;k < products.length; k++){ |
||||
|
if(products[k].pk == pk){ |
||||
|
exist = true |
||||
|
index = k |
||||
|
} |
||||
|
} |
||||
|
if(exist){ |
||||
|
products[index].quantity += 1; |
||||
|
}else{ |
||||
|
products.push({"pk": pk, "barcode": barcode, "name": name, "amount": amount, "quantity": 1}); |
||||
|
} |
||||
|
generate_html() |
||||
|
} |
||||
|
|
||||
|
function generate_html(){ |
||||
|
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>" + String(product.quantity) + "</td><td>" + String(product.quantity * product.amount) + "</td></tr>" |
||||
|
} |
||||
|
$("#items").html(html) |
||||
|
updateTotal() |
||||
|
} |
||||
|
|
||||
|
function updateTotal(){ |
||||
|
total = 0 |
||||
|
for(k=0;k<products.length;k++){ |
||||
|
total += products[k].quantity * products[k].amount |
||||
|
} |
||||
|
$("#totalAmount").text(String(total) + "€") |
||||
|
if(paymentMethod == "compte"){ |
||||
|
totalAfter = solde - total |
||||
|
$("#totalAfter").text(totalAfter + "€") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
$(document).ready(function(){ |
||||
|
$(".product").click(function(){ |
||||
|
product = get_product($(this).attr('target')); |
||||
|
}); |
||||
|
$("#id_paymentMethod").on('change', function(){ |
||||
|
alert('lol') |
||||
|
}); |
||||
|
}); |
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,67 @@ |
|||||
|
totalAmount = 0 |
||||
|
products = [] |
||||
|
paymentMethod = null |
||||
|
balance = 0 |
||||
|
username = "" |
||||
|
id = 0 |
||||
|
|
||||
|
function get_product(barcode){ |
||||
|
res = $.get("getProduct/" + barcode, function(data){ |
||||
|
add_product(data.pk, data.barcode, data.name, data.amount); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function add_product(pk, barcode, name, amount){ |
||||
|
exist = false |
||||
|
index = -1 |
||||
|
for(k=0;k < products.length; k++){ |
||||
|
if(products[k].pk == pk){ |
||||
|
exist = true |
||||
|
index = k |
||||
|
} |
||||
|
} |
||||
|
if(exist){ |
||||
|
products[index].quantity += 1; |
||||
|
}else{ |
||||
|
products.push({"pk": pk, "barcode": barcode, "name": name, "amount": amount, "quantity": 1}); |
||||
|
} |
||||
|
generate_html() |
||||
|
} |
||||
|
|
||||
|
function generate_html(){ |
||||
|
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>" + String(product.quantity) + "</td><td>" + String(product.quantity * product.amount) + "</td></tr>" |
||||
|
} |
||||
|
$("#items").html(html) |
||||
|
updateTotal() |
||||
|
} |
||||
|
|
||||
|
function updateTotal(){ |
||||
|
total = 0 |
||||
|
for(k=0;k<products.length;k++){ |
||||
|
total += products[k].quantity * products[k].amount |
||||
|
} |
||||
|
$("#totalAmount").text(String(total) + "€") |
||||
|
totalAfter = balance - total |
||||
|
$("#totalAfter").text(totalAfter + "€") |
||||
|
} |
||||
|
|
||||
|
$(document).ready(function(){ |
||||
|
$(".product").click(function(){ |
||||
|
product = get_product($(this).attr('target')); |
||||
|
}); |
||||
|
$("#id_client").on('change', function(){ |
||||
|
id = $("#id_client").val(); |
||||
|
$.get("/users/getUser/" + id, function(data){ |
||||
|
balance = data.balance; |
||||
|
username = data.username; |
||||
|
$("#balance").html(balance + "€"); |
||||
|
updateTotal(); |
||||
|
}).fail(function(){ |
||||
|
alert("Une erreur inconnue est survenue"); |
||||
|
window.location.reload() |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,48 @@ |
|||||
|
{% load vip %} |
||||
|
<section> |
||||
|
<h2>A propos</h2> |
||||
|
<p>{% lorem %}</p> |
||||
|
<ul class="actions"> |
||||
|
<li> |
||||
|
<a class="button" href="">En savoir plus</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</section> |
||||
|
<section> |
||||
|
<h2>Contacts</h2> |
||||
|
<table> |
||||
|
<tr> |
||||
|
<td>Email</td> |
||||
|
<td>coopemetz@gmail.com</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td>Prez</td> |
||||
|
<td>{% president %}</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td>V-Prez</td> |
||||
|
<td>{% vice_president %}</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td>Trésorier</td> |
||||
|
<td>{% treasurer %}</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td>Secrétaire</td> |
||||
|
<td>{% secretary %}</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td>Maitre brasseur</td> |
||||
|
<td>{% brewer %}</td> |
||||
|
</tr> |
||||
|
<tr> |
||||
|
<td>Epic Epicier</td> |
||||
|
<td>{% grocer %}</td> |
||||
|
</tr> |
||||
|
</table> |
||||
|
<ul class="icons"> |
||||
|
<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">coopeV3 © 2016 - 2018. Remi Delannoy - Guillaume Goessel - Yoann Pietri. Design: <a href="https://html5up.net">HTML5 UP</a>.</p> |
||||
|
|
||||
@ -0,0 +1,37 @@ |
|||||
|
{% if request.user.is_authenticated %} |
||||
|
<span class="tabulation2"> |
||||
|
<a href="{% url 'users:profile' request.user.pk %}">Mon profil</a> |
||||
|
</span> |
||||
|
<span class="tabulation2"> |
||||
|
<a href="{% url 'gestion:manage' %}">Caisse</a> |
||||
|
</span> |
||||
|
<span class="tabulation2"> |
||||
|
<a href="{% url 'users:index' %}">Gestion des clients</a> |
||||
|
</span> |
||||
|
<span class="tabulation2"> |
||||
|
<a href="{% url 'gestion:productsIndex' %}">Gestion des produits</a> |
||||
|
</span> |
||||
|
<span class="tabulation2"> |
||||
|
<a href="">Comptabilité</a> |
||||
|
</span> |
||||
|
<span class="tabulation2"> |
||||
|
<a href="">Classement</a> |
||||
|
</span> |
||||
|
<span class="tabulation2"> |
||||
|
<a href="">Classement sur l'année</a> |
||||
|
</span> |
||||
|
<span class="tabulation2"> |
||||
|
<a href="{% url 'preferences:generalPreferences' %}">Admin</a> |
||||
|
</span> |
||||
|
<span class="tabulation2"> |
||||
|
<a href="{% url 'preferences:cotisationsIndex' %}">Cotisations</a> |
||||
|
</span> |
||||
|
<span class="tabulation2"> |
||||
|
<a href="{% url 'preferences:paymentMethodsIndex' %}">Moyens de paiement</a> |
||||
|
</span> |
||||
|
<span class="tabulation2"> |
||||
|
<a href="{% url 'users:logout' %}">Deconnexion</a> |
||||
|
</span> |
||||
|
{% else %} |
||||
|
<a href="{% url 'users:login' %}">Connexion</a> |
||||
|
{% endif %} |
||||
@ -0,0 +1,8 @@ |
|||||
|
<input {{attrs}} name="{{name}}" type="text" class="form-control" placeholder="{{placeholder}}"/> |
||||
|
<script> |
||||
|
$(document).ready(function(){ |
||||
|
$("#" + {{attrs.id}}).click(function(){ |
||||
|
alert('lol') |
||||
|
}); |
||||
|
}; |
||||
|
</script> |
||||
@ -1,7 +1,8 @@ |
|||||
from django.contrib import admin |
from django.contrib import admin |
||||
|
|
||||
from .models import School, Profile |
from .models import School, Profile, CotisationHistory |
||||
|
|
||||
admin.site.register(School) |
admin.site.register(School) |
||||
admin.site.register(Profile) |
admin.site.register(Profile) |
||||
|
admin.site.register(CotisationHistory) |
||||
# Register your models here. |
# Register your models here. |
||||
|
|||||
@ -0,0 +1,33 @@ |
|||||
|
{% extends "base.html" %} |
||||
|
{% block entete %}<h1>Gestion des écoles</h1>{% endblock %} |
||||
|
{% block navbar %} |
||||
|
<ul> |
||||
|
<li><a href="#first">Liste des écoles</a></li> |
||||
|
</ul> |
||||
|
{% endblock %} |
||||
|
{% block content %} |
||||
|
<section id="first" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Liste des écoles</h2> |
||||
|
</header> |
||||
|
<a class="button" href="{% url 'users:createSchool' %}">Créer une école</a><br><br> |
||||
|
<div class="table-wrapper"> |
||||
|
<table> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th>Ecole</th> |
||||
|
<th>Administration</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
{% for school in schools %} |
||||
|
<tr> |
||||
|
<td>{{ school }}</td> |
||||
|
<td><a class="button small" href="{% url 'users:editSchool' school.pk %}">Modifier</a> <a class="button small" href="{% url 'users:deleteSchool' school.pk %}">Supprimer</a></td> |
||||
|
</tr> |
||||
|
{% endfor %} |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
</section> |
||||
|
{% endblock %} |
||||
@ -0,0 +1,33 @@ |
|||||
|
{% extends "base.html" %} |
||||
|
{% block entete %}<h1>Gestion des utilisateurs</h1>{% endblock %} |
||||
|
{% block navbar %} |
||||
|
<ul> |
||||
|
<li><a href="#first">Liste des utilisateurs</a></li> |
||||
|
</ul> |
||||
|
{% endblock %} |
||||
|
{% block content %} |
||||
|
<section id="first" class="main"> |
||||
|
<header class="major"> |
||||
|
<h2>Liste des utilisateurs</h2> |
||||
|
</header> |
||||
|
<a class="button" href="{% url 'users:createUser' %}">Créer un utilisateur</a><br><br> |
||||
|
<div class="table-wrapper"> |
||||
|
<table> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th>Utilisateur</th> |
||||
|
<th>Profil</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
{% for user in users %} |
||||
|
<tr> |
||||
|
<td>{{ user }}</td> |
||||
|
<td><a class="button small" href="{% url 'users:profile' user.pk %}">Profil</a></td> |
||||
|
</tr> |
||||
|
{% endfor %} |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
</section> |
||||
|
{% endblock %} |
||||
Loading…
Reference in new issue