Merge branch 'Aufinal/refactor_articles' into 'aureplop/kpsul_js_refactor'

Aufinal/refactor articles

See merge request !173
This commit is contained in:
Aurélien Delobelle 2017-04-05 17:56:07 +02:00
commit 6a8f41849b
5 changed files with 809 additions and 307 deletions

View file

@ -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;
}

View file

@ -12,7 +12,11 @@
* A model subclasses {@link Models.ModelObject}.<br>
* A model whose instances can be got from API subclasses
* {@link Models.APIModelObject}.<br>
* These two classes should not be used directly.
* A model to manage ModelObject forests
* {@link Models.ModelForest}.<br>
* A ModelObject that can be fetched through API
* {@link Models.APIModelForest}.<br>
* These classes should not be used directly.
* <br><br>
*
* Models with API support:
@ -20,7 +24,9 @@
* {@link Models.Checkout} (partial).
* <br>
* 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 <tt>['id', 'name']</tt>
* @see {@link Models.ModelObject.props|ModelObject.props}
*/
static get props() {
return ['id', 'name'];
}
/**
* Default values for ArticleCategory model instances.
* @default <tt>{ 'id': 0, 'name': '' }</tt>
* @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 <tt>['id', 'name']</tt>
* @see {@link Models.ModelObject.props|ModelObject.props}
*/
static get props() {
return ['id', 'name', 'price', 'stock', 'category'];
}
/**
* Default values for Article model instances.
* @default <tt>{ 'id': 0, 'name': '', 'price': 0, 'stock': 0,
* 'category': new ArticleCategory() }</tt>
* @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.<br>
* 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 = $('<div></div>');
$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 <tt>{'article': Article,
'category': ArticleCategory}</tt>
*/
static get models() {
return {'article': Article,
'category': ArticleCategory};
}
/**
* Default url to get ArticlList data
* @abstract
* @default <tt>django-js-reverse('kfet.kpsul.articles_data')</tt>
* @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; }
}
}

View file

@ -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': '<div class="category"><span class="name"></span></div>',
'article' : '<div class="article"><span class="name"></span><span class="price"></span><span class="stock"></span></div>'}
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 = [];
}
}

View file

@ -128,13 +128,8 @@
<input type="text" id="article_autocomplete" autocomplete="off">
<input type="number" id="article_number" step="1" min="1">
<span type="stock" id="article_stock"></span>
<input type="hidden" id="article_id" value="">
</div>
<div id="articles_data">
<table>
<tbody>
</tbody>
</table>
</div>
</div>
<div class="row kpsul_middle_left_bottom">
@ -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 = '<tr class="category"><td colspan="3"></td></tr>';
var article_default_html = '<tr class="article"><td class="name"></td><td class="price"></td><td class="stock"></td></tr>';
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<data['articles'].length; i++) {
addArticle(data['articles'][i]);
}
});
}
function resetArticles() {
articles_container.find('tr').remove();
articlesList = [];
}
// -----
// Article selection
// -----
var articleSelect = $('#article_autocomplete');
var articleId = $('#article_id');
var articleNb = $('#article_number');
var articleStock = $('#article_stock');
// 8:Backspace|9:Tab|13:Enter|38-40:Arrows|46:DEL|112-117:F1-6|119-123:F8-F12
var normalKeys = /^(8|9|13|37|38|39|40|46|112|113|114|115|116|117|119|120|121|122|123)$/;
var articlesList = [];
function deleteNonMatching(array, str) {
var dup = [];
var lower_str = str.toLowerCase();
for (var i=0; i<array.length; i++) {
if (((array[i][0]).toLowerCase()).indexOf(lower_str) === 0)
dup.push(array[i])
}
return dup;
}
function callbackForPrefix(elt) {
return elt[0].toLowerCase();
}
function sharedPrefix(array) {
var dup = array.map(callbackForPrefix);
dup.sort(); // On trie l'array
// On récupère le préfixe du premier et du dernier élément
var first = dup[0], last = dup[array.length-1],
length = first.length, i = 0;
while (i < length && first.charAt(i) === last.charAt(i)) i++;
return first.substring(0, i);
}
function displayMatchedArticles(array) {
var categories_to_display = [];
for (var i=0; i<articlesList.length; i++) {
if (array.indexOf(articlesList[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<categories_to_display.length; i++) {
articles_container
.find('#data-category-'+categories_to_display[i])
.show();
}
}
function updateMatchedArticles(str, commit = true) {
var lower_str = str.toLowerCase();
var articlesMatch = deleteNonMatching(articlesList, lower_str);
if (articlesMatch.length == 1) {
articleId.val(0);
// 1 seul résultat, victoire
if (commit) {
articleId.val(articlesMatch[0][1]);
articleSelect.val(articlesMatch[0][0]);
articleStock.text('/'+articlesMatch[0][4]);
displayMatchedArticles(articlesList);
return true;
}
displayMatchedArticles(articlesMatch);
} else if (articlesMatch.length > 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<articlesList.length && id != articlesList[i][1]) i++;
var article_data = articlesList[i];
var amount_euro = - article_data[3] * nb ;
function amountEuroPurchase(article, 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'))
amount_euro -= Config.get('addcost_amount') * nb;
var reduc_divisor = 1;
@ -514,45 +291,34 @@ $(document).ready(function() {
return amount_euro / reduc_divisor;
}
function addPurchase(id, nb) {
var i = 0;
while (i<articlesList.length && id != articlesList[i][1]) i++;
var article_data = articlesList[i];
function addPurchase(article, nb) {
var existing = false;
formset_container.find('[data-opeindex]').each(function () {
var opeindex = $(this).attr('data-opeindex');
var article_id = $(this).find('#id_form-'+opeindex+'-article').val();
if (article_id == id) {
if (article_id == article.id) {
existing = true ;
addExistingPurchase(opeindex, nb);
}
});
if (!existing) {
var amount_euro = amountEuroPurchase(id, nb).toFixed(2);
var index = addPurchaseToFormset(article_data[1], nb, amount_euro);
var amount_euro = amountEuroPurchase(article, nb).toFixed(2);
var index = addPurchaseToFormset(article.id, nb, amount_euro);
var article_basket_html = $(item_basket_default_html);
article_basket_html
.attr('data-opeindex', index)
.find('.number').text('('+nb+'/'+article_data[4]+')').end()
.find('.name').text(article_data[0]).end()
.find('.amount').text(amountToUKF(amount_euro, kpsul.account_manager.account.is_cof, false));
.find('.number').text(nb).end()
.find('.name').text(article.name).end()
.find('.amount').text(amountToUKF(amount_euro, kpsul.account_manager.account.is_cof));
basket_container.prepend(article_basket_html);
if (is_low_stock(id, nb))
if (article.is_low_stock(nb))
article_basket_html.find('.lowstock')
.show();
updateBasketRel();
}
}
function is_low_stock(id, nb) {
var i = 0 ;
while (i<articlesList.length && id != articlesList[i][1]) i++;
var article_data = articlesList[i];
var stock = article_data[4] ;
return (-5 <= stock - nb && stock - nb <= 5);
}
function addDeposit(amount) {
var deposit_basket_html = $(item_basket_default_html);
var amount = parseFloat(amount).toFixed(2);
@ -685,15 +451,12 @@ $(document).ready(function() {
function addExistingPurchase(opeindex, nb) {
var type = formset_container.find("#id_form-"+opeindex+"-type").val();
var id = formset_container.find("#id_form-"+opeindex+"-article").val();
var article = kpsul.article_manager.get_article(parseInt(id));
var nb_before = formset_container.find("#id_form-"+opeindex+"-article_nb").val();
var nb_after = parseInt(nb_before) + parseInt(nb);
var amountEuro_after = amountEuroPurchase(id, nb_after);
var amountEuro_after = amountEuroPurchase(article, nb_after);
var amountUKF_after = amountToUKF(amountEuro_after, kpsul.account_manager.account.is_cof, false);
var i = 0;
while (i<articlesList.length && id != articlesList[i][1]) i++;
var article_data = articlesList[i];
if (type == 'purchase') {
if (nb_after == 0) {
deleteFromBasket(opeindex);
@ -701,20 +464,20 @@ $(document).ready(function() {
if (nb_before > 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<data['articles'].length; i++) {
var article = data['articles'][i];
var article_line = articles_container.find('#data-article-'+article.id);
if (article.stock <= 5 && article.stock >= -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);

View file

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