diff --git a/cof/urls.py b/cof/urls.py
index 3cc4dfdd..aaab8271 100644
--- a/cof/urls.py
+++ b/cof/urls.py
@@ -87,7 +87,7 @@ urlpatterns = [
url(r'^jsreverse/$', cache_page(3600)(urls_js), name='js_reverse'),
]
-if settings.DEBUG:
+if 'debug_toolbar' in settings.INSTALLED_APPS:
import debug_toolbar
urlpatterns += [
url(r'^__debug__/', include(debug_toolbar.urls)),
diff --git a/kfet/forms.py b/kfet/forms.py
index 0fc02dd3..7acd0880 100644
--- a/kfet/forms.py
+++ b/kfet/forms.py
@@ -233,6 +233,16 @@ class CheckoutStatementUpdateForm(forms.ModelForm):
model = CheckoutStatement
exclude = ['by', 'at', 'checkout', 'amount_error', 'amount_taken']
+
+# -----
+# Category
+# -----
+
+class CategoryForm(forms.ModelForm):
+ class Meta:
+ model = ArticleCategory
+ fields = ['name', 'has_addcost']
+
# -----
# Article forms
# -----
@@ -463,7 +473,7 @@ class InventoryArticleForm(forms.Form):
queryset = Article.objects.all(),
widget = forms.HiddenInput(),
)
- stock_new = forms.IntegerField(required = False)
+ stock_new = forms.IntegerField(required=False)
def __init__(self, *args, **kwargs):
super(InventoryArticleForm, self).__init__(*args, **kwargs)
@@ -472,6 +482,7 @@ class InventoryArticleForm(forms.Form):
self.stock_old = kwargs['initial']['stock_old']
self.category = kwargs['initial']['category']
self.category_name = kwargs['initial']['category__name']
+ self.box_capacity = kwargs['initial']['box_capacity']
# -----
# Order forms
diff --git a/kfet/migrations/0052_category_addcost.py b/kfet/migrations/0052_category_addcost.py
new file mode 100644
index 00000000..83346a1a
--- /dev/null
+++ b/kfet/migrations/0052_category_addcost.py
@@ -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'),
+ ),
+ ]
diff --git a/kfet/models.py b/kfet/models.py
index c039ab06..cb8c324b 100644
--- a/kfet/models.py
+++ b/kfet/models.py
@@ -338,13 +338,20 @@ class CheckoutStatement(models.Model):
balance=F('balance') - last_statement.balance_new + self.balance_new)
super(CheckoutStatement, self).save(*args, **kwargs)
+
@python_2_unicode_compatible
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):
return self.name
+
@python_2_unicode_compatible
class Article(models.Model):
name = models.CharField("nom", max_length = 45)
@@ -491,24 +498,29 @@ class TransferGroup(models.Model):
related_name = "+",
blank = True, null = True, default = None)
+
class Transfer(models.Model):
group = models.ForeignKey(
- TransferGroup, on_delete = models.PROTECT,
- related_name = "transfers")
+ TransferGroup, on_delete=models.PROTECT,
+ related_name="transfers")
from_acc = models.ForeignKey(
- Account, on_delete = models.PROTECT,
- related_name = "transfers_from")
+ Account, on_delete=models.PROTECT,
+ related_name="transfers_from")
to_acc = models.ForeignKey(
- Account, on_delete = models.PROTECT,
- related_name = "transfers_to")
- amount = models.DecimalField(max_digits = 6, decimal_places = 2)
+ Account, on_delete=models.PROTECT,
+ related_name="transfers_to")
+ amount = models.DecimalField(max_digits=6, decimal_places=2)
# Optional
canceled_by = models.ForeignKey(
- Account, on_delete = models.PROTECT,
- null = True, blank = True, default = None,
- related_name = "+")
+ Account, on_delete=models.PROTECT,
+ null=True, blank=True, default=None,
+ related_name="+")
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):
on_acc = models.ForeignKey(
diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css
index 563d3839..f21fdaba 100644
--- a/kfet/static/kfet/css/index.css
+++ b/kfet/static/kfet/css/index.css
@@ -32,6 +32,7 @@ textarea {
.table {
margin-bottom:0;
+ border-bottom:1px solid #ddd;
}
.table {
@@ -105,6 +106,7 @@ textarea {
.panel-md-margin{
background-color: white;
+ overflow:hidden;
padding-left: 15px;
padding-right: 15px;
padding-bottom: 15px;
@@ -230,6 +232,9 @@ textarea {
height:28px;
margin:3px 0px;
}
+ .content-center .auth-form {
+ margin:15px;
+}
/*
* Pages formulaires seuls
@@ -549,3 +554,18 @@ thead .tooltip {
.help-block {
padding-top: 15px;
}
+
+/* Inventaires */
+
+.inventory_modified {
+ background:rgba(236,100,0,0.15);
+}
+
+.stock_diff {
+ padding-left: 5px;
+ color:#C8102E;
+}
+
+.inventory_update {
+ display:none;
+}
diff --git a/kfet/static/kfet/css/jconfirm-kfet.css b/kfet/static/kfet/css/jconfirm-kfet.css
index 6c27f77c..bb8ba849 100644
--- a/kfet/static/kfet/css/jconfirm-kfet.css
+++ b/kfet/static/kfet/css/jconfirm-kfet.css
@@ -28,6 +28,7 @@
.jconfirm .jconfirm-box .content {
border-bottom:1px solid #ddd;
+ padding:5px 10px;
}
.jconfirm .jconfirm-box input {
diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js
index 4f73e20b..8fccf247 100644
--- a/kfet/static/kfet/js/kfet.api.js
+++ b/kfet/static/kfet/js/kfet.api.js
@@ -454,7 +454,7 @@ class ArticleCategory extends ModelObject {
* @see {@link Models.ModelObject.props|ModelObject.props}
*/
static get props() {
- return ['id', 'name'];
+ return ['id', 'name', 'has_addcost'];
}
/**
@@ -463,7 +463,7 @@ class ArticleCategory extends ModelObject {
* @see {@link Models.ModelObject.default_data|ModelObject.default_data}
*/
static get default_data() {
- return {'id': 0, 'name': ''};
+ return {'id': 0, 'name': '', 'has_addcost': true};
}
/**
@@ -531,7 +531,7 @@ class Article extends ModelObject {
set price(v) { this._price = floatCheck(v); }
is_low_stock(nb) {
- return (-5 <= this.stock - nb && this.stock - nb <= 5);
+ return (-5 <= this.stock - nb && this.stock - nb <= 5);
}
}
@@ -541,12 +541,12 @@ class Article extends ModelObject {
* @memberof Models
*/
class TreeNode {
-
+
constructor(type, content) {
this.modelname = type;
this.content = content;
this.parent = null;
- this.children = [];
+ this.children = [];
}
}
@@ -557,7 +557,7 @@ class TreeNode {
*/
class ModelForest {
- /**
+ /**
* Dictionary associating types to classes
* @abstract
* @type {Object}
@@ -585,7 +585,7 @@ class ModelForest {
this.from(datalist || []);
}
- /**
+ /**
* Fetches an object from the instance data, or creates it if
* it does not exist yet.
* If direction >= 0, parent objects are created recursively.
@@ -609,7 +609,7 @@ class ModelForest {
if (direction <= 0) {
if (data.parent) {
- var parent = this.get_or_create(data.parent, -1);
+ var parent = this.get_or_create(data.parent, -1);
node.parent = parent;
parent.children.push(node);
} else {
@@ -628,7 +628,7 @@ class ModelForest {
return node ;
}
- /**
+ /**
* Resets then populates the instance with the given data.
* @param {Object[]} datalist
*/
@@ -639,7 +639,7 @@ class ModelForest {
}
}
- /**
+ /**
* Removes all Models.TreeNode from the tree.
*/
clear() {
@@ -647,7 +647,7 @@ class ModelForest {
}
/**
- * Renders a node (and all its offspring) and returns the
+ * Renders a node (and all its offspring) and returns the
* corresponding jQuery object.
* @param {Models.TreeNode} node
* @param {Object} templates Templates to render each model
@@ -773,7 +773,7 @@ class ModelForest {
* @memberof Models
*/
class APIModelForest extends ModelForest {
-
+
/**
* Request url to fill the model.
* @abstract
@@ -782,7 +782,7 @@ class APIModelForest extends ModelForest {
static get url_model() {}
/**
- * Fills the instance with distant data. It sends a GET HTTP request to
+ * Fills the instance with distant data. It sends a GET HTTP request to
* {@link Models.APIModelForest#url_model}.
* @param {object} [api_options] Additional data appended to the request.
*/
@@ -810,7 +810,7 @@ class ArticleList extends APIModelForest {
* @default {'article': Article,
'category': ArticleCategory}
*/
- static get models() {
+ static get models() {
return {'article': Article,
'category': ArticleCategory};
}
@@ -1114,7 +1114,7 @@ class ArticleFormatter extends Formatter {
static get _data_stock() {
return {
- 'default': '', 'low': 'low',
+ 'default': '', 'low': 'low',
'ok': 'ok', 'neg': 'neg',
};
}
diff --git a/kfet/static/kfet/js/statistic.js b/kfet/static/kfet/js/statistic.js
index 7ab56f1d..f210c11d 100644
--- a/kfet/static/kfet/js/statistic.js
+++ b/kfet/static/kfet/js/statistic.js
@@ -1,10 +1,10 @@
(function($){
window.StatsGroup = function (url, target) {
// a class to properly display statictics
-
+
// url : points to an ObjectResumeStat that lists the options through JSON
// target : element of the DOM where to put the stats
-
+
var self = this;
var element = $(target);
var content = $("
");
@@ -22,28 +22,29 @@
return array;
}
- function handleTimeChart (dict) {
+ function handleTimeChart (data) {
// 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++) {
var source = data[i];
- data[i] = { x: new Date(source.at),
- y: source.balance,
- label: source.label }
+ chart_data[i] = {
+ x: new Date(source.at),
+ y: source.balance,
+ label: source.label,
+ }
}
- return data;
+ return chart_data;
}
-
+
function showStats () {
// CALLBACK : called when a button is selected
-
+
// shows the focus on the correct button
buttons.find(".focus").removeClass("focus");
$(this).addClass("focus");
// loads data and shows it
- $.getJSON(this.stats_target_url + "?format=json",
- displayStats);
+ $.getJSON(this.stats_target_url, {format: 'json'}, displayStats);
}
function displayStats (data) {
@@ -51,14 +52,14 @@
var chart_datasets = [];
var charts = dictToArray(data.charts);
-
+
// are the points indexed by timestamps?
var is_time_chart = data.is_time_chart || false;
// reads the charts data
for (var i = 0; i < charts.length; i++) {
var chart = charts[i];
-
+
// format the data
var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 1);
@@ -78,6 +79,7 @@
var chart_options =
{
responsive: true,
+ maintainAspectRatio: false,
tooltips: {
mode: 'index',
intersect: false,
@@ -130,25 +132,25 @@
type: 'line',
options: chart_options,
data: {
- labels: dictToArray(data.labels, 1),
+ labels: (data.labels || []).slice(1),
datasets: chart_datasets,
}
};
// saves the previous charts to be destroyed
var prev_chart = content.children();
-
+
+ // clean
+ prev_chart.remove();
+
// creates a blank canvas element and attach it to the DOM
- var canvas = $("
");
+ var canvas = $("");
content.append(canvas);
// create the chart
var chart = new Chart(canvas, chart_model);
-
- // clean
- prev_chart.remove();
}
-
+
// initialize the interface
function initialize (data) {
// creates the bar with the buttons
@@ -158,8 +160,8 @@
"aria-label": "select-period"});
var to_click;
- var context = dictToArray(data.stats);
-
+ var context = data.stats;
+
for (var i = 0; i < context.length; i++) {
// creates the button
var btn_wrapper = $("",
@@ -191,7 +193,7 @@
// constructor
(function () {
- $.getJSON(url + "?format=json", initialize);
+ $.getJSON(url, {format: 'json'}, initialize);
})();
};
})(jQuery);
diff --git a/kfet/statistic.py b/kfet/statistic.py
index 09f9e935..fe948f73 100644
--- a/kfet/statistic.py
+++ b/kfet/statistic.py
@@ -1,98 +1,155 @@
# -*- 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.db.models import Sum
-KFET_WAKES_UP_AT = 7
-
-# 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
+KFET_WAKES_UP_AT = time(7, 0)
-# donne le nom des semaines une liste de dates
-# dans un dico ordonné
-def weeknames(dates):
- names = {}
- for i in dates:
- names[i] = dates[i].strftime("Semaine %W")
- return names
+def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT):
+ """datetime wrapper with time offset."""
+ return datetime.combine(date(year, month, day), start_at)
-# donne le nom des mois d'une liste de dates
-# dans un dico ordonné
-def monthnames(dates):
- names = {}
- for i in dates:
- names[i] = dates[i].strftime("%B")
- return names
+def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT):
+ kfet_dt = kfet_day(year=dt.year, month=dt.month, day=dt.day)
+ if dt.time() < start_at:
+ kfet_dt -= timedelta(days=1)
+ return kfet_dt
-# rend les dates des nb derniers jours
-# dans l'ordre chronologique
-# aujourd'hui compris
-# nb = 1 : rend hier
-def lastdays(nb):
- morning = this_morning()
- days = {}
- for i in range(1, nb+1):
- days[i] = morning - timezone.timedelta(days=nb - i + 1)
- return days
+class Scale(object):
+ name = None
+ step = None
+
+ def __init__(self, n_steps=0, begin=None, end=None,
+ last=False, std_chunk=True):
+ self.std_chunk = std_chunk
+ if last:
+ end = timezone.now()
+
+ 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):
- monday_morning = this_monday_morning()
- mondays = {}
- for i in range(1, nb+1):
- mondays[i] = monday_morning \
- - timezone.timedelta(days=7*(nb - i + 1))
- return mondays
+class DayScale(Scale):
+ name = 'day'
+ step = timedelta(days=1)
+ label_fmt = '%A'
+
+ @classmethod
+ def get_chunk_start(cls, dt):
+ return to_kfet_day(dt)
-def lastmonths(nb):
- first_month_day = this_first_month_day()
- first_days = {}
- this_year = first_month_day.year
- this_month = first_month_day.month
- for i in range(1, nb+1):
- month = ((this_month - 1 - (nb - i)) % 12) + 1
- year = this_year + (nb - i) // 12
- first_days[i] = timezone.datetime(year=year,
- month=month,
- day=1,
- hour=KFET_WAKES_UP_AT)
- return first_days
+class WeekScale(Scale):
+ name = 'week'
+ step = timedelta(days=7)
+ label_fmt = 'Semaine %W'
+
+ @classmethod
+ def get_chunk_start(cls, dt):
+ dt_kfet = to_kfet_day(dt)
+ offset = timedelta(days=dt_kfet.weekday())
+ return dt_kfet - offset
-def this_first_month_day():
- now = timezone.now()
- first_day = timezone.datetime(year=now.year,
- month=now.month,
- day=1,
- hour=KFET_WAKES_UP_AT)
- return first_day
+class MonthScale(Scale):
+ name = 'month'
+ step = relativedelta(months=1)
+ label_fmt = '%B'
+
+ @classmethod
+ def get_chunk_start(cls, dt):
+ return to_kfet_day(dt).replace(day=1)
-def this_monday_morning():
- now = timezone.now()
- monday = now - timezone.timedelta(days=now.isoweekday()-1)
- monday_morning = timezone.datetime(year=monday.year,
- month=monday.month,
- day=monday.day,
- hour=KFET_WAKES_UP_AT)
- return monday_morning
+def stat_manifest(scales_def=None, scale_args=None, scale_prefix=None,
+ **other_url_params):
+ if scale_prefix is None:
+ scale_prefix = 'scale_'
+ if scales_def is None:
+ scales_def = []
+ if scale_args is None:
+ 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():
- now = timezone.now()
- morning = timezone.datetime(year=now.year,
- month=now.month,
- day=now.day,
- hour=KFET_WAKES_UP_AT)
- return morning
+def last_stats_manifest(scales_def=None, scale_args=None, scale_prefix=None,
+ **url_params):
+ scales_def = [
+ ('Derniers mois', MonthScale, ),
+ ('Dernières semaines', WeekScale, ),
+ ('Derniers jours', DayScale, ),
+ ]
+ 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
@@ -100,3 +157,78 @@ def this_morning():
def tot_ventes(queryset):
res = queryset.aggregate(Sum('article_nb'))['article_nb__sum']
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
+ ]
diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html
index 50ab7f20..3c2ccbcd 100644
--- a/kfet/templates/kfet/account_read.html
+++ b/kfet/templates/kfet/account_read.html
@@ -13,12 +13,17 @@
{% if account.user == request.user %}
-
{% endif %}
@@ -66,22 +71,22 @@
{% if account.user == request.user %}
Statistiques
-
-
{% endif %}
diff --git a/kfet/templates/kfet/account_read_title.html b/kfet/templates/kfet/account_read_title.html
deleted file mode 100644
index 6712ed77..00000000
--- a/kfet/templates/kfet/account_read_title.html
+++ /dev/null
@@ -1,5 +0,0 @@
-{% if account.user == request.user %}
- Mon compte
-{% else %}
- Informations du compte {{ account.trigramme }}
-{% endif %}
diff --git a/kfet/templates/kfet/article.html b/kfet/templates/kfet/article.html
index 17c831df..123f4cfa 100644
--- a/kfet/templates/kfet/article.html
+++ b/kfet/templates/kfet/article.html
@@ -16,6 +16,9 @@
Nouvel article
+
+ Catégories
+
diff --git a/kfet/templates/kfet/article_read.html b/kfet/templates/kfet/article_read.html
index 35b484a5..6fe025f6 100644
--- a/kfet/templates/kfet/article_read.html
+++ b/kfet/templates/kfet/article_read.html
@@ -1,6 +1,11 @@
{% extends 'kfet/base.html' %}
{% load staticfiles %}
+{% block extra_head %}
+
+
+{% endblock %}
+
{% block title %}Informations sur l'article {{ article }}{% endblock %}
{% block content-header-title %}Article - {{ article.name }}{% endblock %}
@@ -82,27 +87,26 @@
Statistiques
-
-
-
-
Ventes de {{ article.name }}
-
-
+
+
+
+
Ventes de {{ article.name }}
+
-
+
+
-{% endblock %}
-{% block extra_head %}
-
-
-
+
{% endblock %}
diff --git a/kfet/templates/kfet/article_update.html b/kfet/templates/kfet/article_update.html
index 85a29f6b..a3bfbcc6 100644
--- a/kfet/templates/kfet/article_update.html
+++ b/kfet/templates/kfet/article_update.html
@@ -12,7 +12,7 @@
diff --git a/kfet/templates/kfet/order_to_inventory.html b/kfet/templates/kfet/order_to_inventory.html
index 5fe920e9..321b8b92 100644
--- a/kfet/templates/kfet/order_to_inventory.html
+++ b/kfet/templates/kfet/order_to_inventory.html
@@ -42,11 +42,13 @@
{% endfor %}
- {% if not perms.kfet.order_to_inventory %}
-
- {% endif %}
{{ formset.management_form }}
-
+ {% if not perms.kfet.add_inventory %}
+
+ {% include "kfet/form_authentication_snippet.html" %}
+
+ {% endif %}
+
diff --git a/kfet/tests.py b/kfet/tests.py
index 5bea7afa..991b2545 100644
--- a/kfet/tests.py
+++ b/kfet/tests.py
@@ -1,5 +1,70 @@
# -*- coding: utf-8 -*-
-from django.test import TestCase
+from unittest.mock import patch
-# Écrire les tests ici
+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, "/")
diff --git a/kfet/urls.py b/kfet/urls.py
index 60f6545b..6eb12ce5 100644
--- a/kfet/urls.py
+++ b/kfet/urls.py
@@ -8,7 +8,7 @@ from kfet.decorators import teamkfet_required
urlpatterns = [
url(r'^$', views.Home.as_view(),
- name = 'kfet.home'),
+ name='kfet.home'),
url(r'^login/genericteam$', views.login_genericteam,
name='kfet.login.genericteam'),
url(r'^history$', views.history,
@@ -69,28 +69,19 @@ urlpatterns = [
name='kfet.account.negative'),
# Account - Statistics
- url('^accounts/(?P.{3})/stat/last/$',
- views.AccountStatLastAll.as_view(),
- name = 'kfet.account.stat.last'),
- url('^accounts/(?P.{3})/stat/last/month/$',
- views.AccountStatLastMonth.as_view(),
- name = 'kfet.account.stat.last.month'),
- url('^accounts/(?P.{3})/stat/last/week/$',
- views.AccountStatLastWeek.as_view(),
- name = 'kfet.account.stat.last.week'),
- url('^accounts/(?P.{3})/stat/last/day/$',
- views.AccountStatLastDay.as_view(),
- name = 'kfet.account.stat.last.day'),
+ url(r'^accounts/(?P.{3})/stat/operations/list$',
+ views.AccountStatOperationList.as_view(),
+ name='kfet.account.stat.operation.list'),
+ url(r'^accounts/(?P.{3})/stat/operations$',
+ views.AccountStatOperation.as_view(),
+ name='kfet.account.stat.operation'),
- url('^accounts/(?P.{3})/stat/balance/$',
- views.AccountStatBalanceAll.as_view(),
- name = 'kfet.account.stat.balance'),
- url('^accounts/(?P.{3})/stat/balance/d/(?P\d*)/$',
+ url(r'^accounts/(?P.{3})/stat/balance/list$',
+ views.AccountStatBalanceList.as_view(),
+ name='kfet.account.stat.balance.list'),
+ url(r'^accounts/(?P.{3})/stat/balance$',
views.AccountStatBalance.as_view(),
- name = 'kfet.account.stat.balance.days'),
- url('^accounts/(?P.{3})/stat/balance/anytime/$',
- views.AccountStatBalance.as_view(),
- name = 'kfet.account.stat.balance.anytime'),
+ name='kfet.account.stat.balance'),
# -----
# Checkout urls
@@ -134,6 +125,14 @@ urlpatterns = [
# Article urls
# -----
+ # Category - General
+ url('^categories/$',
+ teamkfet_required(views.CategoryList.as_view()),
+ name='kfet.category'),
+ # Category - Update
+ url('^categories/(?P\d+)/edit$',
+ teamkfet_required(views.CategoryUpdate.as_view()),
+ name='kfet.category.update'),
# Article - General
url('^articles/$',
teamkfet_required(views.ArticleList.as_view()),
@@ -149,20 +148,14 @@ urlpatterns = [
# Article - Update
url('^articles/(?P\d+)/edit$',
teamkfet_required(views.ArticleUpdate.as_view()),
- name = 'kfet.article.update'),
+ name='kfet.article.update'),
# Article - Statistics
- url('^articles/(?P\d+)/stat/last/$',
- views.ArticleStatLastAll.as_view(),
- name = 'kfet.article.stat.last'),
- url('^articles/(?P\d+)/stat/last/month/$',
- views.ArticleStatLastMonth.as_view(),
- name = 'kfet.article.stat.last.month'),
- url('^articles/(?P\d+)/stat/last/week/$',
- views.ArticleStatLastWeek.as_view(),
- name = 'kfet.article.stat.last.week'),
- url('^articles/(?P\d+)/stat/last/day/$',
- views.ArticleStatLastDay.as_view(),
- name = 'kfet.article.stat.last.day'),
+ url(r'^articles/(?P\d+)/stat/sales/list$',
+ views.ArticleStatSalesList.as_view(),
+ name='kfet.article.stat.sales.list'),
+ url(r'^articles/(?P\d+)/stat/sales$',
+ views.ArticleStatSales.as_view(),
+ name='kfet.article.stat.sales'),
# -----
# K-Psul urls
diff --git a/kfet/views.py b/kfet/views.py
index d522d753..5643e1f2 100644
--- a/kfet/views.py
+++ b/kfet/views.py
@@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
+import ast
+from urllib.parse import urlencode
+
from django.shortcuts import render, get_object_or_404, redirect
from django.core.exceptions import PermissionDenied
from django.core.cache import cache
from django.views.generic import ListView, DetailView, TemplateView
-from django.views.generic.list import BaseListView, MultipleObjectTemplateResponseMixin
-from django.views.generic.detail import BaseDetailView, SingleObjectTemplateResponseMixin
-from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
+from django.views.generic.detail import BaseDetailView
+from django.views.generic.edit import CreateView, UpdateView
from django.core.urlresolvers import reverse, reverse_lazy
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
@@ -28,7 +30,7 @@ from kfet.models import (
Account, Checkout, Article, Settings, AccountNegative,
CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
InventoryArticle, Order, OrderArticle, Operation, OperationGroup,
- TransferGroup, Transfer)
+ TransferGroup, Transfer, ArticleCategory)
from kfet.forms import (
AccountTriForm, AccountBalanceForm, AccountNoTriForm, UserForm, CofForm,
UserRestrictTeamForm, UserGroupForm, AccountForm, CofRestrictForm,
@@ -38,7 +40,7 @@ from kfet.forms import (
KPsulOperationGroupForm, KPsulAccountForm, KPsulCheckoutForm,
KPsulOperationFormSet, AddcostForm, FilterHistoryForm, SettingsForm,
TransferFormSet, InventoryArticleForm, OrderArticleForm,
- OrderArticleToInventoryForm
+ OrderArticleToInventoryForm, CategoryForm
)
from collections import defaultdict
from kfet import consumers
@@ -47,10 +49,8 @@ from decimal import Decimal
import django_cas_ng
import heapq
import statistics
-from .statistic import daynames, monthnames, weeknames, \
- lastdays, lastweeks, lastmonths, \
- this_morning, this_monday_morning, this_first_month_day, \
- tot_ventes
+from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes
+
# source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/
@@ -79,7 +79,7 @@ class JSONResponseMixin(object):
class Home(TemplateView):
- template_name = "kfet/home.html"
+ template_name = "kfet/home.html"
def get_context_data(self, **kwargs):
context = super(TemplateView, self).get_context_data(**kwargs)
@@ -791,28 +791,60 @@ class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView):
form.instance.amount_taken = getAmountTaken(form.instance)
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 - General
+# Article - General
class ArticleList(ListView):
queryset = (Article.objects
- .select_related('category')
- .prefetch_related(Prefetch('inventories',
- queryset = Inventory.objects.order_by('-at'),
- to_attr = 'inventory'))
- .order_by('category', '-is_sold', 'name'))
+ .select_related('category')
+ .prefetch_related(Prefetch('inventories',
+ queryset=Inventory.objects.order_by('-at'),
+ to_attr='inventory'))
+ .order_by('category', '-is_sold', 'name'))
template_name = 'kfet/article.html'
context_object_name = 'articles'
-# Article - Create
+# Article - Create
class ArticleCreate(SuccessMessageMixin, CreateView):
- model = Article
- template_name = 'kfet/article_create.html'
- form_class = ArticleForm
+ model = Article
+ template_name = 'kfet/article_create.html'
+ form_class = ArticleForm
success_message = 'Nouvel item : %(category)s - %(name)s'
# Surcharge de la validation
@@ -827,7 +859,7 @@ class ArticleCreate(SuccessMessageMixin, CreateView):
# Save des suppliers déjà existant
for supplier in form.cleaned_data['suppliers']:
SupplierArticle.objects.create(
- article = article, supplier = supplier)
+ article=article, supplier=supplier)
# Nouveau supplier
supplier_new = form.cleaned_data['supplier_new'].strip()
@@ -836,49 +868,49 @@ class ArticleCreate(SuccessMessageMixin, CreateView):
name=supplier_new)
if created:
SupplierArticle.objects.create(
- article = article, supplier = supplier)
+ article=article, supplier=supplier)
# Inventaire avec stock initial
inventory = Inventory()
inventory.by = self.request.user.profile.account_kfet
inventory.save()
InventoryArticle.objects.create(
- inventory = inventory,
- article = article,
- stock_old = article.stock,
- stock_new = article.stock,
+ inventory=inventory,
+ article=article,
+ stock_old=article.stock,
+ stock_new=article.stock,
)
# Creating
return super(ArticleCreate, self).form_valid(form)
-# Article - Read
+# Article - Read
class ArticleRead(DetailView):
- model = Article
+ model = Article
template_name = 'kfet/article_read.html'
context_object_name = 'article'
def get_context_data(self, **kwargs):
context = super(ArticleRead, self).get_context_data(**kwargs)
inventoryarts = (InventoryArticle.objects
- .filter(article = self.object)
- .select_related('inventory')
- .order_by('-inventory__at'))
+ .filter(article=self.object)
+ .select_related('inventory')
+ .order_by('-inventory__at'))
context['inventoryarts'] = inventoryarts
supplierarts = (SupplierArticle.objects
- .filter(article = self.object)
- .select_related('supplier')
- .order_by('-at'))
+ .filter(article=self.object)
+ .select_related('supplier')
+ .order_by('-at'))
context['supplierarts'] = supplierarts
return context
-# Article - Update
+# Article - Update
class ArticleUpdate(SuccessMessageMixin, UpdateView):
- model = Article
- template_name = 'kfet/article_update.html'
- form_class = ArticleRestrictForm
+ model = Article
+ template_name = 'kfet/article_update.html'
+ form_class = ArticleRestrictForm
success_message = "Informations mises à jour pour l'article : %(name)s"
# Surcharge de la validation
@@ -894,13 +926,13 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView):
for supplier in form.cleaned_data['suppliers']:
if supplier not in article.suppliers.all():
SupplierArticle.objects.create(
- article = article, supplier = supplier)
+ article=article, supplier=supplier)
# On vire les suppliers désélectionnés
for supplier in article.suppliers.all():
if supplier not in form.cleaned_data['suppliers']:
SupplierArticle.objects.filter(
- article = article, supplier = supplier).delete()
+ article=article, supplier=supplier).delete()
# Nouveau supplier
supplier_new = form.cleaned_data['supplier_new'].strip()
@@ -909,7 +941,7 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView):
name=supplier_new)
if created:
SupplierArticle.objects.create(
- article = article, supplier = supplier)
+ article=article, supplier=supplier)
# Updating
return super(ArticleUpdate, self).form_valid(form)
@@ -954,13 +986,14 @@ def kpsul_update_addcost(request):
addcost_form = AddcostForm(request.POST)
if not addcost_form.is_valid():
- data = { 'errors': { 'addcost': list(addcost_form.errors) } }
+ data = {'errors': {'addcost': list(addcost_form.errors)}}
return JsonResponse(data, status=400)
required_perms = ['kfet.manage_addcosts']
if not request.user.has_perms(required_perms):
data = {
'errors': {
- 'missing_perms': get_missing_perms(required_perms, request.user)
+ 'missing_perms': get_missing_perms(required_perms,
+ request.user)
}
}
return JsonResponse(data, status=403)
@@ -968,7 +1001,8 @@ def kpsul_update_addcost(request):
trigramme = addcost_form.cleaned_data['trigramme']
account = trigramme and Account.objects.get(trigramme=trigramme) or None
Settings.objects.filter(name='ADDCOST_FOR').update(value_account=account)
- Settings.objects.filter(name='ADDCOST_AMOUNT').update(value_decimal=addcost_form.cleaned_data['amount'])
+ (Settings.objects.filter(name='ADDCOST_AMOUNT')
+ .update(value_decimal=addcost_form.cleaned_data['amount']))
cache.delete('ADDCOST_FOR')
cache.delete('ADDCOST_AMOUNT')
data = {
@@ -980,20 +1014,24 @@ def kpsul_update_addcost(request):
consumers.KPsul.group_send('kfet.kpsul', data)
return JsonResponse(data)
+
def get_missing_perms(required_perms, user):
- missing_perms_codenames = [ (perm.split('.'))[1]
- for perm in required_perms if not user.has_perm(perm)]
+ missing_perms_codenames = [(perm.split('.'))[1]
+ for perm in required_perms
+ if not user.has_perm(perm)]
missing_perms = list(
Permission.objects
- .filter(codename__in=missing_perms_codenames)
- .values_list('name', flat=True))
+ .filter(codename__in=missing_perms_codenames)
+ .values_list('name', flat=True)
+ )
return missing_perms
+
@teamkfet_required
def kpsul_perform_operations(request):
# Initializing response data
- data = { 'operationgroup': 0, 'operations': [],
- 'warnings': {}, 'errors': {} }
+ data = {'operationgroup': 0, 'operations': [],
+ 'warnings': {}, 'errors': {}}
# Checking operationgroup
operationgroup_form = KPsulOperationGroupForm(request.POST)
@@ -1001,7 +1039,7 @@ def kpsul_perform_operations(request):
data['errors']['operation_group'] = list(operationgroup_form.errors)
# Checking operation_formset
- operation_formset = KPsulOperationFormSet(request.POST)
+ operation_formset = KPsulOperationFormSet(request.POST)
if not operation_formset.is_valid():
data['errors']['operations'] = list(operation_formset.errors)
@@ -1010,39 +1048,41 @@ def kpsul_perform_operations(request):
return JsonResponse(data, status=400)
# Pre-saving (no commit)
- operationgroup = operationgroup_form.save(commit = False)
- operations = operation_formset.save(commit = False)
+ operationgroup = operationgroup_form.save(commit=False)
+ operations = operation_formset.save(commit=False)
# Retrieving COF grant
cof_grant = Settings.SUBVENTION_COF()
# Retrieving addcosts data
addcost_amount = Settings.ADDCOST_AMOUNT()
- addcost_for = Settings.ADDCOST_FOR()
+ addcost_for = Settings.ADDCOST_FOR()
# Initializing vars
- required_perms = set() # Required perms to perform all operations
+ required_perms = set() # Required perms to perform all operations
cof_grant_divisor = 1 + cof_grant / 100
- to_addcost_for_balance = 0 # For balance of addcost_for
- to_checkout_balance = 0 # For balance of selected checkout
- to_articles_stocks = defaultdict(lambda:0) # For stocks articles
- is_addcost = (addcost_for and addcost_amount
- and addcost_for != operationgroup.on_acc)
+ to_addcost_for_balance = 0 # For balance of addcost_for
+ to_checkout_balance = 0 # For balance of selected checkout
+ to_articles_stocks = defaultdict(lambda: 0) # For stocks articles
+ is_addcost = all((addcost_for, addcost_amount,
+ addcost_for != operationgroup.on_acc))
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:
if operation.type == Operation.PURCHASE:
operation.amount = - operation.article.price * operation.article_nb
- if is_addcost:
- operation.addcost_for = addcost_for
- operation.addcost_amount = addcost_amount * operation.article_nb
- operation.amount -= operation.addcost_amount
- to_addcost_for_balance += operation.addcost_amount
+ if is_addcost & operation.article.category.has_addcost:
+ operation.addcost_for = addcost_for
+ operation.addcost_amount = addcost_amount \
+ * operation.article_nb
+ operation.amount -= operation.addcost_amount
+ to_addcost_for_balance += operation.addcost_amount
if operationgroup.on_acc.is_cash:
to_checkout_balance += -operation.amount
if operationgroup.on_acc.is_cof:
- if is_addcost:
- operation.addcost_amount = operation.addcost_amount / cof_grant_divisor
+ if is_addcost and operation.article.category.has_addcost:
+ operation.addcost_amount /= cof_grant_divisor
operation.amount = operation.amount / cof_grant_divisor
to_articles_stocks[operation.article] -= operation.article_nb
else:
@@ -1059,8 +1099,10 @@ def kpsul_perform_operations(request):
if operationgroup.on_acc.is_cof:
to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor
- (perms, stop) = operationgroup.on_acc.perms_to_perform_operation(
- amount = operationgroup.amount)
+ (perms, stop) = (operationgroup.on_acc
+ .perms_to_perform_operation(
+ amount=operationgroup.amount)
+ )
required_perms |= perms
if need_comment:
@@ -1091,7 +1133,7 @@ def kpsul_perform_operations(request):
# saving account's balance and adding to Negative if not in
if not operationgroup.on_acc.is_cash:
Account.objects.filter(pk=operationgroup.on_acc.pk).update(
- balance = F('balance') + operationgroup.amount)
+ balance=F('balance') + operationgroup.amount)
operationgroup.on_acc.refresh_from_db()
if operationgroup.on_acc.balance < 0:
if hasattr(operationgroup.on_acc, 'negative'):
@@ -1100,21 +1142,21 @@ def kpsul_perform_operations(request):
operationgroup.on_acc.negative.save()
else:
negative = AccountNegative(
- account = operationgroup.on_acc, start = timezone.now())
+ account=operationgroup.on_acc, start=timezone.now())
negative.save()
- elif (hasattr(operationgroup.on_acc, 'negative')
- and not operationgroup.on_acc.negative.balance_offset):
+ elif (hasattr(operationgroup.on_acc, 'negative') and
+ not operationgroup.on_acc.negative.balance_offset):
operationgroup.on_acc.negative.delete()
# Updating checkout's balance
if to_checkout_balance:
Checkout.objects.filter(pk=operationgroup.checkout.pk).update(
- balance = F('balance') + to_checkout_balance)
+ balance=F('balance') + to_checkout_balance)
# Saving addcost_for with new balance if there is one
if is_addcost and to_addcost_for_balance:
Account.objects.filter(pk=addcost_for.pk).update(
- balance = F('balance') + to_addcost_for_balance)
+ balance=F('balance') + to_addcost_for_balance)
# Saving operation group
operationgroup.save()
@@ -1129,7 +1171,7 @@ def kpsul_perform_operations(request):
# Updating articles stock
for article in to_articles_stocks:
Article.objects.filter(pk=article.pk).update(
- stock = F('stock') + to_articles_stocks[article])
+ stock=F('stock') + to_articles_stocks[article])
# Websocket data
websocket_data = {}
@@ -1141,17 +1183,20 @@ def kpsul_perform_operations(request):
'at': operationgroup.at,
'is_cof': operationgroup.is_cof,
'comment': operationgroup.comment,
- 'valid_by__trigramme': ( operationgroup.valid_by and
- operationgroup.valid_by.trigramme or None),
+ 'valid_by__trigramme': (operationgroup.valid_by and
+ operationgroup.valid_by.trigramme or None),
'on_acc__trigramme': operationgroup.on_acc.trigramme,
'opes': [],
}]
for operation in operations:
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_for__trigramme': operation.addcost_for and addcost_for.trigramme or None,
- 'article__name': operation.article and operation.article.name or None,
+ 'addcost_for__trigramme': (
+ operation.addcost_for and addcost_for.trigramme or None),
+ 'article__name': (
+ operation.article and operation.article.name or None),
'article_nb': operation.article_nb,
'group_id': operationgroup.pk,
'canceled_by__trigramme': None, 'canceled_at': None,
@@ -1165,7 +1210,7 @@ def kpsul_perform_operations(request):
}]
websocket_data['articles'] = []
# Need refresh from db cause we used update on querysets
- articles_pk = [ article.pk for article in to_articles_stocks]
+ articles_pk = [article.pk for article in to_articles_stocks]
articles = Article.objects.values('id', 'stock').filter(pk__in=articles_pk)
for article in articles:
websocket_data['articles'].append({
@@ -1175,6 +1220,7 @@ def kpsul_perform_operations(request):
consumers.KPsul.group_send('kfet.kpsul', websocket_data)
return JsonResponse(data)
+
@teamkfet_required
def kpsul_cancel_operations(request):
# Pour la réponse
@@ -1442,12 +1488,14 @@ def kpsul_articles_data(request):
'content': {
'id': article.category.id,
'name': article.category.name,
+ 'has_addcost': article.category.has_addcost,
},
'child_sort': 'article',
}
})
return JsonResponse(articlelist, safe=False)
+
@teamkfet_required
def history(request):
data = {
@@ -1705,7 +1753,8 @@ def inventory_create(request):
'stock_old': article.stock,
'name' : article.name,
'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(
@@ -2056,87 +2105,54 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView):
# ---------------
-class JSONDetailView(JSONResponseMixin,
- BaseDetailView):
- """
- Returns a DetailView that renders a JSON
- """
+class JSONDetailView(JSONResponseMixin, BaseDetailView):
+ """Returns a DetailView that renders a JSON."""
+
def render_to_response(self, context):
return self.render_to_json_response(context)
-class HybridDetailView(JSONResponseMixin,
- SingleObjectTemplateResponseMixin,
- BaseDetailView):
- """
- Returns a DetailView 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(HybridDetailView, self).render_to_response(context)
+
+class PkUrlMixin(object):
+
+ def get_object(self, *args, **kwargs):
+ get_by = self.kwargs.get(self.pk_url_kwarg)
+ return get_object_or_404(self.model, **{self.pk_url_kwarg: get_by})
-class HybridListView(JSONResponseMixin,
- MultipleObjectTemplateResponseMixin,
- 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)
+class SingleResumeStat(JSONDetailView):
+ """Manifest for a kind of a stat about an object.
+ 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 = ''
- # nombre de vues à résumer
- nb_stat = 2
- # Le combienième est celui par defaut ?
- # (entre 0 et nb_stat-1)
nb_default = 0
- stat_labels = ['stat_1', 'stat_2']
- stat_urls = ['url_1', 'url_2']
- # sert à renverser les urls
- # utile de le surcharger quand l'url prend d'autres arguments que l'id
- def get_object_url_kwargs(self, **kwargs):
- return {'pk': self.object.id}
-
- def url_kwargs(self, **kwargs):
- return [{}] * self.nb_stat
+ stats = []
+ url_stat = None
def get_context_data(self, **kwargs):
# On n'hérite pas
object_id = self.object.id
- url_kwargs = self.url_kwargs()
context = {}
- stats = {}
- for i in range(self.nb_stat):
- stats[i] = {
- 'label': self.stat_labels[i],
- 'btn': "btn_%s_%d_%d" % (self.id_prefix,
- object_id,
- i),
- 'url': reverse(self.stat_urls[i],
- kwargs=dict(
- self.get_object_url_kwargs(),
- **url_kwargs[i]
- ),
- ),
- }
- prefix = "%s_%d" % (self.id_prefix, object_id)
+ stats = []
+ prefix = '{}_{}'.format(self.id_prefix, object_id)
+ for i, stat_def in enumerate(self.stats):
+ url_pk = getattr(self.object, self.pk_url_kwarg)
+ url_params_d = stat_def.get('url_params', {})
+ if len(url_params_d) > 0:
+ url_params = '?{}'.format(urlencode(url_params_d))
+ else:
+ url_params = ''
+ stats.append({
+ 'label': stat_def['label'],
+ 'btn': 'btn_{}_{}'.format(prefix, i),
+ 'url': '{url}{params}'.format(
+ url=reverse(self.url_stat, args=[url_pk]),
+ params=url_params,
+ ),
+ })
context['id_prefix'] = prefix
context['content_id'] = "content_%s" % prefix
context['stats'] = stats
@@ -2151,87 +2167,84 @@ class ObjectResumeStat(JSONDetailView):
ID_PREFIX_ACC_BALANCE = "balance_acc"
-# Un résumé de toutes les vues ArticleStatBalance
-# REND DU JSON
-class AccountStatBalanceAll(ObjectResumeStat):
+class AccountStatBalanceList(PkUrlMixin, SingleResumeStat):
+ """Manifest for balance stats of an account."""
model = 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
- 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
- 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):
- trigramme = self.kwargs.get(self.trigramme_url_kwarg)
- return get_object_or_404(Account, trigramme=trigramme)
-
- def get_object_url_kwargs(self, **kwargs):
- 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
+ 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)
def dispatch(self, *args, **kwargs):
- return super(AccountStatBalanceAll, self).dispatch(*args, **kwargs)
+ return super().dispatch(*args, **kwargs)
-class AccountStatBalance(JSONDetailView):
- """
- Returns a JSON containing the evolution a the personnal
- balance of a trigramme between timezone.now() and `nb_days`
- ago (specified to the view as an argument)
- takes into account the Operations and the Transfers
- does not takes into account the balance offset
+class AccountStatBalance(PkUrlMixin, JSONDetailView):
+ """Datasets of balance of an account.
+
+ Operations and Transfers are taken into account.
+
"""
model = Account
- trigramme_url_kwarg = 'trigramme'
- nb_date_url_kwargs = 'nb_date'
+ pk_url_kwarg = 'trigramme'
context_object_name = 'account'
- id_prefix = ""
- def get_object(self, **kwargs):
- trigramme = self.kwargs.get(self.trigramme_url_kwarg)
- return get_object_or_404(Account, trigramme=trigramme)
-
- def get_changes_list(self, **kwargs):
+ def get_changes_list(self, last_days=None, begin_date=None, end_date=None):
account = self.object
- nb_date = self.kwargs.get(self.nb_date_url_kwargs, None)
- end_date = this_morning()
- if nb_date is None:
- begin_date = timezone.datetime(year=1980, month=1, day=1)
- anytime = True
- else:
- begin_date = this_morning() \
- - timezone.timedelta(days=int(nb_date))
- anytime = False
- # On récupère les opérations
+
+ # prepare filters
+ if last_days is not None:
+ end_date = timezone.now()
+ begin_date = end_date - timezone.timedelta(days=last_days)
+
+ # prepare querysets
# TODO: retirer les opgroup dont tous les op sont annulées
- opgroups = list(OperationGroup.objects
- .filter(on_acc=account)
- .filter(at__gte=begin_date)
- .filter(at__lte=end_date))
- # On récupère les transferts reçus
- received_transfers = list(Transfer.objects
- .filter(to_acc=account)
- .filter(canceled_at=None)
- .filter(group__at__gte=begin_date)
- .filter(group__at__lte=end_date))
- # On récupère les transferts émis
- emitted_transfers = list(Transfer.objects
- .filter(from_acc=account)
- .filter(canceled_at=None)
- .filter(group__at__gte=begin_date)
- .filter(group__at__lte=end_date))
+ opegroups = OperationGroup.objects.filter(on_acc=account)
+ recv_transfers = Transfer.objects.filter(to_acc=account,
+ canceled_at=None)
+ sent_transfers = Transfer.objects.filter(from_acc=account,
+ canceled_at=None)
+
+ # apply filters
+ if begin_date is not None:
+ opegroups = opegroups.filter(at__gte=begin_date)
+ recv_transfers = recv_transfers.filter(group__at__gte=begin_date)
+ sent_transfers = sent_transfers.filter(group__at__gte=begin_date)
+
+ if end_date is not None:
+ opegroups = opegroups.filter(at__lte=end_date)
+ recv_transfers = recv_transfers.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
# {'at': date,
# 'amount': changement de la balance (négatif si diminue la balance,
@@ -2241,76 +2254,86 @@ class AccountStatBalance(JSONDetailView):
# sera mis à jour lors d'une
# autre passe)
# }
- actions = [
- # Maintenant (à changer si on gère autre chose que now)
+
+ actions = []
+
+ actions.append({
+ 'at': (begin_date or account.created_at).isoformat(),
+ 'amount': 0,
+ 'label': 'début',
+ 'balance': 0,
+ })
+ actions.append({
+ 'at': (end_date or timezone.now()).isoformat(),
+ 'amount': 0,
+ 'label': 'fin',
+ 'balance': 0,
+ })
+
+ actions += [
{
- 'at': end_date.isoformat(),
- 'amout': 0,
- 'label': "actuel",
+ 'at': ope_grp.at.isoformat(),
+ 'amount': ope_grp.amount,
+ 'label': str(ope_grp),
'balance': 0,
- }
- ] + [
- {
- 'at': op.at.isoformat(),
- 'amount': op.amount,
- 'label': str(op),
- 'balance': 0,
- } for op in opgroups
+ } for ope_grp in opegroups
] + [
{
'at': tr.group.at.isoformat(),
'amount': tr.amount,
- 'label': "%d€: %s -> %s" % (tr.amount,
- tr.from_acc.trigramme,
- tr.to_acc.trigramme),
+ 'label': str(tr),
'balance': 0,
- } for tr in received_transfers
+ } for tr in recv_transfers
] + [
{
'at': tr.group.at.isoformat(),
'amount': -tr.amount,
- 'label': "%d€: %s -> %s" % (tr.amount,
- tr.from_acc.trigramme,
- tr.to_acc.trigramme),
+ 'label': str(tr),
'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
# an plus ancien et on met à jour la balance
- actions = sorted(actions, key=lambda k: k['at'], reverse=True)
- actions[0]['balance'] = account.balance
- for i in range(len(actions)-1):
- actions[i+1]['balance'] = actions[i]['balance'] \
- - actions[i+1]['amount']
+ if len(actions) > 1:
+ actions = sorted(actions, key=lambda k: k['at'], reverse=True)
+ actions[0]['balance'] = account.balance
+ for i in range(len(actions)-1):
+ actions[i+1]['balance'] = \
+ actions[i]['balance'] - actions[i+1]['amount']
return actions
- def get_context_data(self, **kwargs):
+ def get_context_data(self, *args, **kwargs):
context = {}
- changes = self.get_changes_list()
- nb_days = self.kwargs.get(self.nb_date_url_kwargs, None)
- if nb_days is None:
- nb_days_string = 'anytime'
- else:
- nb_days_string = str(int(nb_days))
- context['charts'] = [ { "color": "rgb(255, 99, 132)",
- "label": "Balance",
- "values": changes } ]
+
+ last_days = self.request.GET.get('last_days', None)
+ if last_days is not None:
+ last_days = int(last_days)
+ begin_date = self.request.GET.get('begin_date', None)
+ end_date = self.request.GET.get('end_date', None)
+
+ 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",
+ "values": changes,
+ }]
context['is_time_chart'] = True
- context['min_date'] = changes[len(changes)-1]['at']
- context['max_date'] = changes[0]['at']
+ if len(changes) > 0:
+ context['min_date'] = changes[-1]['at']
+ context['max_date'] = changes[0]['at']
# TODO: offset
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)
def dispatch(self, *args, **kwargs):
return super(AccountStatBalance, self).dispatch(*args, **kwargs)
@@ -2325,140 +2348,77 @@ ID_PREFIX_ACC_LAST_WEEKS = "last_weeks_acc"
ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc"
-# Un résumé de toutes les vues ArticleStatLast
-# NE REND PAS DE JSON
-class AccountStatLastAll(ObjectResumeStat):
+class AccountStatOperationList(PkUrlMixin, SingleResumeStat):
+ """Manifest for operations stats of an account."""
model = Account
context_object_name = 'account'
- trigramme_url_kwarg = 'trigramme'
+ pk_url_kwarg = 'trigramme'
id_prefix = ID_PREFIX_ACC_LAST
- nb_stat = 3
nb_default = 2
- stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"]
- stat_urls = ['kfet.account.stat.last.month',
- 'kfet.account.stat.last.week',
- 'kfet.account.stat.last.day']
+ stats = last_stats_manifest(types=[Operation.PURCHASE])
+ url_stat = 'kfet.account.stat.operation'
- def get_object(self, **kwargs):
- trigramme = self.kwargs.get(self.trigramme_url_kwarg)
- return get_object_or_404(Account, trigramme=trigramme)
-
- def get_object_url_kwargs(self, **kwargs):
- return {'trigramme': self.object.trigramme}
+ 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)
def dispatch(self, *args, **kwargs):
- return super(AccountStatLastAll, self).dispatch(*args, **kwargs)
+ return super().dispatch(*args, **kwargs)
-class AccountStatLast(JSONDetailView):
- """
- Returns a JSON containing the evolution a the personnal
- consommation of a trigramme at the diffent dates specified
- """
+class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView):
+ """Datasets of operations of an account."""
model = Account
- trigramme_url_kwarg = 'trigramme'
+ pk_url_kwarg = 'trigramme'
context_object_name = 'account'
- end_date = timezone.now()
id_prefix = ""
- # 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):
- 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
+ def get_operations(self, scale, types=None):
# On selectionne les opérations qui correspondent
# à l'article en question et qui ne sont pas annulées
# puis on choisi pour chaques intervalle les opérations
# effectuées dans ces intervalles de temps
all_operations = (Operation.objects
- .filter(type='purchase')
.filter(group__on_acc=self.object)
.filter(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])
- )
- return operations
+ if types is not None:
+ all_operations = all_operations.filter(type__in=types)
+ chunks = self.chunkify_qs(all_operations, scale, field='group__at')
+ return chunks
- def get_context_data(self, **kwargs):
- context = {}
- nb_ventes = {}
- # On récupère les labels des dates
- context['labels'] = self.get_labels().copy()
+ def get_context_data(self, *args, **kwargs):
+ old_ctx = super().get_context_data(*args, **kwargs)
+ context = {'labels': old_ctx['labels']}
+ scale = self.scale
+
+ 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
- operations = self.sort_operations()
- for i in operations:
- nb_ventes[i] = tot_ventes(operations[i])
- context['charts'] = [ { "color": "rgb(255, 99, 132)",
- "label": "NB items achetés",
- "values": nb_ventes } ]
+ nb_ventes = []
+ for chunk in operations:
+ nb_ventes.append(tot_ventes(chunk))
+
+ context['charts'] = [{"color": "rgb(255, 99, 132)",
+ "label": "NB items achetés",
+ "values": nb_ventes}]
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)
def dispatch(self, *args, **kwargs):
- return super(AccountStatLast, self).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)
+ return super().dispatch(*args, **kwargs)
# ------------------------
@@ -2470,143 +2430,64 @@ ID_PREFIX_ART_LAST_WEEKS = "last_weeks_art"
ID_PREFIX_ART_LAST_MONTHS = "last_months_art"
-# Un résumé de toutes les vues ArticleStatLast
-# NE REND PAS DE JSON
-class ArticleStatLastAll(ObjectResumeStat):
+class ArticleStatSalesList(SingleResumeStat):
+ """Manifest for sales stats of an article."""
model = Article
context_object_name = 'article'
id_prefix = ID_PREFIX_ART_LAST
- nb_stat = 3
nb_default = 2
- stat_labels = ["Derniers mois", "Dernières semaines", "Derniers jours"]
- stat_urls = ['kfet.article.stat.last.month',
- 'kfet.article.stat.last.week',
- 'kfet.article.stat.last.day']
+ url_stat = 'kfet.article.stat.sales'
+ stats = last_stats_manifest()
- @method_decorator(login_required)
+ @method_decorator(teamkfet_required)
def dispatch(self, *args, **kwargs):
- return super(ArticleStatLastAll, self).dispatch(*args, **kwargs)
+ return super().dispatch(*args, **kwargs)
-class ArticleStatLast(JSONDetailView):
- """
- Returns a JSON containing the consommation
- of an article at the diffent dates precised
- """
+class ArticleStatSales(ScaleMixin, JSONDetailView):
+ """Datasets of sales of an article."""
model = Article
context_object_name = 'article'
- end_date = timezone.now()
- id_prefix = ""
- 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(ArticleStatLast, self).render_to_response(context)
+ def get_context_data(self, *args, **kwargs):
+ old_ctx = super().get_context_data(*args, **kwargs)
+ context = {'labels': old_ctx['labels']}
+ scale = self.scale
- # 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
# à l'article en question et qui ne sont pas annulées
# puis on choisi pour chaques intervalle les opérations
# effectuées dans ces intervalles de temps
- all_operations = (Operation.objects
- .filter(type='purchase')
- .filter(article=self.object)
- .filter(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])
- )
+ all_operations = (
+ Operation.objects
+ .filter(type=Operation.PURCHASE,
+ article=self.object,
+ canceled_at=None,
+ )
+ )
+ chunks = self.chunkify_qs(all_operations, scale, field='group__at')
# On compte les opérations
- nb_ventes = {}
- nb_accounts = {}
- nb_liq = {}
- for i in operations:
- nb_ventes[i] = tot_ventes(operations[i])
- nb_liq[i] = tot_ventes(
- operations[i]
- .filter(group__on_acc__trigramme='LIQ')
- )
- nb_accounts[i] = tot_ventes(
- operations[i]
- .exclude(group__on_acc__trigramme='LIQ')
- )
- context['charts'] = [ { "color": "rgb(255, 99, 132)",
- "label": "Toutes consommations",
- "values": nb_ventes },
- { "color": "rgb(54, 162, 235)",
- "label": "LIQ",
- "values": nb_liq },
- { "color": "rgb(255, 205, 86)",
- "label": "Comptes K-Fêt",
- "values": nb_accounts } ]
+ nb_ventes = []
+ nb_accounts = []
+ nb_liq = []
+ for qs in chunks:
+ nb_ventes.append(
+ tot_ventes(qs))
+ nb_liq.append(
+ tot_ventes(qs.filter(group__on_acc__trigramme='LIQ')))
+ nb_accounts.append(
+ tot_ventes(qs.exclude(group__on_acc__trigramme='LIQ')))
+ context['charts'] = [{"color": "rgb(255, 99, 132)",
+ "label": "Toutes consommations",
+ "values": nb_ventes},
+ {"color": "rgb(54, 162, 235)",
+ "label": "LIQ",
+ "values": nb_liq},
+ {"color": "rgb(255, 205, 86)",
+ "label": "Comptes K-Fêt",
+ "values": nb_accounts}]
return context
- @method_decorator(login_required)
+ @method_decorator(teamkfet_required)
def dispatch(self, *args, **kwargs):
- return super(ArticleStatLast, self).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)
+ return super().dispatch(*args, **kwargs)
diff --git a/requirements.txt b/requirements.txt
index db9f7e4c..0a59ed06 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -21,3 +21,4 @@ git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_customma
ldap3
git+https://github.com/Aureplop/channels.git#egg=channels
django-js-reverse==0.7.3
+python-dateutil