Merge branch 'master' into aureplop/kfet_config
This commit is contained in:
commit
784ba9bd10
|
@ -18,7 +18,10 @@ class Tirage(models.Model):
|
||||||
fermeture = models.DateTimeField("Date et heure de fermerture du tirage")
|
fermeture = models.DateTimeField("Date et heure de fermerture du tirage")
|
||||||
tokens = models.TextField("Graine(s) du tirage", blank=True)
|
tokens = models.TextField("Graine(s) du tirage", blank=True)
|
||||||
active = models.BooleanField("Tirage actif", default=False)
|
active = models.BooleanField("Tirage actif", default=False)
|
||||||
appear_catalogue = models.BooleanField("Tirage à afficher dans le catalogue", default=False)
|
appear_catalogue = models.BooleanField(
|
||||||
|
"Tirage à afficher dans le catalogue",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
enable_do_tirage = models.BooleanField("Le tirage peut être lancé",
|
enable_do_tirage = models.BooleanField("Le tirage peut être lancé",
|
||||||
default=False)
|
default=False)
|
||||||
|
|
||||||
|
|
89
bda/tests.py
89
bda/tests.py
|
@ -1,22 +1,79 @@
|
||||||
# -*- coding: utf-8 -*-
|
import json
|
||||||
"""
|
|
||||||
This file demonstrates writing tests using the unittest module. These will pass
|
|
||||||
when you run "manage.py test".
|
|
||||||
|
|
||||||
Replace this with more appropriate tests for your application.
|
from django.test import TestCase, Client
|
||||||
"""
|
from django.utils import timezone
|
||||||
|
|
||||||
from __future__ import division
|
from .models import Tirage, Spectacle, Salle, CategorieSpectacle
|
||||||
from __future__ import print_function
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
|
||||||
from django.test import TestCase
|
class TestBdAViews(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tirage = Tirage.objects.create(
|
||||||
|
title="Test tirage",
|
||||||
|
appear_catalogue=True,
|
||||||
|
ouverture=timezone.now(),
|
||||||
|
fermeture=timezone.now(),
|
||||||
|
)
|
||||||
|
self.category = CategorieSpectacle.objects.create(name="Category")
|
||||||
|
self.location = Salle.objects.create(name="here")
|
||||||
|
Spectacle.objects.bulk_create([
|
||||||
|
Spectacle(
|
||||||
|
title="foo", date=timezone.now(), location=self.location,
|
||||||
|
price=0, slots=42, tirage=self.tirage, listing=False,
|
||||||
|
category=self.category
|
||||||
|
),
|
||||||
|
Spectacle(
|
||||||
|
title="bar", date=timezone.now(), location=self.location,
|
||||||
|
price=1, slots=142, tirage=self.tirage, listing=False,
|
||||||
|
category=self.category
|
||||||
|
),
|
||||||
|
Spectacle(
|
||||||
|
title="baz", date=timezone.now(), location=self.location,
|
||||||
|
price=2, slots=242, tirage=self.tirage, listing=False,
|
||||||
|
category=self.category
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_catalogue(self):
|
||||||
|
"""Test the catalogue JSON API"""
|
||||||
|
client = Client()
|
||||||
|
|
||||||
class SimpleTest(TestCase):
|
# The `list` hooh
|
||||||
def test_basic_addition(self):
|
resp = client.get("/bda/catalogue/list")
|
||||||
"""
|
self.assertJSONEqual(
|
||||||
Tests that 1 + 1 always equals 2.
|
resp.content.decode("utf-8"),
|
||||||
"""
|
[{"id": self.tirage.id, "title": self.tirage.title}]
|
||||||
self.assertEqual(1 + 1, 2)
|
)
|
||||||
|
|
||||||
|
# The `details` hook
|
||||||
|
resp = client.get(
|
||||||
|
"/bda/catalogue/details?id={}".format(self.tirage.id)
|
||||||
|
)
|
||||||
|
self.assertJSONEqual(
|
||||||
|
resp.content.decode("utf-8"),
|
||||||
|
{
|
||||||
|
"categories": [{
|
||||||
|
"id": self.category.id,
|
||||||
|
"name": self.category.name
|
||||||
|
}],
|
||||||
|
"locations": [{
|
||||||
|
"id": self.location.id,
|
||||||
|
"name": self.location.name
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# The `descriptions` hook
|
||||||
|
resp = client.get(
|
||||||
|
"/bda/catalogue/descriptions?id={}".format(self.tirage.id)
|
||||||
|
)
|
||||||
|
raw = resp.content.decode("utf-8")
|
||||||
|
try:
|
||||||
|
results = json.loads(raw)
|
||||||
|
except ValueError:
|
||||||
|
self.fail("Not valid JSON: {}".format(raw))
|
||||||
|
self.assertEqual(len(results), 3)
|
||||||
|
self.assertEqual(
|
||||||
|
{(s["title"], s["price"], s["slots"]) for s in results},
|
||||||
|
{("foo", 0, 42), ("bar", 1, 142), ("baz", 2, 242)}
|
||||||
|
)
|
||||||
|
|
87
bda/views.py
87
bda/views.py
|
@ -22,7 +22,6 @@ from django.core.urlresolvers import reverse
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone, formats
|
from django.utils import timezone, formats
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
from gestioncof.decorators import cof_required, buro_required
|
from gestioncof.decorators import cof_required, buro_required
|
||||||
from bda.models import (
|
from bda.models import (
|
||||||
Spectacle, Participant, ChoixSpectacle, Attribution, Tirage,
|
Spectacle, Participant, ChoixSpectacle, Attribution, Tirage,
|
||||||
|
@ -657,29 +656,35 @@ def catalogue(request, request_type):
|
||||||
if request_type == "list":
|
if request_type == "list":
|
||||||
# Dans ce cas on retourne la liste des tirages et de leur id en JSON
|
# Dans ce cas on retourne la liste des tirages et de leur id en JSON
|
||||||
data_return = list(
|
data_return = list(
|
||||||
Tirage.objects.filter(appear_catalogue=True).values('id', 'title'))
|
Tirage.objects.filter(appear_catalogue=True).values('id', 'title')
|
||||||
|
)
|
||||||
return JsonResponse(data_return, safe=False)
|
return JsonResponse(data_return, safe=False)
|
||||||
if request_type == "details":
|
if request_type == "details":
|
||||||
# Dans ce cas on retourne une liste des catégories et des salles
|
# Dans ce cas on retourne une liste des catégories et des salles
|
||||||
tirage_id = request.GET.get('id', '')
|
tirage_id = request.GET.get('id', None)
|
||||||
try:
|
if tirage_id is None:
|
||||||
tirage = Tirage.objects.get(id=tirage_id)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
return HttpResponseBadRequest(
|
return HttpResponseBadRequest(
|
||||||
"Aucun tirage correspondant à l'id "
|
"Missing GET parameter: id <int>"
|
||||||
+ tirage_id)
|
)
|
||||||
|
try:
|
||||||
|
tirage = get_object_or_404(Tirage, id=int(tirage_id))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return HttpResponseBadRequest(
|
return HttpResponseBadRequest(
|
||||||
"Mauvais format d'identifiant : "
|
"Bad format: int expected for `id`"
|
||||||
+ tirage_id)
|
)
|
||||||
|
shows = tirage.spectacle_set.values_list("id", flat=True)
|
||||||
categories = list(
|
categories = list(
|
||||||
CategorieSpectacle.objects.filter(
|
CategorieSpectacle.objects
|
||||||
spectacle__in=tirage.spectacle_set.all())
|
.filter(spectacle__in=shows)
|
||||||
.distinct().values('id', 'name'))
|
.distinct()
|
||||||
|
.values('id', 'name')
|
||||||
|
)
|
||||||
locations = list(
|
locations = list(
|
||||||
Salle.objects.filter(
|
Salle.objects
|
||||||
spectacle__in=tirage.spectacle_set.all())
|
.filter(spectacle__in=shows)
|
||||||
.distinct().values('id', 'name'))
|
.distinct()
|
||||||
|
.values('id', 'name')
|
||||||
|
)
|
||||||
data_return = {'categories': categories, 'locations': locations}
|
data_return = {'categories': categories, 'locations': locations}
|
||||||
return JsonResponse(data_return, safe=False)
|
return JsonResponse(data_return, safe=False)
|
||||||
if request_type == "descriptions":
|
if request_type == "descriptions":
|
||||||
|
@ -687,33 +692,35 @@ def catalogue(request, request_type):
|
||||||
# à la salle spécifiées
|
# à la salle spécifiées
|
||||||
|
|
||||||
tirage_id = request.GET.get('id', '')
|
tirage_id = request.GET.get('id', '')
|
||||||
categories = request.GET.get('category', '[0]')
|
categories = request.GET.get('category', '[]')
|
||||||
locations = request.GET.get('location', '[0]')
|
locations = request.GET.get('location', '[]')
|
||||||
try:
|
try:
|
||||||
category_id = json.loads(categories)
|
tirage_id = int(tirage_id)
|
||||||
location_id = json.loads(locations)
|
categories_id = json.loads(categories)
|
||||||
tirage = Tirage.objects.get(id=tirage_id)
|
locations_id = json.loads(locations)
|
||||||
|
# Integers expected
|
||||||
shows_qs = tirage.spectacle_set
|
if not all(isinstance(id, int) for id in categories_id):
|
||||||
if not(0 in category_id):
|
raise ValueError
|
||||||
shows_qs = shows_qs.filter(
|
if not all(isinstance(id, int) for id in locations_id):
|
||||||
category__id__in=category_id)
|
raise ValueError
|
||||||
if not(0 in location_id):
|
|
||||||
shows_qs = shows_qs.filter(
|
|
||||||
location__id__in=location_id)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
return HttpResponseBadRequest(
|
|
||||||
"Impossible de trouver des résultats correspondant "
|
|
||||||
"à ces caractéristiques : "
|
|
||||||
+ "id = " + tirage_id
|
|
||||||
+ ", catégories = " + categories
|
|
||||||
+ ", salles = " + locations)
|
|
||||||
except ValueError: # Contient JSONDecodeError
|
except ValueError: # Contient JSONDecodeError
|
||||||
return HttpResponseBadRequest(
|
return HttpResponseBadRequest(
|
||||||
"Impossible de parser les paramètres donnés : "
|
"Parse error, please ensure the GET parameters have the "
|
||||||
+ "id = " + request.GET.get('id', '')
|
"following types:\n"
|
||||||
+ ", catégories = " + request.GET.get('category', '[0]')
|
"id: int, category: [int], location: [int]\n"
|
||||||
+ ", salles = " + request.GET.get('location', '[0]'))
|
"Data received:\n"
|
||||||
|
"id = {}, category = {}, locations = {}"
|
||||||
|
.format(request.GET.get('id', ''),
|
||||||
|
request.GET.get('category', '[]'),
|
||||||
|
request.GET.get('location', '[]'))
|
||||||
|
)
|
||||||
|
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||||
|
|
||||||
|
shows_qs = tirage.spectacle_set
|
||||||
|
if categories_id:
|
||||||
|
shows_qs = shows_qs.filter(category__id__in=categories_id)
|
||||||
|
if locations_id:
|
||||||
|
shows_qs = shows_qs.filter(location__id__in=locations_id)
|
||||||
|
|
||||||
# On convertit les descriptions à envoyer en une liste facilement
|
# On convertit les descriptions à envoyer en une liste facilement
|
||||||
# JSONifiable (il devrait y avoir un moyen plus efficace en
|
# JSONifiable (il devrait y avoir un moyen plus efficace en
|
||||||
|
|
|
@ -84,7 +84,7 @@ urlpatterns = [
|
||||||
url(r'^k-fet/', include('kfet.urls')),
|
url(r'^k-fet/', include('kfet.urls')),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if 'debug_toolbar' in settings.INSTALLED_APPS:
|
||||||
import debug_toolbar
|
import debug_toolbar
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
url(r'^__debug__/', include(debug_toolbar.urls)),
|
url(r'^__debug__/', include(debug_toolbar.urls)),
|
||||||
|
|
|
@ -239,6 +239,16 @@ class CheckoutStatementUpdateForm(forms.ModelForm):
|
||||||
model = CheckoutStatement
|
model = CheckoutStatement
|
||||||
exclude = ['by', 'at', 'checkout', 'amount_error', 'amount_taken']
|
exclude = ['by', 'at', 'checkout', 'amount_error', 'amount_taken']
|
||||||
|
|
||||||
|
|
||||||
|
# -----
|
||||||
|
# Category
|
||||||
|
# -----
|
||||||
|
|
||||||
|
class CategoryForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = ArticleCategory
|
||||||
|
fields = ['name', 'has_addcost']
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# Article forms
|
# Article forms
|
||||||
# -----
|
# -----
|
||||||
|
@ -480,6 +490,7 @@ class InventoryArticleForm(forms.Form):
|
||||||
self.stock_old = kwargs['initial']['stock_old']
|
self.stock_old = kwargs['initial']['stock_old']
|
||||||
self.category = kwargs['initial']['category']
|
self.category = kwargs['initial']['category']
|
||||||
self.category_name = kwargs['initial']['category__name']
|
self.category_name = kwargs['initial']['category__name']
|
||||||
|
self.box_capacity = kwargs['initial']['box_capacity']
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# Order forms
|
# Order forms
|
||||||
|
|
24
kfet/migrations/0052_category_addcost.py
Normal file
24
kfet/migrations/0052_category_addcost.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0051_verbose_names'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='articlecategory',
|
||||||
|
name='has_addcost',
|
||||||
|
field=models.BooleanField(default=True, help_text="Si oui et qu'une majoration est active, celle-ci sera appliquée aux articles de cette catégorie.", verbose_name='majorée'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='articlecategory',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=45, verbose_name='nom'),
|
||||||
|
),
|
||||||
|
]
|
20
kfet/migrations/0053_created_at.py
Normal file
20
kfet/migrations/0053_created_at.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('kfet', '0052_category_addcost'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='account',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||||
|
),
|
||||||
|
]
|
|
@ -37,7 +37,7 @@ class Account(models.Model):
|
||||||
max_digits = 6, decimal_places = 2,
|
max_digits = 6, decimal_places = 2,
|
||||||
default = 0)
|
default = 0)
|
||||||
is_frozen = models.BooleanField("est gelé", default = False)
|
is_frozen = models.BooleanField("est gelé", default = False)
|
||||||
created_at = models.DateTimeField(auto_now_add = True, null = True)
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
# Optional
|
# Optional
|
||||||
PROMO_CHOICES = [(r,r) for r in range(1980, date.today().year+1)]
|
PROMO_CHOICES = [(r,r) for r in range(1980, date.today().year+1)]
|
||||||
promo = models.IntegerField(
|
promo = models.IntegerField(
|
||||||
|
@ -334,13 +334,20 @@ class CheckoutStatement(models.Model):
|
||||||
balance=F('balance') - last_statement.balance_new + self.balance_new)
|
balance=F('balance') - last_statement.balance_new + self.balance_new)
|
||||||
super(CheckoutStatement, self).save(*args, **kwargs)
|
super(CheckoutStatement, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class ArticleCategory(models.Model):
|
class ArticleCategory(models.Model):
|
||||||
name = models.CharField(max_length = 45)
|
name = models.CharField("nom", max_length=45)
|
||||||
|
has_addcost = models.BooleanField("majorée", default=True,
|
||||||
|
help_text="Si oui et qu'une majoration "
|
||||||
|
"est active, celle-ci sera "
|
||||||
|
"appliquée aux articles de "
|
||||||
|
"cette catégorie.")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Article(models.Model):
|
class Article(models.Model):
|
||||||
name = models.CharField("nom", max_length = 45)
|
name = models.CharField("nom", max_length = 45)
|
||||||
|
@ -487,6 +494,7 @@ class TransferGroup(models.Model):
|
||||||
related_name = "+",
|
related_name = "+",
|
||||||
blank = True, null = True, default = None)
|
blank = True, null = True, default = None)
|
||||||
|
|
||||||
|
|
||||||
class Transfer(models.Model):
|
class Transfer(models.Model):
|
||||||
group = models.ForeignKey(
|
group = models.ForeignKey(
|
||||||
TransferGroup, on_delete=models.PROTECT,
|
TransferGroup, on_delete=models.PROTECT,
|
||||||
|
@ -506,6 +514,10 @@ class Transfer(models.Model):
|
||||||
canceled_at = models.DateTimeField(
|
canceled_at = models.DateTimeField(
|
||||||
null=True, blank=True, default=None)
|
null=True, blank=True, default=None)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '{} -> {}: {}€'.format(self.from_acc, self.to_acc, self.amount)
|
||||||
|
|
||||||
|
|
||||||
class OperationGroup(models.Model):
|
class OperationGroup(models.Model):
|
||||||
on_acc = models.ForeignKey(
|
on_acc = models.ForeignKey(
|
||||||
Account, on_delete = models.PROTECT,
|
Account, on_delete = models.PROTECT,
|
||||||
|
|
|
@ -32,6 +32,7 @@ textarea {
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
margin-bottom:0;
|
margin-bottom:0;
|
||||||
|
border-bottom:1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
|
@ -85,6 +86,16 @@ textarea {
|
||||||
color:#FFF;
|
color:#FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttons .nav-pills > li > a {
|
||||||
|
border-radius:0;
|
||||||
|
border:1px solid rgba(200,16,46,0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons .nav-pills > li.active > a {
|
||||||
|
background-color:rgba(200,16,46,0.9);
|
||||||
|
background-clip:padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
.row-page-header {
|
.row-page-header {
|
||||||
background-color:rgba(200,16,46,1);
|
background-color:rgba(200,16,46,1);
|
||||||
color:#FFF;
|
color:#FFF;
|
||||||
|
@ -105,6 +116,7 @@ textarea {
|
||||||
|
|
||||||
.panel-md-margin{
|
.panel-md-margin{
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
overflow:hidden;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
|
@ -230,6 +242,9 @@ textarea {
|
||||||
height:28px;
|
height:28px;
|
||||||
margin:3px 0px;
|
margin:3px 0px;
|
||||||
}
|
}
|
||||||
|
.content-center .auth-form {
|
||||||
|
margin:15px;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Pages formulaires seuls
|
* Pages formulaires seuls
|
||||||
|
@ -549,3 +564,18 @@ thead .tooltip {
|
||||||
.help-block {
|
.help-block {
|
||||||
padding-top: 15px;
|
padding-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Inventaires */
|
||||||
|
|
||||||
|
.inventory_modified {
|
||||||
|
background:rgba(236,100,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock_diff {
|
||||||
|
padding-left: 5px;
|
||||||
|
color:#C8102E;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory_update {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
|
|
||||||
.jconfirm .jconfirm-box .content {
|
.jconfirm .jconfirm-box .content {
|
||||||
border-bottom:1px solid #ddd;
|
border-bottom:1px solid #ddd;
|
||||||
|
padding:5px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.jconfirm .jconfirm-box input {
|
.jconfirm .jconfirm-box input {
|
||||||
|
|
|
@ -22,16 +22,18 @@
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTimeChart (dict) {
|
function handleTimeChart (data) {
|
||||||
// reads the balance data and put it into chartjs formatting
|
// reads the balance data and put it into chartjs formatting
|
||||||
var data = dictToArray(dict, 0);
|
chart_data = new Array();
|
||||||
for (var i = 0; i < data.length; i++) {
|
for (var i = 0; i < data.length; i++) {
|
||||||
var source = data[i];
|
var source = data[i];
|
||||||
data[i] = { x: new Date(source.at),
|
chart_data[i] = {
|
||||||
|
x: new Date(source.at),
|
||||||
y: source.balance,
|
y: source.balance,
|
||||||
label: source.label }
|
label: source.label,
|
||||||
}
|
}
|
||||||
return data;
|
}
|
||||||
|
return chart_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showStats () {
|
function showStats () {
|
||||||
|
@ -42,8 +44,7 @@
|
||||||
$(this).addClass("focus");
|
$(this).addClass("focus");
|
||||||
|
|
||||||
// loads data and shows it
|
// loads data and shows it
|
||||||
$.getJSON(this.stats_target_url + "?format=json",
|
$.getJSON(this.stats_target_url, {format: 'json'}, displayStats);
|
||||||
displayStats);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayStats (data) {
|
function displayStats (data) {
|
||||||
|
@ -78,6 +79,7 @@
|
||||||
var chart_options =
|
var chart_options =
|
||||||
{
|
{
|
||||||
responsive: true,
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
tooltips: {
|
tooltips: {
|
||||||
mode: 'index',
|
mode: 'index',
|
||||||
intersect: false,
|
intersect: false,
|
||||||
|
@ -130,7 +132,7 @@
|
||||||
type: 'line',
|
type: 'line',
|
||||||
options: chart_options,
|
options: chart_options,
|
||||||
data: {
|
data: {
|
||||||
labels: dictToArray(data.labels, 1),
|
labels: (data.labels || []).slice(1),
|
||||||
datasets: chart_datasets,
|
datasets: chart_datasets,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -138,15 +140,15 @@
|
||||||
// saves the previous charts to be destroyed
|
// saves the previous charts to be destroyed
|
||||||
var prev_chart = content.children();
|
var prev_chart = content.children();
|
||||||
|
|
||||||
|
// clean
|
||||||
|
prev_chart.remove();
|
||||||
|
|
||||||
// creates a blank canvas element and attach it to the DOM
|
// creates a blank canvas element and attach it to the DOM
|
||||||
var canvas = $("<canvas>");
|
var canvas = $("<canvas height='250'>");
|
||||||
content.append(canvas);
|
content.append(canvas);
|
||||||
|
|
||||||
// create the chart
|
// create the chart
|
||||||
var chart = new Chart(canvas, chart_model);
|
var chart = new Chart(canvas, chart_model);
|
||||||
|
|
||||||
// clean
|
|
||||||
prev_chart.remove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialize the interface
|
// initialize the interface
|
||||||
|
@ -158,7 +160,7 @@
|
||||||
"aria-label": "select-period"});
|
"aria-label": "select-period"});
|
||||||
|
|
||||||
var to_click;
|
var to_click;
|
||||||
var context = dictToArray(data.stats);
|
var context = data.stats;
|
||||||
|
|
||||||
for (var i = 0; i < context.length; i++) {
|
for (var i = 0; i < context.length; i++) {
|
||||||
// creates the button
|
// creates the button
|
||||||
|
@ -191,7 +193,7 @@
|
||||||
|
|
||||||
// constructor
|
// constructor
|
||||||
(function () {
|
(function () {
|
||||||
$.getJSON(url + "?format=json", initialize);
|
$.getJSON(url, {format: 'json'}, initialize);
|
||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|
|
@ -1,98 +1,155 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from datetime import date, datetime, time, timedelta
|
||||||
|
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from dateutil.parser import parse as dateutil_parse
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
|
|
||||||
KFET_WAKES_UP_AT = 7
|
KFET_WAKES_UP_AT = time(7, 0)
|
||||||
|
|
||||||
# donne le nom des jours d'une liste de dates
|
|
||||||
# dans un dico ordonné
|
|
||||||
def daynames(dates):
|
|
||||||
names = {}
|
|
||||||
for i in dates:
|
|
||||||
names[i] = dates[i].strftime("%A")
|
|
||||||
return names
|
|
||||||
|
|
||||||
|
|
||||||
# donne le nom des semaines une liste de dates
|
def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT):
|
||||||
# dans un dico ordonné
|
"""datetime wrapper with time offset."""
|
||||||
def weeknames(dates):
|
return datetime.combine(date(year, month, day), start_at)
|
||||||
names = {}
|
|
||||||
for i in dates:
|
|
||||||
names[i] = dates[i].strftime("Semaine %W")
|
|
||||||
return names
|
|
||||||
|
|
||||||
|
|
||||||
# donne le nom des mois d'une liste de dates
|
def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT):
|
||||||
# dans un dico ordonné
|
kfet_dt = kfet_day(year=dt.year, month=dt.month, day=dt.day)
|
||||||
def monthnames(dates):
|
if dt.time() < start_at:
|
||||||
names = {}
|
kfet_dt -= timedelta(days=1)
|
||||||
for i in dates:
|
return kfet_dt
|
||||||
names[i] = dates[i].strftime("%B")
|
|
||||||
return names
|
|
||||||
|
|
||||||
|
|
||||||
# rend les dates des nb derniers jours
|
class Scale(object):
|
||||||
# dans l'ordre chronologique
|
name = None
|
||||||
# aujourd'hui compris
|
step = None
|
||||||
# nb = 1 : rend hier
|
|
||||||
def lastdays(nb):
|
def __init__(self, n_steps=0, begin=None, end=None,
|
||||||
morning = this_morning()
|
last=False, std_chunk=True):
|
||||||
days = {}
|
self.std_chunk = std_chunk
|
||||||
for i in range(1, nb+1):
|
if last:
|
||||||
days[i] = morning - timezone.timedelta(days=nb - i + 1)
|
end = timezone.now()
|
||||||
return days
|
|
||||||
|
if begin is not None and n_steps != 0:
|
||||||
|
self.begin = self.get_from(begin)
|
||||||
|
self.end = self.do_step(self.begin, n_steps=n_steps)
|
||||||
|
elif end is not None and n_steps != 0:
|
||||||
|
self.end = self.get_from(end)
|
||||||
|
self.begin = self.do_step(self.end, n_steps=-n_steps)
|
||||||
|
elif begin is not None and end is not None:
|
||||||
|
self.begin = self.get_from(begin)
|
||||||
|
self.end = self.get_from(end)
|
||||||
|
else:
|
||||||
|
raise Exception('Two of these args must be specified: '
|
||||||
|
'n_steps, begin, end; '
|
||||||
|
'or use last and n_steps')
|
||||||
|
|
||||||
|
self.datetimes = self.get_datetimes()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def by_name(name):
|
||||||
|
for cls in Scale.__subclasses__():
|
||||||
|
if cls.name == name:
|
||||||
|
return cls
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_from(self, dt):
|
||||||
|
return self.std_chunk and self.get_chunk_start(dt) or dt
|
||||||
|
|
||||||
|
def __getitem__(self, i):
|
||||||
|
return self.datetimes[i], self.datetimes[i+1]
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.datetimes) - 1
|
||||||
|
|
||||||
|
def do_step(self, dt, n_steps=1):
|
||||||
|
return dt + self.step * n_steps
|
||||||
|
|
||||||
|
def get_datetimes(self):
|
||||||
|
datetimes = [self.begin]
|
||||||
|
tmp = self.begin
|
||||||
|
while tmp <= self.end:
|
||||||
|
tmp = self.do_step(tmp)
|
||||||
|
datetimes.append(tmp)
|
||||||
|
return datetimes
|
||||||
|
|
||||||
|
def get_labels(self, label_fmt=None):
|
||||||
|
if label_fmt is None:
|
||||||
|
label_fmt = self.label_fmt
|
||||||
|
return [begin.strftime(label_fmt) for begin, end in self]
|
||||||
|
|
||||||
|
|
||||||
def lastweeks(nb):
|
class DayScale(Scale):
|
||||||
monday_morning = this_monday_morning()
|
name = 'day'
|
||||||
mondays = {}
|
step = timedelta(days=1)
|
||||||
for i in range(1, nb+1):
|
label_fmt = '%A'
|
||||||
mondays[i] = monday_morning \
|
|
||||||
- timezone.timedelta(days=7*(nb - i + 1))
|
@classmethod
|
||||||
return mondays
|
def get_chunk_start(cls, dt):
|
||||||
|
return to_kfet_day(dt)
|
||||||
|
|
||||||
|
|
||||||
def lastmonths(nb):
|
class WeekScale(Scale):
|
||||||
first_month_day = this_first_month_day()
|
name = 'week'
|
||||||
first_days = {}
|
step = timedelta(days=7)
|
||||||
this_year = first_month_day.year
|
label_fmt = 'Semaine %W'
|
||||||
this_month = first_month_day.month
|
|
||||||
for i in range(1, nb+1):
|
@classmethod
|
||||||
month = ((this_month - 1 - (nb - i)) % 12) + 1
|
def get_chunk_start(cls, dt):
|
||||||
year = this_year + (nb - i) // 12
|
dt_kfet = to_kfet_day(dt)
|
||||||
first_days[i] = timezone.datetime(year=year,
|
offset = timedelta(days=dt_kfet.weekday())
|
||||||
month=month,
|
return dt_kfet - offset
|
||||||
day=1,
|
|
||||||
hour=KFET_WAKES_UP_AT)
|
|
||||||
return first_days
|
|
||||||
|
|
||||||
|
|
||||||
def this_first_month_day():
|
class MonthScale(Scale):
|
||||||
now = timezone.now()
|
name = 'month'
|
||||||
first_day = timezone.datetime(year=now.year,
|
step = relativedelta(months=1)
|
||||||
month=now.month,
|
label_fmt = '%B'
|
||||||
day=1,
|
|
||||||
hour=KFET_WAKES_UP_AT)
|
@classmethod
|
||||||
return first_day
|
def get_chunk_start(cls, dt):
|
||||||
|
return to_kfet_day(dt).replace(day=1)
|
||||||
|
|
||||||
|
|
||||||
def this_monday_morning():
|
def stat_manifest(scales_def=None, scale_args=None, scale_prefix=None,
|
||||||
now = timezone.now()
|
**other_url_params):
|
||||||
monday = now - timezone.timedelta(days=now.isoweekday()-1)
|
if scale_prefix is None:
|
||||||
monday_morning = timezone.datetime(year=monday.year,
|
scale_prefix = 'scale_'
|
||||||
month=monday.month,
|
if scales_def is None:
|
||||||
day=monday.day,
|
scales_def = []
|
||||||
hour=KFET_WAKES_UP_AT)
|
if scale_args is None:
|
||||||
return monday_morning
|
scale_args = {}
|
||||||
|
manifest = []
|
||||||
|
for label, cls in scales_def:
|
||||||
|
url_params = {scale_prefix+'name': cls.name}
|
||||||
|
url_params.update({scale_prefix+key: value
|
||||||
|
for key, value in scale_args.items()})
|
||||||
|
url_params.update(other_url_params)
|
||||||
|
manifest.append(dict(
|
||||||
|
label=label,
|
||||||
|
url_params=url_params,
|
||||||
|
))
|
||||||
|
return manifest
|
||||||
|
|
||||||
|
|
||||||
def this_morning():
|
def last_stats_manifest(scales_def=None, scale_args=None, scale_prefix=None,
|
||||||
now = timezone.now()
|
**url_params):
|
||||||
morning = timezone.datetime(year=now.year,
|
scales_def = [
|
||||||
month=now.month,
|
('Derniers mois', MonthScale, ),
|
||||||
day=now.day,
|
('Dernières semaines', WeekScale, ),
|
||||||
hour=KFET_WAKES_UP_AT)
|
('Derniers jours', DayScale, ),
|
||||||
return morning
|
]
|
||||||
|
if scale_args is None:
|
||||||
|
scale_args = {}
|
||||||
|
scale_args.update(dict(
|
||||||
|
last=True,
|
||||||
|
n_steps=7,
|
||||||
|
))
|
||||||
|
return stat_manifest(scales_def=scales_def, scale_args=scale_args,
|
||||||
|
scale_prefix=scale_prefix, **url_params)
|
||||||
|
|
||||||
|
|
||||||
# Étant donné un queryset d'operations
|
# Étant donné un queryset d'operations
|
||||||
|
@ -100,3 +157,78 @@ def this_morning():
|
||||||
def tot_ventes(queryset):
|
def tot_ventes(queryset):
|
||||||
res = queryset.aggregate(Sum('article_nb'))['article_nb__sum']
|
res = queryset.aggregate(Sum('article_nb'))['article_nb__sum']
|
||||||
return res and res or 0
|
return res and res or 0
|
||||||
|
|
||||||
|
|
||||||
|
class ScaleMixin(object):
|
||||||
|
scale_args_prefix = 'scale_'
|
||||||
|
|
||||||
|
def get_scale_args(self, params=None, prefix=None):
|
||||||
|
"""Retrieve scale args from params.
|
||||||
|
|
||||||
|
Should search the same args of Scale constructor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params (dict, optional): Scale args are searched in this.
|
||||||
|
Default to GET params of request.
|
||||||
|
prefix (str, optional): Appended at the begin of scale args names.
|
||||||
|
Default to `self.scale_args_prefix`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if params is None:
|
||||||
|
params = self.request.GET
|
||||||
|
if prefix is None:
|
||||||
|
prefix = self.scale_args_prefix
|
||||||
|
|
||||||
|
scale_args = {}
|
||||||
|
|
||||||
|
name = params.get(prefix+'name', None)
|
||||||
|
if name is not None:
|
||||||
|
scale_args['name'] = name
|
||||||
|
|
||||||
|
n_steps = params.get(prefix+'n_steps', None)
|
||||||
|
if n_steps is not None:
|
||||||
|
scale_args['n_steps'] = int(n_steps)
|
||||||
|
|
||||||
|
begin = params.get(prefix+'begin', None)
|
||||||
|
if begin is not None:
|
||||||
|
scale_args['begin'] = dateutil_parse(begin)
|
||||||
|
|
||||||
|
end = params.get(prefix+'send', None)
|
||||||
|
if end is not None:
|
||||||
|
scale_args['end'] = dateutil_parse(end)
|
||||||
|
|
||||||
|
last = params.get(prefix+'last', None)
|
||||||
|
if last is not None:
|
||||||
|
scale_args['last'] = (
|
||||||
|
last in ['true', 'True', '1'] and True or False)
|
||||||
|
|
||||||
|
return scale_args
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
|
||||||
|
scale_args = self.get_scale_args()
|
||||||
|
scale_name = scale_args.pop('name', None)
|
||||||
|
scale_cls = Scale.by_name(scale_name)
|
||||||
|
|
||||||
|
if scale_cls is None:
|
||||||
|
scale = self.get_default_scale()
|
||||||
|
else:
|
||||||
|
scale = scale_cls(**scale_args)
|
||||||
|
|
||||||
|
self.scale = scale
|
||||||
|
context['labels'] = scale.get_labels()
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_default_scale(self):
|
||||||
|
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
|
||||||
|
]
|
||||||
|
|
|
@ -13,12 +13,17 @@
|
||||||
{% if account.user == request.user %}
|
{% if account.user == request.user %}
|
||||||
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
|
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
|
||||||
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>
|
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>
|
||||||
<script>
|
|
||||||
jQuery(document).ready(function() {
|
<script type="text/javascript">
|
||||||
var stat_last = new StatsGroup("{% url 'kfet.account.stat.last' trigramme=account.trigramme %}",
|
$(document).ready(function() {
|
||||||
$("#stat_last"));
|
var stat_last = new StatsGroup(
|
||||||
var stat_balance = new StatsGroup("{% url 'kfet.account.stat.balance' trigramme=account.trigramme %}",
|
"{% url 'kfet.account.stat.operation.list' trigramme=account.trigramme %}",
|
||||||
$("#stat_balance"));
|
$("#stat_last"),
|
||||||
|
);
|
||||||
|
var stat_balance = new StatsGroup(
|
||||||
|
"{% url 'kfet.account.stat.balance.list' trigramme=account.trigramme %}",
|
||||||
|
$("#stat_balance"),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -51,8 +56,22 @@
|
||||||
<div class="col-sm-8 col-md-9 col-content-right">
|
<div class="col-sm-8 col-md-9 col-content-right">
|
||||||
{% include "kfet/base_messages.html" %}
|
{% include "kfet/base_messages.html" %}
|
||||||
<div class="content-right">
|
<div class="content-right">
|
||||||
{% if addcosts %}
|
|
||||||
<div class="content-right-block">
|
<div class="content-right-block">
|
||||||
|
<div class="col-sm-12 nopadding">
|
||||||
|
{% if account.user == request.user %}
|
||||||
|
<div class='tab-content'>
|
||||||
|
<div class="tab-pane fade in active" id="tab_stats">
|
||||||
|
<h2>Statistiques</h2>
|
||||||
|
<div class="panel-md-margin">
|
||||||
|
<h3>Ma balance</h3>
|
||||||
|
<div id="stat_balance"></div>
|
||||||
|
<h3>Ma consommation</h3>
|
||||||
|
<div id="stat_last"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="tab_history">
|
||||||
|
{% endif %}
|
||||||
|
{% if addcosts %}
|
||||||
<h2>Gagné des majorations</h2>
|
<h2>Gagné des majorations</h2>
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -61,35 +80,16 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if account.user == request.user %}
|
|
||||||
<div class="content-right-block content-right-block-transparent">
|
|
||||||
<h2>Statistiques</h2>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12 nopadding">
|
|
||||||
<div class="panel-md-margin">
|
|
||||||
<h3>Ma balance</h3>
|
|
||||||
<div id="stat_balance" class"stat-graph"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div><!-- /row -->
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-sm-12 nopadding">
|
|
||||||
<div class="panel-md-margin">
|
|
||||||
<h3>Ma consommation</h3>
|
|
||||||
<div id="stat_last" class"stat-graph"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div><!-- /row -->
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="content-right-block">
|
|
||||||
<h2>Historique</h2>
|
<h2>Historique</h2>
|
||||||
<div id="history">
|
<div id="history"></div>
|
||||||
</div>
|
{% if account.user == request.user %}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div><!-- tab-content -->
|
||||||
|
{% endif %}
|
||||||
|
</div><!-- col-sm-12 -->
|
||||||
|
</div><!-- content-right-block -->
|
||||||
|
</div><!-- content-right-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
{% if account.user == request.user %}
|
|
||||||
Mon compte
|
|
||||||
{% else %}
|
|
||||||
Informations du compte {{ account.trigramme }}
|
|
||||||
{% endif %}
|
|
|
@ -16,6 +16,9 @@
|
||||||
<a class="btn btn-primary btn-lg" href="{% url 'kfet.article.create' %}">
|
<a class="btn btn-primary btn-lg" href="{% url 'kfet.article.create' %}">
|
||||||
Nouvel article
|
Nouvel article
|
||||||
</a>
|
</a>
|
||||||
|
<a class="btn btn-primary btn-lg" href="{% url 'kfet.category' %}">
|
||||||
|
Catégories
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
{% extends 'kfet/base.html' %}
|
{% extends 'kfet/base.html' %}
|
||||||
{% load staticfiles %}
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}Informations sur l'article {{ article }}{% endblock %}
|
{% block title %}Informations sur l'article {{ article }}{% endblock %}
|
||||||
{% block content-header-title %}Article - {{ article.name }}{% endblock %}
|
{% block content-header-title %}Article - {{ article.name }}{% endblock %}
|
||||||
|
|
||||||
|
@ -94,15 +99,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block extra_head %}
|
<script type="text/javascript">
|
||||||
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
|
$(document).ready(function() {
|
||||||
<script type="text/javascript" src="{% static 'kfet/js/statistic.js' %}"></script>
|
var stat_last = new StatsGroup(
|
||||||
<script>
|
"{% url 'kfet.article.stat.sales.list' article.id %}",
|
||||||
jQuery(document).ready(function() {
|
$("#stat_last"),
|
||||||
var stat_last = new StatsGroup("{% url 'kfet.article.stat.last' article.id %}",
|
);
|
||||||
$("#stat_last"));
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<div class="row form-only">
|
<div class="row form-only">
|
||||||
<div class="col-sm-12 col-md-8 col-md-offset-2">
|
<div class="col-sm-12 col-md-8 col-md-offset-2">
|
||||||
<div class="content-form">
|
<div class="content-form">
|
||||||
<form submit="" method="post" class="form-horizontal">
|
<form action="" method="post" class="form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include 'kfet/form_snippet.html' with form=form %}
|
{% include 'kfet/form_snippet.html' with form=form %}
|
||||||
{% if not perms.kfet.change_article %}
|
{% if not perms.kfet.change_article %}
|
||||||
|
|
53
kfet/templates/kfet/category.html
Normal file
53
kfet/templates/kfet/category.html
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{% extends 'kfet/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Categories d'articles{% endblock %}
|
||||||
|
{% block content-header-title %}Categories d'articles{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-4 col-md-3 col-content-left">
|
||||||
|
<div class="content-left">
|
||||||
|
<div class="content-left-top">
|
||||||
|
<div class="line line-big">{{ categories|length }}</div>
|
||||||
|
<div class="line line-bigsub">catégorie{{ categories|length|pluralize }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8 col-md-9 col-content-right">
|
||||||
|
{% include 'kfet/base_messages.html' %}
|
||||||
|
<div class="content-right">
|
||||||
|
<div class="content-right-block">
|
||||||
|
<h2>Liste des catégories</h2>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-condensed">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>Nom</td>
|
||||||
|
<td class="text-right">Nombre d'articles</td>
|
||||||
|
<td class="text-right">Peut être majorée</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for category in categories %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-center">
|
||||||
|
<a href="{% url 'kfet.category.update' category.pk %}">
|
||||||
|
<span class="glyphicon glyphicon-cog"></span>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ category.name }}</td>
|
||||||
|
<td class="text-right">{{ category.articles.all|length }}</td>
|
||||||
|
<td class="text-right">{{ category.has_addcost | yesno:"Oui,Non"}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
25
kfet/templates/kfet/category_update.html
Normal file
25
kfet/templates/kfet/category_update.html
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{% extends 'kfet/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Édition de la catégorie {{ category.name }}{% endblock %}
|
||||||
|
{% block content-header-title %}Catégorie {{ category.name }} - Édition{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{% include "kfet/base_messages.html" %}
|
||||||
|
|
||||||
|
<div class="row form-only">
|
||||||
|
<div class="col-sm-12 col-md-8 col-md-offset-2">
|
||||||
|
<div class="content-form">
|
||||||
|
<form action="" method="post" class="form-horizontal">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% include 'kfet/form_snippet.html' with form=form %}
|
||||||
|
{% if not perms.kfet.edit_articlecategory %}
|
||||||
|
{% include 'kfet/form_authentication_snippet.html' %}
|
||||||
|
{% endif %}
|
||||||
|
{% include 'kfet/form_submit_snippet.html' with value="Enregistrer"%}
|
||||||
|
<form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -23,6 +23,7 @@
|
||||||
<div class="column-row">
|
<div class="column-row">
|
||||||
<div class="column-sm-1 column-md-2 column-lg-3">
|
<div class="column-sm-1 column-md-2 column-lg-3">
|
||||||
<div class="unbreakable carte-inverted">
|
<div class="unbreakable carte-inverted">
|
||||||
|
{% if pressions %}
|
||||||
<h3>Pressions du moment</h3>
|
<h3>Pressions du moment</h3>
|
||||||
<ul class="carte">
|
<ul class="carte">
|
||||||
{% for article in pressions %}
|
{% for article in pressions %}
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endif %}
|
||||||
</div><!-- endblock unbreakable -->
|
</div><!-- endblock unbreakable -->
|
||||||
{% for article in articles %}
|
{% for article in articles %}
|
||||||
{% ifchanged article.category %}
|
{% ifchanged article.category %}
|
||||||
|
@ -49,7 +51,7 @@
|
||||||
<span class="carte-label">{{ article.name }}</span>
|
<span class="carte-label">{{ article.name }}</span>
|
||||||
<span class="carte-ukf">{{ article.price | ukf:False}} UKF</span>
|
<span class="carte-ukf">{{ article.price | ukf:False}} UKF</span>
|
||||||
</li>
|
</li>
|
||||||
{% if foorloop.last %}
|
{% if forloop.last %}
|
||||||
</ul>
|
</ul>
|
||||||
</div><!-- endblock unbreakable -->
|
</div><!-- endblock unbreakable -->
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
{% extends 'kfet/base.html' %}
|
{% extends 'kfet/base.html' %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
<script type="text/javascript" src="{% static 'kfet/js/reconnecting-websocket.js' %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}Nouvel inventaire{% endblock %}
|
{% block title %}Nouvel inventaire{% endblock %}
|
||||||
{% block content-header-title %}Nouvel inventaire{% endblock %}
|
{% block content-header-title %}Nouvel inventaire{% endblock %}
|
||||||
|
@ -6,38 +13,194 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% include 'kfet/base_messages.html' %}
|
{% include 'kfet/base_messages.html' %}
|
||||||
|
<div class="content-center">
|
||||||
<form action="" method="post">
|
<div>
|
||||||
<table>
|
<form id='inventoryform' action="" method="post">
|
||||||
|
<table class="table text-center">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Article</td>
|
<td>Article</td>
|
||||||
<td>Théo.</td>
|
<td>Quantité par caisse</td>
|
||||||
<td>Réel</td>
|
<td>Stock Théorique</td>
|
||||||
|
<td>Caisses en réserve</td>
|
||||||
|
<td>Caisses en arrière</td>
|
||||||
|
<td>Vrac</td>
|
||||||
|
<td>Stock total</td>
|
||||||
|
<td>Compte terminé</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for form in formset %}
|
{% for form in formset %}
|
||||||
{% ifchanged form.category %}
|
{% ifchanged form.category %}
|
||||||
<tr>
|
<tr class='section'>
|
||||||
<td colspan="3">{{ form.category_name }}</td>
|
<td>{{ form.category_name }}</td>
|
||||||
|
<td colspan="7"></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endifchanged %}
|
{% endifchanged %}
|
||||||
<tr>
|
<tr>
|
||||||
{{ form.article }}
|
{{ form.article }}
|
||||||
<td>{{ form.name }}</td>
|
<td class='name'>{{ form.name }}</td>
|
||||||
<td>{{ form.stock_old }}</td>
|
<td class='box_capacity'>{{ form.box_capacity }}</td>
|
||||||
<td>{{ form.stock_new }}</td>
|
<td><span class='current_stock'>{{ form.stock_old }}</span><span class='stock_diff'></span></td>
|
||||||
|
<td class='box_cellar'>
|
||||||
|
<div class='col-md-2'></div>
|
||||||
|
<div class='col-md-8'>
|
||||||
|
<input type='number' class='form-control' step='1'>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class='box_bar'>
|
||||||
|
<div class='col-md-offset-2 col-md-8'><input type='number' class='form-control' step='1'></div>
|
||||||
|
</td>
|
||||||
|
<td class='misc'>
|
||||||
|
<div class='col-md-offset-2 col-md-8'><input type='number' class='form-control' step='1'></div>
|
||||||
|
</td>
|
||||||
|
<td class='stock_new'>
|
||||||
|
<div class='col-md-offset-2 col-md-8'>{{ form.stock_new | attr:"readonly"| add_class:"form-control" }}</div>
|
||||||
|
<div class='col-md-2 inventory_update'><button type='button' class='btn-sm btn-primary'>MàJ</button></div>
|
||||||
|
</td>
|
||||||
|
<td class='finished'><input type='checkbox' class='form_control'></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if not perms.kfet.add_inventory %}
|
|
||||||
<input type="password" name="KFETPASSWORD">
|
|
||||||
{% endif %}
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ formset.management_form }}
|
{{ formset.management_form }}
|
||||||
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg">
|
{% if not perms.kfet.add_inventory %}
|
||||||
|
<div class='auth-form form-horizontal'>
|
||||||
|
{% include "kfet/form_authentication_snippet.html" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg btn-block">
|
||||||
|
{% csrf_token %}
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var conflicts = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Autofill new stock from other inputs
|
||||||
|
*/
|
||||||
|
|
||||||
|
$('input[type="number"]').on('input', function() {
|
||||||
|
var $line = $(this).closest('tr');
|
||||||
|
var box_capacity = +$line.find('.box_capacity').text();
|
||||||
|
var box_cellar = $line.find('.box_cellar input').val();
|
||||||
|
var box_bar = $line.find('.box_bar input').val();
|
||||||
|
var misc = $line.find('.misc input').val();
|
||||||
|
if (box_cellar || box_bar || misc)
|
||||||
|
$line.find('.stock_new input').val(
|
||||||
|
box_capacity*((+box_cellar) +(+box_bar))+(+misc));
|
||||||
|
else
|
||||||
|
$line.find('.stock_new input').val('');
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Remove warning and update stock
|
||||||
|
*/
|
||||||
|
|
||||||
|
function update_stock($line, update_count) {
|
||||||
|
$line.removeClass('inventory_modified');
|
||||||
|
$line.find('.inventory_update').hide();
|
||||||
|
|
||||||
|
var old_stock = +$line.find('.current_stock').text()
|
||||||
|
var stock_diff = +$line.find('.stock_diff').text();
|
||||||
|
$line.find('.current_stock').text(old_stock + stock_diff);
|
||||||
|
$line.find('.stock_diff').text('');
|
||||||
|
|
||||||
|
if ($line.find('.stock_new input').val() && update_count) {
|
||||||
|
var old_misc = +$line.find('.misc input').val();
|
||||||
|
$line.find('.misc input').val(old_misc + stock_diff)
|
||||||
|
.trigger('input');
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = $line.find('input[type="hidden"]').val();
|
||||||
|
conflicts.delete(parseInt(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.finished input').change(function() {
|
||||||
|
var $line = $(this).closest('tr');
|
||||||
|
update_stock($line, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.inventory_update button').click(function() {
|
||||||
|
var $line = $(this).closest('tr');
|
||||||
|
update_stock($line, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Websocket
|
||||||
|
*/
|
||||||
|
|
||||||
|
OperationWebSocket.add_handler(function(data) {
|
||||||
|
for (let article of data['articles']) {
|
||||||
|
var $line = $('input[value="'+article.id+'"]').parent();
|
||||||
|
if ($line.find('.finished input').is(":checked")) {
|
||||||
|
conflicts.add(article.id);
|
||||||
|
//Display warning
|
||||||
|
$line.addClass('inventory_modified');
|
||||||
|
|
||||||
|
//Realigning input and displaying update button
|
||||||
|
$line.find('.inventory_update').show();
|
||||||
|
|
||||||
|
//Displaying stock changes
|
||||||
|
var stock = $line.find('.current_stock').text();
|
||||||
|
$line.find('.stock_diff').text(article.stock - stock);
|
||||||
|
} else {
|
||||||
|
// If we haven't counted the article yet, we simply update the expected stock
|
||||||
|
$line.find('.current_stock').text(article.stock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('input[type="submit"]').on("click", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (conflicts.size) {
|
||||||
|
content = '';
|
||||||
|
content += "Conflits possibles :"
|
||||||
|
content += '<ul>';
|
||||||
|
for (let id of conflicts) {
|
||||||
|
var $line = $('input[value="'+id+'"]').closest('tr');
|
||||||
|
var name = $line.find('.name').text();
|
||||||
|
var stock_diff = $line.find('.stock_diff').text();
|
||||||
|
content += '<li>'+name+' ('+stock_diff+')</li>';
|
||||||
|
}
|
||||||
|
content += '</ul>'
|
||||||
|
} else {
|
||||||
|
// Prevent erroneous enter key confirmations
|
||||||
|
// Kinda complicated to filter if click or enter key...
|
||||||
|
content="Voulez-vous confirmer l'inventaire ?";
|
||||||
|
}
|
||||||
|
|
||||||
|
$.confirm({
|
||||||
|
title: "Confirmer l'inventaire",
|
||||||
|
content: content,
|
||||||
|
backgroundDismiss: true,
|
||||||
|
animation: 'top',
|
||||||
|
closeAnimation: 'bottom',
|
||||||
|
keyboardEnabled: true,
|
||||||
|
confirm: function() {
|
||||||
|
$('#inventoryform').submit();
|
||||||
|
},
|
||||||
|
onOpen: function() {
|
||||||
|
var that = this;
|
||||||
|
this.$content.find('input').on('keydown', function(e) {
|
||||||
|
if (e.keyCode == 13)
|
||||||
|
that.$confirmButton.click();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -647,7 +647,7 @@ $(document).ready(function() {
|
||||||
});
|
});
|
||||||
$after.after(article_html);
|
$after.after(article_html);
|
||||||
// Pour l'autocomplétion
|
// Pour l'autocomplétion
|
||||||
articlesList.push([article['name'],article['id'],article['category_id'],article['price'], article['stock']]);
|
articlesList.push([article['name'],article['id'],article['category_id'],article['price'], article['stock'],article['category__has_addcost']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArticles() {
|
function getArticles() {
|
||||||
|
@ -831,12 +831,15 @@ $(document).ready(function() {
|
||||||
while (i<articlesList.length && id != articlesList[i][1]) i++;
|
while (i<articlesList.length && id != articlesList[i][1]) i++;
|
||||||
article_data = articlesList[i];
|
article_data = articlesList[i];
|
||||||
var amount_euro = - article_data[3] * nb ;
|
var amount_euro = - article_data[3] * nb ;
|
||||||
if (settings['addcost_for'] && settings['addcost_amount'] && account_data['trigramme'] != settings['addcost_for'])
|
if (settings['addcost_for']
|
||||||
|
&& settings['addcost_amount']
|
||||||
|
&& account_data['trigramme'] != settings['addcost_for']
|
||||||
|
&& article_data[5])
|
||||||
amount_euro -= settings['addcost_amount'] * nb;
|
amount_euro -= settings['addcost_amount'] * nb;
|
||||||
var reduc_divisor = 1;
|
var reduc_divisor = 1;
|
||||||
if (account_data['is_cof'])
|
if (account_data['is_cof'])
|
||||||
reduc_divisor = 1 + settings['subvention_cof'] / 100;
|
reduc_divisor = 1 + settings['subvention_cof'] / 100;
|
||||||
return amount_euro / reduc_divisor;
|
return (amount_euro / reduc_divisor).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addPurchase(id, nb) {
|
function addPurchase(id, nb) {
|
||||||
|
@ -850,7 +853,7 @@ $(document).ready(function() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
var amount_euro = amountEuroPurchase(id, nb).toFixed(2);
|
var amount_euro = amountEuroPurchase(id, nb);
|
||||||
var index = addPurchaseToFormset(article_data[1], nb, amount_euro);
|
var index = addPurchaseToFormset(article_data[1], nb, amount_euro);
|
||||||
article_basket_html = $(item_basket_default_html);
|
article_basket_html = $(item_basket_default_html);
|
||||||
article_basket_html
|
article_basket_html
|
||||||
|
|
|
@ -36,6 +36,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
|
{% if account.user == request.user %}
|
||||||
|
<ul class='nav nav-pills nav-justified'>
|
||||||
|
<li class="active"><a data-toggle="pill" href="#tab_stats">Statistiques</a></li>
|
||||||
|
<li><a data-toggle="pill" href="#tab_history">Historique</a></li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
<a class="btn btn-primary btn-lg" href="{% url 'kfet.account.update' account.trigramme %}">
|
<a class="btn btn-primary btn-lg" href="{% url 'kfet.account.update' account.trigramme %}">
|
||||||
Modifier
|
Modifier
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -66,11 +66,13 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if not perms.kfet.add_order %}
|
|
||||||
<input type="password" name="KFETPASSWORD">
|
|
||||||
{% endif %}
|
|
||||||
{{ formset.management_form }}
|
{{ formset.management_form }}
|
||||||
<input type="submit" class="btn btn-primary btn-lg btn-block" value="Envoyer">
|
{% if not perms.kfet.add_inventory %}
|
||||||
|
<div class='auth-form form-horizontal'>
|
||||||
|
{% include "kfet/form_authentication_snippet.html" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg btn-block">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -42,11 +42,13 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% if not perms.kfet.order_to_inventory %}
|
|
||||||
<input type="password" name="KFETPASSWORD">
|
|
||||||
{% endif %}
|
|
||||||
{{ formset.management_form }}
|
{{ formset.management_form }}
|
||||||
<input type="submit" class="btn btn-primary btn-lg btn-block" value="Enregistrer">
|
{% if not perms.kfet.add_inventory %}
|
||||||
|
<div class='auth-form form-horizontal'>
|
||||||
|
{% include "kfet/form_authentication_snippet.html" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg btn-block">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
70
kfet/tests.py
Normal file
70
kfet/tests.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.test import TestCase, Client
|
||||||
|
from django.contrib.auth.models import User, Permission
|
||||||
|
|
||||||
|
from .models import Account, Article, ArticleCategory
|
||||||
|
|
||||||
|
|
||||||
|
class TestStats(TestCase):
|
||||||
|
@patch('kfet.signals.messages')
|
||||||
|
def test_user_stats(self, mock_messages):
|
||||||
|
"""
|
||||||
|
Checks that we can get the stat-related pages without any problem.
|
||||||
|
"""
|
||||||
|
# We setup two users and an article. Only the first user is part of the
|
||||||
|
# team.
|
||||||
|
user = User.objects.create(username="Foobar")
|
||||||
|
user.set_password("foobar")
|
||||||
|
user.save()
|
||||||
|
Account.objects.create(trigramme="FOO", cofprofile=user.profile)
|
||||||
|
perm = Permission.objects.get(codename="is_team")
|
||||||
|
user.user_permissions.add(perm)
|
||||||
|
|
||||||
|
user2 = User.objects.create(username="Barfoo")
|
||||||
|
user2.set_password("barfoo")
|
||||||
|
user2.save()
|
||||||
|
Account.objects.create(trigramme="BAR", cofprofile=user2.profile)
|
||||||
|
|
||||||
|
article = Article.objects.create(
|
||||||
|
name="article",
|
||||||
|
category=ArticleCategory.objects.create(name="C")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Each user have its own client
|
||||||
|
client = Client()
|
||||||
|
client.login(username="Foobar", password="foobar")
|
||||||
|
client2 = Client()
|
||||||
|
client2.login(username="Barfoo", password="barfoo")
|
||||||
|
|
||||||
|
# 1. FOO should be able to get these pages but BAR receives a Forbidden
|
||||||
|
# response
|
||||||
|
user_urls = [
|
||||||
|
"/k-fet/accounts/FOO/stat/operations/list",
|
||||||
|
"/k-fet/accounts/FOO/stat/operations?{}".format(
|
||||||
|
'&'.join(["scale=day",
|
||||||
|
"types=['purchase']",
|
||||||
|
"scale_args={'n_steps':+7,+'last':+True}",
|
||||||
|
"format=json"])),
|
||||||
|
"/k-fet/accounts/FOO/stat/balance/list",
|
||||||
|
"/k-fet/accounts/FOO/stat/balance?format=json"
|
||||||
|
]
|
||||||
|
for url in user_urls:
|
||||||
|
resp = client.get(url)
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
resp2 = client2.get(url)
|
||||||
|
self.assertEqual(403, resp2.status_code)
|
||||||
|
|
||||||
|
# 2. FOO is a member of the team and can get these pages but BAR
|
||||||
|
# receives a Redirect response
|
||||||
|
articles_urls = [
|
||||||
|
"/k-fet/articles/{}/stat/sales/list".format(article.pk),
|
||||||
|
"/k-fet/articles/{}/stat/sales".format(article.pk)
|
||||||
|
]
|
||||||
|
for url in articles_urls:
|
||||||
|
resp = client.get(url)
|
||||||
|
self.assertEqual(200, resp.status_code)
|
||||||
|
resp2 = client2.get(url, follow=True)
|
||||||
|
self.assertRedirects(resp2, "/")
|
57
kfet/urls.py
57
kfet/urls.py
|
@ -69,28 +69,19 @@ urlpatterns = [
|
||||||
name='kfet.account.negative'),
|
name='kfet.account.negative'),
|
||||||
|
|
||||||
# Account - Statistics
|
# Account - Statistics
|
||||||
url('^accounts/(?P<trigramme>.{3})/stat/last/$',
|
url(r'^accounts/(?P<trigramme>.{3})/stat/operations/list$',
|
||||||
views.AccountStatLastAll.as_view(),
|
views.AccountStatOperationList.as_view(),
|
||||||
name = 'kfet.account.stat.last'),
|
name='kfet.account.stat.operation.list'),
|
||||||
url('^accounts/(?P<trigramme>.{3})/stat/last/month/$',
|
url(r'^accounts/(?P<trigramme>.{3})/stat/operations$',
|
||||||
views.AccountStatLastMonth.as_view(),
|
views.AccountStatOperation.as_view(),
|
||||||
name = 'kfet.account.stat.last.month'),
|
name='kfet.account.stat.operation'),
|
||||||
url('^accounts/(?P<trigramme>.{3})/stat/last/week/$',
|
|
||||||
views.AccountStatLastWeek.as_view(),
|
|
||||||
name = 'kfet.account.stat.last.week'),
|
|
||||||
url('^accounts/(?P<trigramme>.{3})/stat/last/day/$',
|
|
||||||
views.AccountStatLastDay.as_view(),
|
|
||||||
name = 'kfet.account.stat.last.day'),
|
|
||||||
|
|
||||||
url('^accounts/(?P<trigramme>.{3})/stat/balance/$',
|
url(r'^accounts/(?P<trigramme>.{3})/stat/balance/list$',
|
||||||
views.AccountStatBalanceAll.as_view(),
|
views.AccountStatBalanceList.as_view(),
|
||||||
|
name='kfet.account.stat.balance.list'),
|
||||||
|
url(r'^accounts/(?P<trigramme>.{3})/stat/balance$',
|
||||||
|
views.AccountStatBalance.as_view(),
|
||||||
name='kfet.account.stat.balance'),
|
name='kfet.account.stat.balance'),
|
||||||
url('^accounts/(?P<trigramme>.{3})/stat/balance/d/(?P<nb_date>\d*)/$',
|
|
||||||
views.AccountStatBalance.as_view(),
|
|
||||||
name = 'kfet.account.stat.balance.days'),
|
|
||||||
url('^accounts/(?P<trigramme>.{3})/stat/balance/anytime/$',
|
|
||||||
views.AccountStatBalance.as_view(),
|
|
||||||
name = 'kfet.account.stat.balance.anytime'),
|
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# Checkout urls
|
# Checkout urls
|
||||||
|
@ -134,6 +125,14 @@ urlpatterns = [
|
||||||
# Article urls
|
# Article urls
|
||||||
# -----
|
# -----
|
||||||
|
|
||||||
|
# Category - General
|
||||||
|
url('^categories/$',
|
||||||
|
teamkfet_required(views.CategoryList.as_view()),
|
||||||
|
name='kfet.category'),
|
||||||
|
# Category - Update
|
||||||
|
url('^categories/(?P<pk>\d+)/edit$',
|
||||||
|
teamkfet_required(views.CategoryUpdate.as_view()),
|
||||||
|
name='kfet.category.update'),
|
||||||
# Article - General
|
# Article - General
|
||||||
url('^articles/$',
|
url('^articles/$',
|
||||||
teamkfet_required(views.ArticleList.as_view()),
|
teamkfet_required(views.ArticleList.as_view()),
|
||||||
|
@ -151,18 +150,12 @@ urlpatterns = [
|
||||||
teamkfet_required(views.ArticleUpdate.as_view()),
|
teamkfet_required(views.ArticleUpdate.as_view()),
|
||||||
name='kfet.article.update'),
|
name='kfet.article.update'),
|
||||||
# Article - Statistics
|
# Article - Statistics
|
||||||
url('^articles/(?P<pk>\d+)/stat/last/$',
|
url(r'^articles/(?P<pk>\d+)/stat/sales/list$',
|
||||||
views.ArticleStatLastAll.as_view(),
|
views.ArticleStatSalesList.as_view(),
|
||||||
name = 'kfet.article.stat.last'),
|
name='kfet.article.stat.sales.list'),
|
||||||
url('^articles/(?P<pk>\d+)/stat/last/month/$',
|
url(r'^articles/(?P<pk>\d+)/stat/sales$',
|
||||||
views.ArticleStatLastMonth.as_view(),
|
views.ArticleStatSales.as_view(),
|
||||||
name = 'kfet.article.stat.last.month'),
|
name='kfet.article.stat.sales'),
|
||||||
url('^articles/(?P<pk>\d+)/stat/last/week/$',
|
|
||||||
views.ArticleStatLastWeek.as_view(),
|
|
||||||
name = 'kfet.article.stat.last.week'),
|
|
||||||
url('^articles/(?P<pk>\d+)/stat/last/day/$',
|
|
||||||
views.ArticleStatLastDay.as_view(),
|
|
||||||
name = 'kfet.article.stat.last.day'),
|
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# K-Psul urls
|
# K-Psul urls
|
||||||
|
|
707
kfet/views.py
707
kfet/views.py
|
@ -1,12 +1,14 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.views.generic import ListView, DetailView, TemplateView
|
from django.views.generic import ListView, DetailView, TemplateView
|
||||||
from django.views.generic.list import BaseListView, MultipleObjectTemplateResponseMixin
|
from django.views.generic.detail import BaseDetailView
|
||||||
from django.views.generic.detail import BaseDetailView, SingleObjectTemplateResponseMixin
|
from django.views.generic.edit import CreateView, UpdateView
|
||||||
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
|
|
||||||
from django.core.urlresolvers import reverse, reverse_lazy
|
from django.core.urlresolvers import reverse, reverse_lazy
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
|
@ -29,7 +31,7 @@ from kfet.models import (
|
||||||
Account, Checkout, Article, AccountNegative,
|
Account, Checkout, Article, AccountNegative,
|
||||||
CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
|
CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
|
||||||
InventoryArticle, Order, OrderArticle, Operation, OperationGroup,
|
InventoryArticle, Order, OrderArticle, Operation, OperationGroup,
|
||||||
TransferGroup, Transfer)
|
TransferGroup, Transfer, ArticleCategory)
|
||||||
from kfet.forms import (
|
from kfet.forms import (
|
||||||
AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm,
|
AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm,
|
||||||
UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm,
|
UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm,
|
||||||
|
@ -39,7 +41,7 @@ from kfet.forms import (
|
||||||
KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm,
|
KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm,
|
||||||
KPsulOperationFormSet, AddcostForm, FilterHistoryForm,
|
KPsulOperationFormSet, AddcostForm, FilterHistoryForm,
|
||||||
TransferFormSet, InventoryArticleForm, OrderArticleForm,
|
TransferFormSet, InventoryArticleForm, OrderArticleForm,
|
||||||
OrderArticleToInventoryForm, KFetConfigForm
|
OrderArticleToInventoryForm, CategoryForm, KFetConfigForm
|
||||||
)
|
)
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from kfet import consumers
|
from kfet import consumers
|
||||||
|
@ -48,10 +50,8 @@ from decimal import Decimal
|
||||||
import django_cas_ng
|
import django_cas_ng
|
||||||
import heapq
|
import heapq
|
||||||
import statistics
|
import statistics
|
||||||
from .statistic import daynames, monthnames, weeknames, \
|
from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes
|
||||||
lastdays, lastweeks, lastmonths, \
|
|
||||||
this_morning, this_monday_morning, this_first_month_day, \
|
|
||||||
tot_ventes
|
|
||||||
|
|
||||||
class Home(TemplateView):
|
class Home(TemplateView):
|
||||||
template_name = "kfet/home.html"
|
template_name = "kfet/home.html"
|
||||||
|
@ -724,12 +724,44 @@ class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView):
|
||||||
form.instance.amount_taken = getAmountTaken(form.instance)
|
form.instance.amount_taken = getAmountTaken(form.instance)
|
||||||
return super(CheckoutStatementUpdate, self).form_valid(form)
|
return super(CheckoutStatementUpdate, self).form_valid(form)
|
||||||
|
|
||||||
|
# -----
|
||||||
|
# Category views
|
||||||
|
# -----
|
||||||
|
|
||||||
|
|
||||||
|
# Category - General
|
||||||
|
class CategoryList(ListView):
|
||||||
|
queryset = (ArticleCategory.objects
|
||||||
|
.prefetch_related('articles')
|
||||||
|
.order_by('name'))
|
||||||
|
template_name = 'kfet/category.html'
|
||||||
|
context_object_name = 'categories'
|
||||||
|
|
||||||
|
|
||||||
|
# Category - Update
|
||||||
|
class CategoryUpdate(SuccessMessageMixin, UpdateView):
|
||||||
|
model = ArticleCategory
|
||||||
|
template_name = 'kfet/category_update.html'
|
||||||
|
form_class = CategoryForm
|
||||||
|
success_url = reverse_lazy('kfet.category')
|
||||||
|
success_message = "Informations mises à jour pour la catégorie : %(name)s"
|
||||||
|
|
||||||
|
# Surcharge de la validation
|
||||||
|
def form_valid(self, form):
|
||||||
|
# Checking permission
|
||||||
|
if not self.request.user.has_perm('kfet.change_articlecategory'):
|
||||||
|
form.add_error(None, 'Permission refusée')
|
||||||
|
return self.form_invalid(form)
|
||||||
|
|
||||||
|
# Updating
|
||||||
|
return super(CategoryUpdate, self).form_valid(form)
|
||||||
|
|
||||||
# -----
|
# -----
|
||||||
# Article views
|
# Article views
|
||||||
# -----
|
# -----
|
||||||
|
|
||||||
# Article - General
|
|
||||||
|
|
||||||
|
# Article - General
|
||||||
class ArticleList(ListView):
|
class ArticleList(ListView):
|
||||||
queryset = (Article.objects
|
queryset = (Article.objects
|
||||||
.select_related('category')
|
.select_related('category')
|
||||||
|
@ -740,8 +772,8 @@ class ArticleList(ListView):
|
||||||
template_name = 'kfet/article.html'
|
template_name = 'kfet/article.html'
|
||||||
context_object_name = 'articles'
|
context_object_name = 'articles'
|
||||||
|
|
||||||
# Article - Create
|
|
||||||
|
|
||||||
|
# Article - Create
|
||||||
class ArticleCreate(SuccessMessageMixin, CreateView):
|
class ArticleCreate(SuccessMessageMixin, CreateView):
|
||||||
model = Article
|
model = Article
|
||||||
template_name = 'kfet/article_create.html'
|
template_name = 'kfet/article_create.html'
|
||||||
|
@ -785,8 +817,8 @@ class ArticleCreate(SuccessMessageMixin, CreateView):
|
||||||
# Creating
|
# Creating
|
||||||
return super(ArticleCreate, self).form_valid(form)
|
return super(ArticleCreate, self).form_valid(form)
|
||||||
|
|
||||||
# Article - Read
|
|
||||||
|
|
||||||
|
# Article - Read
|
||||||
class ArticleRead(DetailView):
|
class ArticleRead(DetailView):
|
||||||
model = Article
|
model = Article
|
||||||
template_name = 'kfet/article_read.html'
|
template_name = 'kfet/article_read.html'
|
||||||
|
@ -806,8 +838,8 @@ class ArticleRead(DetailView):
|
||||||
context['supplierarts'] = supplierarts
|
context['supplierarts'] = supplierarts
|
||||||
return context
|
return context
|
||||||
|
|
||||||
# Article - Update
|
|
||||||
|
|
||||||
|
# Article - Update
|
||||||
class ArticleUpdate(SuccessMessageMixin, UpdateView):
|
class ArticleUpdate(SuccessMessageMixin, UpdateView):
|
||||||
model = Article
|
model = Article
|
||||||
template_name = 'kfet/article_update.html'
|
template_name = 'kfet/article_update.html'
|
||||||
|
@ -935,7 +967,7 @@ def kpsul_update_addcost(request):
|
||||||
data = {
|
data = {
|
||||||
'errors': {
|
'errors': {
|
||||||
'missing_perms': get_missing_perms(required_perms,
|
'missing_perms': get_missing_perms(required_perms,
|
||||||
request.user),
|
request.user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return JsonResponse(data, status=403)
|
return JsonResponse(data, status=403)
|
||||||
|
@ -959,13 +991,16 @@ def kpsul_update_addcost(request):
|
||||||
|
|
||||||
def get_missing_perms(required_perms, user):
|
def get_missing_perms(required_perms, user):
|
||||||
missing_perms_codenames = [(perm.split('.'))[1]
|
missing_perms_codenames = [(perm.split('.'))[1]
|
||||||
for perm in required_perms if not user.has_perm(perm)]
|
for perm in required_perms
|
||||||
|
if not user.has_perm(perm)]
|
||||||
missing_perms = list(
|
missing_perms = list(
|
||||||
Permission.objects
|
Permission.objects
|
||||||
.filter(codename__in=missing_perms_codenames)
|
.filter(codename__in=missing_perms_codenames)
|
||||||
.values_list('name', flat=True))
|
.values_list('name', flat=True)
|
||||||
|
)
|
||||||
return missing_perms
|
return missing_perms
|
||||||
|
|
||||||
|
|
||||||
@teamkfet_required
|
@teamkfet_required
|
||||||
def kpsul_perform_operations(request):
|
def kpsul_perform_operations(request):
|
||||||
# Initializing response data
|
# Initializing response data
|
||||||
|
@ -1002,24 +1037,26 @@ def kpsul_perform_operations(request):
|
||||||
to_addcost_for_balance = 0 # For balance of addcost_for
|
to_addcost_for_balance = 0 # For balance of addcost_for
|
||||||
to_checkout_balance = 0 # For balance of selected checkout
|
to_checkout_balance = 0 # For balance of selected checkout
|
||||||
to_articles_stocks = defaultdict(lambda: 0) # For stocks articles
|
to_articles_stocks = defaultdict(lambda: 0) # For stocks articles
|
||||||
is_addcost = (addcost_for and addcost_amount
|
is_addcost = all((addcost_for, addcost_amount,
|
||||||
and addcost_for != operationgroup.on_acc)
|
addcost_for != operationgroup.on_acc))
|
||||||
need_comment = operationgroup.on_acc.need_comment
|
need_comment = operationgroup.on_acc.need_comment
|
||||||
|
|
||||||
# Filling data of each operations + operationgroup + calculating other stuffs
|
# Filling data of each operations
|
||||||
|
# + operationgroup + calculating other stuffs
|
||||||
for operation in operations:
|
for operation in operations:
|
||||||
if operation.type == Operation.PURCHASE:
|
if operation.type == Operation.PURCHASE:
|
||||||
operation.amount = - operation.article.price * operation.article_nb
|
operation.amount = - operation.article.price * operation.article_nb
|
||||||
if is_addcost:
|
if is_addcost & operation.article.category.has_addcost:
|
||||||
operation.addcost_for = addcost_for
|
operation.addcost_for = addcost_for
|
||||||
operation.addcost_amount = addcost_amount * operation.article_nb
|
operation.addcost_amount = addcost_amount \
|
||||||
|
* operation.article_nb
|
||||||
operation.amount -= operation.addcost_amount
|
operation.amount -= operation.addcost_amount
|
||||||
to_addcost_for_balance += operation.addcost_amount
|
to_addcost_for_balance += operation.addcost_amount
|
||||||
if operationgroup.on_acc.is_cash:
|
if operationgroup.on_acc.is_cash:
|
||||||
to_checkout_balance += -operation.amount
|
to_checkout_balance += -operation.amount
|
||||||
if operationgroup.on_acc.is_cof:
|
if operationgroup.on_acc.is_cof:
|
||||||
if is_addcost:
|
if is_addcost and operation.article.category.has_addcost:
|
||||||
operation.addcost_amount = operation.addcost_amount / cof_grant_divisor
|
operation.addcost_amount /= cof_grant_divisor
|
||||||
operation.amount = operation.amount / cof_grant_divisor
|
operation.amount = operation.amount / cof_grant_divisor
|
||||||
to_articles_stocks[operation.article] -= operation.article_nb
|
to_articles_stocks[operation.article] -= operation.article_nb
|
||||||
else:
|
else:
|
||||||
|
@ -1036,8 +1073,10 @@ def kpsul_perform_operations(request):
|
||||||
if operationgroup.on_acc.is_cof:
|
if operationgroup.on_acc.is_cof:
|
||||||
to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor
|
to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor
|
||||||
|
|
||||||
(perms, stop) = operationgroup.on_acc.perms_to_perform_operation(
|
(perms, stop) = (operationgroup.on_acc
|
||||||
|
.perms_to_perform_operation(
|
||||||
amount=operationgroup.amount)
|
amount=operationgroup.amount)
|
||||||
|
)
|
||||||
required_perms |= perms
|
required_perms |= perms
|
||||||
|
|
||||||
if need_comment:
|
if need_comment:
|
||||||
|
@ -1079,8 +1118,8 @@ def kpsul_perform_operations(request):
|
||||||
negative = AccountNegative(
|
negative = AccountNegative(
|
||||||
account=operationgroup.on_acc, start=timezone.now())
|
account=operationgroup.on_acc, start=timezone.now())
|
||||||
negative.save()
|
negative.save()
|
||||||
elif (hasattr(operationgroup.on_acc, 'negative')
|
elif (hasattr(operationgroup.on_acc, 'negative') and
|
||||||
and not operationgroup.on_acc.negative.balance_offset):
|
not operationgroup.on_acc.negative.balance_offset):
|
||||||
operationgroup.on_acc.negative.delete()
|
operationgroup.on_acc.negative.delete()
|
||||||
|
|
||||||
# Updating checkout's balance
|
# Updating checkout's balance
|
||||||
|
@ -1125,10 +1164,13 @@ def kpsul_perform_operations(request):
|
||||||
}]
|
}]
|
||||||
for operation in operations:
|
for operation in operations:
|
||||||
ope_data = {
|
ope_data = {
|
||||||
'id': operation.pk, 'type': operation.type, 'amount': operation.amount,
|
'id': operation.pk, 'type': operation.type,
|
||||||
|
'amount': operation.amount,
|
||||||
'addcost_amount': operation.addcost_amount,
|
'addcost_amount': operation.addcost_amount,
|
||||||
'addcost_for__trigramme': operation.addcost_for and addcost_for.trigramme or None,
|
'addcost_for__trigramme': (
|
||||||
'article__name': operation.article and operation.article.name or None,
|
operation.addcost_for and addcost_for.trigramme or None),
|
||||||
|
'article__name': (
|
||||||
|
operation.article and operation.article.name or None),
|
||||||
'article_nb': operation.article_nb,
|
'article_nb': operation.article_nb,
|
||||||
'group_id': operationgroup.pk,
|
'group_id': operationgroup.pk,
|
||||||
'canceled_by__trigramme': None, 'canceled_at': None,
|
'canceled_by__trigramme': None, 'canceled_at': None,
|
||||||
|
@ -1152,6 +1194,7 @@ def kpsul_perform_operations(request):
|
||||||
consumers.KPsul.group_send('kfet.kpsul', websocket_data)
|
consumers.KPsul.group_send('kfet.kpsul', websocket_data)
|
||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
|
||||||
|
|
||||||
@teamkfet_required
|
@teamkfet_required
|
||||||
def kpsul_cancel_operations(request):
|
def kpsul_cancel_operations(request):
|
||||||
# Pour la réponse
|
# Pour la réponse
|
||||||
|
@ -1400,7 +1443,8 @@ def history_json(request):
|
||||||
def kpsul_articles_data(request):
|
def kpsul_articles_data(request):
|
||||||
articles = (
|
articles = (
|
||||||
Article.objects
|
Article.objects
|
||||||
.values('id', 'name', 'price', 'stock', 'category_id', 'category__name')
|
.values('id', 'name', 'price', 'stock', 'category_id',
|
||||||
|
'category__name', 'category__has_addcost')
|
||||||
.filter(is_sold=True))
|
.filter(is_sold=True))
|
||||||
return JsonResponse({ 'articles': list(articles) })
|
return JsonResponse({ 'articles': list(articles) })
|
||||||
|
|
||||||
|
@ -1655,7 +1699,8 @@ def inventory_create(request):
|
||||||
'stock_old': article.stock,
|
'stock_old': article.stock,
|
||||||
'name' : article.name,
|
'name' : article.name,
|
||||||
'category' : article.category_id,
|
'category' : article.category_id,
|
||||||
'category__name': article.category.name
|
'category__name': article.category.name,
|
||||||
|
'box_capacity': article.box_capacity or 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
cls_formset = formset_factory(
|
cls_formset = formset_factory(
|
||||||
|
@ -2029,87 +2074,54 @@ class JSONResponseMixin(object):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class JSONDetailView(JSONResponseMixin,
|
class JSONDetailView(JSONResponseMixin, BaseDetailView):
|
||||||
BaseDetailView):
|
"""Returns a DetailView that renders a JSON."""
|
||||||
"""
|
|
||||||
Returns a DetailView that renders a JSON
|
|
||||||
"""
|
|
||||||
def render_to_response(self, context):
|
def render_to_response(self, context):
|
||||||
return self.render_to_json_response(context)
|
return self.render_to_json_response(context)
|
||||||
|
|
||||||
class HybridDetailView(JSONResponseMixin,
|
|
||||||
SingleObjectTemplateResponseMixin,
|
class PkUrlMixin(object):
|
||||||
BaseDetailView):
|
|
||||||
"""
|
def get_object(self, *args, **kwargs):
|
||||||
Returns a DetailView as an html page except if a JSON file is requested
|
get_by = self.kwargs.get(self.pk_url_kwarg)
|
||||||
by the GET method in which case it returns a JSON response.
|
return get_object_or_404(self.model, **{self.pk_url_kwarg: get_by})
|
||||||
"""
|
|
||||||
def render_to_response(self, context):
|
|
||||||
# Look for a 'format=json' GET argument
|
|
||||||
if self.request.GET.get('format') == 'json':
|
|
||||||
return self.render_to_json_response(context)
|
|
||||||
else:
|
|
||||||
return super(HybridDetailView, self).render_to_response(context)
|
|
||||||
|
|
||||||
|
|
||||||
class HybridListView(JSONResponseMixin,
|
class SingleResumeStat(JSONDetailView):
|
||||||
MultipleObjectTemplateResponseMixin,
|
"""Manifest for a kind of a stat about an object.
|
||||||
BaseListView):
|
|
||||||
"""
|
|
||||||
Returns a ListView as an html page except if a JSON file is requested
|
|
||||||
by the GET method in which case it returns a JSON response.
|
|
||||||
"""
|
|
||||||
def render_to_response(self, context):
|
|
||||||
# Look for a 'format=json' GET argument
|
|
||||||
if self.request.GET.get('format') == 'json':
|
|
||||||
return self.render_to_json_response(context)
|
|
||||||
else:
|
|
||||||
return super(HybridListView, self).render_to_response(context)
|
|
||||||
|
|
||||||
|
Returns JSON whose payload is an array containing descriptions of a stat:
|
||||||
|
url to retrieve data, label, ...
|
||||||
|
|
||||||
class ObjectResumeStat(JSONDetailView):
|
|
||||||
"""
|
"""
|
||||||
Summarize all the stats of an object
|
|
||||||
Handles JSONResponse
|
|
||||||
"""
|
|
||||||
context_object_name = ''
|
|
||||||
id_prefix = ''
|
id_prefix = ''
|
||||||
# nombre de vues à résumer
|
|
||||||
nb_stat = 2
|
|
||||||
# Le combienième est celui par defaut ?
|
|
||||||
# (entre 0 et nb_stat-1)
|
|
||||||
nb_default = 0
|
nb_default = 0
|
||||||
stat_labels = ['stat_1', 'stat_2']
|
|
||||||
stat_urls = ['url_1', 'url_2']
|
|
||||||
|
|
||||||
# sert à renverser les urls
|
stats = []
|
||||||
# utile de le surcharger quand l'url prend d'autres arguments que l'id
|
url_stat = None
|
||||||
def get_object_url_kwargs(self, **kwargs):
|
|
||||||
return {'pk': self.object.id}
|
|
||||||
|
|
||||||
def url_kwargs(self, **kwargs):
|
|
||||||
return [{}] * self.nb_stat
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
# On n'hérite pas
|
# On n'hérite pas
|
||||||
object_id = self.object.id
|
object_id = self.object.id
|
||||||
url_kwargs = self.url_kwargs()
|
|
||||||
context = {}
|
context = {}
|
||||||
stats = {}
|
stats = []
|
||||||
for i in range(self.nb_stat):
|
prefix = '{}_{}'.format(self.id_prefix, object_id)
|
||||||
stats[i] = {
|
for i, stat_def in enumerate(self.stats):
|
||||||
'label': self.stat_labels[i],
|
url_pk = getattr(self.object, self.pk_url_kwarg)
|
||||||
'btn': "btn_%s_%d_%d" % (self.id_prefix,
|
url_params_d = stat_def.get('url_params', {})
|
||||||
object_id,
|
if len(url_params_d) > 0:
|
||||||
i),
|
url_params = '?{}'.format(urlencode(url_params_d))
|
||||||
'url': reverse(self.stat_urls[i],
|
else:
|
||||||
kwargs=dict(
|
url_params = ''
|
||||||
self.get_object_url_kwargs(),
|
stats.append({
|
||||||
**url_kwargs[i]
|
'label': stat_def['label'],
|
||||||
|
'btn': 'btn_{}_{}'.format(prefix, i),
|
||||||
|
'url': '{url}{params}'.format(
|
||||||
|
url=reverse(self.url_stat, args=[url_pk]),
|
||||||
|
params=url_params,
|
||||||
),
|
),
|
||||||
),
|
})
|
||||||
}
|
|
||||||
prefix = "%s_%d" % (self.id_prefix, object_id)
|
|
||||||
context['id_prefix'] = prefix
|
context['id_prefix'] = prefix
|
||||||
context['content_id'] = "content_%s" % prefix
|
context['content_id'] = "content_%s" % prefix
|
||||||
context['stats'] = stats
|
context['stats'] = stats
|
||||||
|
@ -2124,87 +2136,84 @@ class ObjectResumeStat(JSONDetailView):
|
||||||
ID_PREFIX_ACC_BALANCE = "balance_acc"
|
ID_PREFIX_ACC_BALANCE = "balance_acc"
|
||||||
|
|
||||||
|
|
||||||
# Un résumé de toutes les vues ArticleStatBalance
|
class AccountStatBalanceList(PkUrlMixin, SingleResumeStat):
|
||||||
# REND DU JSON
|
"""Manifest for balance stats of an account."""
|
||||||
class AccountStatBalanceAll(ObjectResumeStat):
|
|
||||||
model = Account
|
model = Account
|
||||||
context_object_name = 'account'
|
context_object_name = 'account'
|
||||||
trigramme_url_kwarg = 'trigramme'
|
pk_url_kwarg = 'trigramme'
|
||||||
|
url_stat = 'kfet.account.stat.balance'
|
||||||
id_prefix = ID_PREFIX_ACC_BALANCE
|
id_prefix = ID_PREFIX_ACC_BALANCE
|
||||||
nb_stat = 5
|
stats = [
|
||||||
|
{
|
||||||
|
'label': 'Tout le temps',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '1 an',
|
||||||
|
'url_params': {'last_days': 365},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '6 mois',
|
||||||
|
'url_params': {'last_days': 183},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '3 mois',
|
||||||
|
'url_params': {'last_days': 90},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': '30 jours',
|
||||||
|
'url_params': {'last_days': 30},
|
||||||
|
},
|
||||||
|
]
|
||||||
nb_default = 0
|
nb_default = 0
|
||||||
stat_labels = ["Tout le temps", "1 an", "6 mois", "3 mois", "30 jours"]
|
|
||||||
stat_urls = ['kfet.account.stat.balance.anytime'] \
|
|
||||||
+ ['kfet.account.stat.balance.days'] * 4
|
|
||||||
|
|
||||||
def get_object(self, **kwargs):
|
def get_object(self, *args, **kwargs):
|
||||||
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
|
obj = super().get_object(*args, **kwargs)
|
||||||
return get_object_or_404(Account, trigramme=trigramme)
|
if self.request.user != obj.user:
|
||||||
|
raise PermissionDenied
|
||||||
def get_object_url_kwargs(self, **kwargs):
|
return obj
|
||||||
return {'trigramme': self.object.trigramme}
|
|
||||||
|
|
||||||
def url_kwargs(self, **kwargs):
|
|
||||||
context_list = (super(AccountStatBalanceAll, self)
|
|
||||||
.url_kwargs(**kwargs))
|
|
||||||
context_list[1] = {'nb_date': 365}
|
|
||||||
context_list[2] = {'nb_date': 183}
|
|
||||||
context_list[3] = {'nb_date': 90}
|
|
||||||
context_list[4] = {'nb_date': 30}
|
|
||||||
return context_list
|
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
return super(AccountStatBalanceAll, self).dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class AccountStatBalance(JSONDetailView):
|
class AccountStatBalance(PkUrlMixin, JSONDetailView):
|
||||||
"""
|
"""Datasets of balance of an account.
|
||||||
Returns a JSON containing the evolution a the personnal
|
|
||||||
balance of a trigramme between timezone.now() and `nb_days`
|
Operations and Transfers are taken into account.
|
||||||
ago (specified to the view as an argument)
|
|
||||||
takes into account the Operations and the Transfers
|
|
||||||
does not takes into account the balance offset
|
|
||||||
"""
|
"""
|
||||||
model = Account
|
model = Account
|
||||||
trigramme_url_kwarg = 'trigramme'
|
pk_url_kwarg = 'trigramme'
|
||||||
nb_date_url_kwargs = 'nb_date'
|
|
||||||
context_object_name = 'account'
|
context_object_name = 'account'
|
||||||
id_prefix = ""
|
|
||||||
|
|
||||||
def get_object(self, **kwargs):
|
def get_changes_list(self, last_days=None, begin_date=None, end_date=None):
|
||||||
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
|
|
||||||
return get_object_or_404(Account, trigramme=trigramme)
|
|
||||||
|
|
||||||
def get_changes_list(self, **kwargs):
|
|
||||||
account = self.object
|
account = self.object
|
||||||
nb_date = self.kwargs.get(self.nb_date_url_kwargs, None)
|
|
||||||
end_date = this_morning()
|
# prepare filters
|
||||||
if nb_date is None:
|
if last_days is not None:
|
||||||
begin_date = timezone.datetime(year=1980, month=1, day=1)
|
end_date = timezone.now()
|
||||||
anytime = True
|
begin_date = end_date - timezone.timedelta(days=last_days)
|
||||||
else:
|
|
||||||
begin_date = this_morning() \
|
# prepare querysets
|
||||||
- timezone.timedelta(days=int(nb_date))
|
|
||||||
anytime = False
|
|
||||||
# On récupère les opérations
|
|
||||||
# TODO: retirer les opgroup dont tous les op sont annulées
|
# TODO: retirer les opgroup dont tous les op sont annulées
|
||||||
opgroups = list(OperationGroup.objects
|
opegroups = OperationGroup.objects.filter(on_acc=account)
|
||||||
.filter(on_acc=account)
|
recv_transfers = Transfer.objects.filter(to_acc=account,
|
||||||
.filter(at__gte=begin_date)
|
canceled_at=None)
|
||||||
.filter(at__lte=end_date))
|
sent_transfers = Transfer.objects.filter(from_acc=account,
|
||||||
# On récupère les transferts reçus
|
canceled_at=None)
|
||||||
received_transfers = list(Transfer.objects
|
|
||||||
.filter(to_acc=account)
|
# apply filters
|
||||||
.filter(canceled_at=None)
|
if begin_date is not None:
|
||||||
.filter(group__at__gte=begin_date)
|
opegroups = opegroups.filter(at__gte=begin_date)
|
||||||
.filter(group__at__lte=end_date))
|
recv_transfers = recv_transfers.filter(group__at__gte=begin_date)
|
||||||
# On récupère les transferts émis
|
sent_transfers = sent_transfers.filter(group__at__gte=begin_date)
|
||||||
emitted_transfers = list(Transfer.objects
|
|
||||||
.filter(from_acc=account)
|
if end_date is not None:
|
||||||
.filter(canceled_at=None)
|
opegroups = opegroups.filter(at__lte=end_date)
|
||||||
.filter(group__at__gte=begin_date)
|
recv_transfers = recv_transfers.filter(group__at__lte=end_date)
|
||||||
.filter(group__at__lte=end_date))
|
sent_transfers = sent_transfers.filter(group__at__lte=end_date)
|
||||||
|
|
||||||
# On transforme tout ça en une liste de dictionnaires sous la forme
|
# On transforme tout ça en une liste de dictionnaires sous la forme
|
||||||
# {'at': date,
|
# {'at': date,
|
||||||
# 'amount': changement de la balance (négatif si diminue la balance,
|
# 'amount': changement de la balance (négatif si diminue la balance,
|
||||||
|
@ -2214,76 +2223,86 @@ class AccountStatBalance(JSONDetailView):
|
||||||
# sera mis à jour lors d'une
|
# sera mis à jour lors d'une
|
||||||
# autre passe)
|
# autre passe)
|
||||||
# }
|
# }
|
||||||
actions = [
|
|
||||||
# Maintenant (à changer si on gère autre chose que now)
|
actions = []
|
||||||
{
|
|
||||||
'at': end_date.isoformat(),
|
actions.append({
|
||||||
'amout': 0,
|
'at': (begin_date or account.created_at).isoformat(),
|
||||||
'label': "actuel",
|
'amount': 0,
|
||||||
|
'label': 'début',
|
||||||
'balance': 0,
|
'balance': 0,
|
||||||
}
|
})
|
||||||
] + [
|
actions.append({
|
||||||
{
|
'at': (end_date or timezone.now()).isoformat(),
|
||||||
'at': op.at.isoformat(),
|
'amount': 0,
|
||||||
'amount': op.amount,
|
'label': 'fin',
|
||||||
'label': str(op),
|
|
||||||
'balance': 0,
|
'balance': 0,
|
||||||
} for op in opgroups
|
})
|
||||||
|
|
||||||
|
actions += [
|
||||||
|
{
|
||||||
|
'at': ope_grp.at.isoformat(),
|
||||||
|
'amount': ope_grp.amount,
|
||||||
|
'label': str(ope_grp),
|
||||||
|
'balance': 0,
|
||||||
|
} for ope_grp in opegroups
|
||||||
] + [
|
] + [
|
||||||
{
|
{
|
||||||
'at': tr.group.at.isoformat(),
|
'at': tr.group.at.isoformat(),
|
||||||
'amount': tr.amount,
|
'amount': tr.amount,
|
||||||
'label': "%d€: %s -> %s" % (tr.amount,
|
'label': str(tr),
|
||||||
tr.from_acc.trigramme,
|
|
||||||
tr.to_acc.trigramme),
|
|
||||||
'balance': 0,
|
'balance': 0,
|
||||||
} for tr in received_transfers
|
} for tr in recv_transfers
|
||||||
] + [
|
] + [
|
||||||
{
|
{
|
||||||
'at': tr.group.at.isoformat(),
|
'at': tr.group.at.isoformat(),
|
||||||
'amount': -tr.amount,
|
'amount': -tr.amount,
|
||||||
'label': "%d€: %s -> %s" % (tr.amount,
|
'label': str(tr),
|
||||||
tr.from_acc.trigramme,
|
|
||||||
tr.to_acc.trigramme),
|
|
||||||
'balance': 0,
|
'balance': 0,
|
||||||
} for tr in emitted_transfers
|
} for tr in sent_transfers
|
||||||
]
|
|
||||||
if not anytime:
|
|
||||||
actions += [
|
|
||||||
# Date de début :
|
|
||||||
{
|
|
||||||
'at': begin_date.isoformat(),
|
|
||||||
'amount': 0,
|
|
||||||
'label': "début",
|
|
||||||
'balance': 0,
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
# Maintenant on trie la liste des actions par ordre du plus récent
|
# Maintenant on trie la liste des actions par ordre du plus récent
|
||||||
# an plus ancien et on met à jour la balance
|
# an plus ancien et on met à jour la balance
|
||||||
|
if len(actions) > 1:
|
||||||
actions = sorted(actions, key=lambda k: k['at'], reverse=True)
|
actions = sorted(actions, key=lambda k: k['at'], reverse=True)
|
||||||
actions[0]['balance'] = account.balance
|
actions[0]['balance'] = account.balance
|
||||||
for i in range(len(actions)-1):
|
for i in range(len(actions)-1):
|
||||||
actions[i+1]['balance'] = actions[i]['balance'] \
|
actions[i+1]['balance'] = \
|
||||||
- actions[i+1]['amount']
|
actions[i]['balance'] - actions[i+1]['amount']
|
||||||
return actions
|
return actions
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
context = {}
|
context = {}
|
||||||
changes = self.get_changes_list()
|
|
||||||
nb_days = self.kwargs.get(self.nb_date_url_kwargs, None)
|
last_days = self.request.GET.get('last_days', None)
|
||||||
if nb_days is None:
|
if last_days is not None:
|
||||||
nb_days_string = 'anytime'
|
last_days = int(last_days)
|
||||||
else:
|
begin_date = self.request.GET.get('begin_date', None)
|
||||||
nb_days_string = str(int(nb_days))
|
end_date = self.request.GET.get('end_date', None)
|
||||||
context['charts'] = [ { "color": "rgb(255, 99, 132)",
|
|
||||||
|
changes = self.get_changes_list(
|
||||||
|
last_days=last_days,
|
||||||
|
begin_date=begin_date, end_date=end_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
context['charts'] = [{
|
||||||
|
"color": "rgb(255, 99, 132)",
|
||||||
"label": "Balance",
|
"label": "Balance",
|
||||||
"values": changes } ]
|
"values": changes,
|
||||||
|
}]
|
||||||
context['is_time_chart'] = True
|
context['is_time_chart'] = True
|
||||||
context['min_date'] = changes[len(changes)-1]['at']
|
if len(changes) > 0:
|
||||||
|
context['min_date'] = changes[-1]['at']
|
||||||
context['max_date'] = changes[0]['at']
|
context['max_date'] = changes[0]['at']
|
||||||
# TODO: offset
|
# TODO: offset
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get_object(self, *args, **kwargs):
|
||||||
|
obj = super().get_object(*args, **kwargs)
|
||||||
|
if self.request.user != obj.user:
|
||||||
|
raise PermissionDenied
|
||||||
|
return obj
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
return super(AccountStatBalance, self).dispatch(*args, **kwargs)
|
return super(AccountStatBalance, self).dispatch(*args, **kwargs)
|
||||||
|
@ -2298,140 +2317,77 @@ ID_PREFIX_ACC_LAST_WEEKS = "last_weeks_acc"
|
||||||
ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc"
|
ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc"
|
||||||
|
|
||||||
|
|
||||||
# Un résumé de toutes les vues ArticleStatLast
|
class AccountStatOperationList(PkUrlMixin, SingleResumeStat):
|
||||||
# NE REND PAS DE JSON
|
"""Manifest for operations stats of an account."""
|
||||||
class AccountStatLastAll(ObjectResumeStat):
|
|
||||||
model = Account
|
model = Account
|
||||||
context_object_name = 'account'
|
context_object_name = 'account'
|
||||||
trigramme_url_kwarg = 'trigramme'
|
pk_url_kwarg = 'trigramme'
|
||||||
id_prefix = ID_PREFIX_ACC_LAST
|
id_prefix = ID_PREFIX_ACC_LAST
|
||||||
nb_stat = 3
|
|
||||||
nb_default = 2
|
nb_default = 2
|
||||||
stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"]
|
stats = last_stats_manifest(types=[Operation.PURCHASE])
|
||||||
stat_urls = ['kfet.account.stat.last.month',
|
url_stat = 'kfet.account.stat.operation'
|
||||||
'kfet.account.stat.last.week',
|
|
||||||
'kfet.account.stat.last.day']
|
|
||||||
|
|
||||||
def get_object(self, **kwargs):
|
def get_object(self, *args, **kwargs):
|
||||||
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
|
obj = super().get_object(*args, **kwargs)
|
||||||
return get_object_or_404(Account, trigramme=trigramme)
|
if self.request.user != obj.user:
|
||||||
|
raise PermissionDenied
|
||||||
def get_object_url_kwargs(self, **kwargs):
|
return obj
|
||||||
return {'trigramme': self.object.trigramme}
|
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
return super(AccountStatLastAll, self).dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class AccountStatLast(JSONDetailView):
|
class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView):
|
||||||
"""
|
"""Datasets of operations of an account."""
|
||||||
Returns a JSON containing the evolution a the personnal
|
|
||||||
consommation of a trigramme at the diffent dates specified
|
|
||||||
"""
|
|
||||||
model = Account
|
model = Account
|
||||||
trigramme_url_kwarg = 'trigramme'
|
pk_url_kwarg = 'trigramme'
|
||||||
context_object_name = 'account'
|
context_object_name = 'account'
|
||||||
end_date = timezone.now()
|
|
||||||
id_prefix = ""
|
id_prefix = ""
|
||||||
|
|
||||||
# doit rendre un dictionnaire des dates
|
def get_operations(self, scale, types=None):
|
||||||
# la première date correspond au début
|
|
||||||
# la dernière date est la fin de la dernière plage
|
|
||||||
def get_dates(self, **kwargs):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# doit rendre un dictionnaire des labels
|
|
||||||
# le dernier label ne sera pas utilisé
|
|
||||||
def get_labels(self, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_object(self, **kwargs):
|
|
||||||
trigramme = self.kwargs.get(self.trigramme_url_kwarg)
|
|
||||||
return get_object_or_404(Account, trigramme=trigramme)
|
|
||||||
|
|
||||||
def sort_operations(self, **kwargs):
|
|
||||||
# On récupère les dates
|
|
||||||
dates = self.get_dates()
|
|
||||||
# On ajoute la date de fin
|
|
||||||
extended_dates = dates.copy()
|
|
||||||
extended_dates[len(dates)+1] = self.end_date
|
|
||||||
# On selectionne les opérations qui correspondent
|
# On selectionne les opérations qui correspondent
|
||||||
# à 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 = (Operation.objects
|
||||||
.filter(type='purchase')
|
|
||||||
.filter(group__on_acc=self.object)
|
.filter(group__on_acc=self.object)
|
||||||
.filter(canceled_at=None)
|
.filter(canceled_at=None)
|
||||||
)
|
)
|
||||||
operations = {}
|
if types is not None:
|
||||||
for i in dates:
|
all_operations = all_operations.filter(type__in=types)
|
||||||
operations[i] = (all_operations
|
chunks = self.chunkify_qs(all_operations, scale, field='group__at')
|
||||||
.filter(group__at__gte=extended_dates[i])
|
return chunks
|
||||||
.filter(group__at__lte=extended_dates[i+1])
|
|
||||||
)
|
|
||||||
return operations
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
context = {}
|
old_ctx = super().get_context_data(*args, **kwargs)
|
||||||
nb_ventes = {}
|
context = {'labels': old_ctx['labels']}
|
||||||
# On récupère les labels des dates
|
scale = self.scale
|
||||||
context['labels'] = self.get_labels().copy()
|
|
||||||
|
types = self.request.GET.get('types', None)
|
||||||
|
if types is not None:
|
||||||
|
types = ast.literal_eval(types)
|
||||||
|
|
||||||
|
operations = self.get_operations(types=types, scale=scale)
|
||||||
# On compte les opérations
|
# On compte les opérations
|
||||||
operations = self.sort_operations()
|
nb_ventes = []
|
||||||
for i in operations:
|
for chunk in operations:
|
||||||
nb_ventes[i] = tot_ventes(operations[i])
|
nb_ventes.append(tot_ventes(chunk))
|
||||||
|
|
||||||
context['charts'] = [{"color": "rgb(255, 99, 132)",
|
context['charts'] = [{"color": "rgb(255, 99, 132)",
|
||||||
"label": "NB items achetés",
|
"label": "NB items achetés",
|
||||||
"values": nb_ventes}]
|
"values": nb_ventes}]
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get_object(self, *args, **kwargs):
|
||||||
|
obj = super().get_object(*args, **kwargs)
|
||||||
|
if self.request.user != obj.user:
|
||||||
|
raise PermissionDenied
|
||||||
|
return obj
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
return super(AccountStatLast, self).dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# Rend les achats pour ce compte des 7 derniers jours
|
|
||||||
# Aujourd'hui non compris
|
|
||||||
class AccountStatLastDay(AccountStatLast):
|
|
||||||
end_date = this_morning()
|
|
||||||
id_prefix = ID_PREFIX_ACC_LAST_DAYS
|
|
||||||
|
|
||||||
def get_dates(self, **kwargs):
|
|
||||||
return lastdays(7)
|
|
||||||
|
|
||||||
def get_labels(self, **kwargs):
|
|
||||||
days = lastdays(7)
|
|
||||||
return daynames(days)
|
|
||||||
|
|
||||||
|
|
||||||
# Rend les achats de ce compte des 7 dernières semaines
|
|
||||||
# La semaine en cours n'est pas comprise
|
|
||||||
class AccountStatLastWeek(AccountStatLast):
|
|
||||||
end_date = this_monday_morning()
|
|
||||||
id_prefix = ID_PREFIX_ACC_LAST_WEEKS
|
|
||||||
|
|
||||||
def get_dates(self, **kwargs):
|
|
||||||
return lastweeks(7)
|
|
||||||
|
|
||||||
def get_labels(self, **kwargs):
|
|
||||||
weeks = lastweeks(7)
|
|
||||||
return weeknames(weeks)
|
|
||||||
|
|
||||||
|
|
||||||
# Rend les achats de ce compte des 7 derniers mois
|
|
||||||
# Le mois en cours n'est pas compris
|
|
||||||
class AccountStatLastMonth(AccountStatLast):
|
|
||||||
end_date = this_monday_morning()
|
|
||||||
id_prefix = ID_PREFIX_ACC_LAST_MONTHS
|
|
||||||
|
|
||||||
def get_dates(self, **kwargs):
|
|
||||||
return lastmonths(7)
|
|
||||||
|
|
||||||
def get_labels(self, **kwargs):
|
|
||||||
months = lastmonths(7)
|
|
||||||
return monthnames(months)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------
|
# ------------------------
|
||||||
|
@ -2443,90 +2399,53 @@ ID_PREFIX_ART_LAST_WEEKS = "last_weeks_art"
|
||||||
ID_PREFIX_ART_LAST_MONTHS = "last_months_art"
|
ID_PREFIX_ART_LAST_MONTHS = "last_months_art"
|
||||||
|
|
||||||
|
|
||||||
# Un résumé de toutes les vues ArticleStatLast
|
class ArticleStatSalesList(SingleResumeStat):
|
||||||
# NE REND PAS DE JSON
|
"""Manifest for sales stats of an article."""
|
||||||
class ArticleStatLastAll(ObjectResumeStat):
|
|
||||||
model = Article
|
model = Article
|
||||||
context_object_name = 'article'
|
context_object_name = 'article'
|
||||||
id_prefix = ID_PREFIX_ART_LAST
|
id_prefix = ID_PREFIX_ART_LAST
|
||||||
nb_stat = 3
|
|
||||||
nb_default = 2
|
nb_default = 2
|
||||||
stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"]
|
url_stat = 'kfet.article.stat.sales'
|
||||||
stat_urls = ['kfet.article.stat.last.month',
|
stats = last_stats_manifest()
|
||||||
'kfet.article.stat.last.week',
|
|
||||||
'kfet.article.stat.last.day']
|
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(teamkfet_required)
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
return super(ArticleStatLastAll, self).dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ArticleStatLast(JSONDetailView):
|
class ArticleStatSales(ScaleMixin, JSONDetailView):
|
||||||
"""
|
"""Datasets of sales of an article."""
|
||||||
Returns a JSON containing the consommation
|
|
||||||
of an article at the diffent dates precised
|
|
||||||
"""
|
|
||||||
model = Article
|
model = Article
|
||||||
context_object_name = 'article'
|
context_object_name = 'article'
|
||||||
end_date = timezone.now()
|
|
||||||
id_prefix = ""
|
|
||||||
|
|
||||||
def render_to_response(self, context):
|
def get_context_data(self, *args, **kwargs):
|
||||||
# Look for a 'format=json' GET argument
|
old_ctx = super().get_context_data(*args, **kwargs)
|
||||||
if self.request.GET.get('format') == 'json':
|
context = {'labels': old_ctx['labels']}
|
||||||
return self.render_to_json_response(context)
|
scale = self.scale
|
||||||
else:
|
|
||||||
return super(ArticleStatLast, self).render_to_response(context)
|
|
||||||
|
|
||||||
# doit rendre un dictionnaire des dates
|
|
||||||
# la première date correspond au début
|
|
||||||
# la dernière date est la fin de la dernière plage
|
|
||||||
def get_dates(self, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# doit rendre un dictionnaire des labels
|
|
||||||
# le dernier label ne sera pas utilisé
|
|
||||||
def get_labels(self, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = {}
|
|
||||||
# On récupère les labels des dates
|
|
||||||
context['labels'] = self.get_labels().copy()
|
|
||||||
# On récupère les dates
|
|
||||||
dates = self.get_dates()
|
|
||||||
# On ajoute la date de fin
|
|
||||||
extended_dates = dates.copy()
|
|
||||||
extended_dates[len(dates)+1] = self.end_date
|
|
||||||
# On selectionne les opérations qui correspondent
|
# On selectionne les opérations qui correspondent
|
||||||
# à 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(type='purchase')
|
Operation.objects
|
||||||
.filter(article=self.object)
|
.filter(type=Operation.PURCHASE,
|
||||||
.filter(canceled_at=None)
|
article=self.object,
|
||||||
|
canceled_at=None,
|
||||||
)
|
)
|
||||||
operations = {}
|
|
||||||
for i in dates:
|
|
||||||
operations[i] = (all_operations
|
|
||||||
.filter(group__at__gte=extended_dates[i])
|
|
||||||
.filter(group__at__lte=extended_dates[i+1])
|
|
||||||
)
|
)
|
||||||
|
chunks = self.chunkify_qs(all_operations, scale, field='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 i in operations:
|
for qs in chunks:
|
||||||
nb_ventes[i] = tot_ventes(operations[i])
|
nb_ventes.append(
|
||||||
nb_liq[i] = tot_ventes(
|
tot_ventes(qs))
|
||||||
operations[i]
|
nb_liq.append(
|
||||||
.filter(group__on_acc__trigramme='LIQ')
|
tot_ventes(qs.filter(group__on_acc__trigramme='LIQ')))
|
||||||
)
|
nb_accounts.append(
|
||||||
nb_accounts[i] = tot_ventes(
|
tot_ventes(qs.exclude(group__on_acc__trigramme='LIQ')))
|
||||||
operations[i]
|
|
||||||
.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},
|
||||||
|
@ -2538,48 +2457,6 @@ class ArticleStatLast(JSONDetailView):
|
||||||
"values": nb_accounts}]
|
"values": nb_accounts}]
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(teamkfet_required)
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
return super(ArticleStatLast, self).dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# Rend les ventes des 7 derniers jours
|
|
||||||
# Aujourd'hui non compris
|
|
||||||
class ArticleStatLastDay(ArticleStatLast):
|
|
||||||
end_date = this_morning()
|
|
||||||
id_prefix = ID_PREFIX_ART_LAST_DAYS
|
|
||||||
|
|
||||||
def get_dates(self, **kwargs):
|
|
||||||
return lastdays(7)
|
|
||||||
|
|
||||||
def get_labels(self, **kwargs):
|
|
||||||
days = lastdays(7)
|
|
||||||
return daynames(days)
|
|
||||||
|
|
||||||
|
|
||||||
# Rend les ventes de 7 dernières semaines
|
|
||||||
# La semaine en cours n'est pas comprise
|
|
||||||
class ArticleStatLastWeek(ArticleStatLast):
|
|
||||||
end_date = this_monday_morning()
|
|
||||||
id_prefix = ID_PREFIX_ART_LAST_WEEKS
|
|
||||||
|
|
||||||
def get_dates(self, **kwargs):
|
|
||||||
return lastweeks(7)
|
|
||||||
|
|
||||||
def get_labels(self, **kwargs):
|
|
||||||
weeks = lastweeks(7)
|
|
||||||
return weeknames(weeks)
|
|
||||||
|
|
||||||
|
|
||||||
# Rend les ventes des 7 derniers mois
|
|
||||||
# Le mois en cours n'est pas compris
|
|
||||||
class ArticleStatLastMonth(ArticleStatLast):
|
|
||||||
end_date = this_monday_morning()
|
|
||||||
id_prefix = ID_PREFIX_ART_LAST_MONTHS
|
|
||||||
|
|
||||||
def get_dates(self, **kwargs):
|
|
||||||
return lastmonths(7)
|
|
||||||
|
|
||||||
def get_labels(self, **kwargs):
|
|
||||||
months = lastmonths(7)
|
|
||||||
return monthnames(months)
|
|
||||||
|
|
|
@ -21,3 +21,4 @@ django-widget-tweaks==1.4.1
|
||||||
git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail
|
git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail
|
||||||
ldap3
|
ldap3
|
||||||
git+https://github.com/Aureplop/channels.git#egg=channels
|
git+https://github.com/Aureplop/channels.git#egg=channels
|
||||||
|
python-dateutil
|
||||||
|
|
Loading…
Reference in a new issue