WIP: Affiche un warning si trop d'alcool consommé #727

Draft
lstephan wants to merge 4 commits from Aufinal/alcoolémie into master
9 changed files with 193 additions and 2 deletions

View file

@ -288,6 +288,8 @@ class ArticleForm(forms.ModelForm):
"hidden", "hidden",
"price", "price",
"stock", "stock",
"abv",
"volume",
"category", "category",
"box_type", "box_type",
"box_capacity", "box_capacity",
@ -301,6 +303,8 @@ class ArticleRestrictForm(ArticleForm):
"is_sold", "is_sold",
"hidden", "hidden",
"price", "price",
"abv",
"volume",
"category", "category",
"box_type", "box_type",
"box_capacity", "box_capacity",

View file

@ -0,0 +1,33 @@
# Generated by Django 2.2.12 on 2020-05-15 15:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("kfet", "0071_promo_2020"),
]
operations = [
migrations.AddField(
model_name="article",
name="abv",
field=models.DecimalField(
decimal_places=1,
default=0,
max_digits=6,
verbose_name="Degré d'alcool (en %)",
),
),
migrations.AddField(
model_name="article",
name="volume",
field=models.DecimalField(
decimal_places=1,
default=0,
max_digits=6,
verbose_name="Volume d'une unité (en cL)",
),
),
]

View file

@ -520,6 +520,13 @@ class Article(models.Model):
"capacité du contenant", blank=True, null=True, default=None "capacité du contenant", blank=True, null=True, default=None
) )
abv = models.DecimalField(
_("Degré d'alcool (en %)"), max_digits=6, default=0, decimal_places=1
)
volume = models.DecimalField(
_("Volume d'une unité (en cL)"), max_digits=6, default=0, decimal_places=1
)
def __str__(self): def __str__(self):
return "%s - %s" % (self.category.name, self.name) return "%s - %s" % (self.category.name, self.name)

View file

@ -112,6 +112,14 @@ input[type=number]::-webkit-outer-spin-button {
right:0; right:0;
} }
#account #alcohol-warning {
padding: 5px;
position: absolute;
top:0;
right:0;
font-size:35px;
}
@media (min-width: 600px) { @media (min-width: 600px) {
#account_form input { font-size:60px; } #account_form input { font-size:60px; }

View file

@ -11,6 +11,7 @@ var Account = Backbone.Model.extend({
'departement': '', 'departement': '',
'nickname': '', 'nickname': '',
'trigramme': '', 'trigramme': '',
'n_units': 0,
}, },
url: function () { url: function () {
@ -45,6 +46,7 @@ var AccountView = Backbone.View.extend({
el: '#account', el: '#account',
input: '#id_trigramme', input: '#id_trigramme',
buttons: '.buttons', buttons: '.buttons',
alcohol_warning: '#alcohol-warning',
props: _.keys(Account.prototype.defaults), props: _.keys(Account.prototype.defaults),
@ -98,6 +100,15 @@ var AccountView = Backbone.View.extend({
return buttons return buttons
}, },
get_alcohol_warning: function () {
n_units = parseFloat(this.model.get("n_units")).toFixed(2);
if (n_units < 4)
return "";
return `<i class="fa fa-beer" title="${n_units} unités"></i>`
},
render: function () { render: function () {
for (let prop of this.props) { for (let prop of this.props) {
var selector = "#account-" + prop; var selector = "#account-" + prop;
@ -106,6 +117,7 @@ var AccountView = Backbone.View.extend({
this.$el.attr("data-balance", this.attr_data_balance()); this.$el.attr("data-balance", this.attr_data_balance());
this.$(this.buttons).html(this.get_buttons()); this.$(this.buttons).html(this.get_buttons());
this.$(this.alcohol_warning).html(this.get_alcohol_warning());
}, },
reset: function () { reset: function () {
@ -124,6 +136,10 @@ var LIQView = AccountView.extend({
return ""; return "";
}, },
get_alcohol_warning: function () {
return ""
},
attr_data_balance: function () { attr_data_balance: function () {
return 'ok'; return 'ok';
} }

View file

@ -17,6 +17,7 @@
<link rel="stylesheet" type="text/css" href="{% static 'vendor/jquery/jquery-ui.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'vendor/jquery/jquery-ui.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'kfet/vendor/jquery/jquery-confirm.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'kfet/vendor/jquery/jquery-confirm.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/index.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'kfet/css/index.css' %}">
<link type="text/css" rel="stylesheet" href="{% static 'gestioncof/vendor/font-awesome/css/font-awesome.min.css' %}">
{# JS #} {# JS #}
<script type="text/javascript" src="{% static 'kfet/vendor/js.cookie.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/vendor/js.cookie.js' %}"></script>

View file

@ -86,6 +86,7 @@
<span id="account-promo"></span> <span id="account-promo"></span>
</div> </div>
<div id="account-email" class="data_line"></div> <div id="account-email" class="data_line"></div>
<div id="alcohol-warning"></div>
<div class="buttons"> <div class="buttons">
</div> </div>
</div> </div>

View file

@ -1,5 +1,5 @@
import json import json
from datetime import datetime, timedelta from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from unittest import mock from unittest import mock
@ -1365,6 +1365,8 @@ class ArticleCreateViewTests(ViewTestCaseMixin, TestCase):
"category": self.category.pk, "category": self.category.pk,
"stock": 5, "stock": 5,
"price": "2.5", "price": "2.5",
"volume": "33",
"abv": "6.3",
} }
def setUp(self): def setUp(self):
@ -1451,6 +1453,8 @@ class ArticleUpdateViewTests(ViewTestCaseMixin, TestCase):
"price": "3.5", "price": "3.5",
"box_type": "carton", "box_type": "carton",
# 'hidden': not checked # 'hidden': not checked
"volume": "33",
"abv": "6.3",
} }
def setUp(self): def setUp(self):
@ -4220,6 +4224,74 @@ class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase):
def url_expected(self): def url_expected(self):
return "/k-fet/accounts/{}/.json".format(self.accounts["user"].trigramme) return "/k-fet/accounts/{}/.json".format(self.accounts["user"].trigramme)
def setUp(self):
super().setUp()
# A Checkout, curently usable, balance=100
self.checkout = Checkout.objects.create(
created_by=self.accounts["team"],
name="Checkout",
valid_from=self.now - timedelta(days=7),
valid_to=self.now + timedelta(days=7),
balance=Decimal("100.00"),
)
# An Article, price=2.5, stock=20
self.article = Article.objects.create(
category=ArticleCategory.objects.create(name="Category"),
name="Article",
price=Decimal("2.5"),
stock=20,
volume=Decimal("33"),
abv=Decimal("7.3"),
)
now = timezone.localtime(self.now)
ope_party1 = OperationGroup.objects.create(
on_acc=self.accounts["user"],
checkout=self.checkout,
at=now.replace(hour=1),
)
ope_party2 = OperationGroup.objects.create(
on_acc=self.accounts["user"],
checkout=self.checkout,
at=now.replace(day=now.day - 1, hour=22),
)
ope_noparty = OperationGroup.objects.create(
on_acc=self.accounts["user"],
checkout=self.checkout,
at=now.replace(day=now.day - 1, hour=19),
)
Operation.objects.create(
group=ope_party1,
type=Operation.PURCHASE,
article=self.article,
article_nb=2,
)
Operation.objects.create(
group=ope_party2,
type=Operation.PURCHASE,
article=self.article,
article_nb=1,
)
Operation.objects.create(
group=ope_party2,
type=Operation.PURCHASE,
article=self.article,
article_nb=1,
)
Operation.objects.create(
group=ope_noparty,
type=Operation.PURCHASE,
article=self.article,
article_nb=3,
)
def test_ok(self): def test_ok(self):
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
@ -4243,10 +4315,33 @@ class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase):
"nickname", "nickname",
"promo", "promo",
"trigramme", "trigramme",
"n_units",
] ]
), ),
) )
@mock.patch("django.utils.timezone.now")
def test_without_party(self, mock_now):
now = timezone.localtime(self.now)
mock_now.return_value = now.replace(hour=20).astimezone(timezone.utc)
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
content = json.loads(r.content.decode("utf-8"))
self.assertEqual(content["n_units"], 0)
@mock.patch("django.utils.timezone.now")
def test_with_party(self, mock_now):
now = timezone.localtime(self.now)
mock_now.return_value = now.replace(hour=5).astimezone(timezone.utc)
r = self.client.get(self.url)
self.assertEqual(r.status_code, 200)
content = json.loads(r.content.decode("utf-8"))
self.assertAlmostEqual(float(content["n_units"]), 3 * 33 * 7.3 * 0.8 / 100)
class SettingsListViewTests(ViewTestCaseMixin, TestCase): class SettingsListViewTests(ViewTestCaseMixin, TestCase):
url_name = "kfet.settings" url_name = "kfet.settings"

View file

@ -2,7 +2,7 @@ import ast
import heapq import heapq
import statistics import statistics
from collections import defaultdict from collections import defaultdict
from datetime import timedelta from datetime import datetime, time, timedelta
from decimal import Decimal from decimal import Decimal
from typing import List from typing import List
from urllib.parse import urlencode from urllib.parse import urlencode
@ -916,6 +916,31 @@ def account_read_json(request, trigramme):
account = get_object_or_404(Account, trigramme=trigramme) account = get_object_or_404(Account, trigramme=trigramme)
if not account.readable: if not account.readable:
raise Http404 raise Http404
# Calcul des unités d'alcool consommées
# 1UA = 10g d'alcool (éthanol) pur
alcohol_density = 0.8
now = timezone.localtime(timezone.now())
# une soirée va de XXh à 06h
if time(21) <= now.time() or now.time() <= time(6):
begin_time = now.replace(hour=21, minute=0, second=0, microsecond=0)
# si on est après minuit, il faut retrancher un jour
if now.time() <= time(6):
begin_time -= timedelta(days=1)
qs = Operation.objects.filter(
group__on_acc=account,
type=Operation.PURCHASE,
group__at__gte=begin_time.astimezone(timezone.utc),
).annotate(
units=F("article__volume") * F("article__abv") * alcohol_density / 100
)
# Sum retourne None sur un queryset vide :
# https://docs.djangoproject.com/en/3.0/ref/models/querysets/#aggregation-functions
n_units = qs.aggregate(agg=Sum("units"))["agg"] or 0
else:
n_units = 0
data = { data = {
"id": account.pk, "id": account.pk,
"name": account.name, "name": account.name,
@ -927,6 +952,7 @@ def account_read_json(request, trigramme):
"departement": account.departement, "departement": account.departement,
"nickname": account.nickname, "nickname": account.nickname,
"trigramme": account.trigramme, "trigramme": account.trigramme,
"n_units": n_units,
} }
return JsonResponse(data) return JsonResponse(data)