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 10e52989..e08efced 100644
--- a/kfet/static/kfet/js/kfet.api.js
+++ b/kfet/static/kfet/js/kfet.api.js
@@ -460,22 +460,28 @@ class ArticleCategory extends ModelObject {
/**
* Properties associated to a category
- * @default ['id', 'name']
+ * @default ['id', 'name', 'has_addcost', 'article']
* @see {@link Models.ModelObject.props|ModelObject.props}
*/
static get props() {
- return ['id', 'name'];
+ return ['id', 'name', 'has_addcost', 'articles'];
}
/**
* Default values for ArticleCategory model instances.
- * @default { 'id': 0, 'name': '' }
+ * @default { 'id': 0, 'name': '', 'has_addcost': true, 'articles': [] }
* @see {@link Models.ModelObject.default_data|ModelObject.default_data}
*/
static get default_data() {
- return {'id': 0, 'name': ''};
+ return {'id': 0, 'name': '', 'has_addcost': true, 'articles': []};
}
+ /**
+ * Verbose name for ArticleCategory model
+ * @default 'category'
+ */
+ static get verbose_name() { return 'category'; }
+
/**
* @default {@link Formatters.ArticleCategoryFormatter}
*/
@@ -500,7 +506,7 @@ class ArticleCategory extends ModelObject {
class Article extends ModelObject {
/**
* Properties associated to an article
- * @default ['id', 'name']
+ * @default ['id', 'name', 'price', 'stock', 'category']
* @see {@link Models.ModelObject.props|ModelObject.props}
*/
static get props() {
@@ -516,6 +522,12 @@ class Article extends ModelObject {
return { 'id': 0, 'name': '', 'price': 0, 'stock': 0 };
}
+ /**
+ * Verbose name for Article model
+ * @default 'article'
+ */
+ static get verbose_name() { return 'article'; }
+
/**
* @default {@link Formatters.ArticleFormatter}
*/
@@ -840,47 +852,18 @@ class Transfer extends Operation {
}
}
-
-/**
- * Node for ModelForest object
- * @memberof Models
- */
-class TreeNode {
-
- constructor(type, content) {
- this.modelname = type;
- this.content = content;
- this.parent = null;
- this.children = [];
- }
-}
-
-
/**
* Simple {@link Models.ModelObject} forest.
* @memberof Models
*/
class ModelForest {
- /**
- * Dictionary associating types to classes
+ /**
+ * Abstract structure of the forest
* @abstract
* @type {Object}
*/
- static get models() { return {}; }
-
- /**
- * Comparison function for nodes
- * @abstract
- * @param {class} model Model to use for comparison
- * @param {Models.TreeNode} a
- * @param {Models.TreeNode} b
- * @see {@link Models.ModelObject.compare|ModelObject.compare}
- */
- static compare(model, a, b) {
- return model.compare(a.content, b.content);
- }
-
+ static get structure() { return {}; }
/**
* Creates empty instance and populates it with data if given
@@ -890,7 +873,20 @@ class ModelForest {
this.from(datalist || []);
}
- /**
+ /**
+ * Shortcut functions to get parent and children of a given node
+ */
+ get_parent(node) {
+ var parent_name = this.constructor.structure[node.constructor.verbose_name].parent;
+ return node[parent_name];
+ }
+
+ get_children(node) {
+ var child_name = this.constructor.structure[node.constructor.verbose_name].children;
+ return node[child_name];
+ }
+
+ /**
* Fetches an object from the instance data, or creates it if
* it does not exist yet.
* If direction >= 0, parent objects are created recursively.
@@ -899,43 +895,45 @@ class ModelForest {
* @param {number} direction
*/
get_or_create(data, direction) {
- var model = this.constructor.models[data.modelname];
+ var struct_data = this.constructor.structure[data.modelname];
+ var model = struct_data.model;
var existing = this.find(data.modelname, data.content.id);
if (existing) {
return existing;
}
- var content = new this.constructor.models[data.modelname](data.content);
- var node = new TreeNode(data.modelname, content);
-
- if (data.child_sort)
- node.child_sort = this.constructor.models[data.child_sort];
- else
- node.child_sort = ModelObject;
+ var node = new model(data.content);
if (direction <= 0) {
- if (data.parent) {
- var parent = this.get_or_create(data.parent, -1);
- node.parent = parent;
- parent.children.push(node);
+ var parent_name = struct_data.parent;
+ var parent_data = data.parent;
+ var parent_struct = this.constructor.structure[parent_name];
+ if (parent_data) {
+ var parent_node = this.get_or_create(parent_data, -1);
+ node[parent_name] = parent_node;
+ parent_node[parent_struct.children].push(node);
} else {
this.roots.push(node);
}
}
- if (direction >= 0 && data.children) {
- for (let child_data of data.children) {
- var child = this.get_or_create(child_data, 1);
- child.parent = node;
- node.children.push(child);
+ if (direction >= 0) {
+ var child_name = struct_data.children;
+ var child_struct = this.constructor.structure[child_name];
+ if (data.children && data.children.length) {
+ for (let child_data of data.children) {
+ var child = this.get_or_create(child_data, 1);
+ child[child_struct.parent] = node;
+ node[child_name].push(child);
+ }
}
}
return node ;
}
- /**
+ /**
* Resets then populates the instance with the given data.
* @param {Object[]} datalist
*/
@@ -946,7 +944,7 @@ class ModelForest {
}
}
- /**
+ /**
* Removes all Models.TreeNode from the tree.
*/
clear() {
@@ -954,28 +952,37 @@ 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
* @param {Object} [options] Options for element render method
*/
render_element(node, templates, options) {
- var template = templates[node.modelname];
+ var modelname = node.constructor.verbose_name;
+ var struct_data = this.constructor.structure[modelname];
+
+ var template = templates[modelname];
var options = options || {} ;
var $container = $('
');
- $container.attr('id', node.modelname+'-'+node.content.id);
+ $container.attr('id', modelname+'-'+node.id);
- var $rendered = node.content.display($(template), options);
+ var $rendered = node.display($(template), options);
$container.append($rendered);
- //dirty
- node.children.sort(this.constructor.compare.bind(null, node.child_sort));
+ var children = this.get_children(node);
- for (let child of node.children) {
- var $child = this.render_element(child, templates, options);
- $container.append($child);
+ if (children && children.length) {
+ if (struct_data.child_sort)
+ children.sort(struct_data.child_sort);
+ else
+ children.sort(children[0].constructor.compare);
+
+ for (let child of children) {
+ var $child = this.render_element(child, templates, options);
+ $container.append($child);
+ }
}
return $container;
@@ -990,12 +997,13 @@ class ModelForest {
* @param {Object} [options] Options for element render method
*/
add_to_container($container, node, templates, options) {
- var existing = node.parent ;
+ var struct = this.constructor.structure;
+ var existing = this.get_parent(node) ;
var first_missing = node;
- while (existing && !($container.find('#'+existing.modelname+'-'+existing.id).length)) {
- first_missing = existing ;
- existing = existing.parent;
+ while (existing && !($container.find('#'+existing.modelname+'-'+existing.id))) {
+ first_missing = existing;
+ existing = this.get_parent(existing);
}
var $to_insert = this.render_element(first_missing, templates, options);
@@ -1013,7 +1021,11 @@ class ModelForest {
* @param {Object} [options] Options for element render method
*/
display($container, templates, options) {
- this.roots.sort(this.constructor.compare.bind(null, this.root_sort));
+ if (this.constructor.root_sort)
+ this.roots.sort(this.constructor.root_sort);
+ else
+ this.roots.sort(this.roots[0].constructor.compare);
+
for (let root of this.roots) {
$container.append(this.render_element(root, templates, options));
}
@@ -1021,34 +1033,54 @@ class ModelForest {
return $container;
}
- traverse(callback) {
+ /**
+ * Performs for each node (in a DFS order) the callback function
+ * on node.content and node.parent.content, if node has given modelname.
+ * @param {string} modelname
+ * @param {function} callback
+ */
+ traverse(modelname, callback) {
+ var that = this;
function recurse(node) {
- callback(node) ;
+ if (node.constructor.verbose_name === modelname) {
+ if (callback(node)) {
+ return true;
+ }
+ }
- for (let child of node.children)
- recurse(child);
+ var children = that.get_children(node);
+ if (children) {
+ for (let child of children)
+ if (recurse(child))
+ return true;
+ }
+
+ return false;
}
for (let root of this.roots)
- recurse(root);
+ if (recurse(root))
+ return ;
}
/**
* Find instance in tree with given type and id
- * @param {string} type
+ * @param {string} modelname
* @param {number} id
*/
- find(type, id) {
+ find(modelname, id) {
var result = null;
+
function callback(node) {
- if (node.modelname === type && node.content.id == id)
+ if (node.id == id) {
result = node ;
+ return true;
+ }
}
- this.traverse(callback);
-
- return result ;
+ this.traverse(modelname, callback);
+ return result;
}
}
@@ -1059,7 +1091,7 @@ class ModelForest {
* @memberof Models
*/
class APIModelForest extends ModelForest {
-
+
/**
* Request url to fill the model.
* @abstract
@@ -1068,7 +1100,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.
*/
@@ -1093,12 +1125,19 @@ class ArticleList extends APIModelForest {
/**
* Default structure for ArticleList instances
* @abstract
- * @default {'article': Article,
- 'category': ArticleCategory}
*/
- static get models() {
- return {'article': Article,
- 'category': ArticleCategory};
+ static get structure() {
+ return {
+ 'article': {
+ 'model': Article,
+ 'parent': 'category',
+ },
+ 'category': {
+ 'model': ArticleCategory,
+ 'children': 'articles',
+ },
+ };
+
}
/**
@@ -1109,15 +1148,6 @@ class ArticleList extends APIModelForest {
static get url_model() {
return Urls['kfet.kpsul.articles_data']();
}
-
- /**
- * Provides model to sort root objects
- * {@see Models.ModelForest.constructor|ModelForest.constructor}
- */
- constructor() {
- super();
- this.root_sort = ArticleCategory;
- }
}
/**
@@ -1437,7 +1467,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/kpsul.js b/kfet/static/kfet/js/kpsul.js
index 65f6fad8..137843c7 100644
--- a/kfet/static/kfet/js/kpsul.js
+++ b/kfet/static/kfet/js/kpsul.js
@@ -458,7 +458,7 @@ class ArticleManager {
}
get_article(id) {
- return this.list.find('article', id).content;
+ return this.list.find('article', id);
}
update_data(data) {
@@ -500,7 +500,7 @@ class ArticleManager {
var id = $(this).parent().attr('id').split('-')[1];
var article = that.list.find('article', id);
if (article)
- that.validate(article.content);
+ that.validate(article);
});
this._$nb.on('keydown', function(e) {
@@ -577,25 +577,20 @@ class ArticleAutocomplete {
var lower = prefix.toLowerCase() ;
var that = this ;
- article_list.traverse(function(node) {
- if (node.modelname === 'article' &&
- node.content.name.toLowerCase()
- .startsWith(lower)) {
- that.matching['article'].push(node.content);
- if (that.matching['category'].indexOf(node.parent.content) == -1)
- that.matching['category'].push(node.parent.content);
- }
+ article_list.traverse('article', function(article) {
+ if (article.name.toLowerCase().startsWith(lower))
+ that.matching.push(article);
});
- if (this.matching['article'].length == 1) {
+ if (this.matching.length == 1) {
if (!backspace) {
- this.manager.validate(this.matching['article'][0]) ;
+ this.manager.validate(this.matching[0]) ;
this.showAll() ;
} else {
this.manager.unset();
this.updateDisplay();
}
- } else if (this.matching['article'].length > 1) {
+ } else if (this.matching.length > 1) {
this.manager.unset();
this.updateDisplay() ;
if (!backspace)
@@ -606,19 +601,27 @@ class ArticleAutocomplete {
updateDisplay() {
var that = this;
- this.manager.list.traverse(function(node) {
- if (that.matching[node.modelname].indexOf(node.content) != -1) {
- that._$container.find('#'+node.modelname+'-'+node.content.id)
- .show();
+ this.manager.list.traverse('category', function(category) {
+ var is_active = false;
+ for (let article of category.articles) {
+ if (that.matching.indexOf(article) != -1) {
+ is_active = true;
+ that._$container.find('#article-'+article.id).show();
+ } else {
+ that._$container.find('#article-'+article.id).hide();
+ }
+ }
+
+ if (is_active) {
+ that._$container.find('#category-'+category.id).show();
} else {
- that._$container.find('#'+node.modelname+'-'+node.content.id)
- .hide();
+ that._$container.find('#category-'+category.id).hide();
}
});
}
updatePrefix() {
- var lower = this.matching['article'].map(function (article) {
+ var lower = this.matching.map(function (article) {
return article.name.toLowerCase() ;
});
@@ -633,14 +636,13 @@ class ArticleAutocomplete {
showAll() {
var that = this;
this.resetMatch();
- this.manager.list.traverse(function(node) {
- that.matching[node.modelname].push(node.content);
+ this.manager.list.traverse('article', function(article) {
+ that.matching.push(article);
});
this.updateDisplay();
}
resetMatch() {
- this.matching = {'article' : [],
- 'category': []};
+ this.matching = [];
}
}
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 = $("