diff --git a/kfet/static/kfet/js/kfet.api.js b/kfet/static/kfet/js/kfet.api.js
index 0020b636..ee5376af 100644
--- a/kfet/static/kfet/js/kfet.api.js
+++ b/kfet/static/kfet/js/kfet.api.js
@@ -153,13 +153,28 @@ class APIModelObject extends ModelObject {
*/
static get url_model() {}
+ /**
+ * Request url to create an instance.
+ * @abstract
+ * @type {string}
+ */
+ static url_create() {}
+
+ /**
+ * Request url to edit an instance of this model.
+ * @abstract
+ * @param {*} api_pk - Identifier of a model instance.
+ * @type {string}
+ */
+ static url_update_for(api_pk) {}
+
/**
* Request url to get a single model instance data.
* @abstract
- * @param {*} api_pk - Identifier of a model instance in api requests.
+ * @param {*} api_pk - Identifier of a model instance.
* @return {string}
*/
- static url_object_for(api_pk) {}
+ static url_read_for(api_pk) {}
/**
* See {@link Models.ModelObject|new ModelObject(data)}.
@@ -181,36 +196,26 @@ class APIModelObject extends ModelObject {
/**
* Request url used to get current instance data.
*/
- get url_object() {
+ get url_read() {
if (this._url_object === undefined)
- return this.is_empty() ? '' : this.constructor.url_object_for(this.api_pk);
+ return this.is_empty() ? '' : this.constructor.url_read_for(this.api_pk);
return this._url_object;
}
- set url_object(v) { this._url_object = v; }
+ set url_read(v) { this._url_object = v; }
/**
* Get data of a distant model instance. It sends a GET HTTP request to
- * {@link Models.APIModelObject#url_object}.
+ * {@link Models.APIModelObject#url_read}.
* @param {object} [api_options] Additional data appended to the request.
- * @param {jQueryAjaxSuccess} [on_success] A function to be called if the request succeeds.
- * @param {jQueryAjaxError} [on_error] A function to be called if the request fails.
*/
- fromAPI(api_options, on_success, on_error) {
- var that = this;
-
+ fromAPI(api_options) {
api_options = api_options || {};
- on_success = on_success || $.noop;
- on_error = on_error || $.noop;
api_options['format'] = 'json';
- $.getJSON(this.url_object, api_options)
- .done(function (json, textStatus, jqXHR) {
- that.from(json);
- on_success(json, textStatus, jqXHR);
- })
- .fail(on_error);
+ return $.getJSON(this.url_read, api_options)
+ .done( (json) => this.from(json) );
}
/**
@@ -221,9 +226,9 @@ class APIModelObject extends ModelObject {
* @param {jQueryAjaxError} [on_error]
* @see {@link Models.APIModelObject#fromAPI|fromAPI}
*/
- get_by_apipk(api_pk, api_options, on_success, on_error) {
- this.url_object = this.constructor.url_object_for(api_pk);
- this.fromAPI(api_options, on_success, on_error);
+ get_by_apipk(api_pk, api_options) {
+ this.url_read = this.constructor.url_read_for(api_pk);
+ return this.fromAPI(api_options);
}
/**
@@ -283,16 +288,30 @@ class Account extends APIModelObject {
*/
static get url_model() { return Urls['kfet.account'](); }
+ static url_create(trigramme) {
+ var url = Urls['kfet.account.create']();
+ if (trigramme) {
+ var trigramme_url = encodeURIComponent(trigramme);
+ url += `?trigramme=${trigramme_url}`
+ }
+ return url;
+ }
+
/**
* @default django-js-reverse('kfet.account.read')(trigramme)
* @param {string} trigramme
- * @see {@link Models.APIModelObject.url_object_for|APIModelObject.url_object_for}
+ * @see {@link Models.APIModelObject.url_read_for|APIModelObject.url_read_for}
*/
- static url_object_for(trigramme) {
+ static url_read_for(trigramme) {
var trigramme_url = encodeURIComponent(trigramme);
return Urls['kfet.account.read'](trigramme_url);
}
+ static url_update_for(trigramme) {
+ var trigramme_url = encodeURIComponent(trigramme);
+ return Urls['kfet.account.update'](trigramme_url);
+ }
+
/**
* @default this.trigramme
*/
@@ -313,7 +332,7 @@ class Account extends APIModelObject {
/**
* Balance converted to UKF according to cof status.
*/
- get balance_ukf() { return amountToUKF(this.balance, this.is_cof); }
+ get balance_ukf() { return amountToUKF(this.balance, this.is_cof, true); }
}
@@ -355,10 +374,14 @@ class Checkout extends APIModelObject {
/**
* @default django-js-reverse('kfet.kpsul.checkout_data.read')(pk)
* @param {string} api_pk - a checkout id
- * @see {@link Models.APIModelObject.url_object_for|APIModelObject.url_object_for}
+ * @see {@link Models.APIModelObject.url_read_for|APIModelObject.url_read_for}
*/
- static url_object_for(api_pk) {
- return Urls['kfet.kpsul.checkout_data.read'](api_pk);
+ static url_read_for(api_pk) {
+ return Urls['kfet.checkout.read'](api_pk);
+ }
+
+ static url_update_for(api_pk) {
+ return Urls['kfet.checkout.update'](api_pk);
}
/**
@@ -404,6 +427,10 @@ class Statement extends ModelObject {
};
}
+ static url_create(checkout_pk) {
+ return Urls['kfet.checkoutstatement.create'](checkout_pk);
+ }
+
/**
* @default {@link Formatters.StatementFormatter}
*/
@@ -508,6 +535,10 @@ class Article extends ModelObject {
// 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);
+ }
}
/**
@@ -1040,29 +1071,14 @@ class APIModelForest extends ModelForest {
* 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.
- * @param {jQueryAjaxSuccess} [on_success] A function to be called if the request succeeds.
- * @param {jQueryAjaxError} [on_error] A function to be called if the request fails.
*/
- fromAPI(api_options, on_success, on_error) {
- var that = this;
-
- api_options = api_options || {}
- on_success = on_success || $.noop;
- on_error = on_error || $.noop;
+ fromAPI(api_options) {
+ api_options = api_options || {};
api_options['format'] = 'json';
- $.ajax({
- dataType: "json",
- url : this.constructor.url_model,
- method : "POST",
- data : api_options,
- })
- .done(function(json, textStatus, jqXHR) {
- that.from(json);
- on_success(json, textStatus, jqXHR);
- })
- .fail(on_error);
+ return $.getJSON(this.constructor.url_model, api_options)
+ .done( (json) => this.from(json) );
}
}
diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js
index c1807165..d9e2624c 100644
--- a/kfet/static/kfet/js/kfet.js
+++ b/kfet/static/kfet/js/kfet.js
@@ -70,6 +70,29 @@ function booleanCheck(v) {
}
+/**
+ * Short: Equivalent to python str format.
+ * Source: [MDN]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals}.
+ * @example
+ * var t1Closure = template`${0}${1}${0}!`;
+ * t1Closure('Y', 'A'); // "YAY!"
+ * @example
+ * var t2Closure = template`${0} ${'foo'}!`;
+ * t2Closure('Hello', {foo: 'World'}); // "Hello World!"
+ */
+function template(strings, ...keys) {
+ return (function(...values) {
+ var dict = values[values.length - 1] || {};
+ var result = [strings[0]];
+ keys.forEach(function(key, i) {
+ var value = Number.isInteger(key) ? values[key] : dict[key];
+ result.push(value, strings[i + 1]);
+ });
+ return result.join('');
+ });
+}
+
+
/**
* Get and store K-Psul config from API.
*
diff --git a/kfet/static/kfet/js/kpsul.js b/kfet/static/kfet/js/kpsul.js
index eade395c..81ee2341 100644
--- a/kfet/static/kfet/js/kpsul.js
+++ b/kfet/static/kfet/js/kpsul.js
@@ -1,3 +1,10 @@
+/**
+ * @file K-Psul JS
+ * @copyright 2017 cof-geek
+ * @license MIT
+ */
+
+
class KPsulManager {
constructor(env) {
@@ -12,12 +19,25 @@ class KPsulManager {
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;
+ }
+
}
@@ -29,6 +49,15 @@ class AccountManager {
this.account = new Account();
this.selection = new AccountSelection(this);
this.search = new AccountSearch(this);
+
+ // buttons: search, read or create
+ this._$buttons_container = this._$container.find('.buttons');
+ this._buttons_templates = {
+ create: template``,
+ read: template``,
+ search: template``,
+ }
+
}
is_empty() { return this.account.is_empty(); }
@@ -45,25 +74,22 @@ class AccountManager {
}
_display_buttons() {
- // dirty
-
- var buttons = '';
+ var buttons;
if (this.is_empty()) {
var trigramme = this.selection.get();
if (trigramme.isValidTri()) {
- var url_base = Urls['kfet.account.create']();
- var url = url_base + '?trigramme=' + encodeURIComponent(trigramme);
- buttons += '';
+ var url = Account.url_create(trigramme);
+ buttons = this._buttons_templates['create']({url: url});
} else { /* trigramme input is empty or invalid */
- buttons += '';
+ buttons = this._buttons_templates['search']();
}
} else { /* an account is loaded */
- var url = this.account.url_object;
- buttons += '';
+ var url = this.account.url_read;
+ buttons = this._buttons_templates['read']({url: url});
}
- this._$container.find('.buttons').html(buttons);
+ this._$buttons_container.html(buttons);
}
update(trigramme) {
@@ -73,10 +99,9 @@ class AccountManager {
trigramme = trigramme || this.selection.get();
if (trigramme.isValidTri()) {
- this.account.get_by_apipk(trigramme, {},
- () => this._update_on_success(),
- () => this.reset_data()
- );
+ this.account.get_by_apipk(trigramme)
+ .done( () => this._update_on_success() )
+ .fail( () => this.reset_data() );
} else {
this.reset_data();
}
@@ -86,7 +111,7 @@ class AccountManager {
$('#id_on_acc').val(this.account.id);
this.display();
- kpsul._env.articleSelect.focus();
+ kpsul.focus();
kpsul._env.updateBasketAmount();
kpsul._env.updateBasketRel();
}
@@ -250,6 +275,12 @@ class CheckoutManager {
this._$laststatement_container = $('#last_statement');
this.laststatement = new Statement();
this.laststatement_display_prefix = '#checkout-last_statement_';
+
+ this._$buttons_container = this._$container.find('.buttons');
+ this._buttons_templates = {
+ read: template``,
+ statement_create: template``,
+ }
}
update(id) {
@@ -262,15 +293,11 @@ class CheckoutManager {
'last_statement': true,
};
- this.checkout.get_by_apipk(id, api_options,
- (data) => this._update_on_success(data),
- () => this.reset_data());
+ this.checkout.get_by_apipk(id, api_options)
+ .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) {
@@ -310,13 +337,13 @@ class CheckoutManager {
_display_buttons() {
var buttons = '';
if (!this.is_empty()) {
- var id = this.checkout.id;
- var url_details = Urls['kfet.checkout.read'](id);
- var url_newcheckout = Urls['kfet.checkoutstatement.create'](id);
- buttons += '';
- buttons += '';
+ var url_newcheckout = Statement.url_create(this.checkout.id);
+ buttons += this._buttons_templates['statement_create']({
+ url: url_newcheckout});
+ var url_read = this.checkout.url_read;
+ buttons += this._buttons_templates['read']({url: url_read});
}
- this._$container.find('.buttons').html(buttons);
+ this._$buttons_container.html(buttons);
}
reset() {
@@ -335,6 +362,11 @@ class CheckoutManager {
this.display();
}
+
+ focus() {
+ this.selection.focus();
+ return this;
+ }
}
@@ -367,6 +399,11 @@ class CheckoutSelection {
reset() {
this._$input.find('option:first').prop('selected', true);
}
+
+ focus() {
+ this._$input.focus();
+ return this;
+ }
}
class ArticleManager {
@@ -415,19 +452,23 @@ class ArticleManager {
reset_data() {
this._$container.html('');
this.list.clear();
- this.list.fromAPI({}, this.display_list.bind(this), $.noop) ;
+ this.list.fromAPI()
+ .done( () => this.display_list() );
+ }
+
+ get_article(id) {
+ return this.list.find('article', id).content;
}
- //TODO: filter articles before ?
update_data(data) {
for (let article_dict of data.articles) {
- var article = this.list.find('article', article_dict['id']);
+ 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_dict['id']+' .stock')
- .text(article_dict['stock']);
+ article.stock = article_dict.stock;
+ this._$container.find('#article-'+article.id+' .stock')
+ .text(article.stock);
}
}
}
@@ -480,7 +521,11 @@ class ArticleManager {
}
focus() {
- this._$input.focus();
+ if (this.is_empty())
+ this._$input.focus();
+ else
+ this._$nb.focus();
+
return this;
}
diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html
index e8bc3c39..2b98838e 100644
--- a/kfet/templates/kfet/kpsul.html
+++ b/kfet/templates/kfet/kpsul.html
@@ -188,7 +188,7 @@ $(document).ready(function() {
commentDialog.open({
callback: confirm_callback,
- next_focus: articleSelect
+ next_focus: kpsul,
});
}
@@ -219,7 +219,7 @@ $(document).ready(function() {
else
displayErrors(getErrorsHtml(response));
},
- next_focus: articleSelect,
+ next_focus: kpsul,
});
}
@@ -228,15 +228,6 @@ $(document).ready(function() {
performOperations();
});
- // -----
- // Articles data
- // -----
-
- var articleSelect = $('#article_autocomplete');
- var articleId = $('#article_id');
- var articleNb = $('#article_number');
- var articlesList = [];
-
// -----
// Basket
// -----
@@ -276,17 +267,13 @@ $(document).ready(function() {
.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(article, nb))
+ if (article.is_low_stock(nb))
article_basket_html.find('.lowstock')
.show();
updateBasketRel();
}
}
- function is_low_stock(article, nb) {
- return (-5 <= article.stock - nb && article.stock - nb <= 5);
- }
-
function addDeposit(amount) {
var deposit_basket_html = $(item_basket_default_html);
var amount = parseFloat(amount).toFixed(2);
@@ -419,15 +406,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 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
@@ -485,11 +469,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,
});
}
@@ -505,11 +487,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,
});
}
@@ -525,11 +505,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,
});
}
@@ -734,7 +712,7 @@ $(document).ready(function() {
} else {
// F2 - Basket reset
resetBasket();
- articleSelect.focus();
+ kpsul.article_manager.focus();
}
return false;
case 114:
@@ -766,7 +744,6 @@ $(document).ready(function() {
// -----
var env = {
- articleSelect: articleSelect,
addPurchase: addPurchase,
updateBasketAmount: updateBasketAmount,
updateBasketRel: updateBasketRel,
diff --git a/kfet/urls.py b/kfet/urls.py
index dd381a14..9c23b178 100644
--- a/kfet/urls.py
+++ b/kfet/urls.py
@@ -169,8 +169,6 @@ urlpatterns = [
# -----
url('^k-psul/$', views.kpsul, name='kfet.kpsul'),
- url(r'^k-psul/checkout_data/(?P\d+)$', views.kpsul_checkout_data,
- name='kfet.kpsul.checkout_data.read'),
url('^k-psul/perform_operations$', views.kpsul_perform_operations,
name='kfet.kpsul.perform_operations'),
url('^k-psul/cancel_operations$', views.kpsul_cancel_operations,
diff --git a/kfet/views.py b/kfet/views.py
index b7d47022..60ba25ac 100644
--- a/kfet/views.py
+++ b/kfet/views.py
@@ -52,6 +52,32 @@ from .statistic import daynames, monthnames, weeknames, \
this_morning, this_monday_morning, this_first_month_day, \
tot_ventes
+
+# source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/
+class JSONResponseMixin(object):
+ """
+ A mixin that can be used to render a JSON response.
+ """
+ def render_to_json_response(self, context, **response_kwargs):
+ """
+ Returns a JSON response, transforming 'context' to make the payload.
+ """
+ return JsonResponse(
+ self.get_data(context),
+ **response_kwargs
+ )
+
+ def get_data(self, context):
+ """
+ Returns an object that will be serialized as JSON by json.dumps().
+ """
+ # Note: This is *EXTREMELY* naive; in reality, you'll need
+ # to do much more complex handling to ensure that arbitrary
+ # objects -- such as Django model instances or querysets
+ # -- can be serialized as JSON.
+ return context
+
+
class Home(TemplateView):
template_name = "kfet/home.html"
@@ -618,18 +644,44 @@ class CheckoutCreate(SuccessMessageMixin, CreateView):
return super(CheckoutCreate, self).form_valid(form)
+
# Checkout - Read
-class CheckoutRead(DetailView):
+class CheckoutRead(JSONResponseMixin, DetailView):
model = Checkout
template_name = 'kfet/checkout_read.html'
context_object_name = 'checkout'
def get_context_data(self, **kwargs):
- context = super(CheckoutRead, self).get_context_data(**kwargs)
- context['statements'] = context['checkout'].statements.order_by('-at')
+ context = super().get_context_data(**kwargs)
+ checkout = self.object
+ if self.request.GET.get('last_statement'):
+ context['laststatement'] = checkout.statements.latest('at')
+ else:
+ context['statements'] = checkout.statements.order_by('-at')
return context
+ def render_to_response(self, context, **kwargs):
+ if self.request.GET.get('format') == 'json':
+ data = model_to_dict(
+ context['checkout'],
+ fields=['id', 'name', 'balance', 'valid_from', 'valid_to']
+ )
+ if 'laststatement' in context:
+ last_statement = context['laststatement']
+ last_statement_data = model_to_dict(
+ last_statement,
+ fields=['id', 'at', 'balance_new', 'balance_old', 'by']
+ )
+ last_statement_data['by'] = str(last_statement.by)
+ # ``at`` is not editable, so skipped by ``model_to_dict``
+ last_statement_data['at'] = last_statement.at
+ data['laststatement'] = last_statement_data
+ return self.render_to_json_response(data)
+ else:
+ return super().render_to_response(context, **kwargs)
+
+
# Checkout - Update
class CheckoutUpdate(SuccessMessageMixin, UpdateView):
@@ -897,28 +949,6 @@ def kpsul_get_settings(request):
return JsonResponse(data)
-@teamkfet_required
-def kpsul_checkout_data(request, pk):
- checkout = get_object_or_404(Checkout, pk=pk)
- data = model_to_dict(
- checkout,
- fields=['id', 'name', 'balance', 'valid_from', 'valid_to']
- )
-
- if request.GET.get('last_statement'):
- last_statement = checkout.statements.latest('at')
- last_statement_data = model_to_dict(
- last_statement,
- fields=['id', 'at', 'balance_new', 'balance_old', 'by']
- )
- last_statement_data['by'] = str(last_statement.by)
- # ``at`` is not editable, so skipped by ``model_to_dict``
- last_statement_data['at'] = last_statement.at
- data['laststatement'] = last_statement_data
-
- return JsonResponse(data)
-
-
@teamkfet_required
def kpsul_update_addcost(request):
addcost_form = AddcostForm(request.POST)
@@ -2145,29 +2175,6 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView):
# ---------------
# Vues génériques
# ---------------
-# source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/
-class JSONResponseMixin(object):
- """
- A mixin that can be used to render a JSON response.
- """
- def render_to_json_response(self, context, **response_kwargs):
- """
- Returns a JSON response, transforming 'context' to make the payload.
- """
- return JsonResponse(
- self.get_data(context),
- **response_kwargs
- )
-
- def get_data(self, context):
- """
- Returns an object that will be serialized as JSON by json.dumps().
- """
- # Note: This is *EXTREMELY* naive; in reality, you'll need
- # to do much more complex handling to ensure that arbitrary
- # objects -- such as Django model instances or querysets
- # -- can be serialized as JSON.
- return context
class JSONDetailView(JSONResponseMixin,