diff --git a/gestion/forms.py b/gestion/forms.py index 95894b7..325f1b2 100644 --- a/gestion/forms.py +++ b/gestion/forms.py @@ -60,4 +60,9 @@ class SelectPositiveKegForm(forms.Form): keg = forms.ModelChoiceField(queryset=Keg.objects.filter(stockHold__gt = 0), required=True, label="Fût", widget=autocomplete.ModelSelect2(url='gestion:kegs-positive-autocomplete')) 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')) \ No newline at end of file + 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) \ No newline at end of file diff --git a/gestion/migrations/0002_pinte.py b/gestion/migrations/0002_pinte.py new file mode 100644 index 0000000..93f21fb --- /dev/null +++ b/gestion/migrations/0002_pinte.py @@ -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)), + ], + ), + ] diff --git a/gestion/migrations/0003_historicalpinte.py b/gestion/migrations/0003_historicalpinte.py new file mode 100644 index 0000000..243fc58 --- /dev/null +++ b/gestion/migrations/0003_historicalpinte.py @@ -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), + ), + ] diff --git a/gestion/migrations/0004_auto_20181223_1830.py b/gestion/migrations/0004_auto_20181223_1830.py new file mode 100644 index 0000000..de74c17 --- /dev/null +++ b/gestion/migrations/0004_auto_20181223_1830.py @@ -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), + ), + ] diff --git a/gestion/models.py b/gestion/models.py index 33828d6..ee03f91 100644 --- a/gestion/models.py +++ b/gestion/models.py @@ -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() diff --git a/gestion/templates/gestion/pintes_list.html b/gestion/templates/gestion/pintes_list.html new file mode 100644 index 0000000..88c0b95 --- /dev/null +++ b/gestion/templates/gestion/pintes_list.html @@ -0,0 +1,75 @@ +{% extends 'base.html' %} +{% block entete %}Gestion des produits{% endblock %} +{% block navbar%} + +{% endblock %} +{% block content %} +
+
+

Général

+
+ {% if perms.gestion.add_pinte %} + Créer une ou plusieurs pintes

+ {% endif %} + Il a y actuellement {{ taken_pintes.count|add:free_pintes.count }} pintes, parmis lesquelles {{ free_pintes.count }} sont rendues et {{ taken_pintes.count }} ne sont pas rendues. +
+
+
+

Liste des pintes non rendues

+
+
+ + + + + + + + + + + + {% for pinte in taken_pintes %} + + + + + + + + {% endfor %} + +
NuméroPossesseur actuelPossesseur précédentDate du dernier changementAdministrer
{{ pinte.pk }}{{ pinte.current_owner }}{{ pinte.previous_owner }}{{ pinte.last_update_date }}{% if perms.gestion.change_pinte %} Libérer{% endif %}
+
+
+
+
+

Liste des pintes rendues

+
+
+ + + + + + + + + + {% for pinte in free_pintes %} + + + + + + {% endfor %} + +
NuméroPossesseur précédentDate du dernier changement
{{ pinte.pk }}{{ pinte.previous_owner }}{{ pinte.last_update_date }}
+
+
+{% endblock %} diff --git a/gestion/templates/gestion/products_index.html b/gestion/templates/gestion/products_index.html index 2a3ade6..7df35e8 100644 --- a/gestion/templates/gestion/products_index.html +++ b/gestion/templates/gestion/products_index.html @@ -5,6 +5,7 @@
  • Produits
  • Futs
  • Menus
  • +
  • Pintes
  • {% endblock %} {% block content %} @@ -58,4 +59,21 @@ {% endif %} +
    +
    +

    Pintes

    +
    + Actions possibles : + +
    {% endblock %} diff --git a/gestion/urls.py b/gestion/urls.py index c05a330..ffce4b8 100644 --- a/gestion/urls.py +++ b/gestion/urls.py @@ -33,6 +33,10 @@ urlpatterns = [ path('cancelConsumption/', views.cancel_consumption, name="cancelConsumption"), path('cancelMenu/', views.cancel_menu, name="cancelMenu"), path('productProfile/', 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/', 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"), diff --git a/gestion/views.py b/gestion/views.py index 0111557..f19d30a 100644 --- a/gestion/views.py +++ b/gestion/views.py @@ -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): @@ -885,4 +903,81 @@ def ranking(request): alcohol = customer.profile.alcohol 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}) \ No newline at end of file + 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}) \ No newline at end of file diff --git a/preferences/migrations/0002_auto_20181221_2151.py b/preferences/migrations/0002_auto_20181221_2151.py new file mode 100644 index 0000000..f02f5f4 --- /dev/null +++ b/preferences/migrations/0002_auto_20181221_2151.py @@ -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), + ), + ] diff --git a/preferences/migrations/0003_auto_20181223_1440.py b/preferences/migrations/0003_auto_20181223_1440.py new file mode 100644 index 0000000..b1a489b --- /dev/null +++ b/preferences/migrations/0003_auto_20181223_1440.py @@ -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), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 8f41f74..0ac2d69 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -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): diff --git a/preferences/templates/preferences/general_preferences.html b/preferences/templates/preferences/general_preferences.html index 1addb98..08596d6 100644 --- a/preferences/templates/preferences/general_preferences.html +++ b/preferences/templates/preferences/general_preferences.html @@ -1,10 +1,11 @@ {% extends 'base.html' %} {% block entete %}Administration{% endblock %} -{% block nav %} +{% block navbar %} {% endblock %} @@ -75,6 +76,27 @@ {{form.brewer}} + + + +
    +
    +
    +
    +

    Suivi de pintes

    +
    +
    +
    + {{form.use_pinte_monitoring}} + +
    +
    +
    +
    + {{form.lost_pintes_allowed}} + +
    +
    diff --git a/preferences/urls.py b/preferences/urls.py index 7981698..dd7d3b8 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -14,4 +14,5 @@ urlpatterns = [ path('editPaymentMethod/', views.editPaymentMethod, name="editPaymentMethod"), path('deletePaymentMethod/', views.deletePaymentMethod, name="deletePaymentMethod"), path('inactive', views.inactive, name="inactive"), + path('getConfig', views.get_config, name="getConfig"), ] diff --git a/preferences/views.py b/preferences/views.py index 08c8976..3f9ec6d 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -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 @@ -238,4 +242,14 @@ 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') \ No newline at end of file diff --git a/staticfiles/manage.js b/staticfiles/manage.js index 39e174a..ad2691b 100644 --- a/staticfiles/manage.js +++ b/staticfiles/manage.js @@ -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' + product.name + '' + String(product.amount) + '' + String(Number((product.quantity * product.amount).toFixed(2))) + ''; @@ -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){ diff --git a/users/models.py b/users/models.py index bda3ba2..c3b0df9 100644 --- a/users/models.py +++ b/users/models.py @@ -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