WIP: Affiche un warning si trop d'alcool consommé #727
9 changed files with 193 additions and 2 deletions
|
@ -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",
|
||||||
|
|
33
kfet/migrations/0072_auto_20200515_1747.py
Normal file
33
kfet/migrations/0072_auto_20200515_1747.py
Normal 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)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue