Merge branch 'skia/counter_rework' into 'master'

counter: make click page dynamic to avoid repetitive loading

See merge request ae/Sith!278
This commit is contained in:
Skia 2021-09-27 19:21:28 +00:00
commit cb3307509d
15 changed files with 288 additions and 164 deletions

View File

@ -79,9 +79,11 @@ class ComTest(TestCase):
) )
r = self.client.get(reverse("core:index")) r = self.client.get(reverse("core:index"))
self.assertTrue(r.status_code == 200) self.assertTrue(r.status_code == 200)
self.assertTrue( self.assertContains(
"""<div id="alert_box">\\n <div class="markdown"><h3>ALERTE!</h3>\\n<p><strong>Caaaataaaapuuuulte!!!!</strong></p>""" r,
in str(r.content) """<div id="alert_box">
<div class="markdown"><h3>ALERTE!</h3>
<p><strong>Caaaataaaapuuuulte!!!!</strong></p>""",
) )
def test_info_msg(self): def test_info_msg(self):
@ -95,9 +97,10 @@ class ComTest(TestCase):
) )
r = self.client.get(reverse("core:index")) r = self.client.get(reverse("core:index"))
self.assertTrue(r.status_code == 200) self.assertTrue(r.status_code == 200)
self.assertTrue( self.assertContains(
"""<div id="info_box">\\n <div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>""" r,
in str(r.content) """<div id="info_box">
<div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>""",
) )
def test_birthday_non_subscribed_user(self): def test_birthday_non_subscribed_user(self):

File diff suppressed because one or more lines are too long

View File

@ -1125,7 +1125,7 @@ u, .underline {
overflow: auto; overflow: auto;
} }
#bar_ui { #click_form {
float: left; float: left;
min-width: 57%; min-width: 57%;
} }

View File

@ -126,6 +126,7 @@
</header> </header>
<div id="info_boxes"> <div id="info_boxes">
{% block info_boxes %}
{% set sith = get_sith() %} {% set sith = get_sith() %}
{% if sith.alert_msg %} {% if sith.alert_msg %}
<div id="alert_box"> <div id="alert_box">
@ -137,6 +138,7 @@
{{ sith.info_msg|markdown }} {{ sith.info_msg|markdown }}
</div> </div>
{% endif %} {% endif %}
{% endblock %}
</div> </div>
{% else %}{# if not popup #} {% else %}{# if not popup #}

View File

@ -4,6 +4,12 @@
{% trans obj=object %}Edit {{ obj }}{% endtrans %} {% trans obj=object %}Edit {{ obj }}{% endtrans %}
{% endblock %} {% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %} {% block content %}
<h2>{% trans %}Make a cash register summary{% endtrans %}</h2> <h2>{% trans %}Make a cash register summary{% endtrans %}</h2>
<form action="" method="post" id="cash_summary_form"> <form action="" method="post" id="cash_summary_form">

View File

@ -1,27 +1,16 @@
{% extends "core/base.jinja" %} {% extends "core/base.jinja" %}
{% from "core/macros.jinja" import user_mini_profile, user_subscription %} {% from "core/macros.jinja" import user_mini_profile, user_subscription %}
{% macro add_product(id, content, class="") %}
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="{{ class }}">
{% csrf_token %}
<input type="hidden" name="action" value="add_product">
<button type="submit" name="product_id" value="{{ id }}"> {{ content|safe }} </button>
</form>
{% endmacro %}
{% macro del_product(id, content, class="") %}
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="{{ class }}">
{% csrf_token %}
<input type="hidden" name="action" value="del_product">
<button type="submit" name="product_id" value="{{ id }}"> {{ content }} </button>
</form>
{% endmacro %}
{% block title %} {% block title %}
{{ counter }} {{ counter }}
{% endblock %} {% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %} {% block content %}
<h4 id="click_interface">{{ counter }}</h4> <h4 id="click_interface">{{ counter }}</h4>
@ -52,56 +41,57 @@
{% endif %} {% endif %}
</div> </div>
<div id="bar_ui"> <div id="bar_ui">
<noscript>
<p class="important">Javascript is required for the counter UI.</p>
</noscript>
<div id="click_form">
<h5>{% trans %}Selling{% endtrans %}</h5> <h5>{% trans %}Selling{% endtrans %}</h5>
<div> <div>
<div class="important"> <div class="important">
{% if request.session['too_young'] %} <p v-for="error in errors"><strong>{{ error }}</strong></p>
<p><strong>{% trans %}Too young for that product{% endtrans %}</strong></p>
{% endif %}
{% if request.session['not_allowed'] %}
<p><strong>{% trans %}Not allowed for that product{% endtrans %}</strong></p>
{% endif %}
{% if request.session['no_age'] %}
<p><strong>{% trans %}No date of birth provided{% endtrans %}</strong></p>
{% endif %}
{% if request.session['not_enough'] %}
<p><strong>{% trans %}Not enough money{% endtrans %}</strong></p>
{% endif %}
</div> </div>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="code_form" @submit.prevent="handle_code">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="action" value="code"> <input type="hidden" name="action" value="code">
<input type="input" name="code" value="" class="focus" id="code_field"/> <input type="input" name="code" value="" class="focus" id="code_field"/>
<input type="submit" value="{% trans %}Go{% endtrans %}" /> <input type="submit" value="{% trans %}Go{% endtrans %}" />
</form> </form>
<p>{% trans %}Basket: {% endtrans %}</p> <p>{% trans %}Basket: {% endtrans %}</p>
{% raw %}
<ul> <ul>
{% for id,infos in request.session['basket']|dictsort %} <li v-for="p_info,p_id in basket">
{% set product = counter.products.filter(id=id).first() %}
{% set s = infos['qty'] * infos['price'] / 100 %} <form method="post" action="" class="inline del_product_form" @submit.prevent="handle_action">
<li>{{ del_product(id, '-', "inline") }} {{ infos['qty'] + infos['bonus_qty'] }} {{ add_product(id, '+', "inline") }} <input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
{{ product.name }}: {{ "%0.2f"|format(s) }} <input type="hidden" name="action" value="del_product">
{% if infos['bonus_qty'] %} <input type="hidden" name="product_id" v-bind:value="p_id">
P <button type="submit"> - </button>
{% endif %} </form>
{{ p_info["qty"] + p_info["bonus_qty"] }}
<form method="post" action="" class="inline add_product_form" @submit.prevent="handle_action">
<input type="hidden" name="csrfmiddlewaretoken" v-bind:value="js_csrf_token">
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" v-bind:value="p_id">
<button type="submit"> + </button>
</form>
{{ products[p_id].name }}: {{ (p_info["qty"]*p_info["price"]/100).toLocaleString(undefined, { minimumFractionDigits: 2 }) }} € <span v-if="p_info['bonus_qty'] > 0">P</span>
</li> </li>
{% endfor %}
</ul> </ul>
<p><strong>{% trans %}Total: {% endtrans %}{{ "%0.2f"|format(basket_total) }} €</strong></p> <p>
<strong>Total: {{ sum_basket().toLocaleString(undefined, { minimumFractionDigits: 2 }) }} €</strong>
</p>
{% endraw %}
<div class="important"> <div class="important">
{% if request.session['too_young'] %} <p v-for="error in errors"><strong>{{ error }}</strong></p>
<p><strong>{% trans %}Too young for that product{% endtrans %}</strong></p>
{% endif %}
{% if request.session['not_allowed'] %}
<p><strong>{% trans %}Not allowed for that product{% endtrans %}</strong></p>
{% endif %}
{% if request.session['no_age'] %}
<p><strong>{% trans %}No date of birth provided{% endtrans %}</strong></p>
{% endif %}
{% if request.session['not_enough'] %}
<p><strong>{% trans %}Not enough money{% endtrans %}</strong></p>
{% endif %}
</div> </div>
<form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}"> <form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="action" value="finish"> <input type="hidden" name="action" value="finish">
@ -141,23 +131,89 @@
{% else %} {% else %}
{% set file = static('core/img/na.gif') %} {% set file = static('core/img/na.gif') %}
{% endif %} {% endif %}
{% set prod = '<strong>%s</strong><hr><img src="%s" /><span>%s €<br>%s</span>' % (p.name, file, p.selling_price, p.code) %} <form method="post" action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}" class="form_button add_product_form" @submit.prevent="handle_action">
{{ add_product(p.id, prod, "form_button") }} {% csrf_token %}
<input type="hidden" name="action" value="add_product">
<input type="hidden" name="product_id" value="{{ p.id }}">
<button type="submit"><strong>{{ p.name }}</strong><hr><img src="{{ file }}" /><span>{{ p.selling_price }} €<br>{{ p.code }}</span></button>
</form>
{%- endfor %} {%- endfor %}
</div> </div>
{%- endfor %} {%- endfor %}
</div> </div>
</div>
{% endblock %} {% endblock %}
{% block script %} {% block script %}
<script>
document.getElementById("click_interface").scrollIntoView();
</script>
{{ super() }} {{ super() }}
<script src="{{ static('core/js/vue.global.prod.js') }}"></script>
<script> <script>
$( function() { $( function() {
var products = [ /* Vue.JS dynamic form */
const click_form_vue = Vue.createApp({
data() {
return {
js_csrf_token: "{{ csrf_token }}",
products: {
{% for p in products -%}
{{ p.id }}: {
code: "{{ p.code }}",
name: "{{ p.name }}",
selling_price: "{{ p.selling_price }}",
special_selling_price: "{{ p.special_selling_price }}",
},
{%- endfor %}
},
basket: {{ request.session["basket"]|tojson }},
errors: [],
}
},
methods: {
sum_basket() {
var vm = this;
var total = 0;
for(idx in vm.basket) {
var item = vm.basket[idx];
console.log(item);
total += item["qty"] * item["price"];
}
return total / 100;
},
handle_code(event) {
var vm = this;
var code = $(event.target).find("#code_field").val().toUpperCase();
console.log("Code:");
console.log(code);
if(code == "{% trans %}END{% endtrans %}" || code == "{% trans %}CAN{% endtrans %}") {
$(event.target).submit();
} else {
vm.handle_action(event);
}
},
handle_action(event) {
var vm = this;
var payload = $(event.target).serialize();
$.ajax({
type: 'post',
dataType: 'json',
data: payload,
success: function(response) {
vm.basket = response.basket;
vm.errors = [];
},
error: function(error) {
vm.basket = error.responseJSON.basket;
vm.errors = error.responseJSON.errors;
}
});
$('form.code_form #code_field').val("").focus();
}
}
}).mount('#bar_ui');
/* Autocompletion in the code field */
var products_autocomplete = [
{% for p in products -%} {% for p in products -%}
{ {
value: "{{ p.code }}", value: "{{ p.code }}",
@ -166,6 +222,7 @@ $( function() {
}, },
{%- endfor %} {%- endfor %}
]; ];
var quantity = ""; var quantity = "";
var search = ""; var search = "";
var pattern = /^(\d+x)?(.*)/i; var pattern = /^(\d+x)?(.*)/i;
@ -183,21 +240,22 @@ $( function() {
quantity = res[1] || ""; quantity = res[1] || "";
search = res[2]; search = res[2];
var matcher = new RegExp( $.ui.autocomplete.escapeRegex( search ), "i" ); var matcher = new RegExp( $.ui.autocomplete.escapeRegex( search ), "i" );
response($.grep( products, function( value ) { response($.grep( products_autocomplete, function( value ) {
value = value.tags; value = value.tags;
return matcher.test( value ); return matcher.test( value );
})); }));
}, },
}); });
});
$( function() { /* Accordion UI between basket and refills */
$("#bar_ui").accordion({ $("#click_form").accordion({
heightStyle: "content", heightStyle: "content",
activate: function(event, ui){ activate: function(event, ui){
$(".focus").focus(); $(".focus").focus();
} }
}); });
$("#products").tabs(); $("#products").tabs();
$("#code_field").focus(); $("#code_field").focus();
}); });
</script> </script>

View File

@ -12,6 +12,12 @@
{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %} {% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}
{% endblock %} {% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %} {% block content %}
<h3>{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}</h3> <h3>{% trans counter_name=counter %}{{ counter_name }} counter{% endtrans %}</h3>

View File

@ -5,6 +5,12 @@
{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %} {% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}
{% endblock %} {% endblock %}
{% block info_boxes %}
{% endblock %}
{% block nav %}
{% endblock %}
{% block content %} {% block content %}
<h3>{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}</h3> <h3>{% trans counter_name=counter %}{{ counter_name }} last operations{% endtrans %}</h3>
<h4>{% trans %}Refillings{% endtrans %}</h4> <h4>{% trans %}Refillings{% endtrans %}</h4>

View File

@ -68,18 +68,29 @@ class CounterTest(TestCase):
location, location,
{ {
"action": "refill", "action": "refill",
"amount": "10", "amount": "5",
"payment_method": "CASH", "payment_method": "CASH",
"bank": "OTHER", "bank": "OTHER",
}, },
) )
response = self.client.post(location, {"action": "code", "code": "BARB"}) response = self.client.post(location, {"action": "code", "code": "BARB"})
response = self.client.post(
location, {"action": "add_product", "product_id": "4"}
)
response = self.client.post(
location, {"action": "del_product", "product_id": "4"}
)
response = self.client.post(location, {"action": "code", "code": "2xdeco"})
response = self.client.post(location, {"action": "code", "code": "1xbarb"})
response = self.client.post(location, {"action": "code", "code": "fin"}) response = self.client.post(location, {"action": "code", "code": "fin"})
response_get = self.client.get(response.get("location")) response_get = self.client.get(response.get("location"))
response_content = response_get.content.decode("utf-8")
self.assertTrue("<li>2 x Barbar" in str(response_content))
self.assertTrue("<li>2 x Déconsigne Eco-cup" in str(response_content))
self.assertTrue( self.assertTrue(
"<p>Client : Richard Batsbak - Nouveau montant : 8.30" "<p>Client : Richard Batsbak - Nouveau montant : 3.60"
in str(response_get.content) in str(response_content)
) )

View File

@ -38,7 +38,7 @@ from django.views.generic.edit import (
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
from django.forms import CheckboxSelectMultiple from django.forms import CheckboxSelectMultiple
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect, HttpResponse, JsonResponse
from django.utils import timezone from django.utils import timezone
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -48,6 +48,7 @@ from django.db import DataError, transaction, models
import re import re
import pytz import pytz
from datetime import date, timedelta, datetime from datetime import date, timedelta, datetime
from http import HTTPStatus
from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField
from ajax_select import make_ajax_field from ajax_select import make_ajax_field
@ -357,6 +358,34 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
pk_url_kwarg = "counter_id" pk_url_kwarg = "counter_id"
current_tab = "counter" current_tab = "counter"
def render_to_response(self, *args, **kwargs):
if self.request.is_ajax(): # JSON response for AJAX requests
response = {"errors": []}
status = HTTPStatus.OK
if self.request.session["too_young"]:
response["errors"].append(_("Too young for that product"))
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
if self.request.session["not_allowed"]:
response["errors"].append(_("Not allowed for that product"))
status = HTTPStatus.FORBIDDEN
if self.request.session["no_age"]:
response["errors"].append(_("No date of birth provided"))
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
if self.request.session["not_enough"]:
response["errors"].append(_("Not enough money"))
status = HTTPStatus.PAYMENT_REQUIRED
if len(response["errors"]) > 1:
status = HTTPStatus.BAD_REQUEST
response["basket"] = self.request.session["basket"]
return JsonResponse(response, status=status)
else: # Standard HTML page
return super().render_to_response(*args, **kwargs)
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"]) self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
obj = self.get_object() obj = self.get_object()
@ -370,7 +399,9 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
) )
or len(obj.get_barmen_list()) < 1 or len(obj.get_barmen_list()) < 1
): ):
raise PermissionDenied return HttpResponseRedirect(
reverse_lazy("counter:details", kwargs={"counter_id": obj.id})
)
else: else:
if not request.user.is_authenticated: if not request.user.is_authenticated:
raise PermissionDenied raise PermissionDenied