Merge branch 'master' into aureplop/kfet_open

This commit is contained in:
Aurélien Delobelle 2017-06-12 15:18:42 +02:00
commit ec59bc2edc
100 changed files with 3401 additions and 2508 deletions

View file

@ -13,32 +13,76 @@ from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente
class ReadOnlyMixin(object):
readonly_fields_update = ()
def get_readonly_fields(self, request, obj=None):
readonly_fields = super().get_readonly_fields(request, obj)
if obj is None:
return readonly_fields
else:
return readonly_fields + self.readonly_fields_update
class ChoixSpectacleInline(admin.TabularInline): class ChoixSpectacleInline(admin.TabularInline):
model = ChoixSpectacle model = ChoixSpectacle
sortable_field_name = "priority" sortable_field_name = "priority"
class AttributionTabularAdminForm(forms.ModelForm):
listing = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
spectacles = Spectacle.objects.select_related('location')
if self.listing is not None:
spectacles = spectacles.filter(listing=self.listing)
self.fields['spectacle'].queryset = spectacles
class WithoutListingAttributionTabularAdminForm(AttributionTabularAdminForm):
listing = False
class WithListingAttributionTabularAdminForm(AttributionTabularAdminForm):
listing = True
class AttributionInline(admin.TabularInline): class AttributionInline(admin.TabularInline):
model = Attribution model = Attribution
extra = 0 extra = 0
listing = None
def get_queryset(self, request): def get_queryset(self, request):
qs = super(AttributionInline, self).get_queryset(request) qs = super().get_queryset(request)
return qs.filter(spectacle__listing=False) if self.listing is not None:
qs.filter(spectacle__listing=self.listing)
return qs
class AttributionInlineListing(admin.TabularInline): class WithListingAttributionInline(AttributionInline):
model = Attribution form = WithListingAttributionTabularAdminForm
listing = True
class WithoutListingAttributionInline(AttributionInline):
exclude = ('given', ) exclude = ('given', )
extra = 0 form = WithoutListingAttributionTabularAdminForm
listing = False
def get_queryset(self, request):
qs = super(AttributionInlineListing, self).get_queryset(request)
return qs.filter(spectacle__listing=True)
class ParticipantAdmin(admin.ModelAdmin): class ParticipantAdminForm(forms.ModelForm):
inlines = [AttributionInline, AttributionInlineListing]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['choicesrevente'].queryset = (
Spectacle.objects
.select_related('location')
)
class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
inlines = [WithListingAttributionInline, WithoutListingAttributionInline]
def get_queryset(self, request): def get_queryset(self, request):
return Participant.objects.annotate(nb_places=Count('attributions'), return Participant.objects.annotate(nb_places=Count('attributions'),
@ -65,6 +109,8 @@ class ParticipantAdmin(admin.ModelAdmin):
actions_on_bottom = True actions_on_bottom = True
list_per_page = 400 list_per_page = 400
readonly_fields = ("total",) readonly_fields = ("total",)
readonly_fields_update = ('user', 'tirage')
form = ParticipantAdminForm
def send_attribs(self, request, queryset): def send_attribs(self, request, queryset):
datatuple = [] datatuple = []
@ -94,6 +140,20 @@ class ParticipantAdmin(admin.ModelAdmin):
class AttributionAdminForm(forms.ModelForm): class AttributionAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'spectacle' in self.fields:
self.fields['spectacle'].queryset = (
Spectacle.objects
.select_related('location')
)
if 'participant' in self.fields:
self.fields['participant'].queryset = (
Participant.objects
.select_related('user', 'tirage')
)
def clean(self): def clean(self):
cleaned_data = super(AttributionAdminForm, self).clean() cleaned_data = super(AttributionAdminForm, self).clean()
participant = cleaned_data.get("participant") participant = cleaned_data.get("participant")
@ -106,7 +166,7 @@ class AttributionAdminForm(forms.ModelForm):
return cleaned_data return cleaned_data
class AttributionAdmin(admin.ModelAdmin): class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
def paid(self, obj): def paid(self, obj):
return obj.participant.paid return obj.participant.paid
paid.short_description = 'A payé' paid.short_description = 'A payé'
@ -116,6 +176,7 @@ class AttributionAdmin(admin.ModelAdmin):
'participant__user__first_name', 'participant__user__first_name',
'participant__user__last_name') 'participant__user__last_name')
form = AttributionAdminForm form = AttributionAdminForm
readonly_fields_update = ('spectacle', 'participant')
class ChoixSpectacleAdmin(admin.ModelAdmin): class ChoixSpectacleAdmin(admin.ModelAdmin):
@ -160,6 +221,24 @@ class SalleAdmin(admin.ModelAdmin):
search_fields = ('name', 'address') search_fields = ('name', 'address')
class SpectacleReventeAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['answered_mail'].queryset = (
Participant.objects
.select_related('user', 'tirage')
)
self.fields['seller'].queryset = (
Participant.objects
.select_related('user', 'tirage')
)
self.fields['soldTo'].queryset = (
Participant.objects
.select_related('user', 'tirage')
)
class SpectacleReventeAdmin(admin.ModelAdmin): class SpectacleReventeAdmin(admin.ModelAdmin):
""" """
Administration des reventes de spectacles Administration des reventes de spectacles
@ -182,6 +261,7 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
actions = ['transfer', 'reinit'] actions = ['transfer', 'reinit']
actions_on_bottom = True actions_on_bottom = True
form = SpectacleReventeAdminForm
def transfer(self, request, queryset): def transfer(self, request, queryset):
""" """

View file

@ -22,8 +22,7 @@ class Algorithm(object):
show.requests show.requests
- on crée des tables de demandes pour chaque personne, afin de - on crée des tables de demandes pour chaque personne, afin de
pouvoir modifier les rankings""" pouvoir modifier les rankings"""
self.max_group = \ self.max_group = 2*max(choice.priority for choice in choices)
2 * choices.aggregate(Max('priority'))['priority__max']
self.shows = [] self.shows = []
showdict = {} showdict = {}
for show in shows: for show in shows:

View file

@ -1,35 +1,40 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django import forms from django import forms
from django.forms.models import BaseInlineFormSet from django.forms.models import BaseInlineFormSet
from django.utils import timezone from django.utils import timezone
from bda.models import Attribution, Spectacle from bda.models import Attribution, Spectacle
class BaseBdaFormSet(BaseInlineFormSet): class InscriptionInlineFormSet(BaseInlineFormSet):
def clean(self):
"""Checks that no two articles have the same title.""" def __init__(self, *args, **kwargs):
super(BaseBdaFormSet, self).clean() super().__init__(*args, **kwargs)
if any(self.errors):
# Don't bother validating the formset unless each form is valid on # self.instance is a Participant object
# its own tirage = self.instance.tirage
return
spectacles = [] # set once for all "spectacle" field choices
for i in range(0, self.total_form_count()): # - restrict choices to the spectacles of this tirage
form = self.forms[i] # - force_choices avoid many db requests
if not form.cleaned_data: spectacles = tirage.spectacle_set.select_related('location')
continue choices = [(sp.pk, str(sp)) for sp in spectacles]
spectacle = form.cleaned_data['spectacle'] self.force_choices('spectacle', choices)
delete = form.cleaned_data['DELETE']
if not delete and spectacle in spectacles: def force_choices(self, name, choices):
raise forms.ValidationError( """Set choices of a field.
"Vous ne pouvez pas vous inscrire deux fois pour le "
"même spectacle.") As ModelChoiceIterator (default use to get choices of a
spectacles.append(spectacle) ModelChoiceField), it appends an empty selection if requested.
"""
for form in self.forms:
field = form.fields[name]
if field.empty_label is not None:
field.choices = [('', field.empty_label)] + choices
else:
field.choices = choices
class TokenForm(forms.Form): class TokenForm(forms.Form):
@ -38,7 +43,7 @@ class TokenForm(forms.Form):
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField): class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj): def label_from_instance(self, obj):
return "%s" % obj.spectacle return "%s" % str(obj.spectacle)
class ResellForm(forms.Form): class ResellForm(forms.Form):
@ -50,9 +55,13 @@ class ResellForm(forms.Form):
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super(ResellForm, self).__init__(*args, **kwargs) super(ResellForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = participant.attribution_set\ self.fields['attributions'].queryset = (
.filter(spectacle__date__gte=timezone.now())\ participant.attribution_set
.filter(spectacle__date__gte=timezone.now())
.exclude(revente__seller=participant) .exclude(revente__seller=participant)
.select_related('spectacle', 'spectacle__location',
'participant__user')
)
class AnnulForm(forms.Form): class AnnulForm(forms.Form):
@ -64,11 +73,15 @@ class AnnulForm(forms.Form):
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super(AnnulForm, self).__init__(*args, **kwargs) super(AnnulForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = participant.attribution_set\ self.fields['attributions'].queryset = (
participant.attribution_set
.filter(spectacle__date__gte=timezone.now(), .filter(spectacle__date__gte=timezone.now(),
revente__isnull=False, revente__isnull=False,
revente__notif_sent=False, revente__notif_sent=False,
revente__soldTo__isnull=True) revente__soldTo__isnull=True)
.select_related('spectacle', 'spectacle__location',
'participant__user')
)
class InscriptionReventeForm(forms.Form): class InscriptionReventeForm(forms.Form):
@ -79,8 +92,11 @@ class InscriptionReventeForm(forms.Form):
def __init__(self, tirage, *args, **kwargs): def __init__(self, tirage, *args, **kwargs):
super(InscriptionReventeForm, self).__init__(*args, **kwargs) super(InscriptionReventeForm, self).__init__(*args, **kwargs)
self.fields['spectacles'].queryset = tirage.spectacle_set.filter( self.fields['spectacles'].queryset = (
date__gte=timezone.now()) tirage.spectacle_set
.select_related('location')
.filter(date__gte=timezone.now())
)
class SoldForm(forms.Form): class SoldForm(forms.Form):
@ -93,7 +109,9 @@ class SoldForm(forms.Form):
super(SoldForm, self).__init__(*args, **kwargs) super(SoldForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = ( self.fields['attributions'].queryset = (
participant.attribution_set participant.attribution_set
.filter(revente__isnull=False, .filter(revente__isnull=False,
revente__soldTo__isnull=False) revente__soldTo__isnull=False)
.exclude(revente__soldTo=participant) .exclude(revente__soldTo=participant)
.select_related('spectacle', 'spectacle__location',
'participant__user')
) )

View file

@ -4,6 +4,8 @@
{% block realcontent %} {% block realcontent %}
<h2>Revente de place</h2> <h2>Revente de place</h2>
{% with resell_attributions=resellform.attributions annul_attributions=annulform.attributions sold_attributions=soldform.attributions %}
{% if resellform.attributions %} {% if resellform.attributions %}
<h3>Places non revendues</h3> <h3>Places non revendues</h3>
<form class="form-horizontal" action="" method="post"> <form class="form-horizontal" action="" method="post">
@ -15,14 +17,14 @@
</form> </form>
{% endif %} {% endif %}
<br> <br>
{% if annulform.attributions or overdue %} {% if annul_attributions or overdue %}
<h3>Places en cours de revente</h3> <h3>Places en cours de revente</h3>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
<div class='form-group'> <div class='form-group'>
<div class='multiple-checkbox'> <div class='multiple-checkbox'>
<ul> <ul>
{% for attrib in annulform.attributions %} {% for attrib in annul_attributions %}
<li>{{attrib.tag}} {{attrib.choice_label}}</li> <li>{{attrib.tag}} {{attrib.choice_label}}</li>
{% endfor %} {% endfor %}
{% for attrib in overdue %} {% for attrib in overdue %}
@ -31,13 +33,13 @@
{{attrib.spectacle}} {{attrib.spectacle}}
</li> </li>
{% endfor %} {% endfor %}
{% if annulform.attributions %} {% if annul_attributions %}
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées"> <input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
{% endif %} {% endif %}
</form> </form>
{% endif %} {% endif %}
<br> <br>
{% if soldform.attributions %} {% if sold_attributions %}
<h3>Places revendues</h3> <h3>Places revendues</h3>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
@ -46,8 +48,9 @@
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button> <button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
</form> </form>
{% endif %} {% endif %}
{% if not resellform.attributions and not soldform.attributions and not overdue and not annulform.attributions %} {% if not resell_attributions and not annul_attributions and not overdue and not sold_attributions %}
<p>Plus de reventes possibles !</p> <p>Plus de reventes possibles !</p>
{% endif %} {% endif %}
{% endwith %}
{% endblock %} {% endblock %}

View file

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from collections import defaultdict
from functools import partial
import random import random
import hashlib import hashlib
import time import time
@ -11,9 +13,9 @@ from custommail.shortcuts import (
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.db import models, transaction from django.db import transaction
from django.core import serializers from django.core import serializers
from django.db.models import Count, Q, Sum from django.db.models import Count, Q, Prefetch
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django.http import ( from django.http import (
HttpResponseBadRequest, HttpResponseRedirect, JsonResponse HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
@ -29,8 +31,8 @@ from bda.models import (
) )
from bda.algorithm import Algorithm from bda.algorithm import Algorithm
from bda.forms import ( from bda.forms import (
BaseBdaFormSet, TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm,
SoldForm InscriptionInlineFormSet,
) )
@ -44,39 +46,44 @@ def etat_places(request, tirage_id):
Et le total de toutes les demandes Et le total de toutes les demandes
""" """
tirage = get_object_or_404(Tirage, id=tirage_id) tirage = get_object_or_404(Tirage, id=tirage_id)
spectacles1 = ChoixSpectacle.objects \
.filter(spectacle__tirage=tirage) \ spectacles = tirage.spectacle_set.select_related('location')
.filter(double_choice="1") \ spectacles_dict = {} # index of spectacle by id
.all() \
.values('spectacle', 'spectacle__title') \
.annotate(total=models.Count('spectacle'))
spectacles2 = ChoixSpectacle.objects \
.filter(spectacle__tirage=tirage) \
.exclude(double_choice="1") \
.all() \
.values('spectacle', 'spectacle__title') \
.annotate(total=models.Count('spectacle'))
spectacles = tirage.spectacle_set.all()
spectacles_dict = {}
total = 0
for spectacle in spectacles: for spectacle in spectacles:
spectacle.total = 0 spectacle.total = 0 # init total requests
spectacle.ratio = 0.0
spectacles_dict[spectacle.id] = spectacle spectacles_dict[spectacle.id] = spectacle
for spectacle in spectacles1:
spectacles_dict[spectacle["spectacle"]].total += spectacle["total"] choices = (
spectacles_dict[spectacle["spectacle"]].ratio = \ ChoixSpectacle.objects
spectacles_dict[spectacle["spectacle"]].total / \ .filter(spectacle__in=spectacles)
spectacles_dict[spectacle["spectacle"]].slots .values('spectacle')
total += spectacle["total"] .annotate(total=Count('spectacle'))
for spectacle in spectacles2: )
spectacles_dict[spectacle["spectacle"]].total += 2*spectacle["total"]
spectacles_dict[spectacle["spectacle"]].ratio = \ # choices *by spectacles* whose only 1 place is requested
spectacles_dict[spectacle["spectacle"]].total / \ choices1 = choices.filter(double_choice="1")
spectacles_dict[spectacle["spectacle"]].slots # choices *by spectacles* whose 2 places is requested
total += 2*spectacle["total"] choices2 = choices.exclude(double_choice="1")
for spectacle in choices1:
pk = spectacle['spectacle']
spectacles_dict[pk].total += spectacle['total']
for spectacle in choices2:
pk = spectacle['spectacle']
spectacles_dict[pk].total += 2*spectacle['total']
# here, each spectacle.total contains the number of requests
slots = 0 # proposed slots
total = 0 # requests
for spectacle in spectacles:
slots += spectacle.slots
total += spectacle.total
spectacle.ratio = spectacle.total / spectacle.slots
context = { context = {
"proposed": tirage.spectacle_set.aggregate(Sum('slots'))['slots__sum'], "proposed": slots,
"spectacles": spectacles, "spectacles": spectacles,
"total": total, "total": total,
'tirage': tirage 'tirage': tirage
@ -94,11 +101,16 @@ def _hash_queryset(queryset):
@cof_required @cof_required
def places(request, tirage_id): def places(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id) tirage = get_object_or_404(Tirage, id=tirage_id)
participant, created = Participant.objects.get_or_create( participant, _ = (
user=request.user, tirage=tirage) Participant.objects
places = participant.attribution_set.order_by( .get_or_create(user=request.user, tirage=tirage)
"spectacle__date", "spectacle").all() )
total = sum([place.spectacle.price for place in places]) places = (
participant.attribution_set
.order_by("spectacle__date", "spectacle")
.select_related("spectacle", "spectacle__location")
)
total = sum(place.spectacle.price for place in places)
filtered_places = [] filtered_places = []
places_dict = {} places_dict = {}
spectacles = [] spectacles = []
@ -146,35 +158,31 @@ def inscription(request, tirage_id):
messages.error(request, "Le tirage n'est pas encore ouvert : " messages.error(request, "Le tirage n'est pas encore ouvert : "
"ouverture le {:s}".format(opening)) "ouverture le {:s}".format(opening))
return render(request, 'bda/resume-inscription-tirage.html', {}) return render(request, 'bda/resume-inscription-tirage.html', {})
participant, _ = (
Participant.objects.select_related('tirage')
.get_or_create(user=request.user, tirage=tirage)
)
if timezone.now() > tirage.fermeture: if timezone.now() > tirage.fermeture:
# Le tirage est fermé. # Le tirage est fermé.
participant, created = Participant.objects.get_or_create( choices = participant.choixspectacle_set.order_by("priority")
user=request.user, tirage=tirage)
choices = participant.choixspectacle_set.order_by("priority").all()
messages.error(request, messages.error(request,
" C'est fini : tirage au sort dans la journée !") " C'est fini : tirage au sort dans la journée !")
return render(request, "bda/resume-inscription-tirage.html", return render(request, "bda/resume-inscription-tirage.html",
{"choices": choices}) {"choices": choices})
def formfield_callback(f, **kwargs):
"""
Fonction utilisée par inlineformset_factory ci dessous.
Restreint les spectacles proposés aux spectacles du bo tirage.
"""
if f.name == "spectacle":
kwargs['queryset'] = tirage.spectacle_set
return f.formfield(**kwargs)
BdaFormSet = inlineformset_factory( BdaFormSet = inlineformset_factory(
Participant, Participant,
ChoixSpectacle, ChoixSpectacle,
fields=("spectacle", "double_choice", "priority"), fields=("spectacle", "double_choice", "priority"),
formset=BaseBdaFormSet, formset=InscriptionInlineFormSet,
formfield_callback=formfield_callback) )
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
success = False success = False
stateerror = False stateerror = False
if request.method == "POST": if request.method == "POST":
# use *this* queryset
dbstate = _hash_queryset(participant.choixspectacle_set.all()) dbstate = _hash_queryset(participant.choixspectacle_set.all())
if "dbstate" in request.POST and dbstate != request.POST["dbstate"]: if "dbstate" in request.POST and dbstate != request.POST["dbstate"]:
stateerror = True stateerror = True
@ -187,9 +195,14 @@ def inscription(request, tirage_id):
formset = BdaFormSet(instance=participant) formset = BdaFormSet(instance=participant)
else: else:
formset = BdaFormSet(instance=participant) formset = BdaFormSet(instance=participant)
# use *this* queryset
dbstate = _hash_queryset(participant.choixspectacle_set.all()) dbstate = _hash_queryset(participant.choixspectacle_set.all())
total_price = 0 total_price = 0
for choice in participant.choixspectacle_set.all(): choices = (
participant.choixspectacle_set
.select_related('spectacle')
)
for choice in choices:
total_price += choice.spectacle.price total_price += choice.spectacle.price
if choice.double: if choice.double:
total_price += choice.spectacle.price total_price += choice.spectacle.price
@ -218,9 +231,9 @@ def do_tirage(tirage_elt, token):
# Initialisation du dictionnaire data qui va contenir les résultats # Initialisation du dictionnaire data qui va contenir les résultats
start = time.time() start = time.time()
data = { data = {
'shows': tirage_elt.spectacle_set.select_related().all(), 'shows': tirage_elt.spectacle_set.select_related('location'),
'token': token, 'token': token,
'members': tirage_elt.participant_set.all(), 'members': tirage_elt.participant_set.select_related('user'),
'total_slots': 0, 'total_slots': 0,
'total_losers': 0, 'total_losers': 0,
'total_sold': 0, 'total_sold': 0,
@ -233,7 +246,7 @@ def do_tirage(tirage_elt, token):
ChoixSpectacle.objects ChoixSpectacle.objects
.filter(spectacle__tirage=tirage_elt) .filter(spectacle__tirage=tirage_elt)
.order_by('participant', 'priority') .order_by('participant', 'priority')
.select_related().all() .select_related('participant', 'participant__user', 'spectacle')
) )
results = Algorithm(data['shows'], data['members'], choices)(token) results = Algorithm(data['shows'], data['members'], choices)(token)
@ -290,10 +303,30 @@ def do_tirage(tirage_elt, token):
]) ])
# On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues # On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues
for (show, _, losers) in results: ChoixRevente = Participant.choicesrevente.through
for (loser, _, _, _) in losers:
loser.choicesrevente.add(show) # Suppression des reventes demandées/enregistrées (si le tirage est relancé)
loser.save() (
ChoixRevente.objects
.filter(spectacle__tirage=tirage_elt)
.delete()
)
(
SpectacleRevente.objects
.filter(attribution__spectacle__tirage=tirage_elt)
.delete()
)
lost_by = defaultdict(set)
for show, _, losers in results:
for loser, _, _, _ in losers:
lost_by[loser].add(show)
ChoixRevente.objects.bulk_create(
ChoixRevente(participant=member, spectacle=show)
for member, shows in lost_by.items()
for show in shows
)
data["duration"] = time.time() - start data["duration"] = time.time() - start
data["results"] = results data["results"] = results
@ -458,7 +491,6 @@ def list_revente(request, tirage_id):
) )
if min_resell is not None: if min_resell is not None:
min_resell.answered_mail.add(participant) min_resell.answered_mail.add(participant)
min_resell.save()
inscrit_revente.append(spectacle) inscrit_revente.append(spectacle)
success = True success = True
else: else:
@ -496,13 +528,13 @@ def buy_revente(request, spectacle_id):
# Si l'utilisateur veut racheter une place qu'il est en train de revendre, # Si l'utilisateur veut racheter une place qu'il est en train de revendre,
# on supprime la revente en question. # on supprime la revente en question.
if reventes.filter(seller=participant).exists(): own_reventes = reventes.filter(seller=participant)
revente = reventes.filter(seller=participant)[0] if len(own_reventes) > 0:
revente.delete() own_reventes[0].delete()
return HttpResponseRedirect(reverse("bda-shotgun", return HttpResponseRedirect(reverse("bda-shotgun",
args=[tirage.id])) args=[tirage.id]))
reventes_shotgun = list(reventes.filter(shotgun=True).all()) reventes_shotgun = reventes.filter(shotgun=True)
if not reventes_shotgun: if not reventes_shotgun:
return render(request, "bda-no-revente.html", {}) return render(request, "bda-no-revente.html", {})
@ -534,16 +566,21 @@ def buy_revente(request, spectacle_id):
@login_required @login_required
def revente_shotgun(request, tirage_id): def revente_shotgun(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id) tirage = get_object_or_404(Tirage, id=tirage_id)
spectacles = tirage.spectacle_set.filter( spectacles = (
date__gte=timezone.now()) tirage.spectacle_set
shotgun = [] .filter(date__gte=timezone.now())
for spectacle in spectacles: .select_related('location')
reventes = SpectacleRevente.objects.filter( .prefetch_related(Prefetch(
attribution__spectacle=spectacle, 'attribues',
shotgun=True, queryset=(
soldTo__isnull=True) Attribution.objects
if reventes.exists(): .filter(revente__shotgun=True,
shotgun.append(spectacle) revente__soldTo__isnull=True)
),
to_attr='shotguns',
))
)
shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0]
return render(request, "bda-shotgun.html", return render(request, "bda-shotgun.html",
{"shotgun": shotgun}) {"shotgun": shotgun})
@ -553,7 +590,10 @@ def revente_shotgun(request, tirage_id):
def spectacle(request, tirage_id, spectacle_id): def spectacle(request, tirage_id, spectacle_id):
tirage = get_object_or_404(Tirage, id=tirage_id) tirage = get_object_or_404(Tirage, id=tirage_id)
spectacle = get_object_or_404(Spectacle, id=spectacle_id, tirage=tirage) spectacle = get_object_or_404(Spectacle, id=spectacle_id, tirage=tirage)
attributions = spectacle.attribues.all() attributions = (
spectacle.attribues
.select_related('participant', 'participant__user')
)
participants = {} participants = {}
for attrib in attributions: for attrib in attributions:
participant = attrib.participant participant = attrib.participant
@ -582,7 +622,10 @@ class SpectacleListView(ListView):
def get_queryset(self): def get_queryset(self):
self.tirage = get_object_or_404(Tirage, id=self.kwargs['tirage_id']) self.tirage = get_object_or_404(Tirage, id=self.kwargs['tirage_id'])
categories = self.tirage.spectacle_set.all() categories = (
self.tirage.spectacle_set
.select_related('location')
)
return categories return categories
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -595,9 +638,12 @@ class SpectacleListView(ListView):
@buro_required @buro_required
def unpaid(request, tirage_id): def unpaid(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id) tirage = get_object_or_404(Tirage, id=tirage_id)
unpaid = tirage.participant_set \ unpaid = (
.annotate(nb_attributions=Count('attribution')) \ tirage.participant_set
.filter(paid=False, nb_attributions__gt=0).all() .annotate(nb_attributions=Count('attribution'))
.filter(paid=False, nb_attributions__gt=0)
.select_related('user')
)
return render(request, "bda-unpaid.html", {"unpaid": unpaid}) return render(request, "bda-unpaid.html", {"unpaid": unpaid})
@ -632,7 +678,11 @@ def send_rappel(request, spectacle_id):
def descriptions_spectacles(request, tirage_id): def descriptions_spectacles(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id) tirage = get_object_or_404(Tirage, id=tirage_id)
shows_qs = tirage.spectacle_set shows_qs = (
tirage.spectacle_set
.select_related('location')
.prefetch_related('quote_set')
)
category_name = request.GET.get('category', '') category_name = request.GET.get('category', '')
location_id = request.GET.get('location', '') location_id = request.GET.get('location', '')
if category_name: if category_name:
@ -643,7 +693,7 @@ def descriptions_spectacles(request, tirage_id):
except ValueError: except ValueError:
return HttpResponseBadRequest( return HttpResponseBadRequest(
"La variable GET 'location' doit contenir un entier") "La variable GET 'location' doit contenir un entier")
return render(request, 'descriptions.html', {'shows': shows_qs.all()}) return render(request, 'descriptions.html', {'shows': shows_qs})
def catalogue(request, request_type): def catalogue(request, request_type):
@ -716,7 +766,11 @@ def catalogue(request, request_type):
) )
tirage = get_object_or_404(Tirage, id=tirage_id) tirage = get_object_or_404(Tirage, id=tirage_id)
shows_qs = tirage.spectacle_set shows_qs = (
tirage.spectacle_set
.select_related('location')
.prefetch_related('quote_set')
)
if categories_id: if categories_id:
shows_qs = shows_qs.filter(category__id__in=categories_id) shows_qs = shows_qs.filter(category__id__in=categories_id)
if locations_id: if locations_id:
@ -735,14 +789,15 @@ def catalogue(request, request_type):
'vips': spectacle.vips, 'vips': spectacle.vips,
'description': spectacle.description, 'description': spectacle.description,
'slots_description': spectacle.slots_description, 'slots_description': spectacle.slots_description,
'quotes': list(Quote.objects.filter(spectacle=spectacle).values( 'quotes': [dict(author=quote.author,
'author', 'text')), text=quote.text)
for quote in spectacle.quote_set.all()],
'image': spectacle.getImgUrl(), 'image': spectacle.getImgUrl(),
'ext_link': spectacle.ext_link, 'ext_link': spectacle.ext_link,
'price': spectacle.price, 'price': spectacle.price,
'slots': spectacle.slots 'slots': spectacle.slots
} }
for spectacle in shows_qs.all() for spectacle in shows_qs
] ]
return JsonResponse(data_return, safe=False) return JsonResponse(data_return, safe=False)
# Si la requête n'est pas de la forme attendue, on quitte avec une erreur # Si la requête n'est pas de la forme attendue, on quitte avec une erreur

1
cof/settings/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
secret.py

View file

@ -1,32 +1,41 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Django settings for cof project. Django common settings for cof project.
For more information on this file, see Everything which is supposed to be identical between the production server and
https://docs.djangoproject.com/en/1.8/topics/settings/ the local development server should be here.
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.8/ref/settings/
""" """
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Database credentials
try:
from .secret import DBNAME, DBUSER, DBPASSWD
except ImportError:
# On the local development VM, theses credentials are in the environment
DBNAME = os.environ["DBNAME"]
DBUSER = os.environ["DBUSER"]
DBPASSWD = os.environ["DBPASSWD"]
except KeyError:
raise RuntimeError("Secrets missing")
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # Other secrets
try:
from .secret import (
SECRET_KEY, RECAPTCHA_PUBLIC_KEY, RECAPTCHA_PRIVATE_KEY, ADMINS,
REDIS_PASSWD, REDIS_DB, REDIS_HOST, REDIS_PORT
)
except ImportError:
raise RuntimeError("Secrets missing")
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! BASE_DIR = os.path.dirname(
SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah' os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
)
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
# Application definition # Application definition
INSTALLED_APPS = ( INSTALLED_APPS = [
'gestioncof', 'gestioncof',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
@ -41,16 +50,15 @@ INSTALLED_APPS = (
'autocomplete_light', 'autocomplete_light',
'captcha', 'captcha',
'django_cas_ng', 'django_cas_ng',
'debug_toolbar',
'bootstrapform', 'bootstrapform',
'kfet', 'kfet',
'channels', 'channels',
'widget_tweaks', 'widget_tweaks',
'custommail', 'custommail',
) 'djconfig',
]
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
@ -60,7 +68,8 @@ MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
) 'djconfig.middleware.DjConfigMiddleware',
]
ROOT_URLCONF = 'cof.urls' ROOT_URLCONF = 'cof.urls'
@ -78,25 +87,22 @@ TEMPLATES = [
'django.core.context_processors.i18n', 'django.core.context_processors.i18n',
'django.core.context_processors.media', 'django.core.context_processors.media',
'django.core.context_processors.static', 'django.core.context_processors.static',
'djconfig.context_processors.config',
'gestioncof.shared.context_processor', 'gestioncof.shared.context_processor',
'kfet.context_processors.auth', 'kfet.context_processors.auth',
'kfet.context_processors.kfet_open', 'kfet.context_processors.kfet_open',
'kfet.context_processors.config',
], ],
}, },
}, },
] ]
# WSGI_APPLICATION = 'cof.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ['DBNAME'], 'NAME': DBNAME,
'USER': os.environ['DBUSER'], 'USER': DBUSER,
'PASSWORD': os.environ['DBPASSWD'], 'PASSWORD': DBPASSWD,
'HOST': os.environ.get('DBHOST', 'localhost'), 'HOST': os.environ.get('DBHOST', 'localhost'),
} }
} }
@ -115,19 +121,6 @@ USE_L10N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.8/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT = '/var/www/static/'
# Media upload (through ImageField, SiteField)
# https://docs.djangoproject.com/en/1.9/ref/models/fields/
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
MEDIA_URL = '/media/'
# Various additional settings # Various additional settings
SITE_ID = 1 SITE_ID = 1
@ -160,12 +153,6 @@ AUTHENTICATION_BACKENDS = (
'kfet.backends.GenericTeamBackend', 'kfet.backends.GenericTeamBackend',
) )
# LDAP_SERVER_URL = 'ldaps://ldap.spi.ens.fr:636'
# EMAIL_HOST="nef.ens.fr"
RECAPTCHA_PUBLIC_KEY = "DUMMY"
RECAPTCHA_PRIVATE_KEY = "DUMMY"
RECAPTCHA_USE_SSL = True RECAPTCHA_USE_SSL = True
# Channels settings # Channels settings
@ -174,28 +161,14 @@ CHANNEL_LAYERS = {
"default": { "default": {
"BACKEND": "asgi_redis.RedisChannelLayer", "BACKEND": "asgi_redis.RedisChannelLayer",
"CONFIG": { "CONFIG": {
"hosts": [(os.environ.get("REDIS_HOST", "localhost"), 6379)], "hosts": [(
"redis://:{passwd}@{host}:{port}/{db}"
.format(passwd=REDIS_PASSWD, host=REDIS_HOST,
port=REDIS_PORT, db=REDIS_DB)
)],
}, },
"ROUTING": "cof.routing.channel_routing", "ROUTING": "cof.routing.channel_routing",
} }
} }
def show_toolbar(request):
"""
On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar
car cela interfère avec l'utilisation de Vagrant. En effet, l'adresse de la
machine physique n'est pas forcément connue, et peut difficilement être
mise dans les INTERNAL_IPS.
"""
if not DEBUG:
return False
if request.is_ajax():
return False
return True
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
}
FORMAT_MODULE_PATH = 'cof.locale' FORMAT_MODULE_PATH = 'cof.locale'

47
cof/settings/dev.py Normal file
View file

@ -0,0 +1,47 @@
"""
Django development settings for the cof project.
The settings that are not listed here are imported from .common
"""
import os
from .common import *
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
DEBUG = True
# ---
# Apache static/media config
# ---
STATIC_URL = '/static/'
STATIC_ROOT = '/var/www/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
MEDIA_URL = '/media/'
# ---
# Debug tool bar
# ---
def show_toolbar(request):
"""
On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar
car cela interfère avec l'utilisation de Vagrant. En effet, l'adresse de la
machine physique n'est pas forcément connue, et peut difficilement être
mise dans les INTERNAL_IPS.
"""
return DEBUG
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
MIDDLEWARE_CLASSES = (
["debug_panel.middleware.DebugPanelMiddleware"]
+ MIDDLEWARE_CLASSES
)
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
}

26
cof/settings/prod.py Normal file
View file

@ -0,0 +1,26 @@
"""
Django development settings for the cof project.
The settings that are not listed here are imported from .common
"""
import os
from .common import *
DEBUG = False
ALLOWED_HOSTS = [
"cof.ens.fr",
"www.cof.ens.fr",
"dev.cof.ens.fr"
]
STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), "static")
STATIC_URL = "/gestion/static/"
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media")
MEDIA_URL = "/gestion/media/"
LDAP_SERVER_URL = "ldaps://ldap.spi.ens.fr:636"
EMAIL_HOST = "nef.ens.fr"

View file

@ -0,0 +1,8 @@
SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah'
RECAPTCHA_PUBLIC_KEY = "DUMMY"
RECAPTCHA_PRIVATE_KEY = "DUMMY"
REDIS_PASSWD = "dummy"
REDIS_PORT = 6379
REDIS_DB = 0
REDIS_HOST = "127.0.0.1"
ADMINS = None

View file

@ -82,6 +82,8 @@ urlpatterns = [
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof), url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof),
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente), url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente),
url(r'^k-fet/', include('kfet.urls')), url(r'^k-fet/', include('kfet.urls')),
# djconfig
url(r"^config", gestioncof_views.ConfigUpdate.as_view())
] ]
if 'debug_toolbar' in settings.INSTALLED_APPS: if 'debug_toolbar' in settings.INSTALLED_APPS:

View file

@ -0,0 +1 @@
default_app_config = 'gestioncof.apps.GestioncofConfig'

View file

@ -1,9 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -18,13 +12,12 @@ from django.contrib.auth.admin import UserAdmin
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.db.models import Q from django.db.models import Q
import django.utils.six as six
import autocomplete_light import autocomplete_light
def add_link_field(target_model='', field='', link_text=six.text_type, def add_link_field(target_model='', field='', link_text=str,
desc_text=six.text_type): desc_text=str):
def add_link(cls): def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower() reverse_name = target_model or cls.model.__name__.lower()
@ -139,7 +132,6 @@ def ProfileInfo(field, short_description, boolean=False):
User.profile_login_clipper = FkeyLookup("profile__login_clipper", User.profile_login_clipper = FkeyLookup("profile__login_clipper",
"Login clipper") "Login clipper")
User.profile_num = FkeyLookup("profile__num", "Numéro")
User.profile_phone = ProfileInfo("phone", "Téléphone") User.profile_phone = ProfileInfo("phone", "Téléphone")
User.profile_occupation = ProfileInfo("occupation", "Occupation") User.profile_occupation = ProfileInfo("occupation", "Occupation")
User.profile_departement = ProfileInfo("departement", "Departement") User.profile_departement = ProfileInfo("departement", "Departement")
@ -166,10 +158,12 @@ class UserProfileAdmin(UserAdmin):
is_cof.short_description = 'Membre du COF' is_cof.short_description = 'Membre du COF'
is_cof.boolean = True is_cof.boolean = True
list_display = ('profile_num',) + UserAdmin.list_display \ list_display = (
UserAdmin.list_display
+ ('profile_login_clipper', 'profile_phone', 'profile_occupation', + ('profile_login_clipper', 'profile_phone', 'profile_occupation',
'profile_mailing_cof', 'profile_mailing_bda', 'profile_mailing_cof', 'profile_mailing_bda',
'profile_mailing_bda_revente', 'is_cof', 'is_buro', ) 'profile_mailing_bda_revente', 'is_cof', 'is_buro', )
)
list_display_links = ('username', 'email', 'first_name', 'last_name') list_display_links = ('username', 'email', 'first_name', 'last_name')
list_filter = UserAdmin.list_filter \ list_filter = UserAdmin.list_filter \
+ ('profile__is_cof', 'profile__is_buro', 'profile__mailing_cof', + ('profile__is_cof', 'profile__is_buro', 'profile__mailing_cof',
@ -215,21 +209,17 @@ class UserProfileAdmin(UserAdmin):
# FIXME: This is absolutely horrible. # FIXME: This is absolutely horrible.
def user_unicode(self): def user_str(self):
if self.first_name and self.last_name: if self.first_name and self.last_name:
return "%s %s (%s)" % (self.first_name, self.last_name, self.username) return "{} ({})".format(self.get_full_name(), self.username)
else: else:
return self.username return self.username
if six.PY2: User.__str__ = user_str
User.__unicode__ = user_unicode
else:
User.__str__ = user_unicode
class EventRegistrationAdmin(admin.ModelAdmin): class EventRegistrationAdmin(admin.ModelAdmin):
form = autocomplete_light.modelform_factory(EventRegistration, exclude=[]) form = autocomplete_light.modelform_factory(EventRegistration, exclude=[])
list_display = ('__unicode__' if six.PY2 else '__str__', 'event', 'user', list_display = ('__str__', 'event', 'user', 'paid')
'paid')
list_filter = ('paid',) list_filter = ('paid',)
search_fields = ('user__username', 'user__first_name', 'user__last_name', search_fields = ('user__username', 'user__first_name', 'user__last_name',
'user__email', 'event__title') 'user__email', 'event__title')

14
gestioncof/apps.py Normal file
View file

@ -0,0 +1,14 @@
from django.apps import AppConfig
class GestioncofConfig(AppConfig):
name = 'gestioncof'
verbose_name = "Gestion des adhérents du COF"
def ready(self):
self.register_config()
def register_config(self):
import djconfig
from .forms import GestioncofConfigForm
djconfig.register(GestioncofConfigForm)

View file

@ -1,21 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.forms.widgets import RadioSelect, CheckboxSelectMultiple from django.forms.widgets import RadioSelect, CheckboxSelectMultiple
from django.forms.formsets import BaseFormSet, formset_factory from django.forms.formsets import BaseFormSet, formset_factory
from django.db.models import Max
from django.core.validators import MinLengthValidator from django.core.validators import MinLengthValidator
from djconfig.forms import ConfigForm
from gestioncof.models import CofProfile, EventCommentValue, \ from gestioncof.models import CofProfile, EventCommentValue, \
CalendarSubscription, Club CalendarSubscription, Club
from gestioncof.widgets import TriStateCheckbox from gestioncof.widgets import TriStateCheckbox
from gestioncof.shared import lock_table, unlock_table
from bda.models import Spectacle from bda.models import Spectacle
@ -243,7 +237,6 @@ class RegistrationProfileForm(forms.ModelForm):
self.fields['mailing_cof'].initial = True self.fields['mailing_cof'].initial = True
self.fields['mailing_bda'].initial = True self.fields['mailing_bda'].initial = True
self.fields['mailing_bda_revente'].initial = True self.fields['mailing_bda_revente'].initial = True
self.fields['num'].widget.attrs['readonly'] = True
self.fields.keyOrder = [ self.fields.keyOrder = [
'login_clipper', 'login_clipper',
@ -251,7 +244,6 @@ class RegistrationProfileForm(forms.ModelForm):
'occupation', 'occupation',
'departement', 'departement',
'is_cof', 'is_cof',
'num',
'type_cotiz', 'type_cotiz',
'mailing_cof', 'mailing_cof',
'mailing_bda', 'mailing_bda',
@ -259,24 +251,9 @@ class RegistrationProfileForm(forms.ModelForm):
'comments' 'comments'
] ]
def save(self, *args, **kw):
instance = super(RegistrationProfileForm, self).save(*args, **kw)
if instance.is_cof and not instance.num:
# Generate new number
try:
lock_table(CofProfile)
aggregate = CofProfile.objects.aggregate(Max('num'))
instance.num = aggregate['num__max'] + 1
instance.save()
self.cleaned_data['num'] = instance.num
self.data['num'] = instance.num
finally:
unlock_table(CofProfile)
return instance
class Meta: class Meta:
model = CofProfile model = CofProfile
fields = ("login_clipper", "num", "phone", "occupation", fields = ("login_clipper", "phone", "occupation",
"departement", "is_cof", "type_cotiz", "mailing_cof", "departement", "is_cof", "type_cotiz", "mailing_cof",
"mailing_bda", "mailing_bda_revente", "comments") "mailing_bda", "mailing_bda_revente", "comments")
@ -403,3 +380,16 @@ class ClubsForm(forms.Form):
queryset=Club.objects.all(), queryset=Club.objects.all(),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False) required=False)
# ---
# Announcements banner
# TODO: move this to the `gestion` app once the supportBDS branch is merged
# ---
class GestioncofConfigForm(ConfigForm):
gestion_banner = forms.CharField(
label=_("Announcements banner"),
help_text=_("An empty banner disables annoucements"),
max_length=2048
)

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0010_delete_custommail'),
]
operations = [
migrations.RemoveField(
model_name='cofprofile',
name='num',
),
]

View file

@ -1,11 +1,7 @@
# -*- coding: utf-8 -*-
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
import django.utils.six as six
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
from gestioncof.petits_cours_models import choices_length from gestioncof.petits_cours_models import choices_length
@ -35,12 +31,10 @@ TYPE_COMMENT_FIELD = (
) )
@python_2_unicode_compatible
class CofProfile(models.Model): class CofProfile(models.Model):
user = models.OneToOneField(User, related_name="profile") user = models.OneToOneField(User, related_name="profile")
login_clipper = models.CharField("Login clipper", max_length=8, blank=True) login_clipper = models.CharField("Login clipper", max_length=8, blank=True)
is_cof = models.BooleanField("Membre du COF", default=False) is_cof = models.BooleanField("Membre du COF", default=False)
num = models.IntegerField("Numéro d'adhérent", blank=True, default=0)
phone = models.CharField("Téléphone", max_length=20, blank=True) phone = models.CharField("Téléphone", max_length=20, blank=True)
occupation = models.CharField(_("Occupation"), occupation = models.CharField(_("Occupation"),
default="1A", default="1A",
@ -72,7 +66,7 @@ class CofProfile(models.Model):
verbose_name_plural = "Profils COF" verbose_name_plural = "Profils COF"
def __str__(self): def __str__(self):
return six.text_type(self.user.username) return self.user.username
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
@ -86,7 +80,6 @@ def post_delete_user(sender, instance, *args, **kwargs):
instance.user.delete() instance.user.delete()
@python_2_unicode_compatible
class Club(models.Model): class Club(models.Model):
name = models.CharField("Nom", max_length=200, unique=True) name = models.CharField("Nom", max_length=200, unique=True)
description = models.TextField("Description", blank=True) description = models.TextField("Description", blank=True)
@ -98,7 +91,6 @@ class Club(models.Model):
return self.name return self.name
@python_2_unicode_compatible
class Event(models.Model): class Event(models.Model):
title = models.CharField("Titre", max_length=200) title = models.CharField("Titre", max_length=200)
location = models.CharField("Lieu", max_length=200) location = models.CharField("Lieu", max_length=200)
@ -115,10 +107,9 @@ class Event(models.Model):
verbose_name = "Événement" verbose_name = "Événement"
def __str__(self): def __str__(self):
return six.text_type(self.title) return self.title
@python_2_unicode_compatible
class EventCommentField(models.Model): class EventCommentField(models.Model):
event = models.ForeignKey(Event, related_name="commentfields") event = models.ForeignKey(Event, related_name="commentfields")
name = models.CharField("Champ", max_length=200) name = models.CharField("Champ", max_length=200)
@ -130,10 +121,9 @@ class EventCommentField(models.Model):
verbose_name = "Champ" verbose_name = "Champ"
def __str__(self): def __str__(self):
return six.text_type(self.name) return self.name
@python_2_unicode_compatible
class EventCommentValue(models.Model): class EventCommentValue(models.Model):
commentfield = models.ForeignKey(EventCommentField, related_name="values") commentfield = models.ForeignKey(EventCommentField, related_name="values")
registration = models.ForeignKey("EventRegistration", registration = models.ForeignKey("EventRegistration",
@ -144,7 +134,6 @@ class EventCommentValue(models.Model):
return "Commentaire de %s" % self.commentfield return "Commentaire de %s" % self.commentfield
@python_2_unicode_compatible
class EventOption(models.Model): class EventOption(models.Model):
event = models.ForeignKey(Event, related_name="options") event = models.ForeignKey(Event, related_name="options")
name = models.CharField("Option", max_length=200) name = models.CharField("Option", max_length=200)
@ -154,10 +143,9 @@ class EventOption(models.Model):
verbose_name = "Option" verbose_name = "Option"
def __str__(self): def __str__(self):
return six.text_type(self.name) return self.name
@python_2_unicode_compatible
class EventOptionChoice(models.Model): class EventOptionChoice(models.Model):
event_option = models.ForeignKey(EventOption, related_name="choices") event_option = models.ForeignKey(EventOption, related_name="choices")
value = models.CharField("Valeur", max_length=200) value = models.CharField("Valeur", max_length=200)
@ -167,10 +155,9 @@ class EventOptionChoice(models.Model):
verbose_name_plural = "Choix" verbose_name_plural = "Choix"
def __str__(self): def __str__(self):
return six.text_type(self.value) return self.value
@python_2_unicode_compatible
class EventRegistration(models.Model): class EventRegistration(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
event = models.ForeignKey(Event) event = models.ForeignKey(Event)
@ -184,11 +171,9 @@ class EventRegistration(models.Model):
unique_together = ("user", "event") unique_together = ("user", "event")
def __str__(self): def __str__(self):
return "Inscription de %s à %s" % (six.text_type(self.user), return "Inscription de {} à {}".format(self.user, self.event.title)
six.text_type(self.event.title))
@python_2_unicode_compatible
class Survey(models.Model): class Survey(models.Model):
title = models.CharField("Titre", max_length=200) title = models.CharField("Titre", max_length=200)
details = models.TextField("Détails", blank=True) details = models.TextField("Détails", blank=True)
@ -199,10 +184,9 @@ class Survey(models.Model):
verbose_name = "Sondage" verbose_name = "Sondage"
def __str__(self): def __str__(self):
return six.text_type(self.title) return self.title
@python_2_unicode_compatible
class SurveyQuestion(models.Model): class SurveyQuestion(models.Model):
survey = models.ForeignKey(Survey, related_name="questions") survey = models.ForeignKey(Survey, related_name="questions")
question = models.CharField("Question", max_length=200) question = models.CharField("Question", max_length=200)
@ -212,10 +196,9 @@ class SurveyQuestion(models.Model):
verbose_name = "Question" verbose_name = "Question"
def __str__(self): def __str__(self):
return six.text_type(self.question) return self.question
@python_2_unicode_compatible
class SurveyQuestionAnswer(models.Model): class SurveyQuestionAnswer(models.Model):
survey_question = models.ForeignKey(SurveyQuestion, related_name="answers") survey_question = models.ForeignKey(SurveyQuestion, related_name="answers")
answer = models.CharField("Réponse", max_length=200) answer = models.CharField("Réponse", max_length=200)
@ -224,10 +207,9 @@ class SurveyQuestionAnswer(models.Model):
verbose_name = "Réponse" verbose_name = "Réponse"
def __str__(self): def __str__(self):
return six.text_type(self.answer) return self.answer
@python_2_unicode_compatible
class SurveyAnswer(models.Model): class SurveyAnswer(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
survey = models.ForeignKey(Survey) survey = models.ForeignKey(Survey)
@ -244,7 +226,6 @@ class SurveyAnswer(models.Model):
self.survey.title) self.survey.title)
@python_2_unicode_compatible
class CalendarSubscription(models.Model): class CalendarSubscription(models.Model):
token = models.UUIDField() token = models.UUIDField()
user = models.OneToOneField(User) user = models.OneToOneField(User)

View file

@ -12,28 +12,34 @@ from django.views.decorators.csrf import csrf_exempt
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.db import transaction
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from gestioncof.petits_cours_models import ( from gestioncof.petits_cours_models import (
PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter, PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
PetitCoursAbility, PetitCoursSubject PetitCoursAbility
) )
from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
from gestioncof.decorators import buro_required from gestioncof.decorators import buro_required
from gestioncof.shared import lock_table, unlock_tables
class DemandeListView(ListView): class DemandeListView(ListView):
model = PetitCoursDemande queryset = (
PetitCoursDemande.objects
.prefetch_related('matieres')
.order_by('traitee', '-id')
)
template_name = "petits_cours_demandes_list.html" template_name = "petits_cours_demandes_list.html"
paginate_by = 20 paginate_by = 20
def get_queryset(self):
return PetitCoursDemande.objects.order_by('traitee', '-id').all()
class DemandeDetailView(DetailView): class DemandeDetailView(DetailView):
model = PetitCoursDemande model = PetitCoursDemande
queryset = (
PetitCoursDemande.objects
.prefetch_related('petitcoursattribution_set',
'matieres')
)
template_name = "gestioncof/details_demande_petit_cours.html" template_name = "gestioncof/details_demande_petit_cours.html"
context_object_name = "demande" context_object_name = "demande"
@ -268,17 +274,17 @@ def _traitement_post(request, demande):
headers={'Reply-To': replyto})) headers={'Reply-To': replyto}))
connection = mail.get_connection(fail_silently=False) connection = mail.get_connection(fail_silently=False)
connection.send_messages(mails_to_send) connection.send_messages(mails_to_send)
lock_table(PetitCoursAttributionCounter, PetitCoursAttribution, User) with transaction.atomic():
for matiere in proposals: for matiere in proposals:
for rank, user in enumerate(proposals[matiere]): for rank, user in enumerate(proposals[matiere]):
counter = PetitCoursAttributionCounter.objects.get(user=user, counter = PetitCoursAttributionCounter.objects.get(
matiere=matiere) user=user, matiere=matiere
counter.count += 1 )
counter.save() counter.count += 1
attrib = PetitCoursAttribution(user=user, matiere=matiere, counter.save()
demande=demande, rank=rank + 1) attrib = PetitCoursAttribution(user=user, matiere=matiere,
attrib.save() demande=demande, rank=rank + 1)
unlock_tables() attrib.save()
demande.traitee = True demande.traitee = True
demande.traitee_par = request.user demande.traitee_par = request.user
demande.processed = datetime.now() demande.processed = datetime.now()
@ -303,17 +309,15 @@ def inscription(request):
profile.petits_cours_accept = "receive_proposals" in request.POST profile.petits_cours_accept = "receive_proposals" in request.POST
profile.petits_cours_remarques = request.POST["remarques"] profile.petits_cours_remarques = request.POST["remarques"]
profile.save() profile.save()
lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User, with transaction.atomic():
PetitCoursSubject) abilities = (
abilities = ( PetitCoursAbility.objects.filter(user=request.user).all()
PetitCoursAbility.objects.filter(user=request.user).all()
)
for ability in abilities:
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
) )
unlock_tables() for ability in abilities:
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
)
success = True success = True
formset = MatieresFormSet(instance=request.user) formset = MatieresFormSet(instance=request.user)
else: else:

View file

@ -1,15 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.conf import settings from django.conf import settings
from django_cas_ng.backends import CASBackend from django_cas_ng.backends import CASBackend
from django_cas_ng.utils import get_cas_client from django_cas_ng.utils import get_cas_client
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import connection
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
@ -74,25 +67,3 @@ def context_processor(request):
"site": Site.objects.get_current(), "site": Site.objects.get_current(),
} }
return data return data
def lock_table(*models):
query = "LOCK TABLES "
for i, model in enumerate(models):
table = model._meta.db_table
if i > 0:
query += ", "
query += "%s WRITE" % table
cursor = connection.cursor()
cursor.execute(query)
row = cursor.fetchone()
return row
def unlock_tables(*models):
cursor = connection.cursor()
cursor.execute("UNLOCK TABLES")
row = cursor.fetchone()
return row
unlock_table = unlock_tables

View file

@ -778,6 +778,17 @@ header .open > .dropdown-toggle.btn-default {
border-color: white; border-color: white;
} }
/* Announcements banner ------------------ */
#banner {
background-color: #d86b01;
width: 100%;
text-align: center;
padding: 10px;
color: white;
font-size: larger;
}
/* FORMS --------------------------------- */ /* FORMS --------------------------------- */

View file

@ -8,13 +8,13 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
{# CSS #} {# CSS #}
<link type="text/css" rel="stylesheet" href="{% static "css/bootstrap.min.css" %}" /> <link type="text/css" rel="stylesheet" href="{% static "css/bootstrap.min.css" %}" />
<link type="text/css" rel="stylesheet" href="{% static "css/cof.css" %}" /> <link type="text/css" rel="stylesheet" href="{% static "css/cof.css" %}" />
<link href="https://fonts.googleapis.com/css?family=Dosis|Dosis:700|Raleway|Roboto:300,300i,700" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Dosis|Dosis:700|Raleway|Roboto:300,300i,700" rel="stylesheet">
<link rel="stylesheet" href="{% static "font-awesome/css/font-awesome.min.css" %}"> <link rel="stylesheet" href="{% static "font-awesome/css/font-awesome.min.css" %}">
{# JS #} {# JS #}
<script src="https://code.jquery.com/jquery-3.1.0.min.js" integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.1.0.min.js" integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
{% block extra_head %}{% endblock %} {% block extra_head %}{% endblock %}

View file

@ -0,0 +1,23 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% load i18n %}
{% block page_size %}col-sm-8{%endblock%}
{% block realcontent %}
<h2>{% trans "Global configuration" %}</h2>
<form id="profile form-horizontal" method="post" action="">
<div class="row" style="margin: 0 15%;">
{% csrf_token %}
<fieldset"center-block">
{% for field in form %}
{{ field | bootstrap }}
{% endfor %}
</fieldset>
</div>
<div class="form-actions">
<input type="submit" class="btn btn-primary pull-right"
value={% trans "Save" %} />
</div>
</form>
{% endblock %}

View file

@ -16,6 +16,14 @@
<h2 class="member-status">{% if user.first_name %}{{ user.first_name }}{% else %}<tt>{{ user.username }}</tt>{% endif %}, {% if user.profile.is_cof %}<tt class="user-is-cof">au COF{% else %}<tt class="user-is-not-cof">non-COF{% endif %}</tt></h2> <h2 class="member-status">{% if user.first_name %}{{ user.first_name }}{% else %}<tt>{{ user.username }}</tt>{% endif %}, {% if user.profile.is_cof %}<tt class="user-is-cof">au COF{% else %}<tt class="user-is-not-cof">non-COF{% endif %}</tt></h2>
</div><!-- /.container --> </div><!-- /.container -->
</header> </header>
{% if config.gestion_banner %}
<div id="banner" class="container">
<span class="glyphicon glyphicon-bullhorn"></span>
<span>{{ config.gestion_banner }}</span>
</div>
{% endif %}
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="messages"> <div class="messages">

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import unicodecsv import unicodecsv
import uuid import uuid
from datetime import timedelta from datetime import timedelta
@ -12,9 +10,10 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import login as django_login_view from django.contrib.auth.views import login as django_login_view
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse_lazy
from django.views.generic import FormView
from django.utils import timezone from django.utils import timezone
from django.contrib import messages from django.contrib import messages
import django.utils.six as six
from gestioncof.models import Survey, SurveyAnswer, SurveyQuestion, \ from gestioncof.models import Survey, SurveyAnswer, SurveyQuestion, \
SurveyQuestionAnswer SurveyQuestionAnswer
@ -24,10 +23,11 @@ from gestioncof.models import EventCommentField, EventCommentValue, \
CalendarSubscription CalendarSubscription
from gestioncof.models import CofProfile, Club from gestioncof.models import CofProfile, Club
from gestioncof.decorators import buro_required, cof_required from gestioncof.decorators import buro_required, cof_required
from gestioncof.forms import UserProfileForm, EventStatusFilterForm, \ from gestioncof.forms import (
SurveyForm, SurveyStatusFilterForm, RegistrationUserForm, \ UserProfileForm, EventStatusFilterForm, SurveyForm, SurveyStatusFilterForm,
RegistrationProfileForm, EventForm, CalendarForm, EventFormset, \ RegistrationUserForm, RegistrationProfileForm, EventForm, CalendarForm,
RegistrationPassUserForm, ClubsForm EventFormset, RegistrationPassUserForm, ClubsForm, GestioncofConfigForm
)
from bda.models import Tirage, Spectacle from bda.models import Tirage, Spectacle
@ -94,7 +94,10 @@ def logout(request):
@login_required @login_required
def survey(request, survey_id): def survey(request, survey_id):
survey = get_object_or_404(Survey, id=survey_id) survey = get_object_or_404(
Survey.objects.prefetch_related('questions', 'questions__answers'),
id=survey_id,
)
if not survey.survey_open or survey.old: if not survey.survey_open or survey.old:
raise Http404 raise Http404
success = False success = False
@ -400,12 +403,8 @@ def registration_form2(request, login_clipper=None, username=None,
def registration(request): def registration(request):
if request.POST: if request.POST:
request_dict = request.POST.copy() request_dict = request.POST.copy()
# num ne peut pas être défini manuellement
if "num" in request_dict:
del request_dict["num"]
member = None member = None
login_clipper = None login_clipper = None
success = False
# ----- # -----
# Remplissage des formulaires # Remplissage des formulaires
@ -442,7 +441,6 @@ def registration(request):
member = user_form.save() member = user_form.save()
profile, _ = CofProfile.objects.get_or_create(user=member) profile, _ = CofProfile.objects.get_or_create(user=member)
was_cof = profile.is_cof was_cof = profile.is_cof
request_dict["num"] = profile.num
# Maintenant on remplit le formulaire de profil # Maintenant on remplit le formulaire de profil
profile_form = RegistrationProfileForm(request_dict, profile_form = RegistrationProfileForm(request_dict,
instance=profile) instance=profile)
@ -496,16 +494,18 @@ def registration(request):
for club in clubs_form.cleaned_data['clubs']: for club in clubs_form.cleaned_data['clubs']:
club.membres.add(member) club.membres.add(member)
club.save() club.save()
success = True
# Messages # ---
if success: # Success
msg = ("L'inscription de {:s} (<tt>{:s}</tt>) a été " # ---
"enregistrée avec succès"
.format(member.get_full_name(), member.email)) msg = ("L'inscription de {:s} (<tt>{:s}</tt>) a été "
if member.profile.is_cof: "enregistrée avec succès."
msg += "Il est désormais membre du COF n°{:d} !".format( .format(member.get_full_name(), member.email))
member.profile.num) if profile.is_cof:
messages.success(request, msg, extra_tags='safe') msg += "\nIl est désormais membre du COF n°{:d} !".format(
member.profile.id)
messages.success(request, msg, extra_tags='safe')
return render(request, "gestioncof/registration_post.html", return render(request, "gestioncof/registration_post.html",
{"user_form": user_form, {"user_form": user_form,
"profile_form": profile_form, "profile_form": profile_form,
@ -569,10 +569,10 @@ def export_members(request):
writer = unicodecsv.writer(response) writer = unicodecsv.writer(response)
for profile in CofProfile.objects.filter(is_cof=True).all(): for profile in CofProfile.objects.filter(is_cof=True).all():
user = profile.user user = profile.user
bits = [profile.num, user.username, user.first_name, user.last_name, bits = [profile.id, user.username, user.first_name, user.last_name,
user.email, profile.phone, profile.occupation, user.email, profile.phone, profile.occupation,
profile.departement, profile.type_cotiz] profile.departement, profile.type_cotiz]
writer.writerow([six.text_type(bit) for bit in bits]) writer.writerow([str(bit) for bit in bits])
return response return response
@ -588,10 +588,10 @@ def csv_export_mega(filename, qs):
comments = "---".join( comments = "---".join(
[comment.content for comment in reg.comments.all()]) [comment.content for comment in reg.comments.all()])
bits = [user.username, user.first_name, user.last_name, user.email, bits = [user.username, user.first_name, user.last_name, user.email,
profile.phone, profile.num, profile.phone, profile.id,
profile.comments if profile.comments else "", comments] profile.comments if profile.comments else "", comments]
writer.writerow([six.text_type(bit) for bit in bits]) writer.writerow([str(bit) for bit in bits])
return response return response
@ -610,8 +610,8 @@ def export_mega_remarksonly(request):
user = reg.user user = reg.user
profile = user.profile profile = user.profile
bits = [user.username, user.first_name, user.last_name, user.email, bits = [user.username, user.first_name, user.last_name, user.email,
profile.phone, profile.num, profile.comments, val.content] profile.phone, profile.id, profile.comments, val.content]
writer.writerow([six.text_type(bit) for bit in bits]) writer.writerow([str(bit) for bit in bits])
return response return response
@ -761,3 +761,18 @@ def calendar_ics(request, token):
response = HttpResponse(content=vcal.to_ical()) response = HttpResponse(content=vcal.to_ical())
response['Content-Type'] = "text/calendar" response['Content-Type'] = "text/calendar"
return response return response
class ConfigUpdate(FormView):
form_class = GestioncofConfigForm
template_name = "gestioncof/banner_update.html"
success_url = reverse_lazy("home")
def dispatch(self, request, *args, **kwargs):
if request.user is None or not request.user.is_superuser:
raise Http404
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
form.save()
return super().form_valid(form)

View file

@ -12,3 +12,9 @@ class KFetConfig(AppConfig):
def ready(self): def ready(self):
import kfet.signals import kfet.signals
self.register_config()
def register_config(self):
import djconfig
from kfet.forms import KFetConfigForm
djconfig.register(KFetConfigForm)

View file

@ -1,15 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
import hashlib import hashlib
from django.contrib.auth.models import User, Permission from django.contrib.auth.models import User, Permission
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from kfet.models import Account, GenericTeamToken from kfet.models import Account, GenericTeamToken
class KFetBackend(object): class KFetBackend(object):
def authenticate(self, request): def authenticate(self, request):
password = request.POST.get('KFETPASSWORD', '') password = request.POST.get('KFETPASSWORD', '')
@ -18,13 +15,15 @@ class KFetBackend(object):
return None return None
try: try:
password_sha256 = hashlib.sha256(password.encode('utf-8')).hexdigest() password_sha256 = (
hashlib.sha256(password.encode('utf-8'))
.hexdigest()
)
account = Account.objects.get(password=password_sha256) account = Account.objects.get(password=password_sha256)
user = account.cofprofile.user return account.cofprofile.user
except Account.DoesNotExist: except Account.DoesNotExist:
return None return None
return user
class GenericTeamBackend(object): class GenericTeamBackend(object):
def authenticate(self, username=None, token=None): def authenticate(self, username=None, token=None):
@ -46,6 +45,10 @@ class GenericTeamBackend(object):
def get_user(self, user_id): def get_user(self, user_id):
try: try:
return User.objects.get(pk=user_id) return (
User.objects
.select_related('profile__account_kfet')
.get(pk=user_id)
)
except User.DoesNotExist: except User.DoesNotExist:
return None return None

71
kfet/config.py Normal file
View file

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
from django.core.exceptions import ValidationError
from django.db import models
from djconfig import config
class KFetConfig(object):
"""kfet app configuration.
Enhance isolation with backend used to store config.
Usable after DjConfig middleware was called.
"""
prefix = 'kfet_'
def __getattr__(self, key):
if key == 'subvention_cof':
# Allows accessing to the reduction as a subvention
# Other reason: backward compatibility
reduction_mult = 1 - self.reduction_cof/100
return (1/reduction_mult - 1) * 100
return getattr(config, self._get_dj_key(key))
def list(self):
"""Get list of kfet app configuration.
Returns:
(key, value) for each configuration entry as list.
"""
# prevent circular imports
from kfet.forms import KFetConfigForm
return [(field.label, getattr(config, name), )
for name, field in KFetConfigForm.base_fields.items()]
def _get_dj_key(self, key):
return '{}{}'.format(self.prefix, key)
def set(self, **kwargs):
"""Update configuration value(s).
Args:
**kwargs: Keyword arguments. Keys must be in kfet config.
Config entries are updated to given values.
"""
# prevent circular imports
from kfet.forms import KFetConfigForm
# get old config
new_cfg = KFetConfigForm().initial
# update to new config
for key, value in kwargs.items():
dj_key = self._get_dj_key(key)
if isinstance(value, models.Model):
new_cfg[dj_key] = value.pk
else:
new_cfg[dj_key] = value
# save new config
cfg_form = KFetConfigForm(new_cfg)
if cfg_form.is_valid():
cfg_form.save()
else:
raise ValidationError(
'Invalid values in kfet_config.set: %(fields)s',
params={'fields': list(cfg_form.errors)})
kfet_config = KFetConfig()

View file

@ -1,29 +1,40 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, from django.core.serializers.json import json, DjangoJSONEncoder
print_function, unicode_literals)
from builtins import *
from channels import Group
from channels.generic.websockets import JsonWebsocketConsumer from channels.generic.websockets import JsonWebsocketConsumer
class KPsul(JsonWebsocketConsumer):
# Set to True if you want them, else leave out class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer):
strict_ordering = False """Custom Json Websocket Consumer.
slight_ordering = False
def connection_groups(self, **kwargs): Encode to JSON with DjangoJSONEncoder.
return ['kfet.kpsul']
"""
@classmethod
def encode_json(cls, content):
return json.dumps(content, cls=DjangoJSONEncoder)
class PermConsumerMixin(object):
"""Add support to check permissions on Consumers.
Attributes:
perms_connect (list): Required permissions to connect to this
consumer.
"""
http_user = True # Enable message.user
perms_connect = []
def connect(self, message, **kwargs): def connect(self, message, **kwargs):
pass """Check permissions on connection."""
if message.user.has_perms(self.perms_connect):
super().connect(message, **kwargs)
else:
self.close()
def receive(self, content, **kwargs):
pass
def disconnect(self, message, **kwargs):
pass
class KfetOpen(JsonWebsocketConsumer): class KfetOpen(JsonWebsocketConsumer):
def connection_groups(self, **kwargs): def connection_groups(self, **kwargs):
@ -37,3 +48,8 @@ class KfetOpen(JsonWebsocketConsumer):
def disconnect(self, message, **kwargs): def disconnect(self, message, **kwargs):
pass pass
class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer):
groups = ['kfet.kpsul']
perms_connect = ['kfet.is_team']

View file

@ -1,13 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
from django.contrib.auth.context_processors import PermWrapper from django.contrib.auth.context_processors import PermWrapper
from .views import KFET_OPEN, KFET_FORCE_CLOSE from .views import KFET_OPEN, KFET_FORCE_CLOSE
from kfet.config import kfet_config
def auth(request): def auth(request):
if hasattr(request, 'real_user'): if hasattr(request, 'real_user'):
return { return {
@ -25,3 +24,7 @@ def kfet_open(request):
'kfet_open_date': kfet_open_date.isoformat(), 'kfet_open_date': kfet_open_date.isoformat(),
'kfet_force_close': kfet_force_close, 'kfet_force_close': kfet_force_close,
} }
def config(request):
return {'kfet_config': kfet_config}

View file

@ -1,19 +1,25 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator from django.core.validators import MinLengthValidator
from django.contrib.auth.models import User, Group, Permission from django.contrib.auth.models import User, Group, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.forms import modelformset_factory from django.forms import modelformset_factory, widgets
from django.utils import timezone from django.utils import timezone
from djconfig.forms import ConfigForm
from kfet.models import ( from kfet.models import (
Account, Checkout, Article, OperationGroup, Operation, Account, Checkout, Article, OperationGroup, Operation,
CheckoutStatement, ArticleCategory, Settings, AccountNegative, Transfer, CheckoutStatement, ArticleCategory, AccountNegative, Transfer,
TransferGroup, Supplier) TransferGroup, Supplier)
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
# ----- # -----
# Widgets # Widgets
# ----- # -----
@ -128,6 +134,7 @@ class UserRestrictTeamForm(UserForm):
class Meta(UserForm.Meta): class Meta(UserForm.Meta):
fields = ['first_name', 'last_name', 'email'] fields = ['first_name', 'last_name', 'email']
class UserGroupForm(forms.ModelForm): class UserGroupForm(forms.ModelForm):
groups = forms.ModelMultipleChoiceField( groups = forms.ModelMultipleChoiceField(
Group.objects.filter(name__icontains='K-Fêt'), Group.objects.filter(name__icontains='K-Fêt'),
@ -135,20 +142,33 @@ class UserGroupForm(forms.ModelForm):
required=False) required=False)
def clean_groups(self): def clean_groups(self):
groups = self.cleaned_data.get('groups') kfet_groups = self.cleaned_data.get('groups')
# Si aucun groupe, on le dénomme other_groups = self.instance.groups.exclude(name__icontains='K-Fêt')
if not groups: return list(kfet_groups) + list(other_groups)
groups = self.instance.groups.exclude(name__icontains='K-Fêt')
return groups
class Meta: class Meta:
model = User model = User
fields = ['groups'] fields = ['groups']
class KFetPermissionsField(forms.ModelMultipleChoiceField):
def __init__(self, *args, **kwargs):
queryset = Permission.objects.filter(
content_type__in=ContentType.objects.filter(app_label="kfet"),
)
super().__init__(
queryset=queryset,
widget=widgets.CheckboxSelectMultiple,
*args, **kwargs
)
def label_from_instance(self, obj):
return obj.name
class GroupForm(forms.ModelForm): class GroupForm(forms.ModelForm):
permissions = forms.ModelMultipleChoiceField( permissions = KFetPermissionsField()
queryset= Permission.objects.filter(content_type__in=
ContentType.objects.filter(app_label='kfet')))
def clean_name(self): def clean_name(self):
name = self.cleaned_data['name'] name = self.cleaned_data['name']
@ -322,12 +342,20 @@ class KPsulAccountForm(forms.ModelForm):
}), }),
} }
class KPsulCheckoutForm(forms.Form): class KPsulCheckoutForm(forms.Form):
checkout = forms.ModelChoiceField( checkout = forms.ModelChoiceField(
queryset=Checkout.objects.filter( queryset=(
is_protected=False, valid_from__lte=timezone.now(), Checkout.objects
valid_to__gte=timezone.now()), .filter(
widget=forms.Select(attrs={'id':'id_checkout_select'})) is_protected=False,
valid_from__lte=timezone.now(),
valid_to__gte=timezone.now(),
)
),
widget=forms.Select(attrs={'id': 'id_checkout_select'}),
)
class KPsulOperationForm(forms.ModelForm): class KPsulOperationForm(forms.ModelForm):
article = forms.ModelChoiceField( article = forms.ModelChoiceField(
@ -389,40 +417,46 @@ class AddcostForm(forms.Form):
self.cleaned_data['amount'] = 0 self.cleaned_data['amount'] = 0
super(AddcostForm, self).clean() super(AddcostForm, self).clean()
# ----- # -----
# Settings forms # Settings forms
# ----- # -----
class SettingsForm(forms.ModelForm):
class Meta:
model = Settings
fields = ['value_decimal', 'value_account', 'value_duration']
def clean(self): class KFetConfigForm(ConfigForm):
name = self.instance.name
value_decimal = self.cleaned_data.get('value_decimal')
value_account = self.cleaned_data.get('value_account')
value_duration = self.cleaned_data.get('value_duration')
type_decimal = ['SUBVENTION_COF', 'ADDCOST_AMOUNT', 'OVERDRAFT_AMOUNT'] kfet_reduction_cof = forms.DecimalField(
type_account = ['ADDCOST_FOR'] label='Réduction COF', initial=Decimal('20'),
type_duration = ['OVERDRAFT_DURATION', 'CANCEL_DURATION'] max_digits=6, decimal_places=2,
help_text="Réduction, à donner en pourcentage, appliquée lors d'un "
"achat par un-e membre du COF sur le montant en euros.",
)
kfet_addcost_amount = forms.DecimalField(
label='Montant de la majoration (en €)', initial=Decimal('0'),
required=False,
max_digits=6, decimal_places=2,
)
kfet_addcost_for = forms.ModelChoiceField(
label='Destinataire de la majoration', initial=None, required=False,
help_text='Laissez vide pour désactiver la majoration.',
queryset=(Account.objects
.select_related('cofprofile', 'cofprofile__user')
.all()),
)
kfet_overdraft_duration = forms.DurationField(
label='Durée du découvert autorisé par défaut',
initial=timedelta(days=1),
)
kfet_overdraft_amount = forms.DecimalField(
label='Montant du découvert autorisé par défaut (en €)',
initial=Decimal('20'),
max_digits=6, decimal_places=2,
)
kfet_cancel_duration = forms.DurationField(
label='Durée pour annuler une commande sans mot de passe',
initial=timedelta(minutes=5),
)
self.cleaned_data['name'] = name
if name in type_decimal:
if not value_decimal:
raise ValidationError('Renseignez une valeur décimale')
self.cleaned_data['value_account'] = None
self.cleaned_data['value_duration'] = None
elif name in type_account:
self.cleaned_data['value_decimal'] = None
self.cleaned_data['value_duration'] = None
elif name in type_duration:
if not value_duration:
raise ValidationError('Renseignez une durée')
self.cleaned_data['value_decimal'] = None
self.cleaned_data['value_account'] = None
super(SettingsForm, self).clean()
class FilterHistoryForm(forms.Form): class FilterHistoryForm(forms.Form):
checkouts = forms.ModelMultipleChoiceField(queryset = Checkout.objects.all()) checkouts = forms.ModelMultipleChoiceField(queryset = Checkout.objects.all())
@ -505,11 +539,7 @@ class OrderArticleForm(forms.Form):
self.category = kwargs['initial']['category'] self.category = kwargs['initial']['category']
self.category_name = kwargs['initial']['category__name'] self.category_name = kwargs['initial']['category__name']
self.box_capacity = kwargs['initial']['box_capacity'] self.box_capacity = kwargs['initial']['box_capacity']
self.v_s1 = kwargs['initial']['v_s1'] self.v_all = kwargs['initial']['v_all']
self.v_s2 = kwargs['initial']['v_s2']
self.v_s3 = kwargs['initial']['v_s3']
self.v_s4 = kwargs['initial']['v_s4']
self.v_s5 = kwargs['initial']['v_s5']
self.v_moy = kwargs['initial']['v_moy'] self.v_moy = kwargs['initial']['v_moy']
self.v_et = kwargs['initial']['v_et'] self.v_et = kwargs['initial']['v_et']
self.v_prev = kwargs['initial']['v_prev'] self.v_prev = kwargs['initial']['v_prev']

View file

@ -14,7 +14,8 @@ from kfet.models import (Account, Article, OperationGroup, Operation,
class Command(BaseCommand): class Command(BaseCommand):
help = "Crée des opérations réparties uniformément sur une période de temps" help = ("Crée des opérations réparties uniformément "
"sur une période de temps")
def add_arguments(self, parser): def add_arguments(self, parser):
# Nombre d'opérations à créer # Nombre d'opérations à créer
@ -29,7 +30,6 @@ class Command(BaseCommand):
parser.add_argument('--transfers', type=int, default=0, parser.add_argument('--transfers', type=int, default=0,
help='Number of transfers to create (default 0)') help='Number of transfers to create (default 0)')
def handle(self, *args, **options): def handle(self, *args, **options):
self.stdout.write("Génération d'opérations") self.stdout.write("Génération d'opérations")
@ -44,6 +44,7 @@ class Command(BaseCommand):
# Convert to seconds # Convert to seconds
time = options['days'] * 24 * 3600 time = options['days'] * 24 * 3600
now = timezone.now()
checkout = Checkout.objects.first() checkout = Checkout.objects.first()
articles = Article.objects.all() articles = Article.objects.all()
accounts = Account.objects.exclude(trigramme='LIQ') accounts = Account.objects.exclude(trigramme='LIQ')
@ -55,6 +56,13 @@ class Command(BaseCommand):
except Account.DoesNotExist: except Account.DoesNotExist:
con_account = random.choice(accounts) con_account = random.choice(accounts)
# use to fetch OperationGroup pk created by bulk_create
at_list = []
# use to lazy set OperationGroup pk on Operation objects
ope_by_grp = []
# OperationGroup objects to bulk_create
opegroup_list = []
for i in range(num_ops): for i in range(num_ops):
# Randomly pick account # Randomly pick account
@ -64,8 +72,7 @@ class Command(BaseCommand):
account = liq_account account = liq_account
# Randomly pick time # Randomly pick time
at = timezone.now() - timedelta( at = now - timedelta(seconds=random.randint(0, time))
seconds=random.randint(0, time))
# Majoration sur compte 'concert' # Majoration sur compte 'concert'
if random.random() < 0.2: if random.random() < 0.2:
@ -78,13 +85,6 @@ class Command(BaseCommand):
# Initialize opegroup amount # Initialize opegroup amount
amount = Decimal('0') amount = Decimal('0')
opegroup = OperationGroup.objects.create(
on_acc=account,
checkout=checkout,
at=at,
is_cof=account.cofprofile.is_cof
)
# Generating operations # Generating operations
ope_list = [] ope_list = []
for j in range(random.randint(1, 4)): for j in range(random.randint(1, 4)):
@ -94,25 +94,26 @@ class Command(BaseCommand):
# 0.1 probability to have a charge # 0.1 probability to have a charge
if typevar > 0.9 and account != liq_account: if typevar > 0.9 and account != liq_account:
ope = Operation( ope = Operation(
group=opegroup,
type=Operation.DEPOSIT, type=Operation.DEPOSIT,
is_checkout=(random.random() > 0.2),
amount=Decimal(random.randint(1, 99)/10) amount=Decimal(random.randint(1, 99)/10)
) )
# 0.1 probability to have a withdrawal # 0.05 probability to have a withdrawal
elif typevar > 0.85 and account != liq_account:
ope = Operation(
type=Operation.WITHDRAW,
amount=-Decimal(random.randint(1, 99)/10)
)
# 0.05 probability to have an edition
elif typevar > 0.8 and account != liq_account: elif typevar > 0.8 and account != liq_account:
ope = Operation( ope = Operation(
group=opegroup, type=Operation.EDIT,
type=Operation.WITHDRAW, amount=Decimal(random.randint(1, 99)/10)
is_checkout=(random.random() > 0.2),
amount=-Decimal(random.randint(1, 99)/10)
) )
else: else:
article = random.choice(articles) article = random.choice(articles)
nb = random.randint(1, 5) nb = random.randint(1, 5)
ope = Operation( ope = Operation(
group=opegroup,
type=Operation.PURCHASE, type=Operation.PURCHASE,
amount=-article.price*nb, amount=-article.price*nb,
article=article, article=article,
@ -129,17 +130,44 @@ class Command(BaseCommand):
ope_list.append(ope) ope_list.append(ope)
amount += ope.amount amount += ope.amount
Operation.objects.bulk_create(ope_list) opegroup_list.append(OperationGroup(
opes_created += len(ope_list) on_acc=account,
opegroup.amount = amount checkout=checkout,
opegroup.save() at=at,
is_cof=account.cofprofile.is_cof,
amount=amount,
))
at_list.append(at)
ope_by_grp.append((at, ope_list, ))
OperationGroup.objects.bulk_create(opegroup_list)
# Fetch created OperationGroup objects pk by at
opegroups = (OperationGroup.objects
.filter(at__in=at_list)
.values('id', 'at'))
opegroups_by = {grp['at']: grp['id'] for grp in opegroups}
all_ope = []
for _ in range(num_ops):
at, ope_list = ope_by_grp.pop()
for ope in ope_list:
ope.group_id = opegroups_by[at]
all_ope.append(ope)
Operation.objects.bulk_create(all_ope)
opes_created = len(all_ope)
# Transfer generation # Transfer generation
transfer_by_grp = []
transfergroup_list = []
at_list = []
for i in range(num_transfers): for i in range(num_transfers):
# Randomly pick time # Randomly pick time
at = timezone.now() - timedelta( at = now - timedelta(seconds=random.randint(0, time))
seconds=random.randint(0, time))
# Choose whether to have a comment # Choose whether to have a comment
if random.random() > 0.5: if random.random() > 0.5:
@ -147,24 +175,40 @@ class Command(BaseCommand):
else: else:
comment = "" comment = ""
transfergroup = TransferGroup.objects.create( transfergroup_list.append(TransferGroup(
at=at, at=at,
comment=comment, comment=comment,
valid_by=random.choice(accounts) valid_by=random.choice(accounts),
) ))
at_list.append(at)
# Randomly generate transfer # Randomly generate transfer
transfer_list = [] transfer_list = []
for i in range(random.randint(1, 4)): for i in range(random.randint(1, 4)):
transfer_list.append(Transfer( transfer_list.append(Transfer(
group=transfergroup,
from_acc=random.choice(accounts), from_acc=random.choice(accounts),
to_acc=random.choice(accounts), to_acc=random.choice(accounts),
amount=Decimal(random.randint(1, 99)/10) amount=Decimal(random.randint(1, 99)/10)
)) ))
Transfer.objects.bulk_create(transfer_list) transfer_by_grp.append((at, transfer_list, ))
transfers += len(transfer_list)
TransferGroup.objects.bulk_create(transfergroup_list)
transfergroups = (TransferGroup.objects
.filter(at__in=at_list)
.values('id', 'at'))
transfergroups_by = {grp['at']: grp['id'] for grp in transfergroups}
all_transfer = []
for _ in range(num_transfers):
at, transfer_list = transfer_by_grp.pop()
for transfer in transfer_list:
transfer.group_id = transfergroups_by[at]
all_transfer.append(transfer)
Transfer.objects.bulk_create(all_transfer)
transfers += len(all_transfer)
self.stdout.write( self.stdout.write(
"- {:d} opérations créées dont {:d} commandes d'articles" "- {:d} opérations créées dont {:d} commandes d'articles"

View file

@ -1,15 +1,27 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, from django.contrib.auth.models import User
print_function, unicode_literals)
from builtins import *
from django.http import HttpResponseForbidden
from kfet.backends import KFetBackend from kfet.backends import KFetBackend
from kfet.models import Account
class KFetAuthenticationMiddleware(object): class KFetAuthenticationMiddleware(object):
"""Authenticate another user for this request if KFetBackend succeeds.
By the way, if a user is authenticated, we refresh its from db to add
values from CofProfile and Account of this user.
"""
def process_request(self, request): def process_request(self, request):
if request.user.is_authenticated():
# avoid multiple db accesses in views and templates
user_pk = request.user.pk
request.user = (
User.objects
.select_related('profile__account_kfet')
.get(pk=user_pk)
)
kfet_backend = KFetBackend() kfet_backend = KFetBackend()
temp_request_user = kfet_backend.authenticate(request) temp_request_user = kfet_backend.authenticate(request)
if temp_request_user: if temp_request_user:

View file

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from kfet.forms import KFetConfigForm
def adapt_settings(apps, schema_editor):
Settings = apps.get_model('kfet', 'Settings')
db_alias = schema_editor.connection.alias
obj = Settings.objects.using(db_alias)
cfg = {}
def try_get(new, old, type_field):
try:
value = getattr(obj.get(name=old), type_field)
cfg[new] = value
except Settings.DoesNotExist:
pass
try:
subvention = obj.get(name='SUBVENTION_COF').value_decimal
subvention_mult = 1 + subvention/100
reduction = (1 - 1/subvention_mult) * 100
cfg['kfet_reduction_cof'] = reduction
except Settings.DoesNotExist:
pass
try_get('kfet_addcost_amount', 'ADDCOST_AMOUNT', 'value_decimal')
try_get('kfet_addcost_for', 'ADDCOST_FOR', 'value_account')
try_get('kfet_overdraft_duration', 'OVERDRAFT_DURATION', 'value_duration')
try_get('kfet_overdraft_amount', 'OVERDRAFT_AMOUNT', 'value_decimal')
try_get('kfet_cancel_duration', 'CANCEL_DURATION', 'value_duration')
cfg_form = KFetConfigForm(initial=cfg)
if cfg_form.is_valid():
cfg_form.save()
class Migration(migrations.Migration):
dependencies = [
('kfet', '0053_created_at'),
('djconfig', '0001_initial'),
]
operations = [
migrations.RunPython(adapt_settings),
migrations.RemoveField(
model_name='settings',
name='value_account',
),
migrations.DeleteModel(
name='Settings',
),
]

View file

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0053_created_at'),
]
operations = [
migrations.AlterModelOptions(
name='globalpermissions',
options={'managed': False, 'permissions': (('is_team', 'Is part of the team'), ('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non récentes'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('perform_commented_operations', 'Enregistrer des commandes avec commentaires'), ('view_negs', 'Voir la liste des négatifs'), ('order_to_inventory', "Générer un inventaire à partir d'une commande"), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"), ('special_add_account', 'Créer un compte avec une balance initiale'), ('force_close_kfet', 'Fermer manuelement la K-Fêt'))},
),
]

View file

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
def forwards_perms(apps, schema_editor):
"""Safely delete content type for old kfet.GlobalPermissions model.
Any permissions (except defaults) linked to this content type are updated
to link at its new content type.
Then, delete the content type. This will delete the three defaults
permissions which are assumed unused.
"""
ContentType = apps.get_model('contenttypes', 'contenttype')
try:
ctype_global = ContentType.objects.get(
app_label="kfet", model="globalpermissions",
)
except ContentType.DoesNotExist:
# We are not migrating from existing data, nothing to do.
return
perms = {
'account': (
'is_team', 'manage_perms', 'manage_addcosts',
'edit_balance_account', 'change_account_password',
'special_add_account',
),
'accountnegative': ('view_negs',),
'inventory': ('order_to_inventory',),
'operation': (
'perform_deposit', 'perform_negative_operations',
'override_frozen_protection', 'cancel_old_operations',
'perform_commented_operations',
),
}
Permission = apps.get_model('auth', 'permission')
global_perms = Permission.objects.filter(content_type=ctype_global)
for modelname, codenames in perms.items():
model = apps.get_model('kfet', modelname)
ctype = ContentType.objects.get_for_model(model)
(
global_perms
.filter(codename__in=codenames)
.update(content_type=ctype)
)
ctype_global.delete()
class Migration(migrations.Migration):
dependencies = [
('kfet', '0054_delete_settings'),
('contenttypes', '__latest__'),
('auth', '__latest__'),
]
operations = [
migrations.AlterModelOptions(
name='account',
options={'permissions': (('is_team', 'Is part of the team'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('edit_balance_account', "Modifier la balance d'un compte"), ('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"), ('special_add_account', 'Créer un compte avec une balance initiale'))},
),
migrations.AlterModelOptions(
name='accountnegative',
options={'permissions': (('view_negs', 'Voir la liste des négatifs'),)},
),
migrations.AlterModelOptions(
name='inventory',
options={'ordering': ['-at'], 'permissions': (('order_to_inventory', "Générer un inventaire à partir d'une commande"),)},
),
migrations.AlterModelOptions(
name='operation',
options={'permissions': (('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non récentes'), ('perform_commented_operations', 'Enregistrer des commandes avec commentaires'))},
),
migrations.RunPython(forwards_perms),
]

View file

@ -1,12 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
from django.db import models from django.db import models
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.exceptions import PermissionDenied, ValidationError
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.contrib.auth.models import User from django.contrib.auth.models import User
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
@ -15,11 +10,12 @@ from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.db import transaction from django.db import transaction
from django.db.models import F from django.db.models import F
from django.core.cache import cache from datetime import date
from datetime import date, timedelta
import re import re
import hashlib import hashlib
from kfet.config import kfet_config
def choices_length(choices): def choices_length(choices):
return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0) return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0)
@ -27,8 +23,19 @@ def default_promo():
now = date.today() now = date.today()
return now.month <= 8 and now.year-1 or now.year return now.month <= 8 and now.year-1 or now.year
@python_2_unicode_compatible
class AccountManager(models.Manager):
"""Manager for Account Model."""
def get_queryset(self):
"""Always append related data to this Account."""
return super().get_queryset().select_related('cofprofile__user',
'negative')
class Account(models.Model): class Account(models.Model):
objects = AccountManager()
cofprofile = models.OneToOneField( cofprofile = models.OneToOneField(
CofProfile, on_delete = models.PROTECT, CofProfile, on_delete = models.PROTECT,
related_name = "account_kfet") related_name = "account_kfet")
@ -56,6 +63,19 @@ class Account(models.Model):
unique = True, unique = True,
blank = True, null = True, default = None) blank = True, null = True, default = None)
class Meta:
permissions = (
('is_team', 'Is part of the team'),
('manage_perms', 'Gérer les permissions K-Fêt'),
('manage_addcosts', 'Gérer les majorations'),
('edit_balance_account', "Modifier la balance d'un compte"),
('change_account_password',
"Modifier le mot de passe d'une personne de l'équipe"),
('special_add_account',
"Créer un compte avec une balance initiale"),
('force_close_kfet', "Fermer manuelement la K-Fêt"),
)
def __str__(self): def __str__(self):
return '%s (%s)' % (self.trigramme, self.name) return '%s (%s)' % (self.trigramme, self.name)
@ -85,7 +105,7 @@ class Account(models.Model):
# Propriétés supplémentaires # Propriétés supplémentaires
@property @property
def real_balance(self): def real_balance(self):
if (hasattr(self, 'negative')): if hasattr(self, 'negative') and self.negative.balance_offset:
return self.balance - self.negative.balance_offset return self.balance - self.negative.balance_offset
return self.balance return self.balance
@ -113,8 +133,8 @@ class Account(models.Model):
return data return data
def perms_to_perform_operation(self, amount): def perms_to_perform_operation(self, amount):
overdraft_duration_max = Settings.OVERDRAFT_DURATION() overdraft_duration_max = kfet_config.overdraft_duration
overdraft_amount_max = Settings.OVERDRAFT_AMOUNT() overdraft_amount_max = kfet_config.overdraft_amount
perms = set() perms = set()
stop_ope = False stop_ope = False
# Checking is cash account # Checking is cash account
@ -214,31 +234,75 @@ class Account(models.Model):
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
pass pass
def update_negative(self):
if self.real_balance < 0:
if hasattr(self, 'negative') and not self.negative.start:
self.negative.start = timezone.now()
self.negative.save()
elif not hasattr(self, 'negative'):
self.negative = (
AccountNegative.objects.create(
account=self, start=timezone.now(),
)
)
elif hasattr(self, 'negative'):
# self.real_balance >= 0
balance_offset = self.negative.balance_offset
if balance_offset:
(
Account.objects
.filter(pk=self.pk)
.update(balance=F('balance')-balance_offset)
)
self.refresh_from_db()
self.negative.delete()
class UserHasAccount(Exception): class UserHasAccount(Exception):
def __init__(self, trigramme): def __init__(self, trigramme):
self.trigramme = trigramme self.trigramme = trigramme
class AccountNegativeManager(models.Manager):
"""Manager for AccountNegative model."""
def get_queryset(self):
return (
super().get_queryset()
.select_related('account__cofprofile__user')
)
class AccountNegative(models.Model): class AccountNegative(models.Model):
objects = AccountNegativeManager()
account = models.OneToOneField( account = models.OneToOneField(
Account, on_delete = models.PROTECT, Account, on_delete=models.PROTECT,
related_name = "negative") related_name="negative",
start = models.DateTimeField( )
blank = True, null = True, default = None) start = models.DateTimeField(blank=True, null=True, default=None)
balance_offset = models.DecimalField( balance_offset = models.DecimalField(
"décalage de balance", "décalage de balance",
help_text="Montant non compris dans l'autorisation de négatif", help_text="Montant non compris dans l'autorisation de négatif",
max_digits = 6, decimal_places = 2, max_digits=6, decimal_places=2,
blank = True, null = True, default = None) blank=True, null=True, default=None,
)
authz_overdraft_amount = models.DecimalField( authz_overdraft_amount = models.DecimalField(
"négatif autorisé", "négatif autorisé",
max_digits = 6, decimal_places = 2, max_digits=6, decimal_places=2,
blank = True, null = True, default = None) blank=True, null=True, default=None,
)
authz_overdraft_until = models.DateTimeField( authz_overdraft_until = models.DateTimeField(
"expiration du négatif", "expiration du négatif",
blank = True, null = True, default = None) blank=True, null=True, default=None,
comment = models.CharField("commentaire", max_length = 255, blank = True) )
comment = models.CharField("commentaire", max_length=255, blank=True)
class Meta:
permissions = (
('view_negs', 'Voir la liste des négatifs'),
)
@python_2_unicode_compatible
class Checkout(models.Model): class Checkout(models.Model):
created_by = models.ForeignKey( created_by = models.ForeignKey(
Account, on_delete = models.PROTECT, Account, on_delete = models.PROTECT,
@ -416,6 +480,10 @@ class Inventory(models.Model):
class Meta: class Meta:
ordering = ['-at'] ordering = ['-at']
permissions = (
('order_to_inventory', "Générer un inventaire à partir d'une commande"),
)
class InventoryArticle(models.Model): class InventoryArticle(models.Model):
inventory = models.ForeignKey( inventory = models.ForeignKey(
@ -592,6 +660,17 @@ class Operation(models.Model):
max_digits=6, decimal_places=2, max_digits=6, decimal_places=2,
blank=True, null=True, default=None) blank=True, null=True, default=None)
class Meta:
permissions = (
('perform_deposit', 'Effectuer une charge'),
('perform_negative_operations',
'Enregistrer des commandes en négatif'),
('override_frozen_protection', "Forcer le gel d'un compte"),
('cancel_old_operations', 'Annuler des commandes non récentes'),
('perform_commented_operations',
'Enregistrer des commandes avec commentaires'),
)
@property @property
def is_checkout(self): def is_checkout(self):
return (self.type == Operation.DEPOSIT or return (self.type == Operation.DEPOSIT or
@ -612,139 +691,5 @@ class Operation(models.Model):
amount=self.amount) amount=self.amount)
class GlobalPermissions(models.Model):
class Meta:
managed = False
permissions = (
('is_team', 'Is part of the team'),
('perform_deposit', 'Effectuer une charge'),
('perform_negative_operations',
'Enregistrer des commandes en négatif'),
('override_frozen_protection', "Forcer le gel d'un compte"),
('cancel_old_operations', 'Annuler des commandes non récentes'),
('manage_perms', 'Gérer les permissions K-Fêt'),
('manage_addcosts', 'Gérer les majorations'),
('perform_commented_operations', 'Enregistrer des commandes avec commentaires'),
('view_negs', 'Voir la liste des négatifs'),
('order_to_inventory', "Générer un inventaire à partir d'une commande"),
('edit_balance_account', "Modifier la balance d'un compte"),
('change_account_password', "Modifier le mot de passe d'une personne de l'équipe"),
('special_add_account', "Créer un compte avec une balance initiale"),
('force_close_kfet', "Fermer manuelement la K-Fêt"),
)
class Settings(models.Model):
name = models.CharField(
max_length = 45,
unique = True,
db_index = True)
value_decimal = models.DecimalField(
max_digits = 6, decimal_places = 2,
blank = True, null = True, default = None)
value_account = models.ForeignKey(
Account, on_delete = models.PROTECT,
blank = True, null = True, default = None)
value_duration = models.DurationField(
blank = True, null = True, default = None)
@staticmethod
def setting_inst(name):
return Settings.objects.get(name=name)
@staticmethod
def SUBVENTION_COF():
subvention_cof = cache.get('SUBVENTION_COF')
if subvention_cof:
return subvention_cof
try:
subvention_cof = Settings.setting_inst("SUBVENTION_COF").value_decimal
except Settings.DoesNotExist:
subvention_cof = 0
cache.set('SUBVENTION_COF', subvention_cof)
return subvention_cof
@staticmethod
def ADDCOST_AMOUNT():
try:
return Settings.setting_inst("ADDCOST_AMOUNT").value_decimal
except Settings.DoesNotExist:
return 0
@staticmethod
def ADDCOST_FOR():
try:
return Settings.setting_inst("ADDCOST_FOR").value_account
except Settings.DoesNotExist:
return None;
@staticmethod
def OVERDRAFT_DURATION():
overdraft_duration = cache.get('OVERDRAFT_DURATION')
if overdraft_duration:
return overdraft_duration
try:
overdraft_duration = Settings.setting_inst("OVERDRAFT_DURATION").value_duration
except Settings.DoesNotExist:
overdraft_duration = timedelta()
cache.set('OVERDRAFT_DURATION', overdraft_duration)
return overdraft_duration
@staticmethod
def OVERDRAFT_AMOUNT():
overdraft_amount = cache.get('OVERDRAFT_AMOUNT')
if overdraft_amount:
return overdraft_amount
try:
overdraft_amount = Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal
except Settings.DoesNotExist:
overdraft_amount = 0
cache.set('OVERDRAFT_AMOUNT', overdraft_amount)
return overdraft_amount
@staticmethod
def CANCEL_DURATION():
cancel_duration = cache.get('CANCEL_DURATION')
if cancel_duration:
return cancel_duration
try:
cancel_duration = Settings.setting_inst("CANCEL_DURATION").value_duration
except Settings.DoesNotExist:
cancel_duration = timedelta()
cache.set('CANCEL_DURATION', cancel_duration)
return cancel_duration
@staticmethod
def create_missing():
s, created = Settings.objects.get_or_create(name='SUBVENTION_COF')
if created:
s.value_decimal = 25
s.save()
s, created = Settings.objects.get_or_create(name='ADDCOST_AMOUNT')
if created:
s.value_decimal = 0.5
s.save()
s, created = Settings.objects.get_or_create(name='ADDCOST_FOR')
s, created = Settings.objects.get_or_create(name='OVERDRAFT_DURATION')
if created:
s.value_duration = timedelta(days=1) # 24h
s.save()
s, created = Settings.objects.get_or_create(name='OVERDRAFT_AMOUNT')
if created:
s.value_decimal = 20
s.save()
s, created = Settings.objects.get_or_create(name='CANCEL_DURATION')
if created:
s.value_duration = timedelta(minutes=5) # 5min
s.save()
@staticmethod
def empty_cache():
cache.delete_many([
'SUBVENTION_COF', 'OVERDRAFT_DURATION', 'OVERDRAFT_AMOUNT',
'CANCEL_DURATION', 'ADDCOST_AMOUNT', 'ADDCOST_FOR',
])
class GenericTeamToken(models.Model): class GenericTeamToken(models.Model):
token = models.CharField(max_length = 50, unique = True) token = models.CharField(max_length = 50, unique = True)

View file

@ -1,16 +1,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.signals import user_logged_in from django.contrib.auth.signals import user_logged_in
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.dispatch import receiver from django.dispatch import receiver
@receiver(user_logged_in) @receiver(user_logged_in)
def messages_on_login(sender, request, user, **kwargs): def messages_on_login(sender, request, user, **kwargs):
if (not user.username == 'kfet_genericteam' if (not user.username == 'kfet_genericteam' and
and user.has_perm('kfet.is_team')): user.has_perm('kfet.is_team') and
messages.info(request, '<a href="%s">Connexion en utilisateur partagé ?</a>' % reverse('kfet.login.genericteam'), extra_tags='safe') 'k-fet' in request.GET.get('next', '')):
messages.info(
request,
('<a href="{}" target="_blank">Connexion en utilisateur partagé ?</a>'
.format(reverse('kfet.login.genericteam'))),
extra_tags='safe')

View file

@ -9,17 +9,20 @@
#history .day { #history .day {
height:40px; height:40px;
line-height:40px; line-height:40px;
background-color:#c8102e; background-color:rgba(200,16,46,0.9);
color:#fff; color:#fff;
padding-left:20px; padding-left:20px;
font-size:16px; font-size:16px;
font-weight:bold; font-weight:bold;
position:sticky;
top:50px;
z-index:10;
} }
#history .opegroup { #history .opegroup {
height:30px; height:30px;
line-height:30px; line-height:30px;
background-color:rgba(200,16,46,0.85); background-color:rgba(200,16,46,0.75);
color:#fff; color:#fff;
font-weight:bold; font-weight:bold;
padding-left:20px; padding-left:20px;

View file

@ -33,10 +33,8 @@ textarea {
.table { .table {
margin-bottom:0; margin-bottom:0;
border-bottom:1px solid #ddd; border-bottom:1px solid #ddd;
}
.table {
width:100%; width:100%;
background-color: #FFF;
} }
.table td { .table td {
@ -70,6 +68,16 @@ textarea {
padding:8px 30px; padding:8px 30px;
} }
.table-responsive {
border: 0;
margin-bottom: 0;
}
.btn {
transition: background-color, color;
transition-duration: 0.15s;
}
.btn, .btn-lg, .btn-group-lg>.btn { .btn, .btn-lg, .btn-group-lg>.btn {
border-radius:0; border-radius:0;
} }
@ -81,8 +89,29 @@ textarea {
border:0; border:0;
} }
.btn-primary:hover, .btn-primary.focus, .btn-primary:focus { .btn-primary:hover,
background-color:#000; .btn-primary.focus, .btn-primary:focus,
.btn-primary.active.focus, .btn-primary.active:focus, .btn-primary.active:hover,
.btn-primary:active.focus, .btn-primary:active:focus, .btn-primary:active:hover,
.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover {
outline: 0;
background-color:rgba(200,16,46,1);
color:#FFF;
}
.btn-primary[disabled]:hover,
.btn-primary[disabled]:focus {
background-color: #000;
color: #666;
}
.nav-pills>li>a {
border-radius:0;
}
.nav-pills>li>a:focus, .nav-pills>li>a:hover {
outline: 0;
background-color:rgba(200,16,46,1);
color:#FFF; color:#FFF;
} }
@ -104,26 +133,17 @@ textarea {
padding: 0 !important; padding: 0 !important;
} }
.panel-md-margin{
background-color: white;
overflow:hidden;
padding-left: 15px;
padding-right: 15px;
padding-bottom: 15px;
padding-top: 1px;
}
@media (min-width: 992px) {
.panel-md-margin{
margin:8px;
background-color: white;
}
}
.col-content-left, .col-content-right { .col-content-left, .col-content-right {
padding:0; padding:0;
} }
@media (min-width: 768px) {
.col-content-left {
position: sticky;
top:50px;
}
}
.content-left-top { .content-left-top {
background:#fff; background:#fff;
padding:10px 30px; padding:10px 30px;
@ -137,6 +157,14 @@ textarea {
display:block; display:block;
} }
.content-left .buttons ul.nav-pills {
margin-bottom:5px;
}
.content-left .buttons ul.nav-pills li {
margin:0 0 -5px;
}
.content-left-top.frozen-account { .content-left-top.frozen-account {
background:#000FBA; background:#000FBA;
color:#fff; color:#fff;
@ -169,25 +197,22 @@ textarea {
text-align:center; text-align:center;
} }
.content-right { @media (min-width: 768px) {
margin:0 15px; .content-right {
margin: 15px;
}
} }
.content-right-block { .content-right-block {
padding-bottom:5px;
position:relative; position:relative;
} }
.content-right-block:last-child { .content-right-block > *:not(.buttons-title) {
padding-bottom:15px; background: #fff;
} }
.content-right-block > div:not(.buttons-title) { .content-right-block > h2 {
background:#fff; background: transparent !important;
}
.content-right-block-transparent > div:not(.buttons-title) {
background-color: transparent;
} }
.content-right-block .buttons-title { .content-right-block .buttons-title {
@ -209,9 +234,8 @@ textarea {
.content-right-block h3 { .content-right-block h3 {
border-bottom: 1px solid #c8102e; border-bottom: 1px solid #c8102e;
margin: 20px 15px 15px; margin: 0px 15px 15px;
padding-bottom: 10px; padding: 20px 20px 10px;
padding-left: 20px;
font-size:25px; font-size:25px;
} }
@ -219,20 +243,34 @@ textarea {
* Pages tableaux seuls * Pages tableaux seuls
*/ */
.content-center > div { .content-center > *:not(.content-right-block) {
background:#fff; background: #fff;
}
@media (min-width: 992px) {
.content-center {
margin: 15px 0;
}
} }
.content-center tbody tr:not(.section) td { .content-center tbody tr:not(.section) td {
padding:0px 5px !important; padding:0px 5px;
} }
.content-center .table .form-control { .table .form-control {
padding: 1px 12px ; padding: 1px 12px ;
height:28px; height:28px;
margin:3px 0px; margin:3px 0px;
} }
.content-center .auth-form {
.table-condensed input.form-control {
margin: 0 !important;
border-top: 0;
border-bottom: 0;
border-radius: 0;
}
.content-center .auth-form {
margin:15px; margin:15px;
} }
@ -240,15 +278,12 @@ textarea {
* Pages formulaires seuls * Pages formulaires seuls
*/ */
.form-only .content-form { .content-form {
margin:15px; background-color: #fff;
padding: 15px;
background:#fff;
padding:15px;
} }
.form-only .account_create #id_trigramme { .account_create #id_trigramme {
display:block; display:block;
width:200px; width:200px;
height:80px; height:80px;
@ -314,6 +349,10 @@ textarea {
* Messages * Messages
*/ */
.messages {
margin: 0;
}
.messages .alert { .messages .alert {
padding:10px 15px; padding:10px 15px;
margin:0; margin:0;
@ -551,21 +590,67 @@ thead .tooltip {
} }
} }
.help-block {
padding-top: 15px;
}
/* Inventaires */ /* Inventaires */
#inventoryform input[type=number] {
text-align: center;
}
.inventory_modified { .inventory_modified {
background:rgba(236,100,0,0.15); background:rgba(236,100,0,0.15);
} }
.stock_diff { .stock_diff {
padding-left: 5px; padding-left: 5px;
color:#C8102E; color:#C8102E;
} }
.inventory_update { .inventory_update {
display:none; display: none;
width: 50px;
margin: 0 auto;
}
/* Multiple select customizations */
.ms-choice {
height: 34px !important;
line-height: 34px !important;
border: 1px solid #ccc !important;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075) !important;
}
.ms-choice > div {
top: 4px !important;
}
/* Checkbox select multiple */
.checkbox-select-multiple label {
font-weight: normal;
margin-bottom: 0;
}
/* Statement creation */
.statement-create-summary table {
margin: 0 auto;
}
.statement-create-summary tr td {
text-align: right;
}
.statement-create-summary tr td:first-child {
padding-right: 15px;
font-weight: bold;
}
.statement-create-summary tr td:last-child {
width: 80px;
}
#detail_taken table td,
#detail_balance table td {
padding: 0;
} }

View file

@ -18,6 +18,17 @@ input[type=number]::-webkit-outer-spin-button {
100% { background: yellow; } 100% { background: yellow; }
} }
/* Announcements banner */
#banner {
background-color: #d86b01;
width: 100%;
text-align: center;
padding: 10px;
color: white;
font-size: larger;
}
/* /*
* Top row * Top row
*/ */
@ -143,7 +154,7 @@ input[type=number]::-webkit-outer-spin-button {
height:50px; height:50px;
padding:0 15px; padding:0 15px;
background:#c8102e; background:rgba(200,16,46,0.9);
color:#fff; color:#fff;
font-weight:bold; font-weight:bold;
@ -232,16 +243,21 @@ input[type=number]::-webkit-outer-spin-button {
float:left; float:left;
background:#c8102e; background: rgba(200,16,46,0.9);
color:#FFF; color:#FFF;
font-size:18px; font-size:18px;
font-weight:bold; font-weight:bold;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} }
#special_operations button:focus, #special_operations button:hover { #special_operations button:focus,
#special_operations button:hover {
outline:none; outline:none;
background:#000; background: rgba(200,16,46,1);
color:#fff; color:#fff;
} }
@ -256,15 +272,14 @@ input[type=number]::-webkit-outer-spin-button {
height:100%; height:100%;
float:left; float:left;
border:0; border:0;
border-right:1px solid #c8102e;
border-bottom:1px solid #c8102e;
border-radius:0; border-radius:0;
border-bottom: 1px solid rgba(200,16,46,0.9);
font-size:16px; font-size:16px;
font-weight:bold; font-weight:bold;
} }
#article_selection input+input #article_selection input+span { #article_selection input:first-child {
border-right:0; border-right: 1px dashed rgba(200,16,46,0.9);
} }
#article_autocomplete { #article_autocomplete {
@ -319,7 +334,7 @@ input[type=number]::-webkit-outer-spin-button {
#articles_data table tr.category { #articles_data table tr.category {
height:35px; height:35px;
background-color:#c8102e; background-color:rgba(200,16,46,0.9);
font-size:16px; font-size:16px;
color:#FFF; color:#FFF;
font-weight:bold; font-weight:bold;
@ -423,3 +438,7 @@ input[type=number]::-webkit-outer-spin-button {
.kpsul_middle_right_col { .kpsul_middle_right_col {
overflow:auto; overflow:auto;
} }
.kpsul_middle_right_col #history .day {
top: 0;
}

View file

@ -1,47 +1,61 @@
nav { .navbar {
background:#000; background: #000;
color:#DDD; color: #DDD;
font-family:Oswald; font-family: Oswald;
border: 0;
} }
.navbar-nav > li > .dropdown-menu { .navbar .navbar-brand {
border:0; padding: 3px 15px 3px 25px;
border-radius:0;
} }
.navbar-fixed-top { .navbar .navbar-brand img {
border:0; height: 44px;
} }
nav .navbar-brand { .navbar .navbar-toggle .icon-bar {
padding:3px 15px 3px 25px; background-color: #FFF;
}
nav .navbar-brand img {
height:44px;
}
nav .navbar-toggle .icon-bar {
background-color:#FFF;
}
nav a {
color:#DDD;
} }
.navbar-nav { .navbar-nav {
font-weight:bold; font-weight: bold;
font-size:14px; font-size: 14px;
text-transform:uppercase; text-transform: uppercase;
margin: 0 -15px;
} }
.nav>li>a:focus, .nav>li>a:hover { @media (min-width: 768px) {
background-color:#C8102E; .navbar-nav {
color:#FFF; margin: 0px;
}
.navbar-right {
margin-right: -15px;
}
} }
.nav .open>a, .nav .open>a:focus, .nav .open>a:hover { .navbar-nav a {
background-color:#C8102E; transition: background-color, box-shadow, color;
transition-duration: 0.15s;
}
.navbar-nav > li > a {
color: #FFF;
}
.navbar-nav > li:hover > a,
.navbar-nav > li > a:focus,
.nav .open > a:hover,
.nav .open > a:focus {
background-color: #C8102E;
color: #FFF;
box-shadow: inset 0 5px 5px -5px #000;
}
.navbar-nav .dropdown .dropdown-menu {
padding: 0;
border: 0;
border-radius: 0;
background-color: #FFF;
} }
#kfet-open { #kfet-open {
@ -62,24 +76,31 @@ nav a {
line-height: 10px; line-height: 10px;
} }
.dropdown-menu { .navbar-nav .dropdown .dropdown-menu > li > a {
padding:0; padding: 8px 20px;
color: #000;
} }
.dropdown-menu>li>a { .navbar-nav .dropdown .dropdown-menu > li > a:hover,
padding:8px 20px; .navbar-nav .dropdown .dropdown-meny > li > a:focus {
color: #c8102e;
background-color: transparent;
} }
.dropdown-menu .divider { .navbar-nav .dropdown .dropdown-menu .divider {
margin:0; margin: 0;
} }
@media (max-width: 767px) { @media (min-width: 768px) {
.navbar-nav .open .dropdown-menu { .navbar-nav .dropdown .dropdown-menu {
background-color:#FFF; display: block;
visibility: hidden;
opacity: 0;
transition: opacity 0.15s;
} }
.navbar-nav { .nav .dropdown:hover .dropdown-menu {
margin:0 -15px; visibility: visible;
opacity: 1;
} }
} }

View file

@ -1,14 +1,4 @@
$(document).ready(function() { $(document).ready(function() {
$(window).scroll(function() {
if ($(window).width() >= 768 && $(this).scrollTop() > 72.6) {
$('.col-content-left').css({'position':'fixed', 'top':'50px'});
$('.col-content-right').addClass('col-sm-offset-4 col-md-offset-3');
} else {
$('.col-content-left').css({'position':'relative', 'top':'0'});
$('.col-content-right').removeClass('col-sm-offset-4 col-md-offset-3');
}
});
if (typeof Cookies !== 'undefined') { if (typeof Cookies !== 'undefined') {
// Retrieving csrf token // Retrieving csrf token
csrftoken = Cookies.get('csrftoken'); csrftoken = Cookies.get('csrftoken');

View file

@ -61,7 +61,7 @@
var chart = charts[i]; var chart = charts[i];
// format the data // format the data
var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 1); var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 0);
chart_datasets.push( chart_datasets.push(
{ {
@ -132,7 +132,7 @@
type: 'line', type: 'line',
options: chart_options, options: chart_options,
data: { data: {
labels: (data.labels || []).slice(1), labels: data.labels || [],
datasets: chart_datasets, datasets: chart_datasets,
} }
}; };

View file

@ -4,6 +4,7 @@ from datetime import date, datetime, time, timedelta
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from dateutil.parser import parse as dateutil_parse from dateutil.parser import parse as dateutil_parse
import pytz
from django.utils import timezone from django.utils import timezone
from django.db.models import Sum from django.db.models import Sum
@ -13,7 +14,8 @@ KFET_WAKES_UP_AT = time(7, 0)
def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT): def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT):
"""datetime wrapper with time offset.""" """datetime wrapper with time offset."""
return datetime.combine(date(year, month, day), start_at) naive = datetime.combine(date(year, month, day), start_at)
return pytz.timezone('Europe/Paris').localize(naive, is_dst=None)
def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT): def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT):
@ -32,16 +34,21 @@ class Scale(object):
self.std_chunk = std_chunk self.std_chunk = std_chunk
if last: if last:
end = timezone.now() end = timezone.now()
if std_chunk:
if begin is not None:
begin = self.get_chunk_start(begin)
if end is not None:
end = self.do_step(self.get_chunk_start(end))
if begin is not None and n_steps != 0: if begin is not None and n_steps != 0:
self.begin = self.get_from(begin) self.begin = begin
self.end = self.do_step(self.begin, n_steps=n_steps) self.end = self.do_step(self.begin, n_steps=n_steps)
elif end is not None and n_steps != 0: elif end is not None and n_steps != 0:
self.end = self.get_from(end) self.end = end
self.begin = self.do_step(self.end, n_steps=-n_steps) self.begin = self.do_step(self.end, n_steps=-n_steps)
elif begin is not None and end is not None: elif begin is not None and end is not None:
self.begin = self.get_from(begin) self.begin = begin
self.end = self.get_from(end) self.end = end
else: else:
raise Exception('Two of these args must be specified: ' raise Exception('Two of these args must be specified: '
'n_steps, begin, end; ' 'n_steps, begin, end; '
@ -71,7 +78,7 @@ class Scale(object):
def get_datetimes(self): def get_datetimes(self):
datetimes = [self.begin] datetimes = [self.begin]
tmp = self.begin tmp = self.begin
while tmp <= self.end: while tmp < self.end:
tmp = self.do_step(tmp) tmp = self.do_step(tmp)
datetimes.append(tmp) datetimes.append(tmp)
return datetimes return datetimes
@ -79,7 +86,103 @@ class Scale(object):
def get_labels(self, label_fmt=None): def get_labels(self, label_fmt=None):
if label_fmt is None: if label_fmt is None:
label_fmt = self.label_fmt label_fmt = self.label_fmt
return [begin.strftime(label_fmt) for begin, end in self] return [
begin.strftime(label_fmt.format(i=i, rev_i=len(self)-i))
for i, (begin, end) in enumerate(self)
]
def chunkify_qs(self, qs, field=None):
if field is None:
field = 'at'
begin_f = '{}__gte'.format(field)
end_f = '{}__lte'.format(field)
return [
qs.filter(**{begin_f: begin, end_f: end})
for begin, end in self
]
def get_by_chunks(self, qs, field_callback=None, field_db='at'):
"""Objects of queryset ranked according to the scale.
Returns a generator whose each item, corresponding to a scale chunk,
is a generator of objects from qs for this chunk.
Args:
qs: Queryset of source objects, must be ordered *first* on the
same field returned by `field_callback`.
field_callback: Callable which gives value from an object used
to compare against limits of the scale chunks.
Default to: lambda obj: getattr(obj, field_db)
field_db: Used to filter against `scale` limits.
Default to 'at'.
Examples:
If queryset `qs` use `values()`, `field_callback` must be set and
could be: `lambda d: d['at']`
If `field_db` use foreign attributes (eg with `__`), it should be
something like: `lambda obj: obj.group.at`.
"""
if field_callback is None:
def field_callback(obj):
return getattr(obj, field_db)
begin_f = '{}__gte'.format(field_db)
end_f = '{}__lte'.format(field_db)
qs = (
qs
.filter(**{begin_f: self.begin, end_f: self.end})
)
obj_iter = iter(qs)
last_obj = None
def _objects_until(obj_iter, field_callback, end):
"""Generator of objects until `end`.
Ends if objects source is empty or when an object not verifying
field_callback(obj) <= end is met.
If this object exists, it is stored in `last_obj` which is found
from outer scope.
Also, if this same variable is non-empty when the function is
called, it first yields its content.
Args:
obj_iter: Source used to get objects.
field_callback: Returned value, when it is called on an object
will be used to test ordering against `end`.
end
"""
nonlocal last_obj
if last_obj is not None:
yield last_obj
last_obj = None
for obj in obj_iter:
if field_callback(obj) <= end:
yield obj
else:
last_obj = obj
return
for begin, end in self:
# forward last seen object, if it exists, to the right chunk,
# and fill with empty generators for intermediate chunks of scale
if last_obj is not None:
if field_callback(last_obj) > end:
yield iter(())
continue
# yields generator for this chunk
# this set last_obj to None if obj_iter reach its end, otherwise
# it's set to the first met object from obj_iter which doesn't
# belong to this chunk
yield _objects_until(obj_iter, field_callback, end)
class DayScale(Scale): class DayScale(Scale):
@ -222,13 +325,3 @@ class ScaleMixin(object):
def get_default_scale(self): def get_default_scale(self):
return DayScale(n_steps=7, last=True) return DayScale(n_steps=7, last=True)
def chunkify_qs(self, qs, scale, field=None):
if field is None:
field = 'at'
begin_f = '{}__gte'.format(field)
end_f = '{}__lte'.format(field)
return [
qs.filter(**{begin_f: begin, end_f: end})
for begin, end in scale
]

View file

@ -1,67 +1,61 @@
{% extends "kfet/base.html" %} {% extends "kfet/base_col_2.html" %}
{% block title %}Liste des comptes{% endblock %} {% block title %}Liste des comptes{% endblock %}
{% block content-header-title %}Comptes{% endblock %} {% block header-title %}Comptes{% endblock %}
{% block content %} {% block fixed-content %}
<div class="row"> <div class="content-left-top">
<div class="col-sm-4 col-md-3 col-content-left"> <div class="line line-big">{{ accounts|length|add:-1 }}</div>
<div class="content-left"> <div class="line line-bigsub">compte{{ accounts|length|add:-1|pluralize }}</div>
<div class="content-left-top"> </div>
<div class="line line-big">{{ accounts|length|add:-1 }}</div> <div class="buttons">
<div class="line line-bigsub">compte{{ accounts|length|add:-1|pluralize }}</div> <a class="btn btn-primary btn-lg" href="{% url 'kfet.account.create' %}">Créer un compte</a>
</div> {% if perms.kfet.manage_perms %}
<div class="buttons"> <a class="btn btn-primary btn-lg" href="{% url 'kfet.account.group' %}">Permissions</a>
<a class="btn btn-primary btn-lg" href="{% url 'kfet.account.create' %}">Créer un compte</a> {% endif %}
{% if perms.kfet.manage_perms %} {% if perms.kfet.view_negs %}
<a class="btn btn-primary btn-lg" href="{% url 'kfet.account.group' %}">Permissions</a> <a class="btn btn-primary btn-lg" href="{% url 'kfet.account.negative' %}">Négatifs</a>
{% endif %} {% endif %}
{% if perms.kfet.view_negs %} </div>
<a class="btn btn-primary btn-lg" href="{% url 'kfet.account.negative' %}">Négatifs</a>
{% endif %} {% endblock %}
</div>
</div> {% block main-content %}
</div>
<div class="col-sm-8 col-md-9 col-content-right"> <div class="content-right-block">
{% include 'kfet/base_messages.html' %} <h2>Liste des comptes</h2>
<div class="content-right"> <div class="table-responsive">
<div class="content-right-block"> <table class="table table-condensed">
<h2>Liste des comptes</h2> <thead>
<div class="table-responsive"> <tr>
<table class="table table-condensed"> <td></td>
<thead> <td>Trigramme</td>
<tr> <td>Nom</td>
<td></td> <td>Balance</td>
<td>Trigramme</td> <td>COF</td>
<td>Nom</td> <td>Dpt</td>
<td>Balance</td> <td>Promo</td>
<td>COF</td> </tr>
<td>Dpt</td> </thead>
<td>Promo</td> <tbody>
</tr> {% for account in accounts %}
</thead> <tr>
<tbody> <td class="text-center">
{% for account in accounts %} <a href="{% url 'kfet.account.read' account.trigramme %}">
<tr> <span class="glyphicon glyphicon-cog"></span>
<td class="text-center"> </a>
<a href="{% url 'kfet.account.read' account.trigramme %}"> </td>
<span class="glyphicon glyphicon-cog"></span> <td>{{ account.trigramme }}</td>
</a> <td>{{ account.name }}</td>
</td> <td class="text-right">{{ account.balance }}€</td>
<td>{{ account.trigramme }}</td> <td>{{ account.is_cof|yesno:"Oui,Non" }}</td>
<td>{{ account.name }}</td> <td>{{ account.departement }}</td>
<td class="text-right">{{ account.balance }}€</td> <td>{{ account.promo|default_if_none:'' }}</td>
<td>{{ account.is_cof }}</td> </tr>
<td>{{ account.departement }}</td> {% endfor %}
<td>{{ account.promo|default_if_none:'' }}</td> </tbody>
</tr> </table>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -1,45 +1,39 @@
{% extends "kfet/base.html" %} {% extends "kfet/base_col_1.html" %}
{% load staticfiles %} {% load staticfiles %}
{% block title %}Nouveau compte{% endblock %} {% block title %}Nouveau compte{% endblock %}
{% block header-title %}Création d'un compte{% endblock %}
{% block extra_head %} {% block extra_head %}
<script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script> <script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script>
{% endblock %} {% endblock %}
{% block content-header-title %}Création d'un compte{% endblock %} {% block main-class %}content-form{% endblock %}
{% block content %} {% block main-content %}
{% include 'kfet/base_messages.html' %} <form action="" method="post" class="account_create">
{% csrf_token %}
<div class="row form-only"> <div>
<div class="col-sm-12 col-md-8 col-md-offset-2"> {{ trigramme_form.trigramme.errors }}
<div class="content-form"> {{ trigramme_form.trigramme }}
<form action="{% url "kfet.account.create" %}" method="post" class="account_create">
{% csrf_token %}
<div>
{{ trigramme_form.trigramme.errors }}
{{ trigramme_form.trigramme }}
</div>
<div id="trigramme_valid"></div>
<p class="help-block">Les mots contenant des caractères non alphanumériques seront ignorés</p>
<input type="text" name="q" id="search_autocomplete" spellcheck="false" placeholder="Chercher un utilisateur par nom, prénom ou identifiant clipper" class="form-control">
<div style="position:relative;">
<div id="search_results"></div>
</div>
<div class="form-horizontal">
<div id="form-placeholder">
{% include 'kfet/account_create_form.html' %}
</div>
{% if not perms.kfet.add_account %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
</div>
</form>
</div>
</div> </div>
</div> <div id="trigramme_valid"></div>
<p class="help-block">Les mots contenant des caractères non alphanumériques seront ignorés</p>
<input type="text" name="q" id="search_autocomplete" spellcheck="false" placeholder="Chercher un utilisateur par nom, prénom ou identifiant clipper" class="form-control">
<div style="position:relative;">
<div id="search_results"></div>
</div>
<div class="form-horizontal">
<div id="form-placeholder">
{% include 'kfet/account_create_form.html' %}
</div>
{% if not perms.kfet.add_account %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
</div>
</form>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
// Affichage des résultats d'autocomplétion // Affichage des résultats d'autocomplétion

View file

@ -2,44 +2,37 @@
{% load staticfiles %} {% load staticfiles %}
{% block title %}Nouveau compte{% endblock %} {% block title %}Nouveau compte{% endblock %}
{% block header-title %}Création d'un compte{% endblock %}
{% block extra_head %} {% block extra_head %}
<script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script> <script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script>
{% endblock %} {% endblock %}
{% block content-header-title %}Création d'un compte{% endblock %} {% block main-class %}content-form{% endblock %}
{% block content %} {% block main-content %}
{% include 'kfet/base_messages.html' %} <form action="" method="post" class="account_create">
{% csrf_token %}
<div class="row form-only"> <div>
<div class="col-sm-12 col-md-8 col-md-offset-2"> {{ trigramme_form.trigramme.errors }}
<div class="content-form"> {{ trigramme_form.trigramme }}
<form action="{% url "kfet.account.create_special" %}" method="post" class="account_create"> {{ balance_form }}
{% csrf_token %}
<div>
{{ trigramme_form.trigramme.errors }}
{{ trigramme_form.trigramme }}
{{ balance_form }}
</div>
<div id="trigramme_valid"></div>
<input type="text" name="q" id="search_autocomplete" spellcheck="false" placeholder="Chercher un utilisateur par nom, prénom ou identifiant clipper" class="form-control">
<div style="position:relative;">
<div id="search_results"></div>
</div>
<div class="form-horizontal">
<div id="form-placeholder">
{% include 'kfet/account_create_form.html' %}
</div>
{% if not perms.kfet.add_account %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
</div>
</form>
</div>
</div> </div>
</div> <div id="trigramme_valid"></div>
<input type="text" name="q" id="search_autocomplete" spellcheck="false" placeholder="Chercher un utilisateur par nom, prénom ou identifiant clipper" class="form-control">
<div style="position:relative;">
<div id="search_results"></div>
</div>
<div class="form-horizontal">
<div id="form-placeholder">
{% include 'kfet/account_create_form.html' %}
</div>
{% if not perms.kfet.add_account %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
</div>
</form>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
// Affichage des résultats d'autocomplétion // Affichage des résultats d'autocomplétion

View file

@ -1,54 +1,51 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_2.html" %}
{% block title %}Groupes de comptes{% endblock %} {% block title %}Groupes de comptes{% endblock %}
{% block content-header-title %}Groupes de comptes{% endblock %} {% block header-title %}Groupes de comptes{% endblock %}
{% block content %} {% block fixed-content %}
<div class="row"> <div class="buttons">
<div class="col-sm-4 col-md-3 col-content-left"> <a class="btn btn-primary btn-lg" href="{% url 'kfet.account.group.create' %}">Créer un groupe</a>
<div class="content-left">
<div class="content-left-top text-center">
<div class="line"></div>
</div>
<div class="buttons">
<a class="btn btn-primary btn-lg" href="{% url 'kfet.account.group.create' %}">Créer un groupe</a>
</div>
</div>
</div>
<div class="col-sm-8 col-md-9 col-content-right">
{% include 'kfet/base_messages.html' %}
<div class="content-right">
{% for group in groups %}
<div class="content-right-block">
<div class="buttons-title">
<a class="btn btn-primary" href="{% url 'kfet.account.group.update' group.pk %}">
<span class="glyphicon glyphicon-cog"></span>
</a>
</div>
<h2>{{ group.name }}</h2>
<div class="row">
<div class="col-sm-6">
<h3>Permissions</h3>
<ul>
{% for perm in group.permissions.all %}
<li>{{ perm.name }}</li>
{% endfor %}
</ul>
</div>
<div class="col-sm-6">
<h3>Comptes</h3>
<ul>
{% for user in group.user_set.all %}
<li>{{ user.profile.account_kfet }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block main-content %}
{% for group in groups %}
<div class="content-right-block">
<div class="buttons-title">
<a class="btn btn-primary" href="{% url 'kfet.account.group.update' group.pk %}">
<span class="glyphicon glyphicon-cog"></span>
</a>
</div>
<h2>{{ group.name }}</h2>
<div class="row">
<div class="col-sm-6">
<h3>Permissions</h3>
{% regroup group.permissions.all by content_type as grouped_perms %}
<ul class="list-unstyled">
{% for perms_group in grouped_perms %}
<li><b>{{ perms_group.grouper|title }}</b>
<ul class="list-unstyled">
{% for perm in perms_group.list %}
<li style="padding-left: 20px">{{ perm.name }}</li>
{% endfor %}
</ul>
{% endfor %}
</ul>
</div>
<div class="col-sm-6">
<h3>Comptes</h3>
<ul class="list-unstyled">
{% for user in group.user_set.all %}
<li>{{ user.profile.account_kfet }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endfor %}
{% endblock %}

View file

@ -1,37 +1,46 @@
{% extends 'kfet/base.html' %} {% extends 'kfet/base_col_1.html' %}
{% load staticfiles %} {% load staticfiles %}
{% load widget_tweaks %}
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" text="text/css" href="{% static 'kfet/css/multiple-select.css' %}"> <link rel="stylesheet" text="text/css" href="{% static 'kfet/css/multiple-select.css' %}">
<script src="{% static 'kfet/js/multiple-select.js' %}"></script> <script src="{% static 'kfet/js/multiple-select.js' %}"></script>
{% endblock %} {% endblock %}
{% block content %} {% block title %}Permissions - Édition{% endblock %}
{% block header-title %}Modification des permissions{% endblock %}
<form action="" method="post"> {% block main-class %}content-form{% endblock %}
{% block main-content %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<div> <div class="form-group">
{{ form.name.errors }} <label for="{{ form.name.id_for_label }}" class="col-sm-2 control-label">{{ form.name.label }}</label>
{{ form.name.label_tag }} <div class="col-sm-10">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon">K-Fêt</span> <span class="input-group-addon">K-Fêt</span>
{{ form.name }} {{ form.name|add_class:"form-control" }}
</div>
{% if form.name.errors %}<span class="help-block">{{ form.name.errors }}</span>{% endif %}
{% if form.name.help_text %}<span class="help-block">{{ form.name.help_text }}</span>{% endif %}
</div> </div>
</div> </div>
<div> {% include "kfet/form_field_snippet.html" with field=form.permissions %}
{{ form.permissions.errors }} {% if not perms.kfet.manage_perms %}
{{ form.permissions.label_tag }} {% include "kfet/form_authentication_snippet.html" %}
{{ form.permissions }} {% endif %}
</div> {% include "kfet/form_submit_snippet.html" with value="Enregistrer" %}
<input type="submit" value="Enregistrer">
</form> </form>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
$("select").multipleSelect({ let $name_input = $("#id_name");
width: 500, let raw_name = $name_input.val();
filter: true, let prefix = "K-Fêt ";
}); if (raw_name.startsWith(prefix))
$name_input.val(raw_name.substring(prefix.length));
}); });
</script> </script>

View file

@ -1,79 +1,73 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_2.html" %}
{% block title %}Comptes en négatifs{% endblock %} {% block title %}Comptes - Négatifs{% endblock %}
{% block content-header-title %}Comptes - Négatifs{% endblock %} {% block header-title %}Comptes en négatifs{% endblock %}
{% block content %} {% block fixed-content %}
<div class="row"> <div class="content-left-top">
<div class="col-sm-4 col-md-3 col-content-left"> <div class="line line-big">{{ negatives|length }}</div>
<div class="content-left"> <div class="line line-bigsub">compte{{ negatives|length|pluralize }} en négatif</div>
<div class="content-left-top"> <div class="block">
<div class="line line-big">{{ negatives|length }}</div> <div class="line"><b>Total:</b> {{ negatives_sum|floatformat:2 }}€</div>
<div class="line line-bigsub">compte{{ negatives|length|pluralize }} en négatif</div>
<div class="block">
<div class="line"><b>Total:</b> {{ negatives_sum|floatformat:2 }}€</div>
</div>
<div class="block">
<div class="line"><b>Découvert autorisé par défaut</b></div>
<div class="line">Montant: {{ settings.overdraft_amount }}€</div>
<div class="line">Pendant: {{ settings.overdraft_duration }}</div>
</div>
</div>
{% if perms.kfet.change_settings %}
<div class="buttons">
<a class="btn btn-primary btn-lg" href="{% url 'kfet.settings' %}">Modifier les valeurs par défaut</a>
</div>
{% endif %}
</div>
</div> </div>
<div class="col-sm-8 col-md-9 col-content-right"> <div class="block">
{% include 'kfet/base_messages.html' %} <div class="line"><b>Découvert autorisé par défaut</b></div>
<div class="content-right"> <div class="line">Montant: {{ kfet_config.overdraft_amount }}€</div>
<div class="content-right-block"> <div class="line">Pendant: {{ kfet_config.overdraft_duration }}</div>
<h2>Liste des comptes en négatifs</h2> </div>
<div class="table-responsive"> </div>
<table class="table table-condensed"> {% if perms.kfet.change_settings %}
<thead> <div class="buttons">
<tr> <a class="btn btn-primary btn-lg" href="{% url 'kfet.settings' %}">Modifier les valeurs par défaut</a>
<td></td> </div>
<td>Tri</td> {% endif %}
<td>Nom</td>
<td>Balance</td> {% endblock %}
<td>Réelle</td>
<td>Début</td> {% block main-content %}
<td>Découvert autorisé</td>
<td>Jusqu'au</td> <div class="content-right-block">
<td>Balance offset</td> <h2>Liste des comptes en négatifs</h2>
</tr> <div class="table-responsive">
</thead> <table class="table table-condensed">
<tbody> <thead>
{% for neg in negatives %} <tr>
<tr> <td></td>
<td class="text-center"> <td>Tri</td>
<a href="{% url 'kfet.account.update' neg.account.trigramme %}"> <td>Nom</td>
<span class="glyphicon glyphicon-cog"></span> <td>Balance</td>
</a> <td>Réelle</td>
</td> <td>Début</td>
<td>{{ neg.account.trigramme }}</td> <td>Découvert autorisé</td>
<td>{{ neg.account.name }}</td> <td>Jusqu'au</td>
<td class="text-right">{{ neg.account.balance|floatformat:2 }}€</td> <td>Balance offset</td>
<td class="text-right"> </tr>
{% if neg.balance_offset %} </thead>
{{ neg.account.real_balance|floatformat:2 }}€ <tbody>
{% endif %} {% for neg in negatives %}
</td> <tr>
<td>{{ neg.start|date:'d/m/Y H:i:s'}}</td> <td class="text-center">
<td>{{ neg.authz_overdraft_amount|default_if_none:'' }}</td> <a href="{% url 'kfet.account.update' neg.account.trigramme %}">
<td>{{ neg.authz_overdrafy_until|default_if_none:'' }}</td> <span class="glyphicon glyphicon-cog"></span>
<td>{{ neg.balance_offset|default_if_none:'' }}</td> </a>
</tr> </td>
{% endfor %} <td>{{ neg.account.trigramme }}</td>
</tbody> <td>{{ neg.account.name }}</td>
</table> <td class="text-right">{{ neg.account.balance|floatformat:2 }}€</td>
</div> <td class="text-right">
</div> {% if neg.balance_offset %}
</div> {{ neg.account.real_balance|floatformat:2 }}€
{% endif %}
</td>
<td>{{ neg.start|date:'d/m/Y H:i:s'}}</td>
<td>{{ neg.authz_overdraft_amount|default_if_none:'' }}</td>
<td>{{ neg.authz_overdrafy_until|default_if_none:'' }}</td>
<td>{{ neg.balance_offset|default_if_none:'' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
{% extends "kfet/base.html" %} {% extends "kfet/base_col_2.html" %}
{% load staticfiles %} {% load staticfiles %}
{% load kfet_tags %} {% load kfet_tags %}
{% load l10n %} {% load l10n %}
@ -8,7 +8,6 @@
<script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% if account.user == request.user %} {% if account.user == request.user %}
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
@ -18,26 +17,18 @@
$(document).ready(function() { $(document).ready(function() {
var stat_last = new StatsGroup( var stat_last = new StatsGroup(
"{% url 'kfet.account.stat.operation.list' trigramme=account.trigramme %}", "{% url 'kfet.account.stat.operation.list' trigramme=account.trigramme %}",
$("#stat_last"), $("#stat_last")
); );
var stat_balance = new StatsGroup( var stat_balance = new StatsGroup(
"{% url 'kfet.account.stat.balance.list' trigramme=account.trigramme %}", "{% url 'kfet.account.stat.balance.list' trigramme=account.trigramme %}",
$("#stat_balance"), $("#stat_balance")
); );
}); });
</script> </script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block title %} {% block title %}
{% if account.user == request.user %}
Mon compte
{% else %}
Informations du compte {{ account.trigramme }}
{% endif %}
{% endblock %}
{% block content-header-title %}
{% if account.user == request.user %} {% if account.user == request.user %}
Mon compte Mon compte
{% else %} {% else %}
@ -45,62 +36,51 @@ $(document).ready(function() {
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block header-title %}
{% if account.user == request.user %}
Mon compte
{% else %}
Informations du compte {{ account.trigramme }}
{% endif %}
{% endblock %}
<div class="row"> {% block fixed-content %}
<div class="col-sm-4 col-md-3 col-content-left"> {% include "kfet/left_account.html" %}
<div class="content-left"> {% endblock %}
{% include 'kfet/left_account.html' %}
{% block main-content %}
<div class="tab-content">
{% if account.user == request.user %}
<div class="content-right-block tab-pane fade in active" id="tab_stats">
<h2>Statistiques</h2>
<div>
<h3>Ma balance</h3>
<div id="stat_balance"></div>
<h3>Ma consommation</h3>
<div id="stat_last"></div>
</div> </div>
</div> </div><!-- content-right-block -->
<div class="col-sm-8 col-md-9 col-content-right"> {% endif %}
{% include "kfet/base_messages.html" %} <div class="content-right-block tab-pane fade {% if account.user != request.user %}in active{% endif %}" id="tab_history">
<div class="content-right"> {% if addcosts %}
{% if addcosts %} <h2>Gagné des majorations</h2>
<div class="content-right-block"> <div>
<h2>Gagné des majorations</h2> <ul>
<div> {% for addcost in addcosts %}
<ul> <li>{{ addcost.date|date:'l j F' }}: +{{ addcost.sum_addcosts }}€</li>
{% for addcost in addcosts %} {% endfor %}
<li>{{ addcost.date|date:'l j F' }}: +{{ addcost.sum_addcosts }}€</li> </ul>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if account.user == request.user %}
<div class="content-right-block content-right-block-transparent">
<h2>Statistiques</h2>
<div class="row">
<div class="col-sm-12 nopadding">
<div class="panel-md-margin">
<h3>Ma balance</h3>
<div id="stat_balance"></div>
</div>
</div>
</div><!-- /row -->
<div class="row">
<div class="col-sm-12 nopadding">
<div class="panel-md-margin">
<h3>Ma consommation</h3>
<div id="stat_last"></div>
</div>
</div>
</div><!-- /row -->
</div>
{% endif %}
<div class="content-right-block">
<h2>Historique</h2>
<div id="history">
</div>
</div>
</div> </div>
</div> {% endif %}
</div> <h2>Historique</h2>
<div id="history"></div>
</div><!-- content-right-block -->
</div><!-- tab-content -->
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
settings = { 'subvention_cof': parseFloat({{ settings.subvention_cof|unlocalize }})} settings = { 'subvention_cof': parseFloat({{ kfet_config.subvention_cof|unlocalize }})}
khistory = new KHistory({ khistory = new KHistory({
display_trigramme: false, display_trigramme: false,

View file

@ -1,6 +1,4 @@
{% extends "kfet/base.html" %} {% extends "kfet/base_col_1.html" %}
{% load widget_tweaks %}
{% load staticfiles %}
{% block extra_head %} {% block extra_head %}
{{ negative_form.media }} {{ negative_form.media }}
@ -14,7 +12,7 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block content-header-title %} {% block header-title %}
{% if account.user == request.user %} {% if account.user == request.user %}
Modification de mes informations Modification de mes informations
{% else %} {% else %}
@ -22,29 +20,23 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block main-class %}content-form{% endblock %}
{% include "kfet/base_messages.html" %} {% block main-content %}
<div class="row form-only"> <form action="" method="post" class="form-horizontal">
<div class="col-sm-12 col-md-8 col-md-offset-2"> {% csrf_token %}
<div class="content-form"> {% include 'kfet/form_snippet.html' with form=user_form %}
<form action="{% url 'kfet.account.update' account.trigramme %}" method="post" class="form-horizontal"> {% include 'kfet/form_snippet.html' with form=cof_form %}
{% csrf_token %} {% include 'kfet/form_snippet.html' with form=account_form %}
{% include 'kfet/form_snippet.html' with form=user_form %} {% include 'kfet/form_snippet.html' with form=group_form %}
{% include 'kfet/form_snippet.html' with form=cof_form %} {% include 'kfet/form_snippet.html' with form=pwd_form %}
{% include 'kfet/form_snippet.html' with form=account_form %} {% include 'kfet/form_snippet.html' with form=negative_form %}
{% include 'kfet/form_snippet.html' with form=group_form %} {% if perms.kfet.is_team %}
{% include 'kfet/form_snippet.html' with form=pwd_form %} {% include 'kfet/form_authentication_snippet.html' %}
{% include 'kfet/form_snippet.html' with form=negative_form %} {% endif %}
{% if perms.kfet.is_team %} {% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %}
{% include 'kfet/form_authentication_snippet.html' %} </form>
{% endif %}
{% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %}
</form>
</div>
</div>
</div>
<script type="text/javascript"> <script type="text/javascript">

View file

@ -1,71 +1,64 @@
{% extends 'kfet/base.html' %} {% extends 'kfet/base_col_2.html' %}
{% block title %}Articles{% endblock %} {% block title %}Articles{% endblock %}
{% block content-header-title %}Articles{% endblock %} {% block header-title %}Articles{% endblock %}
{% block content %} {% block fixed-content %}
<div class="row"> <div class="content-left-top">
<div class="col-sm-4 col-md-3 col-content-left"> <div class="line line-big">{{ articles|length }}</div>
<div class="content-left"> <div class="line line-bigsub">article{{ articles|length|pluralize }}</div>
<div class="content-left-top"> </div>
<div class="line line-big">{{ articles|length }}</div> <div class="buttons">
<div class="line line-bigsub">article{{ articles|length|pluralize }}</div> <a class="btn btn-primary btn-lg" href="{% url 'kfet.article.create' %}">
</div> Nouvel article
<div class="buttons"> </a>
<a class="btn btn-primary btn-lg" href="{% url 'kfet.article.create' %}"> <a class="btn btn-primary btn-lg" href="{% url 'kfet.category' %}">
Nouvel article Catégories
</a> </a>
<a class="btn btn-primary btn-lg" href="{% url 'kfet.category' %}"> </div>
Catégories
</a> {% endblock %}
</div>
</div> {% block main-content %}
</div> <div class="content-right-block">
<div class="col-sm-8 col-md-9 col-content-right"> <h2>Liste des articles</h2>
{% include 'kfet/base_messages.html' %} <div class="table-responsive">
<div class="content-right"> <table class="table table-condensed">
<div class="content-right-block"> <thead>
<h2>Liste des articles</h2> <tr>
<div class="table-responsive"> <td></td>
<table class="table table-condensed"> <td>Nom</td>
<thead> <td class="text-right">Prix</td>
<tr> <td class="text-right">Stock</td>
<td></td> <td class="text-right">En vente</td>
<td>Nom</td> <td class="text-right">Affiché</td>
<td class="text-right">Prix</td> <td class="text-right">Dernier inventaire</td>
<td class="text-right">Stock</td> </tr>
<td class="text-right">En vente</td> </thead>
<td class="text-right">Affiché</td> <tbody>
<td class="text-right">Dernier inventaire</td> {% for article in articles %}
</tr> {% ifchanged article.category %}
</thead> <tr class="section">
<tbody> <td colspan="7">{{ article.category.name }}</td>
{% for article in articles %} </tr>
{% ifchanged article.category %} {% endifchanged %}
<tr class="section"> <tr>
<td colspan="7">{{ article.category.name }}</td> <td class="text-center">
</tr> <a href="{% url 'kfet.article.read' article.pk %}">
{% endifchanged %} <span class="glyphicon glyphicon-cog"></span>
<tr> </a>
<td class="text-center"> </td>
<a href="{% url 'kfet.article.read' article.pk %}"> <td>{{ article.name }}</td>
<span class="glyphicon glyphicon-cog"></span> <td class="text-right">{{ article.price }}€</td>
</a> <td class="text-right">{{ article.stock }}</td>
</td> <td class="text-right">{{ article.is_sold | yesno:"En vente,Non vendu"}}</td>
<td>{{ article.name }}</td> <td class="text-right">{{ article.hidden | yesno:"Caché,Affiché" }}</td>
<td class="text-right">{{ article.price }}€</td> <td class="text-right">{{ article.inventory.0.at }}</td>
<td class="text-right">{{ article.stock }}</td> </tr>
<td class="text-right">{{ article.is_sold | yesno:"En vente,Non vendu"}}</td> {% endfor %}
<td class="text-right">{{ article.hidden | yesno:"Caché,Affiché" }}</td> </tbody>
<td class="text-right">{{ article.inventory.0.at }}</td> </table>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -1,27 +1,12 @@
{% extends 'kfet/base.html' %} {% extends 'kfet/base_col_1.html' %}
{% load widget_tweaks %}
{% load staticfiles %}
{% block title %}Nouvel article{% endblock %} {% block title %}Nouvel article{% endblock %}
{% block content-header-title %}Création d'un article{% endblock %} {% block header-title %}Création d'un article{% endblock %}
{% block content %} {% block main-class %}content-form{% endblock %}
{% include "kfet/base_messages.html" %} {% block main-content %}
<div class="row form-only"> {% include "kfet/base_form.html" with authz=perms.kfet.add_article submit_text="Enregistrer" %}
<div class="col-sm-12 col-md-8 col-md-offset-2">
<div class="content-form">
<form submit="" method="post" class="form-horizontal">
{% csrf_token %}
{% include 'kfet/form_snippet.html' with form=form %}
{% if not perms.kfet.add_article %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
{% include 'kfet/form_submit_snippet.html' with value="Enregistrer" %}
</form>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'kfet/base.html' %} {% extends 'kfet/base_col_2.html' %}
{% load staticfiles %} {% load staticfiles %}
{% block extra_head %} {% block extra_head %}
@ -6,97 +6,89 @@
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>
{% endblock %} {% endblock %}
{% block title %}Informations sur l'article {{ article }}{% endblock %} {% block title %}Article - {{ article.name }}{% endblock %}
{% block content-header-title %}Article - {{ article.name }}{% endblock %} {% block header-title %}Informations sur l'article {{ article.name }}{% endblock %}
{% block content %} {% block fixed-content %}
<div class="row"> <div class="content-left-top">
<div class="col-sm-4 col-md-3 col-content-left"> <div class="line line-big">{{ article.name }}</div>
<div class="content-left"> <div class="line line-bigsub">{{ article.category }}</div>
<div class="content-left-top"> <div class="block">
<div class="line line-big">{{ article.name }}</div> <div class="line">Prix (hors réduc.): {{ article.price }}€</div>
<div class="line line-bigsub">{{ article.category }}</div> <div class="line">Stock: {{ article.stock }}</div>
<div class="block"> <div class="line">En vente: {{ article.is_sold | yesno:"Oui,Non" }}</div>
<div class="line">Prix (hors réduc.): {{ article.price }}€</div> <div class="line">Affiché: {{ article.hidden | yesno:"Non,Oui" }}</div>
<div class="line">Stock: {{ article.stock }}</div>
<div class="line">En vente: {{ article.is_sold | yesno:"Oui,Non" }}</div>
<div class="line">Affiché: {{ article.hidden | yesno:"Non,Oui" }}</div>
</div>
</div>
<div class="buttons">
<a class="btn btn-primary btn-lg" href="{% url 'kfet.article.update' article.pk %}">
Modifier
</a>
</div>
</div>
</div> </div>
<div class="col-sm-8 col-md-9 col-content-right"> </div>
{% include 'kfet/base_messages.html' %} <div class="buttons">
<div class="content-right"> <a class="btn btn-primary btn-lg" href="{% url 'kfet.article.update' article.pk %}">
<div class="content-right-block"> Modifier
<h2>Historique</h2> </a>
<div class="row"> </div>
<div class="col-sm-6">
<h3>Inventaires</h3> {% endblock %}
<table class="table">
<thead> {% block main-content %}
<tr>
<td>Date</td> <div class="content-right-block">
<td>Stock</td> <h2>Historique</h2>
<td>Erreur</td> <div class="row" style="padding-bottom: 15px">
</tr> <div class="col-md-6">
</thead> <h3>Inventaires</h3>
<tbody> <table class="table">
{% for inventoryart in inventoryarts %} <thead>
<tr> <tr>
<td>{{ inventoryart.inventory.at }}</td> <td>Date</td>
<td>{{ inventoryart.stock_new }}</td> <td>Stock</td>
<td>{{ inventoryart.stock_error }}</td> <td>Erreur</td>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for inventoryart in inventoryarts %}
</div> <tr>
<div class="col-sm-6"> <td>{{ inventoryart.inventory.at }}</td>
<h3>Prix fournisseurs</h3> <td>{{ inventoryart.stock_new }}</td>
<table class="table"> <td>{{ inventoryart.stock_error }}</td>
<thead> </tr>
<tr> {% endfor %}
<td>Date</td> </tbody>
<td>Fournisseur</td> </table>
<td>HT</td> </div>
<td>TVA</td> <div class="col-md-6">
<td>Droits</td> <h3>Prix fournisseurs</h3>
</tr> <div class="table-responsive">
</thead> <table class="table">
<tbody> <thead>
{% for supplierart in supplierarts %} <tr>
<tr> <td>Date</td>
<td>{{ supplierart.at }}</td> <td>Fournisseur</td>
<td>{{ supplierart.supplier.name }}</td> <td>HT</td>
<td>{{ supplierart.price_HT }}</td> <td>TVA</td>
<td>{{ supplierart.TVA }}</td> <td>Droits</td>
<td>{{ supplierart.rights }}</td> </tr>
</tr> </thead>
{% endfor %} <tbody>
</tbody> {% for supplierart in supplierarts %}
</table> <tr>
</div> <td>{{ supplierart.at }}</td>
</div><!-- /row--> <td>{{ supplierart.supplier.name }}</td>
</div> <td>{{ supplierart.price_HT }}</td>
<div class="content-right-block content-right-block-transparent"> <td>{{ supplierart.TVA }}</td>
<h2>Statistiques</h2> <td>{{ supplierart.rights }}</td>
<div class="row"> </tr>
<div class="col-sm-12 nopadding"> {% endfor %}
<div class="panel-md-margin"> </tbody>
<h3>Ventes de {{ article.name }}</h3> </table>
<div id="stat_last"></div>
</div>
</div>
</div><!-- /row -->
</div> </div>
</div> </div>
</div><!-- /row-->
</div>
<div class="content-right-block">
<h2>Statistiques</h2>
<div>
<h3>Ventes</h3>
<div id="stat_last"></div>
</div> </div>
</div> </div>
@ -104,7 +96,7 @@
$(document).ready(function() { $(document).ready(function() {
var stat_last = new StatsGroup( var stat_last = new StatsGroup(
"{% url 'kfet.article.stat.sales.list' article.id %}", "{% url 'kfet.article.stat.sales.list' article.id %}",
$("#stat_last"), $("#stat_last")
); );
}); });
</script> </script>

View file

@ -1,27 +1,12 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_1.html" %}
{% load widget_tweaks %}
{% load staticfiles %}
{% block title %}Édition de l'article {{ article.name }}{% endblock %} {% block title %}{{ article.name }} - Édition{% endblock %}
{% block content-header-title %}Article {{ article.name }} - Édition{% endblock %} {% block header-title %}Édition de l'article {{ article.name }}{% endblock %}
{% block content %} {% block main-class %}content-form{% endblock %}
{% include "kfet/base_messages.html" %} {% block main-content %}
<div class="row form-only"> {% include "kfet/base_form.html" with authz=perms.kfet.change_article submit_text="Mettre à jour"%}
<div class="col-sm-12 col-md-8 col-md-offset-2">
<div class="content-form">
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% include 'kfet/form_snippet.html' with form=form %}
{% if not perms.kfet.change_article %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
{% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %}
</form>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -56,12 +56,12 @@
<body> <body>
{% include "kfet/base_nav.html" %} {% include "kfet/base_nav.html" %}
<div class="container-fluid"> <div class="container-fluid">
{% block content-header %} {% block header %}
<div class="row row-page-header"> <div class="row row-page-header">
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header">{% block content-header-title %}{% endblock %}</h1> <h1 class="page-header">{% block header-title %}{% endblock %}</h1>
</div>
</div> </div>
</div>
{% endblock %} {% endblock %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
{% include "kfet/base_footer.html" %} {% include "kfet/base_footer.html" %}

View file

@ -0,0 +1,14 @@
{% extends "kfet/base.html" %}
{% block content %}
<div class="row">
<div class="nopadding {% block main-size %}col-md-8 col-md-offset-2{% endblock %}">
{% include "kfet/base_messages.html" %}
<div class="content-center {% block main-class %}{% endblock %}">
{% block main-content %}{% endblock %}
</div>
</div>
<div>
{% endblock %}

View file

@ -0,0 +1,19 @@
{% extends "kfet/base.html" %}
{% block content %}
<div class="row">
<div class="col-content-left {% block fixed-size %}col-sm-4 col-md-3{% endblock %}">
<div class="content-left">
{% block fixed-content %}{% endblock %}
</div>
</div>
<div class="col-content-right {% block main-size %}col-sm-8 col-md-9{% endblock %}">
{% include "kfet/base_messages.html" %}
<div class="content-right">
{% block main-content %}{% endblock %}
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,10 @@
{% load kfet_tags %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% include "kfet/form_snippet.html" %}
{% if not authz %}
{% include "kfet/form_authentication_snippet.html" %}
{% endif %}
{% include "kfet/form_submit_snippet.html" with value=submit_text %}
</form>

View file

@ -1,8 +1,15 @@
{% if config.gestion_banner %}
<div id="banner" class="container">
<span class="glyphicon glyphicon-bullhorn"></span>
<span>{{ config.gestion_banner }}</span>
</div>
{% endif %}
{% if messages %} {% if messages %}
<div class="row messages"> <div class="row messages">
{% for message in messages %} {% for message in messages %}
<div class="col-sm-12 item"> <div class="col-sm-12 nopadding">
<div class="alert alert-{{ message.level_tag }} alert-dismissible fade in{% if message.tags %} {{ message.tags }}{% endif %}"> <div class="alert alert-{{ message.level_tag }} alert-dismissible fade in {{ message.tags }}">
<button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span></button> <button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span></button>
{% if 'safe' in message.tags %} {% if 'safe' in message.tags %}
{{ message|safe }} {{ message|safe }}

View file

@ -9,47 +9,51 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="navbar-brand" href="#"> <a class="navbar-brand" href="{% url 'kfet.home' %}">
<img src="{% static 'kfet/img/logo3.png' %}"> <img src="{% static 'kfet/img/logo3.png' %}">
</a> </a>
<span id="kfet-open-wrapper"><span id="kfet-open"></span></span> <span id="kfet-open-wrapper"><span id="kfet-open"></span></span>
</div> </div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li><a href="{% url 'kfet.home' %}">Home</a></li> <li class="hidden-xs">
<a href="{% url 'kfet.home' %}">
<span class="glyphicon glyphicon-home"></span>
</a>
</li>
{% if user.profile.account_kfet %}
<li>
<a href="{% url 'kfet.account.read' user.profile.account_kfet.trigramme %}">Mon compte</a>
</li>
{% endif %}
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
{% if user.username == 'kfet_genericteam' %} {% if user.username == 'kfet_genericteam' %}
<li class="navbar-text">Équipe standard</li> <li class="navbar-text">Équipe standard</li>
{% endif %}
{% if user.profile.account_kfet %}
<li>
<a href="{% url 'kfet.account.read' user.profile.account_kfet.trigramme %}">Mes infos</a>
</li>
{% endif %} {% endif %}
{% if perms.kfet.is_team %} {% if perms.kfet.is_team %}
<li class="dropdown"> <li><a href="{% url 'kfet.kpsul' %}">K-Psul</a></li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Admin <span class="caret"></span></a> <li><a href="{% url 'kfet.history' %}">Historique</a></li>
<ul class="dropdown-menu"> <li><a href="{% url 'kfet.transfers' %}">Transferts</a></li>
<li><a href="{% url 'kfet.kpsul' %}">K-Psul</a></li> <li class="dropdown">
<li><a href="{% url 'kfet.history' %}">Historique</a></li> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Admin <span class="caret"></span></a>
<li><a href="{% url 'kfet.transfers' %}">Transferts</a></li> <ul class="dropdown-menu">
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'kfet.account' %}">Comptes</a></li> <li><a href="{% url 'kfet.account' %}">Comptes</a></li>
<li><a href="{% url 'kfet.checkout' %}">Caisses</a></li> <li><a href="{% url 'kfet.checkout' %}">Caisses</a></li>
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'kfet.article' %}">Articles</a></li> <li><a href="{% url 'kfet.article' %}">Articles</a></li>
<li><a href="{% url 'kfet.inventory' %}">Inventaires</a></li> <li><a href="{% url 'kfet.inventory' %}">Inventaires</a></li>
<li><a href="{% url 'kfet.order' %}">Commandes</a></li> <li><a href="{% url 'kfet.order' %}">Commandes</a></li>
{% if user.username != 'kfet_genericteam' %} {% if user.username != 'kfet_genericteam' %}
<li class="divider"></li> <li class="divider"></li>
<li><a href="{% url 'kfet.login.genericteam' %}" target="_blank" id="genericteam">Connexion standard</a></li> <li><a href="{% url 'kfet.login.genericteam' %}" target="_blank" id="genericteam">Connexion standard</a></li>
{% endif %} {% endif %}
{% if perms.kfet.change_settings %} {% if perms.kfet.change_settings %}
<li><a href="{% url 'kfet.settings' %}">Paramètres</a></li> <li><a href="{% url 'kfet.settings' %}">Paramètres</a></li>
{% endif %} {% endif %}
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li><a href="{% url 'gestioncof.views.logout' %}?next=/k-fet/" title="Déconnexion"><span class="glyphicon glyphicon-log-out"></span></a></li> <li><a href="{% url 'gestioncof.views.logout' %}?next=/k-fet/" title="Déconnexion"><span class="glyphicon glyphicon-log-out"></span></a></li>

View file

@ -1,52 +1,46 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_2.html" %}
{% block title %}Categories d'articles{% endblock %} {% block title %}Categories d'articles{% endblock %}
{% block content-header-title %}Categories d'articles{% endblock %} {% block header-title %}Categories d'articles{% endblock %}
{% block content %} {% block fixed-content %}
<div class="row"> <div class="content-left-top">
<div class="col-sm-4 col-md-3 col-content-left"> <div class="line line-big">{{ categories|length }}</div>
<div class="content-left"> <div class="line line-bigsub">catégorie{{ categories|length|pluralize }}</div>
<div class="content-left-top"> </div>
<div class="line line-big">{{ categories|length }}</div>
<div class="line line-bigsub">catégorie{{ categories|length|pluralize }}</div> {% endblock %}
</div>
</div> {% block main-content %}
</div>
<div class="col-sm-8 col-md-9 col-content-right"> <div class="content-right-block">
{% include 'kfet/base_messages.html' %} <h2>Liste des catégories</h2>
<div class="content-right"> <div class="table-responsive">
<div class="content-right-block"> <table class="table table-condensed">
<h2>Liste des catégories</h2> <thead>
<div class="table-responsive"> <tr>
<table class="table table-condensed"> <td></td>
<thead> <td>Nom</td>
<tr> <td class="text-right">Nombre d'articles</td>
<td></td> <td class="text-right">Peut être majorée</td>
<td>Nom</td> </tr>
<td class="text-right">Nombre d'articles</td> </thead>
<td class="text-right">Peut être majorée</td> <tbody>
</tr> {% for category in categories %}
</thead> <tr>
<tbody> <td class="text-center">
{% for category in categories %} <a href="{% url 'kfet.category.update' category.pk %}">
<tr> <span class="glyphicon glyphicon-cog"></span>
<td class="text-center"> </a>
<a href="{% url 'kfet.category.update' category.pk %}"> </td>
<span class="glyphicon glyphicon-cog"></span> <td>{{ category.name }}</td>
</a> <td class="text-right">{{ category.articles.all|length }}</td>
</td> <td class="text-right">{{ category.has_addcost | yesno:"Oui,Non"}}</td>
<td>{{ category.name }}</td> </tr>
<td class="text-right">{{ category.articles.all|length }}</td> {% endfor %}
<td class="text-right">{{ category.has_addcost | yesno:"Oui,Non"}}</td> </tbody>
</tr> </table>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -1,25 +1,12 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_1.html" %}
{% block title %}Édition de la catégorie {{ category.name }}{% endblock %} {% block title %}{{ articlecategory.name }} - Édition{% endblock %}
{% block content-header-title %}Catégorie {{ category.name }} - Édition{% endblock %} {% block header-title %}Édition de la catégorie {{ articlecategory.name }}{% endblock %}
{% block content %} {% block main-class %}content-form{% endblock %}
{% include "kfet/base_messages.html" %} {% block main-content %}
<div class="row form-only"> {% include "kfet/base_form.html" with authz=perms.kfet.edit_articlecategory submit_text="Enregistrer"%}
<div class="col-sm-12 col-md-8 col-md-offset-2">
<div class="content-form">
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% include 'kfet/form_snippet.html' with form=form %}
{% if not perms.kfet.edit_articlecategory %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
{% include 'kfet/form_submit_snippet.html' with value="Enregistrer"%}
<form>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,59 +1,53 @@
{% extends "kfet/base.html" %} {% extends "kfet/base_col_2.html" %}
{% block title %}Liste des caisses{% endblock %} {% block title %}Caisses{% endblock %}
{% block content-header-title %}Caisses{% endblock %} {% block header-title %}Caisses{% endblock %}
{% block content %} {% block fixed-content %}
<div class="row"> <div class="content-left-top text-center">
<div class="col-sm-4 col-md-3 col-content-left"> <div class="line line-big">{{ checkouts|length }}</div>
<div class="content-left"> <div class="line line-bigsub">caisse{{ checkouts|length|pluralize }}</div>
<div class="content-left-top text-center"> </div>
<div class="line line-big">{{ checkouts|length }}</div> <div class="buttons">
<div class="line line-bigsub">caisse{{ checkouts|length|pluralize }}</div> <a class="btn btn-primary btn-lg" href="{% url 'kfet.checkout.create' %}">Créer une caisse</a>
</div> </div>
<div class="buttons">
<a class="btn btn-primary btn-lg" href="{% url 'kfet.checkout.create' %}">Créer une caisse</a> {% endblock %}
</div>
</div> {% block main-content %}
</div>
<div class="col-sm-8 col-md-9 col-content-right"> <div class="content-right-block">
{% include 'kfet/base_messages.html' %} <h2>Liste des caisses</h2>
<div class="content-right"> <div class="table-responsive">
<div class="content-right-block"> <table class="table table-condensed">
<h2>Liste des caisses</h2> <thead>
<div class="table-responsive"> <tr>
<table class="table table-condensed"> <td></td>
<thead> <td>Nom</td>
<tr> <td class="text-right">Balance</td>
<td></td> <td class="text-right">Déb. valid.</td>
<td>Nom</td> <td class="text-right">Fin valid.</td>
<td class="text-right">Balance</td> <td class="text-right">Protégée</td>
<td class="text-right">Déb. valid.</td> </tr>
<td class="text-right">Fin valid.</td> </thead>
<td class="text-right">Protégée</td> <tbody>
</tr> {% for checkout in checkouts %}
</thead> <tr>
<tbody> <td>
{% for checkout in checkouts %} <a href="{% url 'kfet.checkout.read' checkout.pk %}">
<tr> <span class="glyphicon glyphicon-cog"></span>
<td> </a>
<a href="{% url 'kfet.checkout.read' checkout.pk %}"> </td>
<span class="glyphicon glyphicon-cog"></span> <td>{{ checkout.name }}</td>
</a> <td class="text-right">{{ checkout.balance}}€</td>
</td> <td class="text-right">{{ checkout.valid_from }}</td>
<td>{{ checkout.name }}</td> <td class="text-right">{{ checkout.valid_to }}</td>
<td class="text-right">{{ checkout.balance}}€</td> <td class="text-right">{{ checkout.is_protected }}</td>
<td class="text-right">{{ checkout.valid_from }}</td> </tr>
<td class="text-right">{{ checkout.valid_to }}</td> {% endfor %}
<td class="text-right">{{ checkout.is_protected }}</td> </tbody>
</tr> </table>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -1,28 +1,13 @@
{% extends "kfet/base.html" %} {% extends "kfet/base_col_1.html" %}
{% block extra_head %}{{ form.media }}{% endblock %} {% block extra_head %}{{ form.media }}{% endblock %}
{% block title %}Nouvelle caisse{% endblock %} {% block title %}Nouvelle caisse{% endblock %}
{% block content-header-title %}Création d'une caisse{% endblock %} {% block header-title %}Création d'une caisse{% endblock %}
{% block content %} {% block main-class %}content-form{% endblock %}
{% block main-content %}
{% include 'kfet/base_messages.html' %} {% include "kfet/base_form.html" with authz=perms.kfet.add_checkout submit_text="Enregistrer" %}
<form action="" method="post">
{% csrf_token %}
{{ form.non_field_errors}}
{% for field in form %}
{{ field.errors }}
{{ field.label_tag }}
<div style="position:relative">{{ field }}</div>
{% if field.help_text %}
<p class="help">{{ field.help_text|safe }}</p>
{% endif %}
{% endfor %}
{% if not perms.kfet.add_checkout %}
<input type="password" name="KFETPASSWORD">
{% endif %}
<input type="submit" value="Enregistrer">
</form>
<script type="text/javascript"> <script type="text/javascript">

View file

@ -1,50 +1,41 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_2.html" %}
{% block title %}Informations sur la caisse {{ checkout.name }}{% endblock %} {% block title %}Caisse - {{ checkout.name }}{% endblock %}
{% block content-header-title %}Caisse - {{ checkout.name }}{% endblock %} {% block header-title %}Informations sur la caisse {{ checkout.name }}{% endblock %}
{% block content %} {% block fixed-content %}
{% include 'kfet/left_checkout.html' %}
{% endblock %}
<div class="row"> {% block main-content %}
<div class="col-sm-4 col-md-3 col-content-left"> <div class="content-right-block">
<div class="content-left"> <h2>Relevés</h2>
{% include 'kfet/left_checkout.html' %} <div class="table-responsive">
</div> {% if not statements %}
</div> Pas de relevé
<div class="col-sm-8 col-md-9 col-content-right"> {% else %}
{% include "kfet/base_messages.html" %} <table class="table">
<div class="content-right"> <thead>
<div class="content-right-block"> <tr>
<h2>Relevés</h2> <td></td>
<div> <td>Date/heure</td>
{% if not statements %} <td>Montant pris</td>
Pas de relevé <td>Montant laissé</td>
{% else %} <td>Erreur</td>
<table class="table"> </thead>
<thead> <tbody>
<tr> {% for statement in statements %}
<td></td> <tr>
<td>Date/heure</td> <td><a href="{% url 'kfet.checkoutstatement.update' checkout.pk statement.pk %}"><span class="glyphicon glyphicon-cog"></span></a></td>
<td>Montant pris</td> <td>{{ statement.at }}</td>
<td>Montant laissé</td> <td>{{ statement.amount_taken }}</td>
<td>Erreur</td> <td>{{ statement.balance_new }}</td>
</thead> <td>{{ statement.amount_error }}</td>
<tbody> </tr>
{% for statement in statements %} {% endfor %}
<tr> </tbody>
<td><a href="{% url 'kfet.checkoutstatement.update' checkout.pk statement.pk %}"><span class="glyphicon glyphicon-cog"></span></a></td> </table>
<td>{{ statement.at }}</td> {% endif %}
<td>{{ statement.amount_taken }}</td>
<td>{{ statement.balance_new }}</td>
<td>{{ statement.amount_error }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</div>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -1,39 +1,18 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_2.html" %}
{% block extra_head %}{{ form.media }}{% endblock %} {% block extra_head %}{{ form.media }}{% endblock %}
{% block title %}Édition de la caisse {{ checkout.name }}{% endblock %} {% block title %}Caisse {{ checkout.name }} - Édition{% endblock %}
{% block content-header-title %}Caisse {{ checkout.name }} - Édition{% endblock %} {% block header-title %}Édition de la caisse {{ checkout.name }}{% endblock %}
{% block content %} {% block fixed-content %}
{% include "kfet/left_checkout.html" %}
{% endblock %}
<div class="row"> {% block main-content %}
<div class="col-sm-4 col-md-3 col-content-left">
<div class="content-left"> <div class="content-right-block">
{% include 'kfet/left_checkout.html' %} <div style="padding: 15px;">
</div> {% include "kfet/base_form.html" with authz=perms.kfet.change_checkout submit_text="Mettre à jour" %}
</div>
<div class="col-sm-8 col-md-9 col-content-right">
{% include 'kfet/base_messages.html' %}
<div class="content-right">
<div class="content-right-block">
<form action="" method="post">
{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
{{ field.errors }}
{{ field.label_tag }}
<div style="position:relative">{{ field }}</div>
{% if field.help_text %}
<p class="help">{{ field.help_text|safe }}</p>
{% endif %}
{% endfor %}
{% if not perms.kfet.add_checkout %}
<input type="password" name="KFETPASSWORD">
{% endif %}
<input type=submit value="Mettre à jour">
</form>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -1,139 +1,188 @@
{% extends "kfet/base.html" %} {% extends "kfet/base_col_2.html" %}
{% load l10n %} {% load l10n %}
{% load widget_tweaks %}
{% block title %}Nouveau relevé{% endblock %} {% block title %}Nouveau relevé - {{ checkout.name }}{% endblock %}
{% block content-header-title %}Caisse {{ checkout.name }} - Nouveau relevé{% endblock %} {% block header-title %}Création d'un relevé pour la caisse {{ checkout.name }}{% endblock %}
{% block content %} {% block fixed-content %}
{% include "kfet/left_checkout.html" %}
{% endblock %}
<div class="row"> {% block main-content %}
<div class="col-sm-4 col-md-3 col-content-left"> <form action="" method="post">
<div class="content-left"> {% csrf_token %}
{% include 'kfet/left_checkout.html' %} <div class="content-right-block">
<h2>Général</h2>
<div class="content-form statement-create-summary">
<table>
<tr>
<td>
<label for="{{ form.not_count.id_for_label }}" class="control-label">{{ form.not_count.label }}</label>
</td>
<td>{{ form.not_count }}</td>
</tr>
<tr>
<td>Ancienne balance</td>
<td>
<span id="balance_old">{{ checkout.balance|unlocalize }}</span>
</td>
</tr>
<tr>
<td>Nouvelle balance</td>
<td><span id="balance_new">0</span></td>
</tr>
<tr>
<td>Pris</td>
<td><span id="amount_taken">0</span></td>
</tr>
<tr>
<td>Erreur</td>
<td><span id="amount_error">0</span></td>
</tr>
</table>
{% if not perms.kfet.add_checkoutstatement %}
{% include "kfet/form_authentication_snippet.html" %}
{% endif %}
<br>
<div class="row text-center">
<input type="submit" value="Enregistrer" class="btn btn-lg btn-primary">
</div>
</div> </div>
</div> </div>
<div class="col-sm-8 col-md-9 col-content-right"> <div class="content-right-block">
{% include "kfet/base_messages.html" %} <h2>Pris</h2>
<div class="content-right"> <div id="detail_taken">
<form action="" method="post"> <div class="table-responsive">
{% csrf_token %} <table class="table table-condensed">
<div class="content-right-block"> <thead>
<h2>Général</h2> <tr>
<div> <td>5€</td>
<label> <td>10€</td>
Ne pas compter la caisse <td>20€</td>
{{ form.not_count }} <td>50€</td>
</label><br> <td>100€</td>
Ancienne balance : <span id="balance_old">{{ checkout.balance|unlocalize }}</span><br> <td>200€</td>
Nouvelle balance : <span id="balance_new">0</span><br> <td>500€</td>
Pris : <span id="amount_taken">0</span><br> </tr>
Erreur : <span id="amount_error">0</span><br> </thead>
{% if not perms.kfet.add_checkoutstatement %} <tbody>
<label for="password">Mot de passe:</label> <tr>
<input type="password" id="password" name="KFETPASSWORD"> <td><input id="id_taken_5" name="taken_5" data-value="5" min="0" value="0" type="number" class="form-control" required></td>
{% endif %} <td><input id="id_taken_10" name="taken_10" data-value="10" min="0" value="0" type="number" class="form-control" required></td>
<input type="submit" value="Enregistrer"> <td><input id="id_taken_20" name="taken_20" data-value="20" min="0" value="0" type="number" class="form-control" required></td>
</div> <td><input id="id_taken_50" name="taken_50" data-value="50" min="0" value="0" type="number" class="form-control" required></td>
</div> <td><input id="id_taken_100" name="taken_100" data-value="100" min="0" value="0" type="number" class="form-control" required></td>
<div class="content-right-block"> <td><input id="id_taken_200" name="taken_200" data-value="200" min="0" value="0" type="number" class="form-control" required></td>
<h2>Pris</h2> <td><input id="id_taken_500" name="taken_500" data-value="500" min="0" value="0" type="number" class="form-control" required></td>
<div id="detail_taken"> </tr>
<table class="table table-bordered"> </tbody>
<tr style="font-weight:bold;"> </table>
<td>5€</td> </div>
<td>10€</td> <div class="table-responsive">
<td>20€</td> <table class="table table-condensed">
<td>50€</td> <thead>
<td>100€</td> <tr>
<td>200€</td> <td>2€</td>
<td>500€</td> <td>1€</td>
<tr> <td>0.50€</td>
<tr> <td>0.20€</td>
<td><input id="id_taken_5" name="taken_5" data-value="5" min="0" value="0" type="number" class="form-control" required></td> <td>0.10€</td>
<td><input id="id_taken_10" name="taken_10" data-value="10" min="0" value="0" type="number" class="form-control" required></td> <td>0.05€</td>
<td><input id="id_taken_20" name="taken_20" data-value="20" min="0" value="0" type="number" class="form-control" required></td> <td>0.02€</td>
<td><input id="id_taken_50" name="taken_50" data-value="50" min="0" value="0" type="number" class="form-control" required></td> <td>0.01€</td>
<td><input id="id_taken_100" name="taken_100" data-value="100" min="0" value="0" type="number" class="form-control" required></td> </tr>
<td><input id="id_taken_200" name="taken_200" data-value="200" min="0" value="0" type="number" class="form-control" required></td> </thead>
<td><input id="id_taken_500" name="taken_500" data-value="500" min="0" value="0" type="number" class="form-control" required></td> <tbody>
</tr> <tr>
</table> <td><input id="id_taken_2" name="taken_2" data-value="2" min="0" value="0" type="number" class="form-control" required></td>
<table class="table table-bordered"> <td><input id="id_taken_1" name="taken_1" data-value="1" min="0" value="0" type="number" class="form-control" required></td>
<tr style="font-weight:bold;"> <td><input id="id_taken_05" name="taken_05" data-value="0.5" min="0" value="0" type="number" class="form-control" required></td>
<td>2€</td> <td><input id="id_taken_02" name="taken_02" data-value="0.2" min="0" value="0" type="number" class="form-control" required></td>
<td>1€</td> <td><input id="id_taken_01" name="taken_01" data-value="0.1" min="0" value="0" type="number" class="form-control" required></td>
<td>0.50€</td> <td><input id="id_taken_005" name="taken_005" data-value="0.05" min="0" value="0" type="number" class="form-control" required></td>
<td>0.20€</td> <td><input id="id_taken_002" name="taken_002" data-value="0.02" min="0" value="0" type="number" class="form-control" required></td>
<td>0.10€</td> <td><input id="id_taken_001" name="taken_001" data-value="0.01" min="0" value="0" type="number" class="form-control" required></td>
<td>0.05€</td> </tr>
<td>0.02€</td> </tbody>
<td>0.01€</td> </table>
<tr> </div>
<tr> <div class="table-responsive">
<td><input id="id_taken_2" name="taken_2" data-value="2" min="0" value="0" type="number" class="form-control" required></td> <table class="table table-condensed">
<td><input id="id_taken_1" name="taken_1" data-value="1" min="0" value="0" type="number" class="form-control" required></td> <thead>
<td><input id="id_taken_05" name="taken_05" data-value="0.5" min="0" value="0" type="number" class="form-control" required></td> <tr><td>Chèques</td></tr>
<td><input id="id_taken_02" name="taken_02" data-value="0.2" min="0" value="0" type="number" class="form-control" required></td> </thead>
<td><input id="id_taken_01" name="taken_01" data-value="0.1" min="0" value="0" type="number" class="form-control" required></td> <tbody>
<td><input id="id_taken_005" name="taken_005" data-value="0.05" min="0" value="0" type="number" class="form-control" required></td> <tr>
<td><input id="id_taken_002" name="taken_002" data-value="0.02" min="0" value="0" type="number" class="form-control" required></td> <td>
<td><input id="id_taken_001" name="taken_001" data-value="0.01" min="0" value="0" type="number" class="form-control" required></td> <input id="id_taken_cheque" name="taken_cheque" data-value="1" min="0" step="0.01" value="0" type="number" class="form-control" required>
</tr> </td>
</table> </tr>
<p style="font-weight:bold"> Chèque:</p> <input id="id_taken_cheque" name="taken_cheque" data-value="1" min="0" step="0.01" value="0" type="number" class="form-control" required> </tbody>
</div> </table>
</div> </div>
<div class="content-right-block">
<h2>En caisse</h2>
<div id="detail_balance">
<table class="table table-bordered">
<tr style="font-weight:bold;">
<td>5€</td>
<td>10€</td>
<td>20€</td>
<td>50€</td>
<td>100€</td>
<td>200€</td>
<td>500€</td>
<tr>
<tr>
<td><input id="id_balance_5" name="balance_5" data-value="5" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_10" name="balance_10" data-value="10" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_20" name="balance_20" data-value="20" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_50" name="balance_50" data-value="50" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_100" name="balance_100" data-value="100" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_200" name="balance_200" data-value="200" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_500" name="balance_500" data-value="500" min="0" value="0" type="number" class="form-control" required></td>
</tr>
</table>
<table class="table table-bordered">
<tr style="font-weight:bold;">
<td>2€</td>
<td>1€</td>
<td>0.50€</td>
<td>0.20€</td>
<td>0.10€</td>
<td>0.05€</td>
<td>0.02€</td>
<td>0.01€</td>
<tr>
<tr>
<td><input id="id_balance_2" name="balance_2" data-value="2" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_1" name="balance_1" data-value="1" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_05" name="balance_05" data-value="0.5" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_02" name="balance_02" data-value="0.2" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_01" name="balance_01" data-value="0.1" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_005" name="balance_005" data-value="0.05" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_002" name="balance_002" data-value="0.02" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_001" name="balance_001" data-value="0.01" min="0" value="0" type="number" class="form-control" required></td>
</tr>
</table>
</div>
</div>
</form>
</div> </div>
</div> </div>
</div> <div class="content-right-block">
<h2>En caisse</h2>
<div id="detail_balance">
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td>5€</td>
<td>10€</td>
<td>20€</td>
<td>50€</td>
<td>100€</td>
<td>200€</td>
<td>500€</td>
</tr>
</thead>
<tbody>
<tr>
<td><input id="id_balance_5" name="balance_5" data-value="5" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_10" name="balance_10" data-value="10" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_20" name="balance_20" data-value="20" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_50" name="balance_50" data-value="50" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_100" name="balance_100" data-value="100" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_200" name="balance_200" data-value="200" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_500" name="balance_500" data-value="500" min="0" value="0" type="number" class="form-control" required></td>
</tr>
</tbody>
</table>
</div>
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td>2€</td>
<td>1€</td>
<td>0.50€</td>
<td>0.20€</td>
<td>0.10€</td>
<td>0.05€</td>
<td>0.02€</td>
<td>0.01€</td>
</tr>
</thead>
<tbody>
<tr>
<td><input id="id_balance_2" name="balance_2" data-value="2" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_1" name="balance_1" data-value="1" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_05" name="balance_05" data-value="0.5" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_02" name="balance_02" data-value="0.2" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_01" name="balance_01" data-value="0.1" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_005" name="balance_005" data-value="0.05" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_002" name="balance_002" data-value="0.02" min="0" value="0" type="number" class="form-control" required></td>
<td><input id="id_balance_001" name="balance_001" data-value="0.01" min="0" value="0" type="number" class="form-control" required></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</form>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {

View file

@ -1,33 +1,15 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_1.html" %}
{% block title %}Modification d'un relevé{% endblock %} {% block title %}Modification d'un relevé{% endblock %}
{% block content-header-title %} {% block header-title %}
Caisse {{ checkout.name }} - Modification relevé {{ checkoutstatement.at }} Caisse {{ checkout.name }}<br>
Modification du relevé {{ checkoutstatement.at }}
{% endblock %} {% endblock %}
{% block content %} {% block main-class %}content-form{% endblock %}
<div class="row"> {% block main-content %}
<div class="col-sm-4 col-md-3 col-content-left">
<div class="content-left"> {% include "kfet/base_form.html" with authz=perms.kfet.change_checkoutstatement submit_text="Enregistrer"%}
{% include 'kfet/left_checkout.html' %}
</div>
</div>
<div class="col-sm-8 col-md-9 col-content-right">
{% include 'kfet/base_messages.html' %}
<div class="content-right form-only">
<div class="content-form">
<form submit="" method="post" class="form-horizontal">
{% csrf_token %}
{% include 'kfet/form_snippet.html' with form=form %}
{% if not perms.kfet.change_checkoutstatement %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
{% include 'kfet/form_submit_snippet.html' with value="Enregistrer" %}
</form>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -3,7 +3,19 @@
<div class="form-group"> <div class="form-group">
<label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label> <label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label>
<div class="col-sm-10"> <div class="col-sm-10">
{{ field|add_class:'form-control' }} {% if field|widget_type == "checkboxselectmultiple" %}
<ul class="list-unstyled checkbox-select-multiple">
{% for choice in form.permissions %}
<li class="col-sm-6 col-lg-4">
<label for="{{ choice.id_for_label }}">
{{ choice.tag }} {{ choice.choice_label }}
</label>
</li>
{% endfor %}
</ul>
{% else %}
{{ field|add_class:'form-control' }}
{% endif %}
{% if field.errors %} {% if field.errors %}
<span class="help-block">{{field.errors}}</span> <span class="help-block">{{field.errors}}</span>
{% endif %} {% endif %}

View file

@ -14,7 +14,6 @@
<script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/bootstrap-datetimepicker.min.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/bootstrap-datetimepicker.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/multiple-select.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/multiple-select.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% endblock %} {% endblock %}
@ -30,7 +29,6 @@
<div class="line line-big" id="nb_opes"></div> <div class="line line-big" id="nb_opes"></div>
<div class="line line-bigsub">opérations</div> <div class="line line-bigsub">opérations</div>
<div class="block"> <div class="block">
<h2>Filtres</h2>
<div class="line" style="position:relative"><b>De</b> <input type="text" id="from_date" class="form-control"></div> <div class="line" style="position:relative"><b>De</b> <input type="text" id="from_date" class="form-control"></div>
<div class="line" style="position:relative"><b>à</b> <input type="text" id="to_date" class="form-control"></div> <div class="line" style="position:relative"><b>à</b> <input type="text" id="to_date" class="form-control"></div>
<div class="line"><b>Caisses</b> {{ filter_form.checkouts }}</div> <div class="line"><b>Caisses</b> {{ filter_form.checkouts }}</div>
@ -60,7 +58,7 @@
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
settings = { 'subvention_cof': parseFloat({{ settings.subvention_cof|unlocalize }})} settings = { 'subvention_cof': parseFloat({{ kfet_config.subvention_cof|unlocalize }})}
khistory = new KHistory(); khistory = new KHistory();
@ -130,6 +128,9 @@ $(document).ready(function() {
$("select").multipleSelect({ $("select").multipleSelect({
width: '100%', width: '100%',
filter: true, filter: true,
allSelected: " ",
selectAllText: "Tout-te-s",
countSelected: "# sur %"
}); });
$("input").on('dp.change change', function() { $("input").on('dp.change change', function() {

View file

@ -1,78 +1,76 @@
{% extends "kfet/base.html" %} {% extends "kfet/base_col_2.html" %}
{% load staticfiles %} {% load staticfiles %}
{% load kfet_tags %} {% load kfet_tags %}
{% block title %}Accueil{% endblock %} {% block title %}Accueil{% endblock %}
{% block content-header-title %}Accueil{% endblock %} {% block header %}{% endblock %}
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/home.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'kfet/css/home.css' %}">
{% endblock %} {% endblock %}
{% block content %} {% block fixed-size %}col-sm-2{% endblock %}
{% block main-size %}col-sm-10{% endblock %}
<div class="row"> {% block fixed-content %}
<div class="col-sm-4 col-md-3 col-content-left">
<div class="content-left"> <div class="content-left-top">
<div class="content-left-top"> <div class="line line-bigsub">La K-Fêt est</div>
<div class="line line-bigsub">La K-Fêt est</div> </div>
</div> <div class="content-left-top" id="open_status_parent">
<div class="content-left-top" id="open_status_parent"> <div class="line line-big" id="open_status">?????</div>
<div class="line line-big" id="open_status">?????</div> </div>
</div> {% if perms.kfet.is_team %}
{% if perms.kfet.is_team %} <div class="buttons">
<div class="buttons"> <a class="btn btn-primary btn-lg" id="force_close_button">
<a class="btn btn-primary btn-lg" id="force_close_button"> Fermer manuellement
Fermer manuellement </a>
</a> </div>
</div> {% endif %}
{% endif %}
</div> {% endblock %}
</div>
<div class="col-sm-8 col-md-9 col-content-right"> {% block main-content %}
{% include 'kfet/base_messages.html' %}
<div class="content-right"> <div class="content-right-block">
<div class="content-right-block"> <h2>Carte</h2>
<h2>Carte</h2>
<div class="column-row"> <div class="column-row">
<div class="column-sm-1 column-md-2 column-lg-3"> <div class="column-sm-1 column-md-2 column-lg-3">
<div class="unbreakable carte-inverted"> <div class="unbreakable carte-inverted">
{% if pressions %} {% if pressions %}
<h3>Pressions du moment</h3> <h3>Pressions du moment</h3>
<ul class="carte"> <ul class="carte">
{% for article in pressions %} {% for article in pressions %}
<li class="carte-line"> <li class="carte-line">
<div class="filler"></div> <div class="filler"></div>
<span class="carte-label">{{ article.name }}</span> <span class="carte-label">{{ article.name }}</span>
<span class="carte-ukf">{{ article.price | ukf:False}} UKF</span> <span class="carte-ukf">{{ article.price | ukf:False}} UKF</span>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
</div><!-- endblock unbreakable --> </div><!-- endblock unbreakable -->
{% for article in articles %} {% for article in articles %}
{% ifchanged article.category %} {% ifchanged article.category %}
{% if not forloop.first %} {% if not forloop.first %}
</ul> </ul>
</div><!-- endblock unbreakable --> </div><!-- endblock unbreakable -->
{% endif %} {% endif %}
<div class="unbreakable"> <div class="unbreakable">
<h3>{{ article.category.name }}</h3> <h3>{{ article.category.name }}</h3>
<ul class="carte"> <ul class="carte">
{% endifchanged %} {% endifchanged %}
<li class="carte-line"> <li class="carte-line">
<div class="filler"></div> <div class="filler"></div>
<span class="carte-label">{{ article.name }}</span> <span class="carte-label">{{ article.name }}</span>
<span class="carte-ukf">{{ article.price | ukf:False}} UKF</span> <span class="carte-ukf">{{ article.price | ukf:False}} UKF</span>
</li> </li>
{% if forloop.last %} {% if forloop.last %}
</ul> </ul>
</div><!-- endblock unbreakable --> </div><!-- endblock unbreakable -->
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div>
</div>
</div> </div>
</div>
</div> </div>
</div> </div>

View file

@ -1,63 +1,55 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_2.html" %}
{% block title %}Inventaires{% endblock %} {% block title %}Inventaires{% endblock %}
{% block content-header-title %}Inventaires{% endblock %} {% block header-title %}Inventaires{% endblock %}
{% block content %} {% block fixed-content %}
<div class="row"> <div class="buttons">
<div class="col-sm-4 col-md-3 col-content-left"> <a href="{% url 'kfet.inventory.create' %}" class="btn btn-primary btn-lg">
<div class="content-left"> Nouveau
<div class="content-left-top"> </a>
</div> </div>
<div class="buttons">
<a href="{% url 'kfet.inventory.create' %}" class="btn btn-primary btn-lg"> {% endblock %}
Nouveau
</a> {% block main-content %}
</div>
</div> <div class="content-right-block">
</div> <h2>Liste des inventaires</h2>
<div class="col-sm-8 col-md-9 col-content-right"> <div class="table-responsive">
{% include 'kfet/base_messages.html' %} <table class="table table-condensed">
<div class="content-right"> <thead>
<div class="content-right-block"> <tr>
<h2>Liste des inventaires</h2> <td></td>
<div class="table-responsive"> <td>Date</td>
<table class="table table-condensed"> <td>Par</td>
<thead class="thead-default"> <td>Nb articles</td>
<tr> <td>Commande</td>
<td></td> </tr>
<td>Date</td> </thead>
<td>Par</td> <tbody>
<td>Nb articles</td> {% for inventory in inventories %}
<td>Commande</td> <tr>
</tr> <td class="text-center">
</thead> <a href="{% url 'kfet.inventory.read' inventory.pk %}">
<tbody> <span class="glyphicon glyphicon-cog"></span>
{% for inventory in inventories %} </a>
<tr> </td>
<td class="text-center"> <td>{{ inventory.at }}</td>
<a href="{% url 'kfet.inventory.read' inventory.pk %}"> <td>{{ inventory.by.trigramme }}</td>
<span class="glyphicon glyphicon-cog"></span> <td>{{ inventory.nb_articles }}</td>
</a> <td>
</td> {% if inventory.order %}
<td>{{ inventory.at }}</td> <a href="{% url 'kfet.order.read' inventory.order.pk %}">
<td>{{ inventory.by.trigramme }}</td> #{{ inventory.order.pk }}
<td>{{ inventory.nb_articles }}</td> </a>
<td> {% endif %}
{% if inventory.order %} </td>
<a href="{% url 'kfet.order.read' inventory.order.pk %}"> </tr>
#{{ inventory.order.pk }} {% endfor %}
</a> </tbody>
{% endif %} </table>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_1.html" %}
{% load staticfiles %} {% load staticfiles %}
{% load widget_tweaks %} {% load widget_tweaks %}
@ -8,75 +8,98 @@
{% endblock %} {% endblock %}
{% block title %}Nouvel inventaire{% endblock %} {% block title %}Nouvel inventaire{% endblock %}
{% block content-header-title %}Nouvel inventaire{% endblock %} {% block header-title %}Création d'un inventaire{% endblock %}
{% block content %} {% block main-size %}col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2{% endblock %}
{% include 'kfet/base_messages.html' %} {% block main-content %}
<div class="content-center">
<div> <form id='inventoryform' action="" method="post">
<form id='inventoryform' action="" method="post"> <div class="table-responsive">
<table class="table text-center"> <table class="table table-condensed text-center">
<thead> <thead>
<tr> <tr>
<td>Article</td> <td>Article</td>
<td>Quantité par caisse</td> <td>Quantité par caisse</td>
<td>Stock Théorique</td> <td>Stock théorique</td>
<td>Caisses en réserve</td> <td>Caisses en réserve</td>
<td>Caisses en arrière</td> <td>Caisses en arrière</td>
<td>Vrac</td> <td>Vrac</td>
<td>Stock total</td> <td>Stock total</td>
<td>Compte terminé</td> <td>Compte terminé</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for form in formset %} {% for form in formset %}
{% ifchanged form.category %} {% ifchanged form.category %}
<tr class='section'> <tr class='section'>
<td>{{ form.category_name }}</td> <td>{{ form.category_name }}</td>
<td colspan="7"></td> <td colspan="7"></td>
</tr> </tr>
{% endifchanged %} {% endifchanged %}
<tr> <tr>
{{ form.article }} {{ form.article }}
<td class='name'>{{ form.name }}</td> <td class='name'>{{ form.name }}</td>
<td class='box_capacity'>{{ form.box_capacity }}</td> <td class='box_capacity'>{{ form.box_capacity }}</td>
<td><span class='current_stock'>{{ form.stock_old }}</span><span class='stock_diff'></span></td> <td>
<td class='box_cellar'> <span class='current_stock'>{{ form.stock_old }}</span><span class='stock_diff'></span>
<div class='col-md-2'></div> </td>
<div class='col-md-8'> <td class='box_cellar nopadding'>
<input type='number' class='form-control' step='1'> <input type='number' class='form-control' step='1'>
</div> </td>
</td> <td class='box_bar nopadding'>
<td class='box_bar'> <input type='number' class='form-control' step='1'>
<div class='col-md-offset-2 col-md-8'><input type='number' class='form-control' step='1'></div> </td>
</td> <td class='misc nopadding'>
<td class='misc'> <input type='number' class='form-control' step='1'>
<div class='col-md-offset-2 col-md-8'><input type='number' class='form-control' step='1'></div> </td>
</td> <td class='stock_new nopadding'>
<td class='stock_new'> {{ form.stock_new | attr:"readonly"| add_class:"form-control" }}
<div class='col-md-offset-2 col-md-8'>{{ form.stock_new | attr:"readonly"| add_class:"form-control" }}</div> </td>
<div class='col-md-2 inventory_update'><button type='button' class='btn-sm btn-primary'>MàJ</button></div> <td class='finished'>
</td> <div class="inventory_update">
<td class='finished'><input type='checkbox' class='form_control'></td> <button type='button' class='btn-sm btn-primary'>MàJ</button>
</tr> </div>
{% endfor %} <div class="inventory_done">
</tbody> <input type='checkbox' class='form_control'>
</table> </div>
{{ formset.management_form }} </td>
{% if not perms.kfet.add_inventory %} </tr>
<div class='auth-form form-horizontal'> {% endfor %}
{% include "kfet/form_authentication_snippet.html" %} <tr class="section">
</div> <td>Totaux</td>
{% endif %} <td colspan="2"></td>
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg btn-block"> <td class="total_box_cellar"></td>
{% csrf_token %} <td class="total_box_bar"></td>
</form> <td colspan="3"></td>
</tr>
</tbody>
</table>
</div> </div>
</div> {{ formset.management_form }}
{% if not perms.kfet.add_inventory %}
<div class='auth-form form-horizontal'>
{% include "kfet/form_authentication_snippet.html" %}
</div>
{% endif %}
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg btn-block">
{% csrf_token %}
</form>
<script type="text/javascript"> <script type="text/javascript">
function init_total(type) {
update_total(type);
$('.'+type+' input').on('input', () => update_total(type));
}
function update_total(type) {
var total = 0;
$('.'+type+' input').each(function() {
total += +$(this).val();
});
$('.total_'+type).text(total);
}
$(document).ready(function() { $(document).ready(function() {
'use strict'; 'use strict';
@ -106,6 +129,7 @@ $(document).ready(function() {
function update_stock($line, update_count) { function update_stock($line, update_count) {
$line.removeClass('inventory_modified'); $line.removeClass('inventory_modified');
$line.find('.inventory_update').hide(); $line.find('.inventory_update').hide();
$line.find('.inventory_done').show();
var old_stock = +$line.find('.current_stock').text() var old_stock = +$line.find('.current_stock').text()
var stock_diff = +$line.find('.stock_diff').text(); var stock_diff = +$line.find('.stock_diff').text();
@ -117,9 +141,9 @@ $(document).ready(function() {
$line.find('.misc input').val(old_misc + stock_diff) $line.find('.misc input').val(old_misc + stock_diff)
.trigger('input'); .trigger('input');
} }
var id = $line.find('input[type="hidden"]').val(); var id = $line.find('input[type="hidden"]').val();
conflicts.delete(parseInt(id)); conflicts.delete(parseInt(id));
} }
$('.finished input').change(function() { $('.finished input').change(function() {
@ -132,6 +156,12 @@ $(document).ready(function() {
update_stock($line, true); update_stock($line, true);
}); });
/**
* Total row
*/
init_total('box_cellar');
init_total('box_bar');
/** /**
* Websocket * Websocket
@ -147,6 +177,7 @@ $(document).ready(function() {
//Realigning input and displaying update button //Realigning input and displaying update button
$line.find('.inventory_update').show(); $line.find('.inventory_update').show();
$line.find('.inventory_done').hide();
//Displaying stock changes //Displaying stock changes
var stock = $line.find('.current_stock').text(); var stock = $line.find('.current_stock').text();
@ -159,7 +190,9 @@ $(document).ready(function() {
}); });
$('input[type="submit"]').on("click", function(e) { $('input[type="submit"]').on("click", function(e) {
e.preventDefault(); e.preventDefault();
var content;
if (conflicts.size) { if (conflicts.size) {
content = ''; content = '';

View file

@ -1,61 +1,55 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_2.html" %}
{% block title %}Inventaire #{{ inventory.pk }}{% endblock %} {% block title %}Inventaire #{{ inventory.pk }}{% endblock %}
{% block content-header-title %}Inventaire #{{ inventory.pk }}{% endblock %} {% block header-title %}Inventaire #{{ inventory.pk }}{% endblock %}
{% block content %} {% block fixed-content %}
<div class="row"> <div class="content-left-top">
<div class="col-sm-4 col-md-3 col-content-left"> <div class="line"><b>Date:</b> {{ inventory.at }}</div>
<div class="content-left"> <div class="line"><b>Par:</b> {{ inventory.by.trigramme }}</div>
<div class="content-left-top"> {% if inventory.order %}
<div class="line"><b>Date:</b> {{ inventory.at }}</div> <div class="line">
<div class="line"><b>Par:</b> {{ inventory.by.trigramme }}</div> <b>Commande relative:</b>&nbsp;
{% if inventory.order %} <a href="{% url 'kfet.order.read' inventory.order.pk %}">
<div class="line"> #{{ inventory.order.pk }}
<b>Commande relative:</b>&nbsp; </a>
<a href="{% url 'kfet.order.read' inventory.order.pk %}">
#{{ inventory.order.pk }}
</a>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-sm-8 col-md-9 col-content-right">
{% include 'kfet/base_messages.html' %}
<div class="content-right">
<div class="content-right-block">
<h2>Détails</h2>
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td>Article</td>
<td>Stock avant</td>
<td>Stock après</td>
<td>Erreur</td>
</tr>
</thead>
<tbody>
{% for inventoryart in inventoryarts %}
{% ifchanged inventoryart.article.category %}
<tr class="section">
<td colspan="4">{{ inventoryart.article.category.name }}</td>
</tr>
{% endifchanged %}
<tr>
<td>{{ inventoryart.article.name }}</td>
<td>{{ inventoryart.stock_old }}</td>
<td>{{ inventoryart.stock_new }}</td>
<td>{{ inventoryart.stock_error }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div> </div>
{% endif %}
</div>
{% endblock %}
{% block main-content %}
<div class="content-right-block">
<h2>Détails</h2>
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td>Article</td>
<td>Stock avant</td>
<td>Stock après</td>
<td>Erreur</td>
</tr>
</thead>
<tbody>
{% for inventoryart in inventoryarts %}
{% ifchanged inventoryart.article.category %}
<tr class="section">
<td colspan="4">{{ inventoryart.article.category.name }}</td>
</tr>
{% endifchanged %}
<tr>
<td>{{ inventoryart.article.name }}</td>
<td>{{ inventoryart.stock_old }}</td>
<td>{{ inventoryart.stock_new }}</td>
<td>{{ inventoryart.stock_error }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>

View file

@ -13,7 +13,7 @@
{% block title %}K-Psul{% endblock %} {% block title %}K-Psul{% endblock %}
{% block content-header %}{% endblock %} {% block header %}{% endblock %}
{% block help %} {% block help %}
@ -178,7 +178,7 @@ $(document).ready(function() {
settings = {} settings = {}
function resetSettings() { function resetSettings() {
$.ajax({ return $.ajax({
dataType: "json", dataType: "json",
url : "{% url 'kfet.kpsul.get_settings' %}", url : "{% url 'kfet.kpsul.get_settings' %}",
method : "POST", method : "POST",
@ -187,7 +187,6 @@ $(document).ready(function() {
settings['addcost_for'] = data['addcost_for']; settings['addcost_for'] = data['addcost_for'];
settings['addcost_amount'] = parseFloat(data['addcost_amount']); settings['addcost_amount'] = parseFloat(data['addcost_amount']);
settings['subvention_cof'] = parseFloat(data['subvention_cof']); settings['subvention_cof'] = parseFloat(data['subvention_cof']);
displayAddcost();
}); });
} }
@ -389,6 +388,11 @@ $(document).ready(function() {
var last_statement_container = $('#last_statement'); var last_statement_container = $('#last_statement');
var last_statement_html_default = '<b>Dernier relevé: </b><br><span id="checkout-last_statement_balance"></span>€ le <span id="checkout-last_statement_at"></span> par <span id="checkout-last_statement_by_trigramme"></span>'; var last_statement_html_default = '<b>Dernier relevé: </b><br><span id="checkout-last_statement_balance"></span>€ le <span id="checkout-last_statement_at"></span> par <span id="checkout-last_statement_by_trigramme"></span>';
// If only one checkout is available, select it
var checkout_choices = checkoutInput.find("option[value!='']");
if (checkout_choices.length == 1) {
$(checkout_choices[0]).prop("selected", true);
}
// Display data // Display data
function displayCheckoutData() { function displayCheckoutData() {
@ -1422,9 +1426,11 @@ $(document).ready(function() {
resetArticles(); resetArticles();
resetPreviousOp(); resetPreviousOp();
khistory.reset(); khistory.reset();
resetSettings(); resetSettings().done(function (){
getArticles(); getArticles();
getHistory(); getHistory();
displayAddcost();
});
} }
function resetSelectable() { function resetSelectable() {

View file

@ -36,6 +36,12 @@
</div> </div>
</div> </div>
<div class="buttons"> <div class="buttons">
{% if account.user == request.user %}
<ul class='nav nav-pills nav-justified'>
<li class="active"><a class="btn btn-primary" data-toggle="pill" href="#tab_stats">Statistiques</a></li>
<li><a class="btn btn-primary" data-toggle="pill" href="#tab_history">Historique</a></li>
</ul>
{% endif %}
<a class="btn btn-primary btn-lg" href="{% url 'kfet.account.update' account.trigramme %}"> <a class="btn btn-primary btn-lg" href="{% url 'kfet.account.update' account.trigramme %}">
Modifier Modifier
</a> </a>

View file

@ -1,7 +1,7 @@
{% extends 'kfet/base.html' %} {% extends 'kfet/base.html' %}
{% block content %} {% block extra_head %}
<script type="text/javascript">
Connexion utilisateur K-Fêt générique réussie close();
</script>
{% endblock %} {% endblock %}

View file

@ -1,105 +1,97 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_2.html" %}
{% block title %}Commandes{% endblock %} {% block title %}Commandes{% endblock %}
{% block content-header-title %}Commandes{% endblock %} {% block header-title %}Commandes{% endblock %}
{% block content %} {% block fixed-content %}
<div class="row"> <div class="content-left-top">
<div class="col-sm-4 col-md-3 col-content-left"> <div class="line line-big">{{ orders|length }}</div>
<div class="content-left"> <div class="line line-bigsub">commande{{ orders|length|pluralize }}</div>
<div class="content-left-top"> </div>
<div class="line line-big">{{ orders|length }}</div>
<div class="line line-bigsub">commande{{ orders|length|pluralize }}</div> {% endblock %}
</div>
<div class="buttons"> {% block main-content %}
</div>
</div> <div class="content-right-block">
<h2>Fournisseurs</h2>
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td></td>
<td></td>
<td>Nom</td>
<td>Mail</td>
<td>Tél.</td>
<td>Adresse</td>
<td>Commentaire</td>
<tbody>
{% for supplier in suppliers %}
<tr>
<td class="no-padding">
<a href="{% url 'kfet.order.new' supplier.pk %}" class="btn btn-primary">
Passer une commande
</a>
</td>
<td class="text-center">
<a href="{% url 'kfet.order.supplier.update' supplier.pk %}">
<span class="glyphicon glyphicon-cog"></span>
</a>
</td>
<td>{{ supplier.name }}</td>
<td>{{ supplier.email }}</td>
<td>{{ supplier.phone }}</td>
<td>{{ supplier.address }}</td>
<td>{{ supplier.comment }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
<div class="col-sm-8 col-md-9 col-content-right"> </div>
{% include 'kfet/base_messages.html' %} <div class="content-right-block">
<div class="content-right"> <h2>Liste des commandes</h2>
<div class="content-right-block"> <div class="table-responsive">
<h2>Fournisseurs</h2> <table class="table table-condensed">
<div class="table-responsive"> <thead>
<table class="table table-condensed"> <tr>
<thead> <td></td>
<tr> <td></td>
<td></td> <td>Date</td>
<td></td> <td>Fournisseur</td>
<td>Nom</td> <td>Inventaire</td>
<td>Mail</td> </tr>
<td>Tél.</td> </thead>
<td>Adresse</td> <tbody>
<td>Commentaire</td> {% for order in orders %}
<tbody> <tr>
{% for supplier in suppliers %} <td class="no-padding">
<tr> {% if not order.inventory %}
<td class="no-padding"> <a href="{% url 'kfet.order.to_inventory' order.pk %}" class="btn btn-primary">
<a href="{% url 'kfet.order.new' supplier.pk %}" class="btn btn-primary"> Générer inventaire
Passer une commande </a>
</a> {% endif %}
</td> </td>
<td class="text-center"> <td>
<a href="{% url 'kfet.order.supplier.update' supplier.pk %}"> <a href="{% url 'kfet.order.read' order.pk %}">
<span class="glyphicon glyphicon-cog"></span> <span class="glyphicon glyphicon-cog"></span>
</a> </a>
</td> </td>
<td>{{ supplier.name }}</td> <td>{{ order.at }}</td>
<td>{{ supplier.email }}</td> <td>{{ order.supplier }}</td>
<td>{{ supplier.phone }}</td> <td>
<td>{{ supplier.address }}</td> {% if order.inventory %}
<td>{{ supplier.comment }}</td> <a href="{% url 'kfet.inventory.read' order.inventory.pk %}">
</tr> #{{ order.inventory.pk }}
{% endfor %} </a>
</tbody> {% endif %}
</table> </td>
</div> </tr>
</div> {% endfor %}
<div class="content-right-block"> </tbody>
<h2>Liste des commandes</h2> </table>
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td></td>
<td></td>
<td>Date</td>
<td>Fournisseur</td>
<td>Inventaire</td>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td class="no-padding">
{% if not order.inventory %}
<a href="{% url 'kfet.order.to_inventory' order.pk %}" class="btn btn-primary">
Générer inventaire
</a>
{% endif %}
</td>
<td>
<a href="{% url 'kfet.order.read' order.pk %}">
<span class="glyphicon glyphicon-cog"></span>
</a>
</td>
<td>{{ order.at }}</td>
<td>{{ order.supplier }}</td>
<td>
{% if order.inventory %}
<a href="{% url 'kfet.inventory.read' order.inventory.pk %}">
#{{ order.inventory.pk }}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -1,71 +1,72 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_1.html" %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block title %}Nouvelle commande{% endblock %} {% block title %}Nouvelle commande{% endblock %}
{% block content-header-title %}Nouvelle commande {{ supplier.name }}{% endblock %} {% block header-title %}Création d'une commande {{ supplier.name }}{% endblock %}
{% block content %} {% block main-size %}col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2{% endblock %}
{% block main-content %}
<div class="content-center">
<div>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
<table class="table text-center"> <div class="table-responsive">
<table class="table table-condensed text-center">
<thead> <thead>
<tr> <tr>
<td rowspan="2">Article</td> <td rowspan="2">Article</td>
<td colspan="5">Ventes <td colspan="{{ scale|length }}">Ventes
<span class='glyphicon glyphicon-question-sign' title="Ventes des 5 dernières semaines" data-placement="bottom"></span> <span class='glyphicon glyphicon-question-sign' title="Ventes des 5 dernières semaines" data-placement="bottom"></span>
</td> </td>
<td rowspan="2">V. moy. <td rowspan="2">V. moy.<br>
<span class='glyphicon glyphicon-question-sign' title="Moyenne des ventes" data-placement="bottom"></span> <span class='glyphicon glyphicon-question-sign' title="Moyenne des ventes" data-placement="bottom"></span>
</td> </td>
<td rowspan="2">E.T. <td rowspan="2">E.T.<br>
<span class='glyphicon glyphicon-question-sign' title="Écart-type des ventes" data-placement="bottom"></span> <span class='glyphicon glyphicon-question-sign' title="Écart-type des ventes" data-placement="bottom"></span>
</td> </td>
<td rowspan="2">Prév. <td rowspan="2">Prév.<br>
<span class='glyphicon glyphicon-question-sign' title="Prévision de ventes" data-placement="bottom"></span> <span class='glyphicon glyphicon-question-sign' title="Prévision de ventes" data-placement="bottom"></span>
</td> </td>
<td rowspan="2">Stock</td> <td rowspan="2">Stock</td>
<td rowspan="2">Rec. <td rowspan="2">Box<br>
<span class='glyphicon glyphicon-question-sign' title="Capacité d'une boite" data-placement="bottom"></span>
</td>
<td rowspan="2">Rec.<br>
<span class='glyphicon glyphicon-question-sign' title="Quantité conseillée" data-placement="bottom"></span> <span class='glyphicon glyphicon-question-sign' title="Quantité conseillée" data-placement="bottom"></span>
</td> </td>
<td rowspan="2">Commande</td> <td rowspan="2">Commande</td>
</tr> </tr>
<tr> <tr>
<td>S1</td> {% for label in scale.get_labels %}
<td>S2</td> <td>{{ label }}</td>
<td>S3</td> {% endfor %}
<td>S4</td>
<td>S5</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for form in formset %} {% for form in formset %}
{% ifchanged form.category %} {% ifchanged form.category %}
<tr class='section'> <tr class='section text-left'>
<td> {{ form.category_name }}</td> <td colspan="{{ scale|length|add:'8' }}">{{ form.category_name }}</td>
<td colspan="11"></td>
</tr> </tr>
{% endifchanged %} {% endifchanged %}
<tr> <tr>
{{ form.article }} {{ form.article }}
<td>{{ form.name }}</td> <td>{{ form.name }}</td>
<td>{{ form.v_s1 }}</td> {% for v_chunk in form.v_all %}
<td>{{ form.v_s2 }}</td> <td>{{ v_chunk }}</td>
<td>{{ form.v_s3 }}</td> {% endfor %}
<td>{{ form.v_s4 }}</td>
<td>{{ form.v_s5 }}</td>
<td>{{ form.v_moy }}</td> <td>{{ form.v_moy }}</td>
<td>{{ form.v_et }}</td> <td>{{ form.v_et }}</td>
<td>{{ form.v_prev }}</td> <td>{{ form.v_prev }}</td>
<td>{{ form.stock }}</td> <td>{{ form.stock }}</td>
<td>{{ form.box_capacity|default:"" }}</td>
<td>{{ form.c_rec }}</td> <td>{{ form.c_rec }}</td>
<td>{{ form.quantity_ordered | add_class:"form-control" }}</td> <td class="nopadding">{{ form.quantity_ordered | add_class:"form-control" }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
{{ formset.management_form }} {{ formset.management_form }}
{% if not perms.kfet.add_inventory %} {% if not perms.kfet.add_inventory %}
<div class='auth-form form-horizontal'> <div class='auth-form form-horizontal'>
@ -74,8 +75,6 @@
{% endif %} {% endif %}
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg btn-block"> <input type="submit" value="Enregistrer" class="btn btn-primary btn-lg btn-block">
</form> </form>
</div>
</div>
<script type='text/javascript'> <script type='text/javascript'>
$(document).ready(function () { $(document).ready(function () {

View file

@ -1,66 +1,67 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_2.html" %}
{% block title %}Commande #{{ order.pk }}{% endblock %} {% block title %}Commande #{{ order.pk }}{% endblock %}
{% block content-header-title %}Commande #{{ order.pk }}{% endblock %} {% block header-title %}Commande #{{ order.pk }}{% endblock %}
{% block content %} {% block fixed-content %}
<div class="row"> <div class="content-left-top">
<div class="col-sm-4 col-md-3 col-content-left"> <div class="line"><b>Créée le:</b> {{ order.at }}</div>
<div class="content-left"> <div class="line"><b>Fournisseur:</b> {{ order.supplier.name }}</div>
<div class="content-left-top"> </div>
<div class="line"><b>Créée le:</b> {{ order.at }}</div> <div class="buttons">
<div class="line"><b>Fournisseur:</b> {{ order.supplier.name }}</div> {% if not order.inventory %}
</div> <a class="btn btn-primary btn-lg" href="{% url 'kfet.order.to_inventory' order.pk %}">
</div> Générer inventaire
</a>
{% endif %}
</div>
{% endblock %}
{% block main-content %}
<div class="content-right-block">
<h2>Détails</h2>
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td>Article</td>
<td>Commandé</td>
<td>Commandé (#box)</td>
<td>Reçu</td>
</tr>
</thead>
<tbody>
{% for orderart in orderarts %}
{% ifchanged orderart.article.category %}
<tr class="section">
<td colspan="4">{{ orderart.article.category.name }}</td>
</tr>
{% endifchanged %}
<tr>
<td>{{ orderart.article.name }}</td>
<td>{{ orderart.quantity_ordered }}</td>
<td>
{% if orderart.article.box_capacity %}
{# c'est une division ! #}
{% widthratio orderart.quantity_ordered orderart.article.box_capacity 1 %}
{% endif %}
</td>
<td>
{{ orderart.quantity_received|default_if_none:'' }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
<div class="col-sm-8 col-md-9 col-content-right"> </div>
{% include 'kfet/base_messages.html' %} <div class="content-right-block">
<div class="content-right"> <h2>Mail</h2>
<div class="content-right-block"> <div>
<h2>Détails</h2> <textarea class="form-control" style="height:500px;">{{ mail }}</textarea>
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td>Article</td>
<td>Commandé</td>
<td>Commandé (#box)</td>
<td>Reçu</td>
</tr>
</thead>
<tbody>
{% for orderart in orderarts %}
{% ifchanged orderart.article.category %}
<tr class="section">
<td colspan="4">{{ orderart.article.category.name }}</td>
</tr>
{% endifchanged %}
<tr>
<td>{{ orderart.article.name }}</td>
<td>{{ orderart.quantity_ordered }}</td>
<td>
{% if orderart.article.box_capacity %}
{# c'est une division ! #}
{% widthratio orderart.quantity_ordered orderart.article.box_capacity 1 %}
{% endif %}
</td>
<td>
{{ orderart.quantity_received|default_if_none:'' }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="content-right-block">
<h2>Mail</h2>
<div>
<textarea class="form-control" style="height:500px;">{{ mail }}</textarea>
</div>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -1,18 +1,17 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_1.html" %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block title %}{% endblock %} {% block title %}Nouvel inventaire{% endblock %}
{% block content-header-title %}{% endblock %} {% block header-title %}Création d'inventaire depuis la commande #{{ order.pk }}{% endblock %}
{% block content %} {% block main-size %}col-md-10 col-md-offset-1 col-lg-8 col-lg-offset-2{% endblock %}
{% include 'kfet/base_messages.html' %} {% block main-content %}
<div class="content-center">
<div>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
<table class='table text-center'> <div class="table-responsive">
<table class='table table-condensed text-center'>
<thead> <thead>
<tr> <tr>
<td style="width:25%">Article</td> <td style="width:25%">Article</td>
@ -25,23 +24,24 @@
</thead> </thead>
<tbody> <tbody>
{% for form in formset %} {% for form in formset %}
{% ifchanged form.category %} {% ifchanged form.category %}
<tr class='section'> <tr class='section'>
<td>{{ form.category_name }}</td> <td>{{ form.category_name }}</td>
<td colspan="5"></td> <td colspan="5"></td>
</tr> </tr>
{% endifchanged %} {% endifchanged %}
<tr> <tr>
{{ form.article }} <td>{{ form.name }}</td>
<td>{{ form.name }}</td> <td class="nopadding">{{ form.price_HT | add_class:"form-control" }}</td>
<td>{{ form.price_HT | add_class:"form-control" }}</td> <td class="nopadding">{{ form.TVA | add_class:"form-control" }}</td>
<td>{{ form.TVA | add_class:"form-control" }}</td> <td class="nopadding">{{ form.rights | add_class:"form-control" }}</td>
<td>{{ form.rights | add_class:"form-control" }}</td> <td>{{ form.quantity_ordered }}</td>
<td>{{ form.quantity_ordered }}</td> <td class="nopadding">{{ form.quantity_received | add_class:"form-control" }}</td>
<td>{{ form.quantity_received | add_class:"form-control" }}</td> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
{{ formset.management_form }} {{ formset.management_form }}
{% if not perms.kfet.add_inventory %} {% if not perms.kfet.add_inventory %}
<div class='auth-form form-horizontal'> <div class='auth-form form-horizontal'>
@ -50,7 +50,5 @@
{% endif %} {% endif %}
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg btn-block"> <input type="submit" value="Enregistrer" class="btn btn-primary btn-lg btn-block">
</form> </form>
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,24 +1,40 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_2.html" %}
{% block title %}Paramètres{% endblock %} {% block title %}Paramètres{% endblock %}
{% block content-header-title %}Paramètres{% endblock %} {% block header-title %}Paramètres{% endblock %}
{% block content %} {% block fixed-content %}
{% include 'kfet/base_messages.html' %} <div class="buttons">
<table> <a class="btn btn-primary btn-lg" href="{% url 'kfet.settings.update' %}">
<tr> Modifier
<td></td> </a>
<td>Nom</td> </div>
<td>Valeur</td>
</tr> {% endblock %}
{% for setting in settings %}
<tr> {% block main-content %}
<td><a href="{% url 'kfet.settings.update' setting.pk %}">Modifier</a></td>
<td>{{ setting.name }}</td> <div class="content-right-block">
<td>{% firstof setting.value_decimal setting.value_duration setting.value_account %}</td> <h2>Valeurs</h2>
</tr> <div class="table-responsive">
{% endfor %} <table class="table table-condensed">
</table> <thead>
<tr>
<td>Nom</td>
<td>Valeur</td>
</tr>
</thead>
<tbody>
{% for key, value in kfet_config.list %}
<tr>
<td>{{ key }}</td>
<td>{{ value|default_if_none:"" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,14 +1,12 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_1.html" %}
{% block title %}Modification de {{ settings.name }}{% endblock %} {% block title %}Modification des paramètres{% endblock %}
{% block content-header-title %}Modification de {{ settings.name }}{% endblock %} {% block header-title %}Modification des paramètres{% endblock %}
{% block content %} {% block main-class %}content-form{% endblock %}
<form action="" method="post"> {% block main-content %}
{% csrf_token %}
{{ form.as_p }} {% include "kfet/base_form.html" with authz=perms.kfet.change_settings submit_text="Mettre à jour"%}
<input type="submit" value="Mettre à jour">
</form>
{% endblock %} {% endblock %}

View file

@ -1,27 +1,12 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_1.html" %}
{% load widget_tweaks %}
{% load staticfiles %}
{% block title %}Fournisseur - Modification{% endblock %} {% block title %}{{ supplier.name }} - Édition{% endblock %}
{% block content-header-title %}Fournisseur - Modification{% endblock %} {% block header-title %}Édition du fournisseur {{ supplier.name }}{% endblock %}
{% block content %} {% block main-class %}content-form{% endblock %}
{% include 'kfet/base_messages.html' %} {% block main-content %}
<div class="row form-only"> {% include 'kfet/base_form.html' with authz=perms.kfet.change_supplier submit_text="Mettre à jour" %}
<div class="col-sm-12 col-md-8 col-md-offset-2">
<div class="content-form">
<form submit="" method="post" class="form-horizontal">
{% csrf_token %}
{% include 'kfet/form_snippet.html' with form=form %}
{% if not perms.kfet.change_supplier %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
{% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %}
</form>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View file

@ -1,4 +1,4 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_1.html" %}
{% load staticfiles %} {% load staticfiles %}
{% block extra_head %} {% block extra_head %}
@ -7,13 +7,14 @@
{% endblock %} {% endblock %}
{% block title %}Nouveaux transferts{% endblock %} {% block title %}Nouveaux transferts{% endblock %}
{% block content-header-title %}Nouveaux transferts{% endblock %} {% block header-title %}Création de transferts{% endblock %}
{% block content %} {% block main-size %}col-sm-12{% endblock %}
{% csrf_token %} {% block main-content %}
<form id="transfers_form"> <form id="transfers_form" style="background-color: transparent;">
{% csrf_token %}
<div class="transfer_general text-center"> <div class="transfer_general text-center">
<input type="text" name="comment" id="comment" placeholder="Commentaire"><!-- <input type="text" name="comment" id="comment" placeholder="Commentaire"><!--
--><button type="submit" id="submit" class="btn btn-primary btn-lg">Enregistrer</button> --><button type="submit" id="submit" class="btn btn-primary btn-lg">Enregistrer</button>

View file

@ -3,10 +3,11 @@
from django import template from django import template
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from kfet.models import Settings
from math import floor from math import floor
import re import re
from kfet.config import kfet_config
register = template.Library() register = template.Library()
@ -27,7 +28,6 @@ def highlight_user(user, q):
return highlight_text(text, q) return highlight_text(text, q)
@register.filter(is_safe=True) @register.filter(is_safe=True)
def highlight_clipper(clipper, q): def highlight_clipper(clipper, q):
if clipper.fullname: if clipper.fullname:
@ -37,8 +37,12 @@ def highlight_clipper(clipper, q):
return highlight_text(text, q) return highlight_text(text, q)
@register.filter() @register.filter()
def ukf(balance, is_cof): def ukf(balance, is_cof):
grant = is_cof and (1 + Settings.SUBVENTION_COF() / 100) or 1 grant = is_cof and (1 + kfet_config.subvention_cof / 100) or 1
return floor(balance * 10 * grant) return floor(balance * 10 * grant)
@register.filter()
def widget_type(field):
return field.field.widget.__class__.__name__

0
kfet/tests/__init__.py Normal file
View file

56
kfet/tests/test_config.py Normal file
View file

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
from decimal import Decimal
from django.test import TestCase
from django.utils import timezone
import djconfig
from gestioncof.models import User
from kfet.config import kfet_config
from kfet.models import Account
class ConfigTest(TestCase):
"""Tests suite for kfet configuration."""
def setUp(self):
# load configuration as in djconfig middleware
djconfig.reload_maybe()
def test_get(self):
self.assertTrue(hasattr(kfet_config, 'subvention_cof'))
def test_subvention_cof(self):
reduction_cof = Decimal('20')
subvention_cof = Decimal('25')
kfet_config.set(reduction_cof=reduction_cof)
self.assertEqual(kfet_config.subvention_cof, subvention_cof)
def test_set_decimal(self):
"""Test field of decimal type."""
reduction_cof = Decimal('10')
# IUT
kfet_config.set(reduction_cof=reduction_cof)
# check
self.assertEqual(kfet_config.reduction_cof, reduction_cof)
def test_set_modelinstance(self):
"""Test field of model instance type."""
user = User.objects.create(username='foo_user')
account = Account.objects.create(trigramme='FOO',
cofprofile=user.profile)
# IUT
kfet_config.set(addcost_for=account)
# check
self.assertEqual(kfet_config.addcost_for, account)
def test_set_duration(self):
"""Test field of duration type."""
cancel_duration = timezone.timedelta(days=2, hours=4)
# IUT
kfet_config.set(cancel_duration=cancel_duration)
# check
self.assertEqual(kfet_config.cancel_duration, cancel_duration)

56
kfet/tests/test_forms.py Normal file
View file

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
from django.test import TestCase
from django.contrib.auth.models import User, Group
from kfet.forms import UserGroupForm
class UserGroupFormTests(TestCase):
"""Test suite for UserGroupForm."""
def setUp(self):
# create user
self.user = User.objects.create(username="foo", password="foo")
# create some K-Fêt groups
prefix_name = "K-Fêt "
names = ["Group 1", "Group 2", "Group 3"]
self.kfet_groups = [
Group.objects.create(name=prefix_name+name)
for name in names
]
# create a non-K-Fêt group
self.other_group = Group.objects.create(name="Other group")
def test_choices(self):
"""Only K-Fêt groups are selectable."""
form = UserGroupForm(instance=self.user)
groups_field = form.fields['groups']
self.assertQuerysetEqual(
groups_field.queryset,
[repr(g) for g in self.kfet_groups],
ordered=False,
)
def test_keep_others(self):
"""User stays in its non-K-Fêt groups."""
user = self.user
# add user to a non-K-Fêt group
user.groups.add(self.other_group)
# add user to some K-Fêt groups through UserGroupForm
data = {
'groups': [group.pk for group in self.kfet_groups],
}
form = UserGroupForm(data, instance=user)
form.is_valid()
form.save()
self.assertQuerysetEqual(
user.groups.all(),
[repr(g) for g in [self.other_group] + self.kfet_groups],
ordered=False,
)

View file

@ -5,7 +5,7 @@ from unittest.mock import patch
from django.test import TestCase, Client from django.test import TestCase, Client
from django.contrib.auth.models import User, Permission from django.contrib.auth.models import User, Permission
from .models import Account, Article, ArticleCategory from kfet.models import Account, Article, ArticleCategory
class TestStats(TestCase): class TestStats(TestCase):

57
kfet/tests/test_views.py Normal file
View file

@ -0,0 +1,57 @@
from decimal import Decimal
from django.test import TestCase, Client
from django.contrib.auth.models import User
from django.utils import timezone
from ..models import Account, OperationGroup, Checkout, Operation
class AccountTests(TestCase):
"""Account related views"""
def setUp(self):
# A user and its account
self.user = User.objects.create_user(username="foobar", password="foo")
acc = Account.objects.create(
trigramme="FOO", cofprofile=self.user.profile
)
# Dummy operations and operation groups
checkout = Checkout.objects.create(
created_by=acc, name="checkout",
valid_from=timezone.now(),
valid_to=timezone.now() + timezone.timedelta(days=365)
)
opeg_data = [
(timezone.now(), Decimal('10')),
(timezone.now() - timezone.timedelta(days=3), Decimal('3')),
]
OperationGroup.objects.bulk_create([
OperationGroup(
on_acc=acc, checkout=checkout, at=at, is_cof=False,
amount=amount
)
for (at, amount) in opeg_data
])
self.operation_groups = OperationGroup.objects.order_by("-amount")
Operation.objects.create(
group=self.operation_groups[0],
type=Operation.PURCHASE,
amount=Decimal('10')
)
Operation.objects.create(
group=self.operation_groups[1],
type=Operation.PURCHASE,
amount=Decimal('3')
)
def test_account_read(self):
"""We can query the "Accout - Read" page."""
client = Client()
self.assertTrue(client.login(
username="foobar",
password="foo"
))
resp = client.get("/k-fet/accounts/FOO")
self.assertEqual(200, resp.status_code)

View file

@ -193,7 +193,7 @@ urlpatterns = [
permission_required('kfet.change_settings') permission_required('kfet.change_settings')
(views.SettingsList.as_view()), (views.SettingsList.as_view()),
name='kfet.settings'), name='kfet.settings'),
url(r'^settings/(?P<pk>\d+)/edit$', url(r'^settings/edit$',
permission_required('kfet.change_settings') permission_required('kfet.change_settings')
(views.SettingsUpdate.as_view()), (views.SettingsUpdate.as_view()),
name='kfet.settings.update'), name='kfet.settings.update'),

View file

@ -6,7 +6,9 @@ from urllib.parse import urlencode
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.cache import cache from django.core.cache import cache
from django.views.generic import ListView, DetailView, TemplateView, View from django.views.generic import (
DetailView, FormView, ListView, TemplateView, View,
)
from django.views.generic.detail import BaseDetailView from django.views.generic.detail import BaseDetailView
from django.views.generic.edit import CreateView, UpdateView from django.views.generic.edit import CreateView, UpdateView
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
@ -24,9 +26,11 @@ from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from kfet.config import kfet_config
from kfet.decorators import teamkfet_required from kfet.decorators import teamkfet_required
from kfet.models import ( from kfet.models import (
Account, Checkout, Article, Settings, AccountNegative, Account, Checkout, Article, AccountNegative,
CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory, CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
InventoryArticle, Order, OrderArticle, Operation, OperationGroup, InventoryArticle, Order, OrderArticle, Operation, OperationGroup,
TransferGroup, Transfer, ArticleCategory) TransferGroup, Transfer, ArticleCategory)
@ -37,9 +41,9 @@ from kfet.forms import (
GroupForm, CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm, GroupForm, CheckoutForm, CheckoutRestrictForm, CheckoutStatementCreateForm,
CheckoutStatementUpdateForm, ArticleForm, ArticleRestrictForm, CheckoutStatementUpdateForm, ArticleForm, ArticleRestrictForm,
KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm, KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm,
KPsulOperationFormSet, AddcostForm, FilterHistoryForm, SettingsForm, KPsulOperationFormSet, AddcostForm, FilterHistoryForm,
TransferFormSet, InventoryArticleForm, OrderArticleForm, TransferFormSet, InventoryArticleForm, OrderArticleForm,
OrderArticleToInventoryForm, CategoryForm OrderArticleToInventoryForm, CategoryForm, KFetConfigForm
) )
from collections import defaultdict from collections import defaultdict
from kfet import consumers from kfet import consumers
@ -48,18 +52,28 @@ from decimal import Decimal
import django_cas_ng import django_cas_ng
import heapq import heapq
import statistics import statistics
from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale
class Home(TemplateView): class Home(TemplateView):
template_name = "kfet/home.html" template_name = "kfet/home.html"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(TemplateView, self).get_context_data(**kwargs) context = super().get_context_data(**kwargs)
articles = Article.objects.all().filter(is_sold=True, hidden=False) articles = list(
context['pressions'] = articles.filter(category__name='Pression') Article.objects
context['articles'] = (articles.exclude(category__name='Pression') .filter(is_sold=True, hidden=False)
.order_by('category')) .select_related('category')
.order_by('category__name')
)
pressions, others = [], []
while len(articles) > 0:
article = articles.pop()
if article.category.name == 'Pression':
pressions.append(article)
else:
others.append(article)
context['pressions'], context['articles'] = pressions, others
return context return context
@method_decorator(login_required) @method_decorator(login_required)
@ -141,6 +155,8 @@ def login_genericteam(request):
user = authenticate(username="kfet_genericteam", token=token.token) user = authenticate(username="kfet_genericteam", token=token.token)
login(request, user) login(request, user)
messages.success(request, "Connecté en utilisateur partagé")
if need_cas_logout: if need_cas_logout:
# Vue de déconnexion de CAS # Vue de déconnexion de CAS
return logout_cas return logout_cas
@ -404,31 +420,31 @@ def account_create_ajax(request, username=None, login_clipper=None,
'user_form' : forms['user_form'], 'user_form' : forms['user_form'],
}) })
# Account - Read # Account - Read
@login_required @login_required
def account_read(request, trigramme): def account_read(request, trigramme):
try: account = get_object_or_404(Account, trigramme=trigramme)
account = Account.objects.select_related('negative').get(trigramme=trigramme)
except Account.DoesNotExist:
raise Http404
# Checking permissions # Checking permissions
if not request.user.has_perm('kfet.is_team') \ if not request.user.has_perm('kfet.is_team') \
and request.user != account.user: and request.user != account.user:
raise PermissionDenied raise PermissionDenied
addcosts = (OperationGroup.objects addcosts = (
.filter(opes__addcost_for=account,opes__canceled_at=None) OperationGroup.objects
.extra({'date':"date(at)"}) .filter(opes__addcost_for=account,
.values('date') opes__canceled_at=None)
.annotate(sum_addcosts=Sum('opes__addcost_amount')) .extra({'date': "date(at)"})
.order_by('-date')) .values('date')
.annotate(sum_addcosts=Sum('opes__addcost_amount'))
.order_by('-date')
)
return render(request, "kfet/account_read.html", { return render(request, "kfet/account_read.html", {
'account' : account, 'account': account,
'addcosts': addcosts, 'addcosts': addcosts,
'settings': { 'subvention_cof': Settings.SUBVENTION_COF() },
}) })
# Account - Update # Account - Update
@ -575,13 +591,22 @@ def account_update(request, trigramme):
'pwd_form': pwd_form, 'pwd_form': pwd_form,
}) })
@permission_required('kfet.manage_perms') @permission_required('kfet.manage_perms')
def account_group(request): def account_group(request):
groups = (Group.objects user_pre = Prefetch(
.filter(name__icontains='K-Fêt') 'user_set',
.prefetch_related('permissions', 'user_set__profile__account_kfet') queryset=User.objects.select_related('profile__account_kfet'),
) )
return render(request, 'kfet/account_group.html', { 'groups': groups }) groups = (
Group.objects
.filter(name__icontains='K-Fêt')
.prefetch_related('permissions', user_pre)
)
return render(request, 'kfet/account_group.html', {
'groups': groups,
})
class AccountGroupCreate(SuccessMessageMixin, CreateView): class AccountGroupCreate(SuccessMessageMixin, CreateView):
model = Group model = Group
@ -590,34 +615,27 @@ class AccountGroupCreate(SuccessMessageMixin, CreateView):
success_message = 'Nouveau groupe : %(name)s' success_message = 'Nouveau groupe : %(name)s'
success_url = reverse_lazy('kfet.account.group') success_url = reverse_lazy('kfet.account.group')
class AccountGroupUpdate(UpdateView): class AccountGroupUpdate(SuccessMessageMixin, UpdateView):
queryset = Group.objects.filter(name__icontains='K-Fêt') queryset = Group.objects.filter(name__icontains='K-Fêt')
template_name = 'kfet/account_group_form.html' template_name = 'kfet/account_group_form.html'
form_class = GroupForm form_class = GroupForm
success_message = 'Groupe modifié : %(name)s' success_message = 'Groupe modifié : %(name)s'
success_url = reverse_lazy('kfet.account.group') success_url = reverse_lazy('kfet.account.group')
class AccountNegativeList(ListView): class AccountNegativeList(ListView):
queryset = (AccountNegative.objects queryset = (
AccountNegative.objects
.select_related('account', 'account__cofprofile__user') .select_related('account', 'account__cofprofile__user')
.exclude(account__trigramme='#13')) .exclude(account__trigramme='#13')
)
template_name = 'kfet/account_negative.html' template_name = 'kfet/account_negative.html'
context_object_name = 'negatives' context_object_name = 'negatives'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(AccountNegativeList, self).get_context_data(**kwargs) context = super(AccountNegativeList, self).get_context_data(**kwargs)
context['settings'] = { real_balances = (neg.account.real_balance for neg in self.object_list)
'overdraft_amount': Settings.OVERDRAFT_AMOUNT(), context['negatives_sum'] = sum(real_balances)
'overdraft_duration': Settings.OVERDRAFT_DURATION(),
}
negs_sum = (AccountNegative.objects
.exclude(account__trigramme='#13')
.aggregate(
bal = Coalesce(Sum('account__balance'),0),
offset = Coalesce(Sum('balance_offset'),0),
)
)
context['negatives_sum'] = negs_sum['bal'] - negs_sum['offset']
return context return context
# ----- # -----
@ -820,12 +838,18 @@ class CategoryUpdate(SuccessMessageMixin, UpdateView):
# Article - General # Article - General
class ArticleList(ListView): class ArticleList(ListView):
queryset = (Article.objects queryset = (
.select_related('category') Article.objects
.prefetch_related(Prefetch('inventories', .select_related('category')
queryset=Inventory.objects.order_by('-at'), .prefetch_related(
to_attr='inventory')) Prefetch(
.order_by('category', '-is_sold', 'name')) 'inventories',
queryset=Inventory.objects.order_by('-at'),
to_attr='inventory',
)
)
.order_by('category__name', '-is_sold', 'name')
)
template_name = 'kfet/article.html' template_name = 'kfet/article.html'
context_object_name = 'articles' context_object_name = 'articles'
@ -937,7 +961,6 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView):
return super(ArticleUpdate, self).form_valid(form) return super(ArticleUpdate, self).form_valid(form)
# ----- # -----
# K-Psul # K-Psul
# ----- # -----
@ -947,29 +970,24 @@ def kpsul(request):
data = {} data = {}
data['operationgroup_form'] = KPsulOperationGroupForm() data['operationgroup_form'] = KPsulOperationGroupForm()
data['trigramme_form'] = KPsulAccountForm() data['trigramme_form'] = KPsulAccountForm()
initial = {} data['checkout_form'] = KPsulCheckoutForm()
try: data['operation_formset'] = KPsulOperationFormSet(
checkout = Checkout.objects.filter( queryset=Operation.objects.none(),
is_protected=False, valid_from__lte=timezone.now(), )
valid_to__gte=timezone.now()).get()
initial['checkout'] = checkout
except (Checkout.DoesNotExist, Checkout.MultipleObjectsReturned):
pass
data['checkout_form'] = KPsulCheckoutForm(initial=initial)
operation_formset = KPsulOperationFormSet(queryset=Operation.objects.none())
data['operation_formset'] = operation_formset
return render(request, 'kfet/kpsul.html', data) return render(request, 'kfet/kpsul.html', data)
@teamkfet_required @teamkfet_required
def kpsul_get_settings(request): def kpsul_get_settings(request):
addcost_for = Settings.ADDCOST_FOR() addcost_for = kfet_config.addcost_for
data = { data = {
'subvention_cof': Settings.SUBVENTION_COF(), 'subvention_cof': kfet_config.subvention_cof,
'addcost_for' : addcost_for and addcost_for.trigramme or '', 'addcost_for': addcost_for and addcost_for.trigramme or '',
'addcost_amount': Settings.ADDCOST_AMOUNT(), 'addcost_amount': kfet_config.addcost_amount,
} }
return JsonResponse(data) return JsonResponse(data)
@teamkfet_required @teamkfet_required
def account_read_json(request): def account_read_json(request):
trigramme = request.POST.get('trigramme', '') trigramme = request.POST.get('trigramme', '')
@ -1009,6 +1027,7 @@ def kpsul_checkout_data(request):
raise Http404 raise Http404
return JsonResponse(data) return JsonResponse(data)
@teamkfet_required @teamkfet_required
def kpsul_update_addcost(request): def kpsul_update_addcost(request):
addcost_form = AddcostForm(request.POST) addcost_form = AddcostForm(request.POST)
@ -1028,15 +1047,15 @@ def kpsul_update_addcost(request):
trigramme = addcost_form.cleaned_data['trigramme'] trigramme = addcost_form.cleaned_data['trigramme']
account = trigramme and Account.objects.get(trigramme=trigramme) or None account = trigramme and Account.objects.get(trigramme=trigramme) or None
Settings.objects.filter(name='ADDCOST_FOR').update(value_account=account) amount = addcost_form.cleaned_data['amount']
(Settings.objects.filter(name='ADDCOST_AMOUNT')
.update(value_decimal=addcost_form.cleaned_data['amount'])) kfet_config.set(addcost_for=account,
cache.delete('ADDCOST_FOR') addcost_amount=amount)
cache.delete('ADDCOST_AMOUNT')
data = { data = {
'addcost': { 'addcost': {
'for': trigramme and account.trigramme or None, 'for': account and account.trigramme or None,
'amount': addcost_form.cleaned_data['amount'], 'amount': amount,
} }
} }
consumers.KPsul.group_send('kfet.kpsul', data) consumers.KPsul.group_send('kfet.kpsul', data)
@ -1080,10 +1099,10 @@ def kpsul_perform_operations(request):
operations = operation_formset.save(commit=False) operations = operation_formset.save(commit=False)
# Retrieving COF grant # Retrieving COF grant
cof_grant = Settings.SUBVENTION_COF() cof_grant = kfet_config.subvention_cof
# Retrieving addcosts data # Retrieving addcosts data
addcost_amount = Settings.ADDCOST_AMOUNT() addcost_amount = kfet_config.addcost_amount
addcost_for = Settings.ADDCOST_FOR() addcost_for = kfet_config.addcost_for
# Initializing vars # Initializing vars
required_perms = set() # Required perms to perform all operations required_perms = set() # Required perms to perform all operations
@ -1159,22 +1178,15 @@ def kpsul_perform_operations(request):
with transaction.atomic(): with transaction.atomic():
# If not cash account, # If not cash account,
# saving account's balance and adding to Negative if not in # saving account's balance and adding to Negative if not in
if not operationgroup.on_acc.is_cash: on_acc = operationgroup.on_acc
Account.objects.filter(pk=operationgroup.on_acc.pk).update( if not on_acc.is_cash:
balance=F('balance') + operationgroup.amount) (
operationgroup.on_acc.refresh_from_db() Account.objects
if operationgroup.on_acc.balance < 0: .filter(pk=on_acc.pk)
if hasattr(operationgroup.on_acc, 'negative'): .update(balance=F('balance') + operationgroup.amount)
if not operationgroup.on_acc.negative.start: )
operationgroup.on_acc.negative.start = timezone.now() on_acc.refresh_from_db()
operationgroup.on_acc.negative.save() on_acc.update_negative()
else:
negative = AccountNegative(
account=operationgroup.on_acc, start=timezone.now())
negative.save()
elif (hasattr(operationgroup.on_acc, 'negative') and
not operationgroup.on_acc.negative.balance_offset):
operationgroup.on_acc.negative.delete()
# Updating checkout's balance # Updating checkout's balance
if to_checkout_balance: if to_checkout_balance:
@ -1274,7 +1286,7 @@ def kpsul_cancel_operations(request):
opes = [] # Pas déjà annulée opes = [] # Pas déjà annulée
required_perms = set() required_perms = set()
stop_all = False stop_all = False
cancel_duration = Settings.CANCEL_DURATION() cancel_duration = kfet_config.cancel_duration
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes
to_groups_amounts = defaultdict(lambda:0) # ------ sur les montants des groupes d'opé to_groups_amounts = defaultdict(lambda:0) # ------ sur les montants des groupes d'opé
to_checkouts_balances = defaultdict(lambda:0) # ------ sur les balances de caisses to_checkouts_balances = defaultdict(lambda:0) # ------ sur les balances de caisses
@ -1365,8 +1377,15 @@ def kpsul_cancel_operations(request):
(Operation.objects.filter(pk__in=opes) (Operation.objects.filter(pk__in=opes)
.update(canceled_by=canceled_by, canceled_at=canceled_at)) .update(canceled_by=canceled_by, canceled_at=canceled_at))
for account in to_accounts_balances: for account in to_accounts_balances:
Account.objects.filter(pk=account.pk).update( (
balance = F('balance') + to_accounts_balances[account]) Account.objects
.filter(pk=account.pk)
.update(balance=F('balance') + to_accounts_balances[account])
)
if not account.is_cash:
# Should always be true, but we want to be sure
account.refresh_from_db()
account.update_negative()
for checkout in to_checkouts_balances: for checkout in to_checkouts_balances:
Checkout.objects.filter(pk=checkout.pk).update( Checkout.objects.filter(pk=checkout.pk).update(
balance = F('balance') + to_checkouts_balances[checkout]) balance = F('balance') + to_checkouts_balances[checkout])
@ -1502,34 +1521,28 @@ def kpsul_articles_data(request):
.filter(is_sold=True)) .filter(is_sold=True))
return JsonResponse({ 'articles': list(articles) }) return JsonResponse({ 'articles': list(articles) })
@teamkfet_required @teamkfet_required
def history(request): def history(request):
data = { data = {
'filter_form': FilterHistoryForm(), 'filter_form': FilterHistoryForm(),
'settings': {
'subvention_cof': Settings.SUBVENTION_COF(),
}
} }
return render(request, 'kfet/history.html', data) return render(request, 'kfet/history.html', data)
# ----- # -----
# Settings views # Settings views
# ----- # -----
class SettingsList(ListView):
model = Settings class SettingsList(TemplateView):
context_object_name = 'settings'
template_name = 'kfet/settings.html' template_name = 'kfet/settings.html'
def get_context_data(self, **kwargs):
Settings.create_missing()
return super(SettingsList, self).get_context_data(**kwargs)
class SettingsUpdate(SuccessMessageMixin, UpdateView): class SettingsUpdate(SuccessMessageMixin, FormView):
model = Settings form_class = KFetConfigForm
form_class = SettingsForm
template_name = 'kfet/settings_update.html' template_name = 'kfet/settings_update.html'
success_message = 'Paramètre %(name)s mis à jour' success_message = 'Paramètres mis à jour'
success_url = reverse_lazy('kfet.settings') success_url = reverse_lazy('kfet.settings')
def form_valid(self, form): def form_valid(self, form):
@ -1537,9 +1550,10 @@ class SettingsUpdate(SuccessMessageMixin, UpdateView):
if not self.request.user.has_perm('kfet.change_settings'): if not self.request.user.has_perm('kfet.change_settings'):
form.add_error(None, 'Permission refusée') form.add_error(None, 'Permission refusée')
return self.form_invalid(form) return self.form_invalid(form)
# Creating form.save()
Settings.empty_cache() return super().form_valid(form)
return super(SettingsUpdate, self).form_valid(form)
# ----- # -----
# Transfer views # Transfer views
@ -1547,13 +1561,25 @@ class SettingsUpdate(SuccessMessageMixin, UpdateView):
@teamkfet_required @teamkfet_required
def transfers(request): def transfers(request):
transfergroups = (TransferGroup.objects transfers_pre = Prefetch(
.prefetch_related('transfers') 'transfers',
.order_by('-at')) queryset=(
Transfer.objects
.select_related('from_acc', 'to_acc')
),
)
transfergroups = (
TransferGroup.objects
.select_related('valid_by')
.prefetch_related(transfers_pre)
.order_by('-at')
)
return render(request, 'kfet/transfers.html', { return render(request, 'kfet/transfers.html', {
'transfergroups': transfergroups, 'transfergroups': transfergroups,
}) })
@teamkfet_required @teamkfet_required
def transfers_create(request): def transfers_create(request):
transfer_formset = TransferFormSet(queryset=Transfer.objects.none()) transfer_formset = TransferFormSet(queryset=Transfer.objects.none())
@ -1665,7 +1691,7 @@ def cancel_transfers(request):
transfers = [] # Pas déjà annulée transfers = [] # Pas déjà annulée
required_perms = set() required_perms = set()
stop_all = False stop_all = False
cancel_duration = Settings.CANCEL_DURATION() cancel_duration = kfet_config.cancel_duration
to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes to_accounts_balances = defaultdict(lambda:0) # Modifs à faire sur les balances des comptes
for transfer in transfers_all: for transfer in transfers_all:
if transfer.canceled_at: if transfer.canceled_at:
@ -1697,9 +1723,6 @@ def cancel_transfers(request):
if stop: if stop:
negative_accounts.append(account.trigramme) negative_accounts.append(account.trigramme)
print(required_perms)
print(request.user.get_all_permissions())
if stop_all or not request.user.has_perms(required_perms): if stop_all or not request.user.has_perms(required_perms):
missing_perms = get_missing_perms(required_perms, request.user) missing_perms = get_missing_perms(required_perms, request.user)
if missing_perms: if missing_perms:
@ -1837,68 +1860,54 @@ class OrderList(ListView):
context['suppliers'] = Supplier.objects.order_by('name') context['suppliers'] = Supplier.objects.order_by('name')
return context return context
@teamkfet_required @teamkfet_required
def order_create(request, pk): def order_create(request, pk):
supplier = get_object_or_404(Supplier, pk=pk) supplier = get_object_or_404(Supplier, pk=pk)
articles = (Article.objects articles = (
.filter(suppliers=supplier.pk) Article.objects
.distinct() .filter(suppliers=supplier.pk)
.select_related('category') .distinct()
.order_by('category__name', 'name')) .select_related('category')
.order_by('category__name', 'name')
)
initial = [] # Force hit to cache
today = timezone.now().date() articles = list(articles)
sales_q = (Operation.objects
sales_q = (
Operation.objects
.select_related('group') .select_related('group')
.filter(article__in=articles, canceled_at=None) .filter(article__in=articles, canceled_at=None)
.values('article')) .values('article')
sales_s1 = (sales_q
.filter(
group__at__gte = today-timedelta(weeks=5),
group__at__lt = today-timedelta(weeks=4))
.annotate(nb=Sum('article_nb')) .annotate(nb=Sum('article_nb'))
) )
sales_s1 = { d['article']:d['nb'] for d in sales_s1 } scale = WeekScale(last=True, n_steps=5, std_chunk=False)
sales_s2 = (sales_q chunks = scale.chunkify_qs(sales_q, field='group__at')
.filter(
group__at__gte = today-timedelta(weeks=4), sales = [
group__at__lt = today-timedelta(weeks=3)) {d['article']: d['nb'] for d in chunk}
.annotate(nb=Sum('article_nb')) for chunk in chunks
) ]
sales_s2 = { d['article']:d['nb'] for d in sales_s2 }
sales_s3 = (sales_q initial = []
.filter(
group__at__gte = today-timedelta(weeks=3),
group__at__lt = today-timedelta(weeks=2))
.annotate(nb=Sum('article_nb'))
)
sales_s3 = { d['article']:d['nb'] for d in sales_s3 }
sales_s4 = (sales_q
.filter(
group__at__gte = today-timedelta(weeks=2),
group__at__lt = today-timedelta(weeks=1))
.annotate(nb=Sum('article_nb'))
)
sales_s4 = { d['article']:d['nb'] for d in sales_s4 }
sales_s5 = (sales_q
.filter(group__at__gte = today-timedelta(weeks=1))
.annotate(nb=Sum('article_nb'))
)
sales_s5 = { d['article']:d['nb'] for d in sales_s5 }
for article in articles: for article in articles:
v_s1 = sales_s1.get(article.pk, 0) # Get sales for each 5 last weeks
v_s2 = sales_s2.get(article.pk, 0) v_all = [chunk.get(article.pk, 0) for chunk in sales]
v_s3 = sales_s3.get(article.pk, 0) # Take the 3 greatest (eg to avoid 2 weeks of vacations)
v_s4 = sales_s4.get(article.pk, 0)
v_s5 = sales_s5.get(article.pk, 0)
v_all = [v_s1, v_s2, v_s3, v_s4, v_s5]
v_3max = heapq.nlargest(3, v_all) v_3max = heapq.nlargest(3, v_all)
# Get average and standard deviation
v_moy = statistics.mean(v_3max) v_moy = statistics.mean(v_3max)
v_et = statistics.pstdev(v_3max, v_moy) v_et = statistics.pstdev(v_3max, v_moy)
# Expected sales for next week
v_prev = v_moy + v_et v_prev = v_moy + v_et
# We want to have 1.5 * the expected sales in stock
# (because sometimes some articles are not delivered)
c_rec_tot = max(v_prev * 1.5 - article.stock, 0) c_rec_tot = max(v_prev * 1.5 - article.stock, 0)
# If ordered quantity is close enough to a level which can led to free
# boxes, we increase it to this level.
if article.box_capacity: if article.box_capacity:
c_rec_temp = c_rec_tot / article.box_capacity c_rec_temp = c_rec_tot / article.box_capacity
if c_rec_temp >= 10: if c_rec_temp >= 10:
@ -1916,11 +1925,7 @@ def order_create(request, pk):
'category__name': article.category.name, 'category__name': article.category.name,
'stock': article.stock, 'stock': article.stock,
'box_capacity': article.box_capacity, 'box_capacity': article.box_capacity,
'v_s1': v_s1, 'v_all': v_all,
'v_s2': v_s2,
'v_s3': v_s3,
'v_s4': v_s4,
'v_s5': v_s5,
'v_moy': round(v_moy), 'v_moy': round(v_moy),
'v_et': round(v_et), 'v_et': round(v_et),
'v_prev': round(v_prev), 'v_prev': round(v_prev),
@ -1928,8 +1933,9 @@ def order_create(request, pk):
}) })
cls_formset = formset_factory( cls_formset = formset_factory(
form = OrderArticleForm, form=OrderArticleForm,
extra = 0) extra=0,
)
if request.POST: if request.POST:
formset = cls_formset(request.POST, initial=initial) formset = cls_formset(request.POST, initial=initial)
@ -1946,14 +1952,15 @@ def order_create(request, pk):
order.save() order.save()
saved = True saved = True
article = articles.get(pk=form.cleaned_data['article'].pk) article = form.cleaned_data['article']
q_ordered = form.cleaned_data['quantity_ordered'] q_ordered = form.cleaned_data['quantity_ordered']
if article.box_capacity: if article.box_capacity:
q_ordered *= article.box_capacity q_ordered *= article.box_capacity
OrderArticle.objects.create( OrderArticle.objects.create(
order = order, order=order,
article = article, article=article,
quantity_ordered = q_ordered) quantity_ordered=q_ordered,
)
if saved: if saved:
messages.success(request, 'Commande créée') messages.success(request, 'Commande créée')
return redirect('kfet.order.read', order.pk) return redirect('kfet.order.read', order.pk)
@ -1963,11 +1970,15 @@ def order_create(request, pk):
else: else:
formset = cls_formset(initial=initial) formset = cls_formset(initial=initial)
scale.label_fmt = "S -{rev_i}"
return render(request, 'kfet/order_create.html', { return render(request, 'kfet/order_create.html', {
'supplier': supplier, 'supplier': supplier,
'formset' : formset, 'formset': formset,
'scale': scale,
}) })
class OrderRead(DetailView): class OrderRead(DetailView):
model = Order model = Order
template_name = 'kfet/order_read.html' template_name = 'kfet/order_read.html'
@ -2006,6 +2017,7 @@ class OrderRead(DetailView):
context['mail'] = mail context['mail'] = mail
return context return context
@teamkfet_required @teamkfet_required
def order_to_inventory(request, pk): def order_to_inventory(request, pk):
order = get_object_or_404(Order, pk=pk) order = get_object_or_404(Order, pk=pk)
@ -2013,28 +2025,36 @@ def order_to_inventory(request, pk):
if hasattr(order, 'inventory'): if hasattr(order, 'inventory'):
raise Http404 raise Http404
articles = (Article.objects supplier_prefetch = Prefetch(
.filter(orders=order.pk) 'article__supplierarticle_set',
.select_related('category') queryset=(
.prefetch_related(Prefetch('orderarticle_set', SupplierArticle.objects
queryset = OrderArticle.objects.filter(order=order), .filter(supplier=order.supplier)
to_attr = 'order')) .order_by('-at')
.prefetch_related(Prefetch('supplierarticle_set', ),
queryset = (SupplierArticle.objects to_attr='supplier',
.filter(supplier=order.supplier) )
.order_by('-at')),
to_attr = 'supplier')) order_articles = (
.order_by('category__name', 'name')) OrderArticle.objects
.filter(order=order.pk)
.select_related('article', 'article__category')
.prefetch_related(
supplier_prefetch,
)
.order_by('article__category__name', 'article__name')
)
initial = [] initial = []
for article in articles: for order_article in order_articles:
article = order_article.article
initial.append({ initial.append({
'article': article.pk, 'article': article.pk,
'name': article.name, 'name': article.name,
'category': article.category_id, 'category': article.category_id,
'category__name': article.category.name, 'category__name': article.category.name,
'quantity_ordered': article.order[0].quantity_ordered, 'quantity_ordered': order_article.quantity_ordered,
'quantity_received': article.order[0].quantity_ordered, 'quantity_received': order_article.quantity_ordered,
'price_HT': article.supplier[0].price_HT, 'price_HT': article.supplier[0].price_HT,
'TVA': article.supplier[0].TVA, 'TVA': article.supplier[0].TVA,
'rights': article.supplier[0].rights, 'rights': article.supplier[0].rights,
@ -2049,31 +2069,50 @@ def order_to_inventory(request, pk):
messages.error(request, 'Permission refusée') messages.error(request, 'Permission refusée')
elif formset.is_valid(): elif formset.is_valid():
with transaction.atomic(): with transaction.atomic():
inventory = Inventory() inventory = Inventory.objects.create(
inventory.order = order order=order, by=request.user.profile.account_kfet,
inventory.by = request.user.profile.account_kfet )
inventory.save() new_supplierarticle = []
new_inventoryarticle = []
for form in formset: for form in formset:
q_received = form.cleaned_data['quantity_received'] q_received = form.cleaned_data['quantity_received']
article = form.cleaned_data['article'] article = form.cleaned_data['article']
SupplierArticle.objects.create(
supplier = order.supplier, price_HT = form.cleaned_data['price_HT']
article = article, TVA = form.cleaned_data['TVA']
price_HT = form.cleaned_data['price_HT'], rights = form.cleaned_data['rights']
TVA = form.cleaned_data['TVA'],
rights = form.cleaned_data['rights']) if any((form.initial['price_HT'] != price_HT,
(OrderArticle.objects form.initial['TVA'] != TVA,
.filter(order=order, article=article) form.initial['rights'] != rights)):
.update(quantity_received = q_received)) new_supplierarticle.append(
InventoryArticle.objects.create( SupplierArticle(
inventory = inventory, supplier=order.supplier,
article = article, article=article,
stock_old = article.stock, price_HT=price_HT,
stock_new = article.stock + q_received) TVA=TVA,
rights=rights,
)
)
(
OrderArticle.objects
.filter(order=order, article=article)
.update(quantity_received=q_received)
)
new_inventoryarticle.append(
InventoryArticle(
inventory=inventory,
article=article,
stock_old=article.stock,
stock_new=article.stock + q_received,
)
)
article.stock += q_received article.stock += q_received
if q_received > 0: if q_received > 0:
article.is_sold = True article.is_sold = True
article.save() article.save()
SupplierArticle.objects.bulk_create(new_supplierarticle)
InventoryArticle.objects.bulk_create(new_inventoryarticle)
messages.success(request, "C'est tout bon !") messages.success(request, "C'est tout bon !")
return redirect('kfet.order') return redirect('kfet.order')
else: else:
@ -2083,6 +2122,7 @@ def order_to_inventory(request, pk):
return render(request, 'kfet/order_to_inventory.html', { return render(request, 'kfet/order_to_inventory.html', {
'formset': formset, 'formset': formset,
'order': order,
}) })
class SupplierUpdate(SuccessMessageMixin, UpdateView): class SupplierUpdate(SuccessMessageMixin, UpdateView):
@ -2258,10 +2298,13 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView):
# prepare querysets # prepare querysets
# TODO: retirer les opgroup dont tous les op sont annulées # TODO: retirer les opgroup dont tous les op sont annulées
opegroups = OperationGroup.objects.filter(on_acc=account) opegroups = OperationGroup.objects.filter(on_acc=account)
recv_transfers = Transfer.objects.filter(to_acc=account, transfers = (
canceled_at=None) Transfer.objects
sent_transfers = Transfer.objects.filter(from_acc=account, .filter(canceled_at=None)
canceled_at=None) .select_related('group')
)
recv_transfers = transfers.filter(to_acc=account)
sent_transfers = transfers.filter(from_acc=account)
# apply filters # apply filters
if begin_date is not None: if begin_date is not None:
@ -2289,13 +2332,11 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView):
actions.append({ actions.append({
'at': (begin_date or account.created_at).isoformat(), 'at': (begin_date or account.created_at).isoformat(),
'amount': 0, 'amount': 0,
'label': 'début',
'balance': 0, 'balance': 0,
}) })
actions.append({ actions.append({
'at': (end_date or timezone.now()).isoformat(), 'at': (end_date or timezone.now()).isoformat(),
'amount': 0, 'amount': 0,
'label': 'fin',
'balance': 0, 'balance': 0,
}) })
@ -2303,21 +2344,18 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView):
{ {
'at': ope_grp.at.isoformat(), 'at': ope_grp.at.isoformat(),
'amount': ope_grp.amount, 'amount': ope_grp.amount,
'label': str(ope_grp),
'balance': 0, 'balance': 0,
} for ope_grp in opegroups } for ope_grp in opegroups
] + [ ] + [
{ {
'at': tr.group.at.isoformat(), 'at': tr.group.at.isoformat(),
'amount': tr.amount, 'amount': tr.amount,
'label': str(tr),
'balance': 0, 'balance': 0,
} for tr in recv_transfers } for tr in recv_transfers
] + [ ] + [
{ {
'at': tr.group.at.isoformat(), 'at': tr.group.at.isoformat(),
'amount': -tr.amount, 'amount': -tr.amount,
'label': str(tr),
'balance': 0, 'balance': 0,
} for tr in sent_transfers } for tr in sent_transfers
] ]
@ -2410,13 +2448,19 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView):
# à l'article en question et qui ne sont pas annulées # à l'article en question et qui ne sont pas annulées
# puis on choisi pour chaques intervalle les opérations # puis on choisi pour chaques intervalle les opérations
# effectuées dans ces intervalles de temps # effectuées dans ces intervalles de temps
all_operations = (Operation.objects all_operations = (
.filter(group__on_acc=self.object) Operation.objects
.filter(canceled_at=None) .filter(group__on_acc=self.object,
) canceled_at=None)
.values('article_nb', 'group__at')
.order_by('group__at')
)
if types is not None: if types is not None:
all_operations = all_operations.filter(type__in=types) all_operations = all_operations.filter(type__in=types)
chunks = self.chunkify_qs(all_operations, scale, field='group__at') chunks = scale.get_by_chunks(
all_operations, field_db='group__at',
field_callback=(lambda d: d['group__at']),
)
return chunks return chunks
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
@ -2432,7 +2476,8 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView):
# On compte les opérations # On compte les opérations
nb_ventes = [] nb_ventes = []
for chunk in operations: for chunk in operations:
nb_ventes.append(tot_ventes(chunk)) ventes = sum(ope['article_nb'] for ope in chunk)
nb_ventes.append(ventes)
context['charts'] = [{"color": "rgb(255, 99, 132)", context['charts'] = [{"color": "rgb(255, 99, 132)",
"label": "NB items achetés", "label": "NB items achetés",
@ -2483,29 +2528,39 @@ class ArticleStatSales(ScaleMixin, JSONDetailView):
context = {'labels': old_ctx['labels']} context = {'labels': old_ctx['labels']}
scale = self.scale scale = self.scale
# On selectionne les opérations qui correspondent all_purchases = (
# à l'article en question et qui ne sont pas annulées
# puis on choisi pour chaques intervalle les opérations
# effectuées dans ces intervalles de temps
all_operations = (
Operation.objects Operation.objects
.filter(type=Operation.PURCHASE, .filter(
article=self.object, type=Operation.PURCHASE,
canceled_at=None, article=self.object,
) canceled_at=None,
)
.values('group__at', 'article_nb')
.order_by('group__at')
) )
chunks = self.chunkify_qs(all_operations, scale, field='group__at') liq_only = all_purchases.filter(group__on_acc__trigramme='LIQ')
liq_exclude = all_purchases.exclude(group__on_acc__trigramme='LIQ')
chunks_liq = scale.get_by_chunks(
liq_only, field_db='group__at',
field_callback=lambda d: d['group__at'],
)
chunks_no_liq = scale.get_by_chunks(
liq_exclude, field_db='group__at',
field_callback=lambda d: d['group__at'],
)
# On compte les opérations # On compte les opérations
nb_ventes = [] nb_ventes = []
nb_accounts = [] nb_accounts = []
nb_liq = [] nb_liq = []
for qs in chunks: for chunk_liq, chunk_no_liq in zip(chunks_liq, chunks_no_liq):
nb_ventes.append( sum_accounts = sum(ope['article_nb'] for ope in chunk_no_liq)
tot_ventes(qs)) sum_liq = sum(ope['article_nb'] for ope in chunk_liq)
nb_liq.append( nb_ventes.append(sum_accounts + sum_liq)
tot_ventes(qs.filter(group__on_acc__trigramme='LIQ'))) nb_accounts.append(sum_accounts)
nb_accounts.append( nb_liq.append(sum_liq)
tot_ventes(qs.exclude(group__on_acc__trigramme='LIQ')))
context['charts'] = [{"color": "rgb(255, 99, 132)", context['charts'] = [{"color": "rgb(255, 99, 132)",
"label": "Toutes consommations", "label": "Toutes consommations",
"values": nb_ventes}, "values": nb_ventes},

View file

@ -8,9 +8,9 @@ DBNAME="cof_gestion"
DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
# Installation de paquets utiles # Installation de paquets utiles
apt-get update && apt-get install -y python3-pip python3-dev python3-venv \ apt-get update && apt-get upgrade -y
libmysqlclient-dev libjpeg-dev git redis-server apt-get install -y python3-pip python3-dev python3-venv libmysqlclient-dev \
pip install -U pip libjpeg-dev git redis-server
# Configuration et installation de mysql. Le mot de passe root est le même que # Configuration et installation de mysql. Le mot de passe root est le même que
# le mot de passe pour l'utilisateur local - pour rappel, ceci est une instance # le mot de passe pour l'utilisateur local - pour rappel, ceci est une instance
@ -23,6 +23,11 @@ apt-get install -y mysql-server
mysql -uroot -p$DBPASSWD -e "CREATE DATABASE $DBNAME; GRANT ALL PRIVILEGES ON $DBNAME.* TO '$DBUSER'@'localhost' IDENTIFIED BY '$DBPASSWD'" mysql -uroot -p$DBPASSWD -e "CREATE DATABASE $DBNAME; GRANT ALL PRIVILEGES ON $DBNAME.* TO '$DBUSER'@'localhost' IDENTIFIED BY '$DBPASSWD'"
mysql -uroot -p$DBPASSWD -e "GRANT ALL PRIVILEGES ON test_$DBNAME.* TO '$DBUSER'@'localhost'" mysql -uroot -p$DBPASSWD -e "GRANT ALL PRIVILEGES ON test_$DBNAME.* TO '$DBUSER'@'localhost'"
# Configuration de redis
REDIS_PASSWD="dummy"
redis-cli CONFIG SET requirepass $REDIS_PASSWD
redis-cli -a $REDIS_PASSWD CONFIG REWRITE
# Installation et configuration d'Apache # Installation et configuration d'Apache
apt-get install -y apache2 apt-get install -y apache2
a2enmod proxy proxy_http proxy_wstunnel headers a2enmod proxy proxy_http proxy_wstunnel headers
@ -36,7 +41,7 @@ chown -R ubuntu:www-data /var/www/static
# Mise en place du .bash_profile pour tout configurer lors du `vagrant ssh` # Mise en place du .bash_profile pour tout configurer lors du `vagrant ssh`
cat >> ~ubuntu/.bashrc <<EOF cat >> ~ubuntu/.bashrc <<EOF
# On utilise la version de développement de GestioCOF # On utilise la version de développement de GestioCOF
export DJANGO_SETTINGS_MODULE='cof.settings_dev' export DJANGO_SETTINGS_MODULE='cof.settings.dev'
# Identifiants MySQL # Identifiants MySQL
export DBUSER="$DBUSER" export DBUSER="$DBUSER"
@ -62,7 +67,8 @@ sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -U pip
sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -r requirements.txt -r requirements-devel.txt sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -r requirements.txt -r requirements-devel.txt
# Préparation de Django # Préparation de Django
sudo -H -u ubuntu DJANGO_SETTINGS_MODULE='cof.settings_dev' DBUSER=$DBUSER DBNAME=$DBNAME DBPASSWD=$DBPASSWD bash provisioning/prepare_django.sh if [ ! -f cof/settings/secret.py ]; then sudo -H -u ubuntu ln -s secret_example.py cof/settings/secret.py; fi
sudo -H -u ubuntu DJANGO_SETTINGS_MODULE='cof.settings.dev' DBUSER=$DBUSER DBNAME=$DBNAME DBPASSWD=$DBPASSWD bash provisioning/prepare_django.sh
# Installation du cron pour les mails de rappels # Installation du cron pour les mails de rappels
sudo -H -u ubuntu crontab provisioning/cron.dev sudo -H -u ubuntu crontab provisioning/cron.dev

View file

@ -1,5 +1,5 @@
# On utilise la version de développement de GestioCOF # On utilise la version de développement de GestioCOF
DJANGO_SETTINGS_MODULE='cof.settings_dev' DJANGO_SETTINGS_MODULE='cof.settings.dev'
# Identifiants MySQL # Identifiants MySQL
DBUSER="cof_gestion" DBUSER="cof_gestion"

View file

@ -2,7 +2,7 @@
command=/home/ubuntu/venv/bin/python /vagrant/manage.py runworker command=/home/ubuntu/venv/bin/python /vagrant/manage.py runworker
directory=/vagrant/ directory=/vagrant/
user=ubuntu user=ubuntu
environment=DBUSER={DBUSER},DBNAME={DBNAME},DBPASSWD={DBPASSWD},DJANGO_SETTINGS_MODULE="cof.settings_dev" environment=DBUSER={DBUSER},DBNAME={DBNAME},DBPASSWD={DBPASSWD},DJANGO_SETTINGS_MODULE="cof.settings.dev"
autostart=true autostart=true
autorestart=true autorestart=true
redirect_stderr=true redirect_stderr=true
@ -11,7 +11,7 @@ redirect_stderr=true
[program:interface] [program:interface]
command=/home/ubuntu/venv/bin/daphne -b 127.0.0.1 -p 8001 cof.asgi:channel_layer command=/home/ubuntu/venv/bin/daphne -b 127.0.0.1 -p 8001 cof.asgi:channel_layer
environment=DBUSER={DBUSER},DBNAME={DBNAME},DBPASSWD={DBPASSWD},DJANGO_SETTINGS_MODULE="cof.settings_dev" environment=DBUSER={DBUSER},DBNAME={DBNAME},DBPASSWD={DBPASSWD},DJANGO_SETTINGS_MODULE="cof.settings.dev"
directory=/vagrant/ directory=/vagrant/
redirect_stderr=true redirect_stderr=true
autostart=true autostart=true

View file

@ -1,3 +1,4 @@
-r requirements.txt -r requirements.txt
django-debug-toolbar django-debug-toolbar
django-debug-panel
ipython ipython

View file

@ -3,6 +3,7 @@ Django==1.8.*
django-autocomplete-light==2.3.3 django-autocomplete-light==2.3.3
django-autoslug==1.9.3 django-autoslug==1.9.3
django-cas-ng==3.5.5 django-cas-ng==3.5.5
django-djconfig==0.5.3
django-grappelli==2.8.1 django-grappelli==2.8.1
django-recaptcha==1.0.5 django-recaptcha==1.0.5
mysqlclient==1.3.7 mysqlclient==1.3.7
@ -11,13 +12,13 @@ six==1.10.0
unicodecsv==0.14.1 unicodecsv==0.14.1
icalendar==3.10 icalendar==3.10
django-bootstrap-form==3.2.1 django-bootstrap-form==3.2.1
asgiref==0.14.0 asgiref==1.1.1
daphne==0.14.3 daphne==1.2.0
asgi-redis==0.14.0 asgi-redis==1.3.0
statistics==1.0.3.5 statistics==1.0.3.5
future==0.15.2 future==0.15.2
django-widget-tweaks==1.4.1 django-widget-tweaks==1.4.1
git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail
ldap3 ldap3
git+https://github.com/Aureplop/channels.git#egg=channels channels==1.1.3
python-dateutil python-dateutil