diff --git a/kfet/static/kfet/css/kpsul.css b/kfet/static/kfet/css/kpsul.css
index 08e561d4..d592ec3f 100644
--- a/kfet/static/kfet/css/kpsul.css
+++ b/kfet/static/kfet/css/kpsul.css
@@ -306,30 +306,48 @@ input[type=number]::-webkit-outer-spin-button {
#articles_data {
overflow:auto;
max-height:500px;
-}
-
-#articles_data table {
width: 100%;
}
-#articles_data table tr.article {
+#articles_data div.article {
height:25px;
font-size:14px;
}
-#articles_data table tr.article>td:first-child {
- padding-left:10px;
+#articles_data .article[data_stock="low"] {
+ background:rgba(236,100,0,0.3);
}
-#articles_data table tr.category {
+#articles_data span {
+ height:25px;
+ line-height:25px;
+ display: inline-block;
+}
+
+#articles_data span.name {
+ padding-left:10px;
+ width:78%;
+}
+
+#articles_data span.price {
+ width:8%;
+}
+
+#articles_data span.stock {
+ width:14%;
+}
+
+
+#articles_data div.category {
height:35px;
+ line-height:35px;
background-color:#c8102e;
font-size:16px;
color:#FFF;
font-weight:bold;
}
-#articles_data table tr.category>td:first-child {
+#articles_data div.category>span:first-child {
padding-left:20px;
}
diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js
index 6adc1133..4f73e20b 100644
--- a/kfet/static/kfet/js/kfet.api.js
+++ b/kfet/static/kfet/js/kfet.api.js
@@ -12,7 +12,11 @@
* A model subclasses {@link Models.ModelObject}.
* A model whose instances can be got from API subclasses
* {@link Models.APIModelObject}.
- * These two classes should not be used directly.
+ * A model to manage ModelObject forests
+ * {@link Models.ModelForest}.
+ * A ModelObject that can be fetched through API
+ * {@link Models.APIModelForest}.
+ * These classes should not be used directly.
*
*
* Models with API support:
@@ -20,7 +24,9 @@
* {@link Models.Checkout} (partial).
*
* Models without API support:
- * {@link Models.Statement}.
+ * {@link Models.Statement},
+ * {@link Models.ArticleCategory},
+ * {@link Models.Article}.
*
* @namespace Models
*/
@@ -113,6 +119,13 @@ class ModelObject {
return $container;
}
+ /**
+ * Returns a string value for the model, to use in comparisons
+ * @see {@link Models.TreeNode.compare|TreeNode.compare}
+ */
+ comparevalue() {
+ return this.id.toString();
+ }
}
@@ -428,6 +441,401 @@ class Statement extends ModelObject {
}
+/**
+ * ArticleCategory model. Cannot be accessed through API.
+ * @extends Models.ModelObject
+ * @memberof Models
+ */
+class ArticleCategory extends ModelObject {
+
+ /**
+ * Properties associated to a category
+ * @default ['id', 'name']
+ * @see {@link Models.ModelObject.props|ModelObject.props}
+ */
+ static get props() {
+ return ['id', 'name'];
+ }
+
+ /**
+ * Default values for ArticleCategory model instances.
+ * @default { 'id': 0, 'name': '' }
+ * @see {@link Models.ModelObject.default_data|ModelObject.default_data}
+ */
+ static get default_data() {
+ return {'id': 0, 'name': ''};
+ }
+
+ /**
+ * @default {@link Formatters.ArticleCategoryFormatter}
+ */
+ formatter() {
+ return ArticleCategoryFormatter;
+ }
+
+ /**
+ * Comparison function between ArticleCategory model instances.
+ * @see {@link Models.ModelObject.compare|ModelObject.compare}
+ */
+ static compare(a, b) {
+ return a.name.localeCompare(b.name) ;
+ }
+}
+
+/**
+ * Article model. Cannot be accessed through API.
+ * @extends Models.ModelObject
+ * @memberof Models
+ */
+class Article extends ModelObject {
+ /**
+ * Properties associated to an article
+ * @default ['id', 'name']
+ * @see {@link Models.ModelObject.props|ModelObject.props}
+ */
+ static get props() {
+ return ['id', 'name', 'price', 'stock', 'category'];
+ }
+
+ /**
+ * Default values for Article model instances.
+ * @default { 'id': 0, 'name': '', 'price': 0, 'stock': 0,
+ * 'category': new ArticleCategory() }
+ * @see {@link Models.ModelObject.default_data|ModelObject.default_data}
+ */
+ static get default_data() {
+ return {
+ 'id': 0, 'name': '', 'price': 0, 'stock': 0,
+ 'category': new ArticleCategory(),
+ };
+ }
+
+ /**
+ * @default {@link Formatters.ArticleFormatter}
+ */
+ formatter() {
+ return ArticleFormatter;
+ }
+
+ /**
+ * Comparison function between Article model instances.
+ * @see {@link Models.ModelObject.compare|ModelObject.compare}
+ */
+ static compare(a, b) {
+ return a.name.localeCompare(b.name);
+ }
+
+ // Take care of 'price' type
+ // API currently returns a string object (serialization of Decimal type within Django)
+ get price() { return this._price; }
+ set price(v) { this._price = floatCheck(v); }
+
+ is_low_stock(nb) {
+ return (-5 <= this.stock - nb && this.stock - nb <= 5);
+ }
+}
+
+
+/**
+ * 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
+ * @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);
+ }
+
+
+ /**
+ * Creates empty instance and populates it with data if given
+ * @param {Object[]} [datalist=[]]
+ */
+ constructor(datalist) {
+ this.from(datalist || []);
+ }
+
+ /**
+ * Fetches an object from the instance data, or creates it if
+ * it does not exist yet.
+ * If direction >= 0, parent objects are created recursively.
+ * If direction <= 0, child objects are created recursively.
+ * @param {Object} data
+ * @param {number} direction
+ */
+ get_or_create(data, direction) {
+ var model = this.constructor.models[data.modelname];
+
+ var existing = this.find_node(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 = data.child_sort
+
+ if (direction <= 0) {
+ if (data.parent) {
+ var parent = this.get_or_create(data.parent, -1);
+ node.parent = parent;
+ parent.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);
+ }
+ }
+
+ return node ;
+ }
+
+ /**
+ * Resets then populates the instance with the given data.
+ * @param {Object[]} datalist
+ */
+ from(datalist) {
+ this.roots = [];
+ for (let data of datalist) {
+ this.get_or_create(data, 0);
+ }
+ }
+
+ /**
+ * Removes all Models.TreeNode from the tree.
+ */
+ clear() {
+ this.from([]);
+ }
+
+ /**
+ * 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 options = options || {} ;
+
+ var $container = $('
');
+ $container.attr('id', node.modelname+'-'+node.content.id);
+
+ var $rendered = node.content.display($(template), options);
+ $container.append($rendered);
+
+ //dirty
+ node.children.sort(this.constructor.compare.bind(null, this.constructor.models[node.child_sort]));
+
+ for (let child of node.children) {
+ var $child = this.render_element(child, templates, options);
+ $container.append($child);
+ }
+
+ return $container;
+ }
+
+ add_to_container($container, node, templates, options) {
+ var existing = node.parent ;
+ var first_missing = node;
+
+ while (existing && !($container.find('#'+existing.modelname+'-'+existing.id))) {
+ first_missing = existing ;
+ existing = existing.parent;
+ }
+
+ var $to_insert = render_element(first_missing, templates, options);
+ var $insert_in = existing ? $container.find('#'+existing.modelname+'-'+existing.id)
+ : $container ;
+ $insert_in.prepend($to_insert);
+ }
+
+ /**
+ * Display stored data in container.
+ * @param {jQuery} $container
+ * @param {Object} templates Templates to render each model
+ * @param {Object} [options] Options for element render method
+ */
+ display($container, templates, options) {
+ this.roots.sort(this.constructor.compare.bind(null, this.root_sort));
+ for (let root of this.roots) {
+ $container.append(this.render_element(root, templates, options));
+ }
+
+ return $container;
+ }
+
+ /**
+ * Find if node already exists in given tree
+ * @param {Models.TreeNode}
+ */
+ find_node(modelname, id) {
+ var result = null;
+
+ function recurse(node) {
+ if (node.modelname === modelname && node.content.id === id)
+ result = node;
+
+ for (let child of node.children)
+ recurse(child);
+ }
+
+ for (let root of this.roots)
+ recurse(root);
+
+ return result;
+ }
+
+ /**
+ * 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) {
+ function recurse(node) {
+ if (node.modelname === modelname) {
+ var parent = node.parent && node.parent.content || null;
+ var children = node.children.map( (child) => child.content);
+ callback(node.content, children, parent);
+ }
+
+ for (let child of node.children)
+ recurse(child);
+ }
+
+ for (let root of this.roots)
+ recurse(root);
+ }
+
+ /**
+ * Find instance in tree with given type and id
+ * @param {string} modelname
+ * @param {number} id
+ */
+ find(modelname, id) {
+ var result = null;
+
+ function callback(content) {
+ if (content.id == id)
+ result = content ;
+ }
+
+ this.traverse(modelname, callback);
+
+ return result;
+ }
+}
+
+
+/**
+ * Describes a model list that can be filled through API.
+ * @extends Models.ModelForest
+ * @memberof Models
+ */
+class APIModelForest extends ModelForest {
+
+ /**
+ * Request url to fill the model.
+ * @abstract
+ * @type {string}
+ */
+ static get url_model() {}
+
+ /**
+ * 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.
+ */
+ fromAPI(api_options) {
+ api_options = api_options || {};
+
+ api_options['format'] = 'json';
+
+ return $.getJSON(this.constructor.url_model, api_options)
+ .done( (json) => this.from(json) );
+ }
+}
+
+
+/**
+ * ArticleList model. Can be accessed through API.
+ * @extends Models.APIModelForest
+ * @memberof Models
+ */
+class ArticleList extends APIModelForest {
+
+ /**
+ * Default structure for ArticleList instances
+ * @abstract
+ * @default {'article': Article,
+ 'category': ArticleCategory}
+ */
+ static get models() {
+ return {'article': Article,
+ 'category': ArticleCategory};
+ }
+
+ /**
+ * Default url to get ArticlList data
+ * @abstract
+ * @default django-js-reverse('kfet.kpsul.articles_data')
+ * @see {@link Models.APIModelForest.url_model|APIModelForest.url_model}
+ */
+ 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;
+ }
+}
+
+
/* ---------- ---------- */
@@ -662,3 +1070,58 @@ class StatementFormatter extends Formatter {
}
}
+
+
+/**
+ * @memberof Formatters
+ * @extends Formatters.Formatter
+ */
+
+class ArticleCategoryFormatter extends Formatter {
+
+ /**
+ * Properties renderable to html.
+ * @default {@link Models.ArticleCategory.props}
+ */
+ static get props() {
+ return ArticleCategory.props;
+ }
+}
+
+
+/**
+ * @memberof Formatters
+ * @extends Formatters.Formatter
+ */
+
+class ArticleFormatter extends Formatter {
+
+ /**
+ * Properties renderable to html.
+ * @default {@link Models.Article.props}
+ */
+ static get props() {
+ return Article.props;
+ }
+
+ static get attrs() {
+ return ['data_stock'];
+ }
+
+ static prop_price(s) {
+ return amountToUKF(s.price, true);
+ }
+
+ static get _data_stock() {
+ return {
+ 'default': '', 'low': 'low',
+ 'ok': 'ok', 'neg': 'neg',
+ };
+ }
+
+ static attr_data_stock(a) {
+ if (a.stock > 5) { return this._data_stock.ok; }
+ else if (a.stock >= -5) { return this._data_stock.low; }
+ else /* a.stock < -5 */ { return this._data_stock.neg; }
+ }
+}
diff --git a/kfet/static/kfet/js/kpsul.js b/kfet/static/kfet/js/kpsul.js
index c4052d73..e1bb2b55 100644
--- a/kfet/static/kfet/js/kpsul.js
+++ b/kfet/static/kfet/js/kpsul.js
@@ -11,18 +11,32 @@ class KPsulManager {
this._env = env;
this.account_manager = new AccountManager(this);
this.checkout_manager = new CheckoutManager(this);
+ this.article_manager = new ArticleManager(this);
}
reset(soft) {
soft = soft || false;
this.account_manager.reset();
+ this.article_manager.reset();
if (!soft) {
this.checkout_manager.reset();
+ this.article_manager.reset_data();
}
}
+ focus() {
+ if (this.checkout_manager.is_empty())
+ this.checkout_manager.focus();
+ else if (this.account_manager.is_empty())
+ this.account_manager.focus();
+ else
+ this.article_manager.focus();
+
+ return this;
+ }
+
}
@@ -96,7 +110,7 @@ class AccountManager {
$('#id_on_acc').val(this.account.id);
this.display();
- kpsul._env.articleSelect.focus();
+ kpsul.focus();
kpsul._env.updateBasketAmount();
kpsul._env.updateBasketRel();
}
@@ -282,11 +296,7 @@ class CheckoutManager {
.done( (data) => this._update_on_success(data) )
.fail( () => this.reset_data() );
- if (kpsul.account_manager.is_empty()) {
- kpsul.account_manager.focus();
- } else {
- kpsul._env.articleSelect.focus().select();
- }
+ kpsul.focus();
}
_update_on_success(data) {
@@ -351,6 +361,11 @@ class CheckoutManager {
this.display();
}
+
+ focus() {
+ this.selection.focus();
+ return this;
+ }
}
@@ -383,5 +398,249 @@ class CheckoutSelection {
reset() {
this._$input.find('option:first').prop('selected', true);
}
+
+ focus() {
+ this._$input.focus();
+ return this;
+ }
}
+class ArticleManager {
+
+ constructor(env) {
+ this._env = env; // Global K-Psul Manager
+
+ this._$container = $('#articles_data');
+ this._$input = $('#article_autocomplete');
+ this._$nb = $('#article_number');
+ this._$stock = $('#article_stock');
+ this.templates = {'category': '
',
+ 'article' : '
'}
+
+ this.selected = new Article() ;
+ this.list = new ArticleList() ;
+ this.autocomplete = new ArticleAutocomplete(this);
+
+ this._init_events();
+ }
+
+ get nb() {
+ return this._$nb.val() ;
+ }
+
+ display_list() {
+ this.list.display(this._$container, this.templates) ;
+ }
+
+ validate(article) {
+ this.selected.from(article) ;
+ this._$input.val(article.name);
+ this._$nb.val('1');
+ this._$stock.text('/'+article.stock);
+ this._$nb.focus().select();
+ }
+
+ unset() {
+ this.selected.clear();
+ }
+
+ is_empty() {
+ return this.selected.is_empty();
+ }
+
+ reset_data() {
+ this._$container.html('');
+ this.list.clear();
+ this.list.fromAPI()
+ .done( () => this.display_list() );
+ }
+
+ get_article(id) {
+ return this.list.find('article', id);
+ }
+
+ update_data(data) {
+ for (let article_dict of data.articles) {
+ var article = this.get_article(article_dict.id);
+
+ // For now, article additions are disregarded
+ if (article) {
+ article.stock = article_dict.stock;
+ this._$container.find('#article-'+article.id+' .stock')
+ .text(article.stock);
+ }
+ }
+ }
+
+ reset() {
+ this.unset() ;
+ this._$stock.text('');
+ this._$nb.val('');
+ this._$input.val('');
+ this.autocomplete.showAll() ;
+ }
+
+ _init_events() {
+ var that = this;
+
+ // 8:Backspace|9:Tab|13:Enter|46:DEL|112-117:F1-6|119-123:F8-F12
+ var normalKeys = /^(8|9|13|46|112|113|114|115|116|117|119|120|121|122|123)$/;
+ var arrowKeys = /^(37|38|39|40)$/;
+
+ //Global input event (to merge ?)
+ this._$input.on('keydown', function(e) {
+ if (e.keyCode == 13 && that._$input.val() == '') {
+ kpsul._env.performOperations();
+ }
+ });
+
+ this._$container.on('click', '.article', function() {
+ var id = $(this).parent().attr('id').split('-')[1];
+ var article = that.list.find('article', id);
+ if (article)
+ that.validate(article);
+ });
+
+ this._$nb.on('keydown', function(e) {
+ if (e.keyCode == 13 && that.constructor.check_nb(that.nb) && !that.is_empty()) {
+ kpsul._env.addPurchase(that.selected, that.nb);
+ that.reset();
+ that.focus();
+ }
+ if (normalKeys.test(e.keyCode) || arrowKeys.test(e.KeyCode) || e.ctrlKey) {
+ if (e.ctrlKey && e.charCode == 65)
+ that._$nb.val('');
+ return true ;
+ }
+ if (that.constructor.check_nb(that.nb+e.key))
+ return true;
+ return false;
+
+ });
+ }
+
+ focus() {
+ if (this.is_empty())
+ this._$input.focus();
+ else
+ this._$nb.focus();
+
+ return this;
+ }
+
+ static check_nb(nb) {
+ return /^[0-9]+$/.test(nb) && nb > 0 && nb <= 24 ;
+ }
+}
+
+class ArticleAutocomplete {
+
+ constructor(article_manager) {
+ this.manager = article_manager;
+ this._$container = article_manager._$container ;
+ this._$input = $('#article_autocomplete');
+
+ this.showAll() ;
+ this._init_events();
+ }
+
+ _init_events() {
+ var that = this ;
+
+ // 8:Backspace|9:Tab|13:Enter|46:DEL|112-117:F1-6|119-123:F8-F12
+ var normalKeys = /^(8|9|13|46|112|113|114|115|116|117|119|120|121|122|123)$/;
+
+ this._$input
+ .on('keydown', function(e) {
+ var text = that._$input.val() ;
+ if (normalKeys.test(e.keyCode) || e.ctrlKey) {
+ // For the backspace key, we suppose the cursor is at the very end
+ if(e.keyCode == 8) {
+ that.update(text.substring(0, text.length-1), true);
+ }
+ return true ;
+ }
+ that.update(text+e.key, false);
+
+ return false ;
+
+ });
+
+ }
+
+ update(prefix, backspace) {
+
+ this.resetMatch();
+ var article_list = this.manager.list ;
+ var lower = prefix.toLowerCase() ;
+ var that = this ;
+
+ article_list.traverse('article', function(article) {
+ if (article.name.toLowerCase().startsWith(lower))
+ that.matching.push(article);
+ });
+
+ if (this.matching.length == 1) {
+ if (!backspace) {
+ this.manager.validate(this.matching[0]) ;
+ this.showAll() ;
+ } else {
+ this.manager.unset();
+ this.updateDisplay();
+ }
+ } else if (this.matching.length > 1) {
+ this.manager.unset();
+ this.updateDisplay() ;
+ if (!backspace)
+ this.updatePrefix();
+ }
+ }
+
+ updateDisplay() {
+ var that = this;
+
+ this.manager.list.traverse('category', function(category, articles) {
+ var is_active = false;
+ for (let article of 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('#category-'+category.id).hide();
+ }
+ });
+ }
+
+ updatePrefix() {
+ var lower = this.matching.map(function (article) {
+ return article.name.toLowerCase() ;
+ });
+
+ lower.sort() ;
+ var first = lower[0], last = lower[lower.length-1],
+ length = first.length, i = 0;
+ while (i < length && first.charAt(i) === last.charAt(i)) i++;
+
+ this._$input.val(first.substring(0,i)) ;
+ }
+
+ showAll() {
+ var that = this;
+ this.resetMatch();
+ this.manager.list.traverse('article', function(article) {
+ that.matching.push(article);
+ });
+ this.updateDisplay();
+ }
+
+ resetMatch() {
+ this.matching = [];
+ }
+}
diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html
index 7b74021f..38a9e323 100644
--- a/kfet/templates/kfet/kpsul.html
+++ b/kfet/templates/kfet/kpsul.html
@@ -128,13 +128,8 @@
-
@@ -196,7 +191,7 @@ $(document).ready(function() {
commentDialog.open({
callback: confirm_callback,
- next_focus: articleSelect
+ next_focus: kpsul,
});
}
@@ -242,7 +237,7 @@ $(document).ready(function() {
else
displayErrors(getErrorsHtml(response));
},
- next_focus: articleSelect,
+ next_focus: kpsul,
});
}
@@ -278,221 +273,6 @@ $(document).ready(function() {
cancelOperations();
});
- // -----
- // Articles data
- // -----
-
- var articles_container = $('#articles_data tbody');
- var article_category_default_html = '
|
';
- var article_default_html = '
| | |
';
-
- function addArticle(article) {
- var article_html = $(article_default_html);
- article_html.attr('id', 'data-article-'+article['id']);
- article_html.addClass('data-category-'+article['category_id']);
- for (var elem in article) {
- article_html.find('.'+elem).text(article[elem])
- }
- if (-5 <= article['stock'] && article['stock'] <= 5) {
- article_html.addClass('low-stock');
- }
- article_html.find('.price').text(amountToUKF(article['price'], false, false)+' UKF');
- var category_html = articles_container
- .find('#data-category-'+article['category_id']);
- if (category_html.length == 0) {
- category_html = $(article_category_default_html);
- category_html.attr('id', 'data-category-'+article['category_id']);
- category_html.find('td').text(article['category__name']);
- var added = false;
- articles_container.find('.category').each(function() {
- if (article['category__name'].toLowerCase() < $(this).text().toLowerCase()) {
- $(this).before(category_html);
- added = true;
- return false;
- }
- });
- if (!added) articles_container.append(category_html);
- }
- var $after = articles_container.find('#data-category-'+article['category_id']);
- articles_container
- .find('.article.data-category-'+article['category_id']).each(function() {
- if (article['name'].toLowerCase < $('.name', this).text().toLowerCase())
- return false;
- $after = $(this);
- });
- $after.after(article_html);
- // Pour l'autocomplétion
- articlesList.push([article['name'],article['id'],article['category_id'],article['price'], article['stock']]);
- }
-
- function getArticles() {
- $.ajax({
- dataType: "json",
- url : "{% url 'kfet.kpsul.articles_data' %}",
- method : "GET",
- })
- .done(function(data) {
- for (var i=0; i
-1) {
- articles_container.find('#data-article-'+articlesList[i][1]).show();
- if (categories_to_display.indexOf(articlesList[i][2]) == -1)
- categories_to_display.push(articlesList[i][2]);
- } else {
- articles_container.find('#data-article-'+articlesList[i][1]).hide();
- }
- }
- articles_container.find('.category').hide();
- for (var i=0; i 1) {
- articleId.val(0);
- if (commit)
- articleSelect.val(sharedPrefix(articlesMatch));
- displayMatchedArticles(articlesMatch);
- }
- return false;
- }
-
- // A utiliser après la sélection d'un article
- function goToArticleNb() {
- articleNb.val('1');
- articleNb.focus().select();
- }
-
- articleSelect.on('keydown', function(e) {
- var text = articleSelect.val();
- // Comportement normal pour ces touches
- if (normalKeys.test(e.keyCode) || e.ctrlKey) {
- if (text == '' && e.keyCode == 13)
- performOperations();
- if (e.keyCode == 8)
- updateMatchedArticles(text.substring(0,text.length-1), false);
- if (e.charCode == 65 && e.ctrlKey) {
- articleId.val(0);
- articleSelect.val('');
- }
- return true;
- }
- if (updateMatchedArticles(text+e.key))
- goToArticleNb();
- return false;
- });
-
- function getArticleId($article) {
- return $article.attr('id').split('-')[2];
- }
-
- function getArticleName($article) {
- return $article.find('.name').text();
- }
-
- function getArticleStock($article) {
- return $article.find('.stock').text();
- }
-
- // Sélection des articles à la souris/tactile
- articles_container.on('click', '.article', function() {
- articleId.val(getArticleId($(this)));
- articleSelect.val(getArticleName($(this)));
- articleStock.text('/'+getArticleStock($(this)));
- displayMatchedArticles(articlesList);
- goToArticleNb();
- });
-
- function is_nb_ok(nb) {
- return /^[0-9]+$/.test(nb) && nb > 0 && nb <= 24;
- }
-
- articleNb.on('keydown', function(e) {
- if (e.keyCode == 13 && is_nb_ok(articleNb.val()) && articleId.val() > 0) {
- addPurchase(articleId.val(), articleNb.val());
- articleSelect.val('');
- articleNb.val('');
- articleStock.text('');
- articleSelect.focus();
- displayMatchedArticles(articlesList);
- return false;
- }
- if (normalKeys.test(e.keyCode) || arrowKeys.test(e.keyCode) || e.keyCode == 8 || e.ctrlKey) {
- if (e.ctrlKey && e.charCode == 65)
- articleNb.val('');
- return true;
- }
- var nb = articleNb.val()+e.key;
- if (is_nb_ok(nb))
- return true;
- return false;
- });
-
// -----
// Basket
// -----
@@ -501,11 +281,8 @@ $(document).ready(function() {
var basket_container = $('#basket table');
var arrowKeys = /^(37|38|39|40)$/;
- function amountEuroPurchase(id, nb) {
- var i = 0;
- while (i 0) {
var article_html = basket_container.find('[data-opeindex='+opeindex+']');
article_html.find('.amount').text(amountUKF_after).end()
- .find('.number').text('('+nb_after+'/'+article_data[4]+')').end() ;
+ .find('.number').text('('+nb_after+'/'+article.stock+')').end() ;
} else {
article_html = $(item_basket_default_html);
article_html
.attr('data-opeindex', opeindex)
- .find('.number').text('('+nb_after+'/'+article_data[4]+')').end()
- .find('.name').text(article_data[0]).end()
+ .find('.number').text('('+nb_after+'/'+article.stock+')').end()
+ .find('.name').text(article.name).end()
.find('.amount').text(amountUKF_after);
basket_container.prepend(article_basket_html);
}
- if (is_low_stock(id, nb_after))
+ if (article.is_low_stock(nb_after))
article_html.find('.lowstock')
.show();
else
@@ -732,11 +495,7 @@ $(document).ready(function() {
mngmt_total_forms_input.val(1);
formset_container.find('div').remove();
updateBasketRel();
- articleId.val(0);
- articleSelect.val('');
- articleNb.val('');
- articleStock.text('');
- displayMatchedArticles(articlesList);
+ kpsul.article_manager.reset();
}
// -----
@@ -755,11 +514,9 @@ $(document).ready(function() {
addDeposit(amount);
}
- var next_focus = articleSelect.val() ? articleNb : articleSelect ;
-
depositDialog.open({
callback: callback,
- next_focus: next_focus,
+ next_focus: kpsul.article_manager,
});
}
@@ -775,11 +532,9 @@ $(document).ready(function() {
addEdit(amount);
}
- var next_focus = articleSelect.val() ? articleNb : articleSelect ;
-
editDialog.open({
callback: callback,
- next_focus: next_focus,
+ next_focus: kpsul.article_manager,
});
}
@@ -795,11 +550,9 @@ $(document).ready(function() {
addWithdraw(amount);
}
- var next_focus = articleSelect.val() ? articleNb : articleSelect ;
-
withdrawDialog.open({
callback: callback,
- next_focus: next_focus,
+ next_focus: kpsul.article_manager,
});
}
@@ -1013,21 +766,9 @@ $(document).ready(function() {
displayCheckoutData();
}
}
- for (var i=0; i= -5) {
- article_line.addClass('low-stock');
- } else {
- article_line.removeClass('low-stock');
- }
- article_line.find('.stock')
- .text(article['stock']);
- var i = 0;
- while (i < articlesList.length && articlesList[i][1] != article.id) i++ ;
- articlesList[i][4] = article.stock ;
- }
+ kpsul.article_manager.update_data(data);
+
if (data['addcost']) {
Config.set('addcost_for', data['addcost']['for']);
Config.set('addcost_amount', data['addcost']['amount']);
@@ -1053,12 +794,11 @@ $(document).ready(function() {
function hardReset(give_tri_focus=true) {
coolReset(give_tri_focus);
kpsul.checkout_manager.reset();
- resetArticles();
resetPreviousOp();
khistory.reset();
Config.reset(function() {
+ kpsul.article_manager.reset_data();
displayAddcost();
- getArticles();
getHistory();
});
}
@@ -1103,7 +843,7 @@ $(document).ready(function() {
} else {
// F2 - Basket reset
resetBasket();
- articleSelect.focus();
+ kpsul.article_manager.focus();
}
return false;
case 114:
@@ -1135,9 +875,10 @@ $(document).ready(function() {
// -----
var env = {
- articleSelect: articleSelect,
+ addPurchase: addPurchase,
updateBasketAmount: updateBasketAmount,
updateBasketRel: updateBasketRel,
+ performOperations: performOperations,
};
window.kpsul = new KPsulManager(env);
diff --git a/kfet/views.py b/kfet/views.py
index a2e4ca17..d522d753 100644
--- a/kfet/views.py
+++ b/kfet/views.py
@@ -1419,13 +1419,34 @@ def history_json(request):
opegroups_list.append(opegroup_dict)
return JsonResponse({ 'opegroups': opegroups_list })
+
@teamkfet_required
def kpsul_articles_data(request):
articles = (
Article.objects
- .values('id', 'name', 'price', 'stock', 'category_id', 'category__name')
- .filter(is_sold=True))
- return JsonResponse({ 'articles': list(articles) })
+ .filter(is_sold=True)
+ .select_related('category'))
+ articlelist = []
+
+ for article in articles:
+ articlelist.append({
+ 'modelname': 'article',
+ 'content': {
+ 'id': article.id,
+ 'name': article.name,
+ 'price': article.price,
+ 'stock': article.stock,
+ },
+ 'parent': {
+ 'modelname': 'category',
+ 'content': {
+ 'id': article.category.id,
+ 'name': article.category.name,
+ },
+ 'child_sort': 'article',
+ }
+ })
+ return JsonResponse(articlelist, safe=False)
@teamkfet_required
def history(request):