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):