Merge remote-tracking branch 'origin/aureplop/kpsul_js_refactor' into Aufinal/refactor_history
This commit is contained in:
commit
051231a031
25 changed files with 1367 additions and 861 deletions
|
@ -87,7 +87,7 @@ urlpatterns = [
|
||||||
url(r'^jsreverse/$', cache_page(3600)(urls_js), name='js_reverse'),
|
url(r'^jsreverse/$', cache_page(3600)(urls_js), name='js_reverse'),
|
||||||
]
|
]
|
||||||
|
|
||||||
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)),
|
||||||
|
|
|
@ -233,6 +233,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
|
||||||
# -----
|
# -----
|
||||||
|
@ -463,7 +473,7 @@ class InventoryArticleForm(forms.Form):
|
||||||
queryset = Article.objects.all(),
|
queryset = Article.objects.all(),
|
||||||
widget = forms.HiddenInput(),
|
widget = forms.HiddenInput(),
|
||||||
)
|
)
|
||||||
stock_new = forms.IntegerField(required = False)
|
stock_new = forms.IntegerField(required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(InventoryArticleForm, self).__init__(*args, **kwargs)
|
super(InventoryArticleForm, self).__init__(*args, **kwargs)
|
||||||
|
@ -472,6 +482,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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -338,13 +338,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)
|
||||||
|
@ -491,24 +498,29 @@ 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,
|
||||||
related_name = "transfers")
|
related_name="transfers")
|
||||||
from_acc = models.ForeignKey(
|
from_acc = models.ForeignKey(
|
||||||
Account, on_delete = models.PROTECT,
|
Account, on_delete=models.PROTECT,
|
||||||
related_name = "transfers_from")
|
related_name="transfers_from")
|
||||||
to_acc = models.ForeignKey(
|
to_acc = models.ForeignKey(
|
||||||
Account, on_delete = models.PROTECT,
|
Account, on_delete=models.PROTECT,
|
||||||
related_name = "transfers_to")
|
related_name="transfers_to")
|
||||||
amount = models.DecimalField(max_digits = 6, decimal_places = 2)
|
amount = models.DecimalField(max_digits=6, decimal_places=2)
|
||||||
# Optional
|
# Optional
|
||||||
canceled_by = models.ForeignKey(
|
canceled_by = models.ForeignKey(
|
||||||
Account, on_delete = models.PROTECT,
|
Account, on_delete=models.PROTECT,
|
||||||
null = True, blank = True, default = None,
|
null=True, blank=True, default=None,
|
||||||
related_name = "+")
|
related_name="+")
|
||||||
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(
|
||||||
|
|
|
@ -32,6 +32,7 @@ textarea {
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
margin-bottom:0;
|
margin-bottom:0;
|
||||||
|
border-bottom:1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
|
@ -105,6 +106,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 +232,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 +554,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 {
|
||||||
|
|
|
@ -460,22 +460,28 @@ class ArticleCategory extends ModelObject {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Properties associated to a category
|
* Properties associated to a category
|
||||||
* @default <tt>['id', 'name']</tt>
|
* @default <tt>['id', 'name', 'has_addcost', 'article']</tt>
|
||||||
* @see {@link Models.ModelObject.props|ModelObject.props}
|
* @see {@link Models.ModelObject.props|ModelObject.props}
|
||||||
*/
|
*/
|
||||||
static get props() {
|
static get props() {
|
||||||
return ['id', 'name'];
|
return ['id', 'name', 'has_addcost', 'articles'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default values for ArticleCategory model instances.
|
* Default values for ArticleCategory model instances.
|
||||||
* @default <tt>{ 'id': 0, 'name': '' }</tt>
|
* @default <tt>{ 'id': 0, 'name': '', 'has_addcost': true, 'articles': [] }</tt>
|
||||||
* @see {@link Models.ModelObject.default_data|ModelObject.default_data}
|
* @see {@link Models.ModelObject.default_data|ModelObject.default_data}
|
||||||
*/
|
*/
|
||||||
static get default_data() {
|
static get default_data() {
|
||||||
return {'id': 0, 'name': ''};
|
return {'id': 0, 'name': '', 'has_addcost': true, 'articles': []};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verbose name for ArticleCategory model
|
||||||
|
* @default <tt>'category'</tt>
|
||||||
|
*/
|
||||||
|
static get verbose_name() { return 'category'; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @default {@link Formatters.ArticleCategoryFormatter}
|
* @default {@link Formatters.ArticleCategoryFormatter}
|
||||||
*/
|
*/
|
||||||
|
@ -500,7 +506,7 @@ class ArticleCategory extends ModelObject {
|
||||||
class Article extends ModelObject {
|
class Article extends ModelObject {
|
||||||
/**
|
/**
|
||||||
* Properties associated to an article
|
* Properties associated to an article
|
||||||
* @default <tt>['id', 'name']</tt>
|
* @default <tt>['id', 'name', 'price', 'stock', 'category']</tt>
|
||||||
* @see {@link Models.ModelObject.props|ModelObject.props}
|
* @see {@link Models.ModelObject.props|ModelObject.props}
|
||||||
*/
|
*/
|
||||||
static get props() {
|
static get props() {
|
||||||
|
@ -516,6 +522,12 @@ class Article extends ModelObject {
|
||||||
return { 'id': 0, 'name': '', 'price': 0, 'stock': 0 };
|
return { 'id': 0, 'name': '', 'price': 0, 'stock': 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verbose name for Article model
|
||||||
|
* @default <tt>'article'</tt>
|
||||||
|
*/
|
||||||
|
static get verbose_name() { return 'article'; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @default {@link Formatters.ArticleFormatter}
|
* @default {@link Formatters.ArticleFormatter}
|
||||||
*/
|
*/
|
||||||
|
@ -840,22 +852,6 @@ 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.
|
* Simple {@link Models.ModelObject} forest.
|
||||||
* @memberof Models
|
* @memberof Models
|
||||||
|
@ -863,24 +859,11 @@ class TreeNode {
|
||||||
class ModelForest {
|
class ModelForest {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dictionary associating types to classes
|
* Abstract structure of the forest
|
||||||
* @abstract
|
* @abstract
|
||||||
* @type {Object}
|
* @type {Object}
|
||||||
*/
|
*/
|
||||||
static get models() { return {}; }
|
static get structure() { 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates empty instance and populates it with data if given
|
* Creates empty instance and populates it with data if given
|
||||||
|
@ -890,6 +873,19 @@ class ModelForest {
|
||||||
this.from(datalist || []);
|
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
|
* Fetches an object from the instance data, or creates it if
|
||||||
* it does not exist yet.<br>
|
* it does not exist yet.<br>
|
||||||
|
@ -899,36 +895,38 @@ class ModelForest {
|
||||||
* @param {number} direction
|
* @param {number} direction
|
||||||
*/
|
*/
|
||||||
get_or_create(data, 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);
|
var existing = this.find(data.modelname, data.content.id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = new this.constructor.models[data.modelname](data.content);
|
var node = new model(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;
|
|
||||||
|
|
||||||
if (direction <= 0) {
|
if (direction <= 0) {
|
||||||
if (data.parent) {
|
var parent_name = struct_data.parent;
|
||||||
var parent = this.get_or_create(data.parent, -1);
|
var parent_data = data.parent;
|
||||||
node.parent = parent;
|
var parent_struct = this.constructor.structure[parent_name];
|
||||||
parent.children.push(node);
|
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 {
|
} else {
|
||||||
this.roots.push(node);
|
this.roots.push(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (direction >= 0 && data.children) {
|
if (direction >= 0) {
|
||||||
for (let child_data of data.children) {
|
var child_name = struct_data.children;
|
||||||
var child = this.get_or_create(child_data, 1);
|
var child_struct = this.constructor.structure[child_name];
|
||||||
child.parent = node;
|
if (data.children && data.children.length) {
|
||||||
node.children.push(child);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -961,21 +959,30 @@ class ModelForest {
|
||||||
* @param {Object} [options] Options for element render method
|
* @param {Object} [options] Options for element render method
|
||||||
*/
|
*/
|
||||||
render_element(node, templates, options) {
|
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 options = options || {} ;
|
||||||
|
|
||||||
var $container = $('<div></div>');
|
var $container = $('<div></div>');
|
||||||
$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);
|
$container.append($rendered);
|
||||||
|
|
||||||
//dirty
|
var children = this.get_children(node);
|
||||||
node.children.sort(this.constructor.compare.bind(null, node.child_sort));
|
|
||||||
|
|
||||||
for (let child of node.children) {
|
if (children && children.length) {
|
||||||
var $child = this.render_element(child, templates, options);
|
if (struct_data.child_sort)
|
||||||
$container.append($child);
|
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;
|
return $container;
|
||||||
|
@ -990,12 +997,13 @@ class ModelForest {
|
||||||
* @param {Object} [options] Options for element render method
|
* @param {Object} [options] Options for element render method
|
||||||
*/
|
*/
|
||||||
add_to_container($container, node, templates, options) {
|
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;
|
var first_missing = node;
|
||||||
|
|
||||||
while (existing && !($container.find('#'+existing.modelname+'-'+existing.id).length)) {
|
while (existing && !($container.find('#'+existing.modelname+'-'+existing.id))) {
|
||||||
first_missing = existing ;
|
first_missing = existing;
|
||||||
existing = existing.parent;
|
existing = this.get_parent(existing);
|
||||||
}
|
}
|
||||||
|
|
||||||
var $to_insert = this.render_element(first_missing, templates, options);
|
var $to_insert = this.render_element(first_missing, templates, options);
|
||||||
|
@ -1013,7 +1021,11 @@ class ModelForest {
|
||||||
* @param {Object} [options] Options for element render method
|
* @param {Object} [options] Options for element render method
|
||||||
*/
|
*/
|
||||||
display($container, templates, options) {
|
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) {
|
for (let root of this.roots) {
|
||||||
$container.append(this.render_element(root, templates, options));
|
$container.append(this.render_element(root, templates, options));
|
||||||
}
|
}
|
||||||
|
@ -1021,34 +1033,54 @@ class ModelForest {
|
||||||
return $container;
|
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) {
|
function recurse(node) {
|
||||||
callback(node) ;
|
if (node.constructor.verbose_name === modelname) {
|
||||||
|
if (callback(node)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let child of node.children)
|
var children = that.get_children(node);
|
||||||
recurse(child);
|
if (children) {
|
||||||
|
for (let child of children)
|
||||||
|
if (recurse(child))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let root of this.roots)
|
for (let root of this.roots)
|
||||||
recurse(root);
|
if (recurse(root))
|
||||||
|
return ;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find instance in tree with given type and id
|
* Find instance in tree with given type and id
|
||||||
* @param {string} type
|
* @param {string} modelname
|
||||||
* @param {number} id
|
* @param {number} id
|
||||||
*/
|
*/
|
||||||
find(type, id) {
|
find(modelname, id) {
|
||||||
var result = null;
|
var result = null;
|
||||||
|
|
||||||
function callback(node) {
|
function callback(node) {
|
||||||
if (node.modelname === type && node.content.id == id)
|
if (node.id == id) {
|
||||||
result = node ;
|
result = node ;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.traverse(callback);
|
this.traverse(modelname, callback);
|
||||||
|
|
||||||
return result ;
|
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1093,12 +1125,19 @@ class ArticleList extends APIModelForest {
|
||||||
/**
|
/**
|
||||||
* Default structure for ArticleList instances
|
* Default structure for ArticleList instances
|
||||||
* @abstract
|
* @abstract
|
||||||
* @default <tt>{'article': Article,
|
|
||||||
'category': ArticleCategory}</tt>
|
|
||||||
*/
|
*/
|
||||||
static get models() {
|
static get structure() {
|
||||||
return {'article': Article,
|
return {
|
||||||
'category': ArticleCategory};
|
'article': {
|
||||||
|
'model': Article,
|
||||||
|
'parent': 'category',
|
||||||
|
},
|
||||||
|
'category': {
|
||||||
|
'model': ArticleCategory,
|
||||||
|
'children': 'articles',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1109,15 +1148,6 @@ class ArticleList extends APIModelForest {
|
||||||
static get url_model() {
|
static get url_model() {
|
||||||
return Urls['kfet.kpsul.articles_data']();
|
return Urls['kfet.kpsul.articles_data']();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides model to sort root objects
|
|
||||||
* {@see Models.ModelForest.constructor|ModelForest.constructor}
|
|
||||||
*/
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.root_sort = ArticleCategory;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -458,7 +458,7 @@ class ArticleManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
get_article(id) {
|
get_article(id) {
|
||||||
return this.list.find('article', id).content;
|
return this.list.find('article', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
update_data(data) {
|
update_data(data) {
|
||||||
|
@ -500,7 +500,7 @@ class ArticleManager {
|
||||||
var id = $(this).parent().attr('id').split('-')[1];
|
var id = $(this).parent().attr('id').split('-')[1];
|
||||||
var article = that.list.find('article', id);
|
var article = that.list.find('article', id);
|
||||||
if (article)
|
if (article)
|
||||||
that.validate(article.content);
|
that.validate(article);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._$nb.on('keydown', function(e) {
|
this._$nb.on('keydown', function(e) {
|
||||||
|
@ -577,25 +577,20 @@ class ArticleAutocomplete {
|
||||||
var lower = prefix.toLowerCase() ;
|
var lower = prefix.toLowerCase() ;
|
||||||
var that = this ;
|
var that = this ;
|
||||||
|
|
||||||
article_list.traverse(function(node) {
|
article_list.traverse('article', function(article) {
|
||||||
if (node.modelname === 'article' &&
|
if (article.name.toLowerCase().startsWith(lower))
|
||||||
node.content.name.toLowerCase()
|
that.matching.push(article);
|
||||||
.startsWith(lower)) {
|
|
||||||
that.matching['article'].push(node.content);
|
|
||||||
if (that.matching['category'].indexOf(node.parent.content) == -1)
|
|
||||||
that.matching['category'].push(node.parent.content);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.matching['article'].length == 1) {
|
if (this.matching.length == 1) {
|
||||||
if (!backspace) {
|
if (!backspace) {
|
||||||
this.manager.validate(this.matching['article'][0]) ;
|
this.manager.validate(this.matching[0]) ;
|
||||||
this.showAll() ;
|
this.showAll() ;
|
||||||
} else {
|
} else {
|
||||||
this.manager.unset();
|
this.manager.unset();
|
||||||
this.updateDisplay();
|
this.updateDisplay();
|
||||||
}
|
}
|
||||||
} else if (this.matching['article'].length > 1) {
|
} else if (this.matching.length > 1) {
|
||||||
this.manager.unset();
|
this.manager.unset();
|
||||||
this.updateDisplay() ;
|
this.updateDisplay() ;
|
||||||
if (!backspace)
|
if (!backspace)
|
||||||
|
@ -606,19 +601,27 @@ class ArticleAutocomplete {
|
||||||
updateDisplay() {
|
updateDisplay() {
|
||||||
var that = this;
|
var that = this;
|
||||||
|
|
||||||
this.manager.list.traverse(function(node) {
|
this.manager.list.traverse('category', function(category) {
|
||||||
if (that.matching[node.modelname].indexOf(node.content) != -1) {
|
var is_active = false;
|
||||||
that._$container.find('#'+node.modelname+'-'+node.content.id)
|
for (let article of category.articles) {
|
||||||
.show();
|
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 {
|
} else {
|
||||||
that._$container.find('#'+node.modelname+'-'+node.content.id)
|
that._$container.find('#category-'+category.id).hide();
|
||||||
.hide();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePrefix() {
|
updatePrefix() {
|
||||||
var lower = this.matching['article'].map(function (article) {
|
var lower = this.matching.map(function (article) {
|
||||||
return article.name.toLowerCase() ;
|
return article.name.toLowerCase() ;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -633,14 +636,13 @@ class ArticleAutocomplete {
|
||||||
showAll() {
|
showAll() {
|
||||||
var that = this;
|
var that = this;
|
||||||
this.resetMatch();
|
this.resetMatch();
|
||||||
this.manager.list.traverse(function(node) {
|
this.manager.list.traverse('article', function(article) {
|
||||||
that.matching[node.modelname].push(node.content);
|
that.matching.push(article);
|
||||||
});
|
});
|
||||||
this.updateDisplay();
|
this.updateDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
resetMatch() {
|
resetMatch() {
|
||||||
this.matching = {'article' : [],
|
this.matching = [];
|
||||||
'category': []};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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] = {
|
||||||
y: source.balance,
|
x: new Date(source.at),
|
||||||
label: source.label }
|
y: source.balance,
|
||||||
|
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
|
||||||
|
]
|
||||||
|
|
|
@ -18,12 +18,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 %}
|
||||||
|
@ -71,22 +76,22 @@
|
||||||
{% if account.user == request.user %}
|
{% if account.user == request.user %}
|
||||||
<div class="content-right-block content-right-block-transparent">
|
<div class="content-right-block content-right-block-transparent">
|
||||||
<h2>Statistiques</h2>
|
<h2>Statistiques</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12 nopadding">
|
<div class="col-sm-12 nopadding">
|
||||||
<div class="panel-md-margin">
|
<div class="panel-md-margin">
|
||||||
<h3>Ma balance</h3>
|
<h3>Ma balance</h3>
|
||||||
<div id="stat_balance" class"stat-graph"></div>
|
<div id="stat_balance"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /row -->
|
</div>
|
||||||
<div class="row">
|
</div><!-- /row -->
|
||||||
<div class="col-sm-12 nopadding">
|
<div class="row">
|
||||||
<div class="panel-md-margin">
|
<div class="col-sm-12 nopadding">
|
||||||
<h3>Ma consommation</h3>
|
<div class="panel-md-margin">
|
||||||
<div id="stat_last" class"stat-graph"></div>
|
<h3>Ma consommation</h3>
|
||||||
</div>
|
<div id="stat_last"></div>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /row -->
|
</div>
|
||||||
|
</div><!-- /row -->
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="content-right-block">
|
<div class="content-right-block">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
@ -82,27 +87,26 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="content-right-block content-right-block-transparent">
|
<div class="content-right-block content-right-block-transparent">
|
||||||
<h2>Statistiques</h2>
|
<h2>Statistiques</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12 nopadding">
|
<div class="col-sm-12 nopadding">
|
||||||
<div class="panel-md-margin">
|
<div class="panel-md-margin">
|
||||||
<h3>Ventes de {{ article.name }}</h3>
|
<h3>Ventes de {{ article.name }}</h3>
|
||||||
<div id="stat_last"></div>
|
<div id="stat_last"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /row -->
|
</div>
|
||||||
|
</div><!-- /row -->
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
|
@ -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">
|
||||||
<thead>
|
<table class="table text-center">
|
||||||
<tr>
|
<thead>
|
||||||
<td>Article</td>
|
|
||||||
<td>Théo.</td>
|
|
||||||
<td>Réel</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for form in formset %}
|
|
||||||
{% ifchanged form.category %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="3">{{ form.category_name }}</td>
|
<td>Article</td>
|
||||||
|
<td>Quantité par caisse</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>
|
||||||
{% endifchanged %}
|
</thead>
|
||||||
<tr>
|
<tbody>
|
||||||
{{ form.article }}
|
{% for form in formset %}
|
||||||
<td>{{ form.name }}</td>
|
{% ifchanged form.category %}
|
||||||
<td>{{ form.stock_old }}</td>
|
<tr class='section'>
|
||||||
<td>{{ form.stock_new }}</td>
|
<td>{{ form.category_name }}</td>
|
||||||
</tr>
|
<td colspan="7"></td>
|
||||||
{% endfor %}
|
</tr>
|
||||||
</tbody>
|
{% endifchanged %}
|
||||||
</table>
|
<tr>
|
||||||
{% if not perms.kfet.add_inventory %}
|
{{ form.article }}
|
||||||
<input type="password" name="KFETPASSWORD">
|
<td class='name'>{{ form.name }}</td>
|
||||||
{% endif %}
|
<td class='box_capacity'>{{ form.box_capacity }}</td>
|
||||||
{% csrf_token %}
|
<td><span class='current_stock'>{{ form.stock_old }}</span><span class='stock_diff'></span></td>
|
||||||
{{ formset.management_form }}
|
<td class='box_cellar'>
|
||||||
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg">
|
<div class='col-md-2'></div>
|
||||||
</form>
|
<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>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{ formset.management_form }}
|
||||||
|
{% if not perms.kfet.add_inventory %}
|
||||||
|
<div class='auth-form form-horizontal'>
|
||||||
|
{% include "kfet/form_authentication_snippet.html" %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<input type="submit" value="Enregistrer" class="btn btn-primary btn-lg btn-block">
|
||||||
|
{% csrf_token %}
|
||||||
|
</form>
|
||||||
|
</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 %}
|
||||||
|
|
|
@ -238,7 +238,10 @@ $(document).ready(function() {
|
||||||
|
|
||||||
function amountEuroPurchase(article, nb) {
|
function amountEuroPurchase(article, nb) {
|
||||||
var amount_euro = - article.price * nb ;
|
var amount_euro = - article.price * nb ;
|
||||||
if (Config.get('addcost_for') && Config.get('addcost_amount') && kpsul.account_manager.account.trigramme != Config.get('addcost_for'))
|
if (Config.get('addcost_for')
|
||||||
|
&& Config.get('addcost_amount')
|
||||||
|
&& kpsul.account_manager.account.trigramme != Config.get('addcost_for')
|
||||||
|
&& article.category.has_addcost)
|
||||||
amount_euro -= Config.get('addcost_amount') * nb;
|
amount_euro -= Config.get('addcost_amount') * nb;
|
||||||
var reduc_divisor = 1;
|
var reduc_divisor = 1;
|
||||||
if (kpsul.account_manager.account.is_cof)
|
if (kpsul.account_manager.account.is_cof)
|
||||||
|
@ -282,7 +285,7 @@ $(document).ready(function() {
|
||||||
.attr('data-opeindex', index)
|
.attr('data-opeindex', index)
|
||||||
.find('.number').text(amount+"€").end()
|
.find('.number').text(amount+"€").end()
|
||||||
.find('.name').text('Charge').end()
|
.find('.name').text('Charge').end()
|
||||||
.find('.amount').text(amountToUKF(amount, kpsul.account_manager.account.is_cof, false));
|
.find('.amount').text(amountToUKF(amount, kpsul.account_manager.account.is_cof));
|
||||||
basket_container.prepend(deposit_basket_html);
|
basket_container.prepend(deposit_basket_html);
|
||||||
updateBasketRel();
|
updateBasketRel();
|
||||||
}
|
}
|
||||||
|
@ -295,7 +298,7 @@ $(document).ready(function() {
|
||||||
.attr('data-opeindex', index)
|
.attr('data-opeindex', index)
|
||||||
.find('.number').text(amount+"€").end()
|
.find('.number').text(amount+"€").end()
|
||||||
.find('.name').text('Édition').end()
|
.find('.name').text('Édition').end()
|
||||||
.find('.amount').text(amountToUKF(amount, kpsul.account_manager.account.is_cof, false));
|
.find('.amount').text(amountToUKF(amount, kpsul.account_manager.account.is_cof));
|
||||||
basket_container.prepend(deposit_basket_html);
|
basket_container.prepend(deposit_basket_html);
|
||||||
updateBasketRel();
|
updateBasketRel();
|
||||||
}
|
}
|
||||||
|
@ -308,7 +311,7 @@ $(document).ready(function() {
|
||||||
.attr('data-opeindex', index)
|
.attr('data-opeindex', index)
|
||||||
.find('.number').text(amount+"€").end()
|
.find('.number').text(amount+"€").end()
|
||||||
.find('.name').text('Retrait').end()
|
.find('.name').text('Retrait').end()
|
||||||
.find('.amount').text(amountToUKF(amount, kpsul.account_manager.account.is_cof, false));
|
.find('.amount').text(amountToUKF(amount, kpsul.account_manager.account.is_cof));
|
||||||
basket_container.prepend(withdraw_basket_html);
|
basket_container.prepend(withdraw_basket_html);
|
||||||
updateBasketRel();
|
updateBasketRel();
|
||||||
}
|
}
|
||||||
|
@ -645,6 +648,71 @@ $(document).ready(function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
// -----
|
||||||
|
// Cancel from history
|
||||||
|
// -----
|
||||||
|
|
||||||
|
khistory.$container.selectable({
|
||||||
|
filter: 'div.opegroup, div.ope',
|
||||||
|
selected: function(e, ui) {
|
||||||
|
$(ui.selected).each(function() {
|
||||||
|
if ($(this).hasClass('opegroup')) {
|
||||||
|
var opegroup = $(this).data('opegroup');
|
||||||
|
$(this).siblings('.ope').filter(function() {
|
||||||
|
return $(this).data('opegroup') == opegroup
|
||||||
|
}).addClass('ui-selected');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('keydown', function (e) {
|
||||||
|
if (e.keyCode == 46) {
|
||||||
|
// DEL (Suppr)
|
||||||
|
var opes_to_cancel = [];
|
||||||
|
khistory.$container.find('.ope.ui-selected').each(function () {
|
||||||
|
opes_to_cancel.push($(this).data('ope'));
|
||||||
|
});
|
||||||
|
if (opes_to_cancel.length > 0)
|
||||||
|
cancelOperations(opes_to_cancel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----
|
||||||
|
// Synchronization
|
||||||
|
// -----
|
||||||
|
|
||||||
|
OperationWebSocket.add_handler(function(data) {
|
||||||
|
for (var i=0; i<data['opegroups'].length; i++) {
|
||||||
|
if (data['opegroups'][i]['add']) {
|
||||||
|
khistory.addOpeGroup(data['opegroups'][i]);
|
||||||
|
} else if (data['opegroups'][i]['cancellation']) {
|
||||||
|
khistory.cancelOpeGroup(data['opegroups'][i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var i=0; i<data['opes'].length; i++) {
|
||||||
|
if (data['opes'][i]['cancellation']) {
|
||||||
|
khistory.cancelOpe(data['opes'][i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var i=0; i<data['checkouts'].length; i++) {
|
||||||
|
if (checkout_data['id'] == data['checkouts'][i]['id']) {
|
||||||
|
checkout_data['balance'] = data['checkouts'][i]['balance'];
|
||||||
|
displayCheckoutData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kpsul.article_manager.update_data(data);
|
||||||
|
|
||||||
|
if (data['addcost']) {
|
||||||
|
Config.set('addcost_for', data['addcost']['for']);
|
||||||
|
Config.set('addcost_amount', data['addcost']['amount']);
|
||||||
|
displayAddcost();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
>>>>>>> origin/aureplop/kpsul_js_refactor
|
||||||
|
|
||||||
// -----
|
// -----
|
||||||
// General
|
// General
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,5 +1,70 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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, "/")
|
||||||
|
|
61
kfet/urls.py
61
kfet/urls.py
|
@ -8,7 +8,7 @@ from kfet.decorators import teamkfet_required
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', views.Home.as_view(),
|
url(r'^$', views.Home.as_view(),
|
||||||
name = 'kfet.home'),
|
name='kfet.home'),
|
||||||
url(r'^login/genericteam$', views.login_genericteam,
|
url(r'^login/genericteam$', views.login_genericteam,
|
||||||
name='kfet.login.genericteam'),
|
name='kfet.login.genericteam'),
|
||||||
url(r'^history$', views.history,
|
url(r'^history$', views.history,
|
||||||
|
@ -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'),
|
name='kfet.account.stat.balance.list'),
|
||||||
url('^accounts/(?P<trigramme>.{3})/stat/balance/d/(?P<nb_date>\d*)/$',
|
url(r'^accounts/(?P<trigramme>.{3})/stat/balance$',
|
||||||
views.AccountStatBalance.as_view(),
|
views.AccountStatBalance.as_view(),
|
||||||
name = 'kfet.account.stat.balance.days'),
|
name='kfet.account.stat.balance'),
|
||||||
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()),
|
||||||
|
@ -149,20 +148,14 @@ urlpatterns = [
|
||||||
# Article - Update
|
# Article - Update
|
||||||
url('^articles/(?P<pk>\d+)/edit$',
|
url('^articles/(?P<pk>\d+)/edit$',
|
||||||
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
|
||||||
|
|
903
kfet/views.py
903
kfet/views.py
File diff suppressed because it is too large
Load diff
|
@ -21,3 +21,4 @@ git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_customma
|
||||||
ldap3
|
ldap3
|
||||||
git+https://github.com/Aureplop/channels.git#egg=channels
|
git+https://github.com/Aureplop/channels.git#egg=channels
|
||||||
django-js-reverse==0.7.3
|
django-js-reverse==0.7.3
|
||||||
|
python-dateutil
|
||||||
|
|
Loading…
Add table
Reference in a new issue