Merge branch 'master' into aureplop/kpsul_js_refactor
This commit is contained in:
commit
bacc079778
27 changed files with 974 additions and 469 deletions
104
bda/admin.py
104
bda/admin.py
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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:
|
||||||
|
|
78
bda/forms.py
78
bda/forms.py
|
@ -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):
|
||||||
|
@ -96,4 +112,6 @@ class SoldForm(forms.Form):
|
||||||
.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')
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
231
bda/views.py
231
bda/views.py
|
@ -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
1
cof/settings/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
secret.py
|
|
@ -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,7 +50,6 @@ INSTALLED_APPS = (
|
||||||
'autocomplete_light',
|
'autocomplete_light',
|
||||||
'captcha',
|
'captcha',
|
||||||
'django_cas_ng',
|
'django_cas_ng',
|
||||||
'debug_toolbar',
|
|
||||||
'bootstrapform',
|
'bootstrapform',
|
||||||
'kfet',
|
'kfet',
|
||||||
'channels',
|
'channels',
|
||||||
|
@ -49,10 +57,9 @@ INSTALLED_APPS = (
|
||||||
'django_js_reverse',
|
'django_js_reverse',
|
||||||
'custommail',
|
'custommail',
|
||||||
'djconfig',
|
'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',
|
||||||
|
@ -63,7 +70,7 @@ MIDDLEWARE_CLASSES = (
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'djconfig.middleware.DjConfigMiddleware',
|
'djconfig.middleware.DjConfigMiddleware',
|
||||||
)
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'cof.urls'
|
ROOT_URLCONF = 'cof.urls'
|
||||||
|
|
||||||
|
@ -90,17 +97,12 @@ TEMPLATES = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# 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'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,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
|
||||||
|
|
||||||
|
@ -164,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
|
||||||
|
@ -178,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
47
cof/settings/dev.py
Normal 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
26
cof/settings/prod.py
Normal 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"
|
8
cof/settings/secret_example.py
Normal file
8
cof/settings/secret_example.py
Normal 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
|
|
@ -24,16 +24,22 @@ 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"
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -134,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'),
|
||||||
|
@ -141,16 +142,15 @@ 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 GroupForm(forms.ModelForm):
|
class GroupForm(forms.ModelForm):
|
||||||
permissions = forms.ModelMultipleChoiceField(
|
permissions = forms.ModelMultipleChoiceField(
|
||||||
queryset= Permission.objects.filter(content_type__in=
|
queryset= Permission.objects.filter(content_type__in=
|
||||||
|
@ -328,12 +328,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(
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -23,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")
|
||||||
|
@ -237,27 +248,43 @@ class Account(models.Model):
|
||||||
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)
|
||||||
|
|
||||||
@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,
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
|
@ -81,6 +88,99 @@ class Scale(object):
|
||||||
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) for begin, end in 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):
|
||||||
name = 'day'
|
name = 'day'
|
||||||
|
@ -222,13 +322,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
|
|
||||||
]
|
|
||||||
|
|
|
@ -104,7 +104,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>
|
||||||
|
|
|
@ -161,6 +161,8 @@ $(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 = '';
|
||||||
content += "Conflits possibles :"
|
content += "Conflits possibles :"
|
||||||
|
|
56
kfet/tests/test_forms.py
Normal file
56
kfet/tests/test_forms.py
Normal 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,
|
||||||
|
)
|
335
kfet/views.py
335
kfet/views.py
|
@ -50,7 +50,7 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,11 +83,21 @@ 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)
|
||||||
|
@ -379,11 +389,7 @@ def account_create_ajax(request, username=None, login_clipper=None,
|
||||||
|
|
||||||
@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') \
|
||||||
|
@ -399,7 +405,8 @@ def account_read(request, trigramme):
|
||||||
|
|
||||||
addcosts = (
|
addcosts = (
|
||||||
OperationGroup.objects
|
OperationGroup.objects
|
||||||
.filter(opes__addcost_for=account, opes__canceled_at=None)
|
.filter(opes__addcost_for=account,
|
||||||
|
opes__canceled_at=None)
|
||||||
.extra({'date': "date(at)"})
|
.extra({'date': "date(at)"})
|
||||||
.values('date')
|
.values('date')
|
||||||
.annotate(sum_addcosts=Sum('opes__addcost_amount'))
|
.annotate(sum_addcosts=Sum('opes__addcost_amount'))
|
||||||
|
@ -556,13 +563,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
|
||||||
|
@ -578,23 +594,20 @@ class AccountGroupUpdate(UpdateView):
|
||||||
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)
|
||||||
negs_sum = (AccountNegative.objects
|
real_balances = (neg.account.real_balance for neg in self.object_list)
|
||||||
.exclude(account__trigramme='#13')
|
context['negatives_sum'] = sum(real_balances)
|
||||||
.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
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
|
@ -817,12 +830,18 @@ class CategoryUpdate(SuccessMessageMixin, UpdateView):
|
||||||
|
|
||||||
# Article - General
|
# Article - General
|
||||||
class ArticleList(ListView):
|
class ArticleList(ListView):
|
||||||
queryset = (Article.objects
|
queryset = (
|
||||||
|
Article.objects
|
||||||
.select_related('category')
|
.select_related('category')
|
||||||
.prefetch_related(Prefetch('inventories',
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
'inventories',
|
||||||
queryset=Inventory.objects.order_by('-at'),
|
queryset=Inventory.objects.order_by('-at'),
|
||||||
to_attr='inventory'))
|
to_attr='inventory',
|
||||||
.order_by('category', '-is_sold', 'name'))
|
)
|
||||||
|
)
|
||||||
|
.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'
|
||||||
|
|
||||||
|
@ -934,7 +953,6 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView):
|
||||||
return super(ArticleUpdate, self).form_valid(form)
|
return super(ArticleUpdate, self).form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# K-Psul
|
# K-Psul
|
||||||
# -----
|
# -----
|
||||||
|
@ -944,17 +962,10 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1686,6 +1697,7 @@ class SettingsUpdate(SuccessMessageMixin, FormView):
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# Transfer views
|
# Transfer views
|
||||||
# -----
|
# -----
|
||||||
|
@ -1695,6 +1707,7 @@ class SettingsUpdate(SuccessMessageMixin, FormView):
|
||||||
def transfers(request):
|
def transfers(request):
|
||||||
return render(request, 'kfet/transfers.html')
|
return render(request, 'kfet/transfers.html')
|
||||||
|
|
||||||
|
|
||||||
@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())
|
||||||
|
@ -1907,68 +1920,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 = (
|
||||||
|
Article.objects
|
||||||
.filter(suppliers=supplier.pk)
|
.filter(suppliers=supplier.pk)
|
||||||
.distinct()
|
.distinct()
|
||||||
.select_related('category')
|
.select_related('category')
|
||||||
.order_by('category__name', 'name'))
|
.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:
|
||||||
|
@ -1986,11 +1985,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),
|
||||||
|
@ -1999,7 +1994,8 @@ 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)
|
||||||
|
@ -2016,14 +2012,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)
|
||||||
|
@ -2038,6 +2035,7 @@ def order_create(request, pk):
|
||||||
'formset': formset,
|
'formset': formset,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class OrderRead(DetailView):
|
class OrderRead(DetailView):
|
||||||
model = Order
|
model = Order
|
||||||
template_name = 'kfet/order_read.html'
|
template_name = 'kfet/order_read.html'
|
||||||
|
@ -2076,6 +2074,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)
|
||||||
|
@ -2083,28 +2082,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),
|
|
||||||
to_attr = 'order'))
|
|
||||||
.prefetch_related(Prefetch('supplierarticle_set',
|
|
||||||
queryset = (SupplierArticle.objects
|
|
||||||
.filter(supplier=order.supplier)
|
.filter(supplier=order.supplier)
|
||||||
.order_by('-at')),
|
.order_by('-at')
|
||||||
to_attr = 'supplier'))
|
),
|
||||||
.order_by('category__name', 'name'))
|
to_attr='supplier',
|
||||||
|
)
|
||||||
|
|
||||||
|
order_articles = (
|
||||||
|
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,
|
||||||
|
@ -2119,31 +2126,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(
|
|
||||||
|
price_HT = form.cleaned_data['price_HT']
|
||||||
|
TVA = form.cleaned_data['TVA']
|
||||||
|
rights = form.cleaned_data['rights']
|
||||||
|
|
||||||
|
if any((form.initial['price_HT'] != price_HT,
|
||||||
|
form.initial['TVA'] != TVA,
|
||||||
|
form.initial['rights'] != rights)):
|
||||||
|
new_supplierarticle.append(
|
||||||
|
SupplierArticle(
|
||||||
supplier=order.supplier,
|
supplier=order.supplier,
|
||||||
article=article,
|
article=article,
|
||||||
price_HT = form.cleaned_data['price_HT'],
|
price_HT=price_HT,
|
||||||
TVA = form.cleaned_data['TVA'],
|
TVA=TVA,
|
||||||
rights = form.cleaned_data['rights'])
|
rights=rights,
|
||||||
(OrderArticle.objects
|
)
|
||||||
|
)
|
||||||
|
(
|
||||||
|
OrderArticle.objects
|
||||||
.filter(order=order, article=article)
|
.filter(order=order, article=article)
|
||||||
.update(quantity_received = q_received))
|
.update(quantity_received=q_received)
|
||||||
InventoryArticle.objects.create(
|
)
|
||||||
|
new_inventoryarticle.append(
|
||||||
|
InventoryArticle(
|
||||||
inventory=inventory,
|
inventory=inventory,
|
||||||
article=article,
|
article=article,
|
||||||
stock_old=article.stock,
|
stock_old=article.stock,
|
||||||
stock_new = article.stock + q_received)
|
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:
|
||||||
|
@ -2305,10 +2331,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:
|
||||||
|
@ -2336,13 +2365,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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -2350,21 +2377,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
|
||||||
]
|
]
|
||||||
|
@ -2457,13 +2481,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):
|
||||||
|
@ -2479,7 +2509,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",
|
||||||
|
@ -2530,29 +2561,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(
|
||||||
|
type=Operation.PURCHASE,
|
||||||
article=self.object,
|
article=self.object,
|
||||||
canceled_at=None,
|
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},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
django-debug-toolbar
|
django-debug-toolbar
|
||||||
|
django-debug-panel
|
||||||
ipython
|
ipython
|
||||||
|
|
Loading…
Reference in a new issue