3
0
Fork 0
mirror of https://github.com/nanoy42/coope synced 2024-11-25 22:22:27 +00:00

Merge branch 'suivi_pinte' into dev

Close #25
This commit is contained in:
Yoann Pétri 2018-12-23 18:53:02 +01:00
commit cf153f9ed9
17 changed files with 446 additions and 19 deletions

View file

@ -61,3 +61,8 @@ class SelectPositiveKegForm(forms.Form):
class SelectActiveKegForm(forms.Form):
keg = forms.ModelChoiceField(queryset=Keg.objects.filter(is_active = True), required=True, label="Fût", widget=autocomplete.ModelSelect2(url='gestion:kegs-active-autocomplete'))
class PinteForm(forms.Form):
ids = forms.CharField(widget=forms.Textarea, label="Numéros", help_text="Numéros séparés par un espace. Laissez vide pour utiliser le range.", required=False)
begin = forms.IntegerField(label="Début", help_text="Début du range", required=False)
end = forms.IntegerField(label="Fin", help_text="Fin du range", required=False)

View file

@ -0,0 +1,25 @@
# Generated by Django 2.1 on 2018-12-21 20:34
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),
('gestion', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Pinte',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('last_update_date', models.DateTimeField(auto_now=True)),
('current_owner', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pinte_owned_currently', to=settings.AUTH_USER_MODEL)),
('previous_owner', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pinte_owned_previously', to=settings.AUTH_USER_MODEL)),
],
),
]

View file

@ -0,0 +1,37 @@
# Generated by Django 2.1 on 2018-12-21 20:51
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import simple_history.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('gestion', '0002_pinte'),
]
operations = [
migrations.CreateModel(
name='HistoricalPinte',
fields=[
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('last_update_date', models.DateTimeField(blank=True, editable=False)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_date', models.DateTimeField()),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('current_owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('previous_owner', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical pinte',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': 'history_date',
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
]

View file

@ -0,0 +1,25 @@
# Generated by Django 2.1 on 2018-12-23 17:30
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('gestion', '0003_historicalpinte'),
]
operations = [
migrations.AlterField(
model_name='pinte',
name='current_owner',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pintes_owned_currently', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='pinte',
name='previous_owner',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pintes_owned_previously', to=settings.AUTH_USER_MODEL),
),
]

View file

@ -225,3 +225,12 @@ class Consumption(models.Model):
def __str__(self):
return "Consommation de " + str(self.customer) + " concernant le produit " + str(self.product)
class Pinte(models.Model):
"""
Stores a physical pinte
"""
current_owner = models.ForeignKey(User, on_delete=models.PROTECT, null=True, default=None, related_name="pintes_owned_currently")
previous_owner = models.ForeignKey(User, on_delete=models.PROTECT, null=True, default=None, related_name="pintes_owned_previously")
last_update_date = models.DateTimeField(auto_now=True)
history = HistoricalRecords()

View file

@ -0,0 +1,75 @@
{% extends 'base.html' %}
{% block entete %}Gestion des produits{% endblock %}
{% block navbar%}
<ul>
<li><a href="#first">Général</a></li>
<li><a href="#second">Liste des pintes non rendues</a></li>
<li><a href="#third">Liste des pintes rendues</a></li>
</ul>
{% endblock %}
{% block content %}
<section id="first" class="main">
<header class="major">
<h2>Général</h2>
</header>
{% if perms.gestion.add_pinte %}
<a class="button" href="{% url 'gestion:addPintes' %}">Créer une ou plusieurs pintes</a><br><br>
{% endif %}
Il a y actuellement {{ taken_pintes.count|add:free_pintes.count }} pintes, parmis lesquelles <strong>{{ free_pintes.count }} sont rendues</strong> et <strong>{{ taken_pintes.count }} ne sont pas rendues</strong>.
</section>
<section id="second" class="main">
<header class="major">
<h2>Liste des pintes non rendues</h2>
</header>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Numéro</th>
<th>Possesseur actuel</th>
<th>Possesseur précédent</th>
<th>Date du dernier changement</th>
<th>Administrer</th>
</tr>
</thead>
<tbody>
{% for pinte in taken_pintes %}
<tr>
<td>{{ pinte.pk }}</td>
<td>{{ pinte.current_owner }}</td>
<td>{{ pinte.previous_owner }}</td>
<td>{{ pinte.last_update_date }}</td>
<td>{% if perms.gestion.change_pinte %} <a href="{% url 'gestion:release' pinte.pk %}" class="button small">Libérer</a>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
<section id="third" class="main">
<header class="major">
<h2>Liste des pintes rendues</h2>
</header>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Numéro</th>
<th>Possesseur précédent</th>
<th>Date du dernier changement</th>
</tr>
</thead>
<tbody>
{% for pinte in free_pintes %}
<tr>
<td>{{ pinte.pk }}</td>
<td>{{ pinte.previous_owner }}</td>
<td>{{ pinte.last_update_date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% endblock %}

View file

@ -5,6 +5,7 @@
<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">Pintes</a></li>
</ul>
{% endblock %}
{% block content %}
@ -58,4 +59,21 @@
{% endif %}
</ul>
</section>
<section id="fourth" class="main">
<header class="major">
<h2>Pintes</h2>
</header>
Actions possibles :
<ul>
{% if perms.gestion.add_pinte %}
<li><a href="{% url 'gestion:addPintes' %}">Créer une ou plusieurs pintes</a></li>
{% endif %}
{% if perms.gestion.change_pinte %}
<li><a href="{% url 'gestion:releasePintes' %}">Libérer des pintes</a></li>
{% endif %}
{% if perms.gestion.view_pinte %}
<li><a href="{% url 'gestion:pintesList' %}">Lister les pintes</a></li>
{% endif %}
</ul>
</section>
{% endblock %}

View file

@ -33,6 +33,10 @@ urlpatterns = [
path('cancelConsumption/<int:pk>', views.cancel_consumption, name="cancelConsumption"),
path('cancelMenu/<int:pk>', views.cancel_menu, name="cancelMenu"),
path('productProfile/<int:pk>', views.productProfile, name="productProfile"),
path('addPintes', views.add_pintes, name="addPintes"),
path('releasePintes', views.release_pintes, name="releasePintes"),
path('pintesList', views.pintes_list, name="pintesList"),
path('release/<int:pinte_pk>', views.release, name="release"),
path('products-autocomplete', views.ProductsAutocomplete.as_view(), name="products-autocomplete"),
path('kegs-positive-autocomplete', views.KegPositiveAutocomplete.as_view(), name="kegs-positive-autocomplete"),
path('kegs-active-autocomplete', views.KegActiveAutocomplete.as_view(), name="kegs-active-autocomplete"),

View file

@ -13,9 +13,9 @@ import simplejson as json
from dal import autocomplete
from decimal import *
from .forms import ReloadForm, RefundForm, ProductForm, KegForm, MenuForm, GestionForm, SearchMenuForm, SearchProductForm, SelectPositiveKegForm, SelectActiveKegForm
from .models import Product, Menu, Keg, ConsumptionHistory, KegHistory, Consumption, MenuHistory
from preferences.models import PaymentMethod
from .forms import ReloadForm, RefundForm, ProductForm, KegForm, MenuForm, GestionForm, SearchMenuForm, SearchProductForm, SelectPositiveKegForm, SelectActiveKegForm, PinteForm
from .models import Product, Menu, Keg, ConsumptionHistory, KegHistory, Consumption, MenuHistory, Pinte
from preferences.models import PaymentMethod, GeneralPreferences
@active_required
@login_required
@ -107,6 +107,8 @@ def order(request):
amount = Decimal(request.POST['amount'])
order = json.loads(request.POST["order"])
menus = json.loads(request.POST["menus"])
listPintes = json.loads(request.POST["listPintes"])
gp,_ = GeneralPreferences.objects.get_or_create(pk=1)
if (not order) and (not menus):
return HttpResponse("Pas de commande")
adherentRequired = False
@ -118,6 +120,14 @@ def order(request):
adherentRequired = adherentRequired or menu.adherent_required
if(adherentRequired and not user.profile.is_adherent):
return HttpResponse("N'est pas adhérent et devrait l'être")
# Partie un peu complexe : je libère toutes les pintes de la commande, puis je test
# s'il a trop de pintes non rendues, puis je réalloue les pintes
for pinte in listPintes:
allocate(pinte, None)
if(gp.lost_pintes_allowed and user.profile.nb_pintes >= gp.lost_pintes_allowed):
return HttpResponse("Impossible de réaliser la commande : l'utilisateur a perdu trop de pintes.")
for pinte in listPintes:
allocate(pinte, user)
if(paymentMethod.affect_balance):
if(user.profile.balance < amount):
return HttpResponse("Solde inférieur au prix de la commande")
@ -418,7 +428,11 @@ def getProduct(request, barcode):
The requested barcode
"""
product = Product.objects.get(barcode=barcode)
data = json.dumps({"pk": product.pk, "barcode" : product.barcode, "name": product.name, "amount" : product.amount})
if product.category == Product.P_PRESSION:
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})
return HttpResponse(data, content_type='application/json')
@active_required
@ -845,7 +859,11 @@ def get_menu(request, barcode):
The requested barcode
"""
menu = get_object_or_404(Menu, barcode=barcode)
data = json.dumps({"pk": menu.pk, "barcode" : menu.barcode, "name": menu.name, "amount" : menu.amount})
nb_pintes = 0
for article in menu.articles:
if article.category == Product.P_PRESSION:
nb_pintes +=1
data = json.dumps({"pk": menu.pk, "barcode" : menu.barcode, "name": menu.name, "amount" : menu.amount, needQuantityButton: False, "nb_pintes": nb_pintes})
return HttpResponse(data, content_type='application/json')
class MenusAutocomplete(autocomplete.Select2QuerySetView):
@ -886,3 +904,80 @@ def ranking(request):
list.append([customer, alcohol])
bestDrinkers = sorted(list, key=lambda x: x[1], reverse=True)[:25]
return render(request, "gestion/ranking.html", {"bestBuyers": bestBuyers, "bestDrinkers": bestDrinkers})
########## Pinte monitoring ##########
def allocate(pinte_pk, user):
"""
Allocate a pinte to a user or release the pinte if user is None
"""
try:
pinte = Pinte.objects.get(pk=pinte_pk)
if pinte.current_owner is not None:
pinte.previous_owner = pinte.current_owner
pinte.current_owner = user
pinte.save()
return True
except Pinte.DoesNotExist:
return False
@active_required
@login_required
@permission_required('gestion.change_pinte')
def release(request, pinte_pk):
"""
View to release a pinte
"""
if allocate(pinte_pk, None):
messages.success(request, "La pinte a bien été libérée")
else:
messages.error(request, "Impossible de libérer la pinte")
return redirect(reverse('gestion:pintesList'))
@active_required
@login_required
@permission_required('gestion.add_pinte')
def add_pintes(request):
form = PinteForm(request.POST or None)
if form.is_valid():
ids = form.cleaned_data['ids']
if ids != "":
ids = ids.split(" ")
else:
ids = range(form.cleaned_data['begin'], form.cleaned_data['end'] + 1)
i = 0
for id in ids:
if not Pinte.objects.filter(pk=id).exists():
new_pinte = Pinte(pk=int(id))
new_pinte.save()
i += 1
messages.success(request, str(i) + " pinte(s) a(ont) été ajoutée(s)")
return redirect(reverse('gestion:productsIndex'))
return render(request, "form.html", {"form": form, "form_title": "Ajouter des pintes", "form_button": "Ajouter"})
@active_required
@login_required
@permission_required('gestion.change_pinte')
def release_pintes(request):
form = PinteForm(request.POST or None)
if form.is_valid():
ids = form.cleaned_data['ids']
if ids != "":
ids = ids.split(" ")
else:
ids = range(form.cleaned_data['begin'], form.cleaned_data['end'] + 1)
i = 0
for id in ids:
if allocate(id, None):
i += 1
messages.success(request, str(i) + " pinte(s) a(ont) été libérée(s)")
return redirect(reverse('gestion:productsIndex'))
return render(request, "form.html", {"form": form, "form_title": "Libérer des pintes", "form_button": "Libérer"})
@active_required
@login_required
@permission_required('gestion.view_pinte')
def pintes_list(request):
free_pintes = Pinte.objects.filter(current_owner=None)
taken_pintes = Pinte.objects.exclude(current_owner=None)
return render(request, "gestion/pintes_list.html", {"free_pintes": free_pintes, "taken_pintes": taken_pintes})

View file

@ -0,0 +1,23 @@
# Generated by Django 2.1 on 2018-12-21 20:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='generalpreferences',
name='use_pinte_monitoring',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='historicalgeneralpreferences',
name='use_pinte_monitoring',
field=models.BooleanField(default=False),
),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 2.1 on 2018-12-23 13:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0002_auto_20181221_2151'),
]
operations = [
migrations.AddField(
model_name='generalpreferences',
name='lost_pintes_allowed',
field=models.PositiveIntegerField(default=0),
),
migrations.AddField(
model_name='historicalgeneralpreferences',
name='lost_pintes_allowed',
field=models.PositiveIntegerField(default=0),
),
]

View file

@ -30,6 +30,8 @@ class GeneralPreferences(models.Model):
secretary = models.CharField(max_length=255, blank=True)
brewer = models.CharField(max_length=255, blank=True)
grocer = models.CharField(max_length=255, blank=True)
use_pinte_monitoring = models.BooleanField(default=False)
lost_pintes_allowed = models.PositiveIntegerField(default=0)
history = HistoricalRecords()
class Cotisation(models.Model):

View file

@ -1,10 +1,11 @@
{% extends 'base.html' %}
{% block entete %}Administration{% endblock %}
{% block nav %}
{% block navbar %}
<ul>
<li><a href="#first" class="active">Message global</a></li>
<li><a href="#first">Message global</a></li>
<li><a href="#second">Site actif</a></li>
<li><a href="#third">Bureau</a></li>
<li><a href="#fourth">Suivi de pintes</a></li>
</ul>
{% endblock %}
@ -75,6 +76,27 @@
{{form.brewer}}
</div>
</div>
</div>
</div>
</section>
<section id="fourth" class="main">
<div class="spotlight">
<div class="content">
<header class="major">
<h2>Suivi de pintes</h2>
</header>
<div class="row uniform">
<div class="12u">
{{form.use_pinte_monitoring}}
<label for="{{form.use_pinte_monitoring.id_for_label}}">Utiliser le suivi de pinte ?</label>
</div>
</div>
<div class="row uniform">
<div class="12u">
{{form.lost_pintes_allowed}}
<label for="{{form.lost_pintes_allowed.id_for_label}}">Nombre de pintes non rendues avant d'interdire la consommation (0 pour ne jamais l'interdire, nécessite le suivi de pinte actif)</label>
</div>
</div>
<div class="row uniform">
<div class="12u">
<button type="submit">Enregistrer</button>

View file

@ -14,4 +14,5 @@ urlpatterns = [
path('editPaymentMethod/<int:pk>', views.editPaymentMethod, name="editPaymentMethod"),
path('deletePaymentMethod/<int:pk>', views.deletePaymentMethod, name="deletePaymentMethod"),
path('inactive', views.inactive, name="inactive"),
path('getConfig', views.get_config, name="getConfig"),
]

View file

@ -1,7 +1,11 @@
import json
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.urls import reverse
from django.contrib.auth.decorators import login_required, permission_required
from django.http import HttpResponse
from django.forms.models import model_to_dict
from coopeV3.acl import active_required
@ -239,3 +243,13 @@ def inactive(request):
gp, _ = GeneralPreferences.objects.get_or_create(pk=1)
return render(request, 'preferences/inactive.html', {"message": gp.active_message})
########## Config ##########
def get_config(request):
"""
Load the config and return it in a json format
"""
gp,_ = GeneralPreferences.objects.get_or_create(pk=1)
data = json.dumps(model_to_dict(gp))
return HttpResponse(data, content_type='application/json')

View file

@ -5,20 +5,32 @@ paymentMethod = null
balance = 0
username = ""
id = 0
listPintes = []
nbPintes = 0;
use_pinte_monitoring = false;
function get_config(){
res = $.get("../preferences/getConfig", function(data){
console.log(data.use_pinte_monitoring)
use_pinte_monitoring = data.use_pinte_monitoring;
});
}
function get_product(barcode){
res = $.get("getProduct/" + barcode, function(data){
add_product(data.pk, data.barcode, data.name, data.amount);
nbPintes += data.nb_pintes;
add_product(data.pk, data.barcode, data.name, data.amount, data.needQuantityButton);
});
}
function get_menu(barcode){
res = $.get("getMenu/" + barcode, function(data){
add_menu(data.pk, data.barcode, data.name, data.amount);
nbPintes += data.nb_pintes;
add_menu(data.pk, data.barcode, data.name, data.amount, data.needQuantityButton);
});
}
function add_product(pk, barcode, name, amount){
function add_product(pk, barcode, name, amount, needQuantityButton){
exist = false
index = -1
for(k=0;k < products.length; k++){
@ -27,10 +39,18 @@ function add_product(pk, barcode, name, amount){
index = k
}
}
if(exist){
products[index].quantity += 1;
if(needQuantityButton){
quantity = parseInt(window.prompt("Quantité ?",""));
}else{
products.push({"pk": pk, "barcode": barcode, "name": name, "amount": amount, "quantity": 1});
quantity = 1;
}
if(quantity == null || !Number.isInteger(quantity)){
quantity = 1;
}
if(exist){
products[index].quantity += quantity;
}else{
products.push({"pk": pk, "barcode": barcode, "name": name, "amount": amount, "quantity": quantity});
}
generate_html()
}
@ -53,7 +73,7 @@ function add_menu(pk, barcode, name, amount){
}
function generate_html(){
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><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>';
@ -94,6 +114,7 @@ function updateMenuInput(a){
}
$(document).ready(function(){
get_config();
$(".product").click(function(){
product = get_product($(this).attr('target'));
});
@ -113,7 +134,25 @@ $(document).ready(function(){
});
});
$(".pay_button").click(function(){
$.post("order", {"user":id, "paymentMethod": $(this).attr('data-payment'), "order_length": products.length + menus.length, "order": JSON.stringify(products), "amount": total, "menus": JSON.stringify(menus)}, function(data){
if(use_pinte_monitoring){
message = "Il reste " + nbPintes.toString() + " pintes à renseigner. Numéro de la pinte ?"
while(nbPintes > 0){
id_pinte = window.prompt(message,"");
if(id_pinte == null){
return;
}else{
id_pinte = parseInt(id_pinte);
if(!Number.isInteger(id_pinte) || id_pinte < 0){
message = "Numéro incorrect. Il reste " + nbPintes.toString() + " pintes à renseigner. Numéro de la pinte ?";
}else{
listPintes.push(id_pinte)
nbPintes -= 1;
message = "Il reste " + nbPintes.toString() + " pintes à renseigner. Numéro de la pinte ?"
}
}
}
}
$.post("order", {"user":id, "paymentMethod": $(this).attr('data-payment'), "order_length": products.length + menus.length, "order": JSON.stringify(products), "amount": total, "menus": JSON.stringify(menus), "listPintes": JSON.stringify(listPintes)}, function(data){
alert(data);
location.reload();
}).fail(function(data){

View file

@ -119,6 +119,13 @@ class Profile(models.Model):
alcohol += consumption.quantity * float(product.deg) * product.volume * 0.79 /10 /1000
return alcohol
@property
def nb_pintes(self):
"""
Return the number of pintes currently owned
"""
return self.user.pintes_owned_currently.count()
def __str__(self):
return str(self.user)
@ -128,9 +135,12 @@ class Profile(models.Model):
tente de retourner l'attribut de l'user associé à l'instance
"""
try:
r = super().__getattr__(name)
r = self.__getattribute__(name)
except AttributeError:
r = getattr(self.user, name)
try:
r = super().__getattr__(name)
except AttributeError:
r = getattr(self.user, name)
return r