Compare commits

..

No commits in common. "90787707d1717c507fd4098bd4a42d376397fdf3" and "072c762ad0b9fca32b03e612ad2ac6dbf9ba1203" have entirely different histories.

33 changed files with 212 additions and 620 deletions

View file

@ -48,7 +48,7 @@ factory-boy==3.2.1
# via -r requirements-dev.in
faker==18.3.1
# via factory-boy
filelock==3.10.7
filelock==3.10.4
# via virtualenv
h11==0.14.0
# via wsproto
@ -81,7 +81,7 @@ pathspec==0.11.1
# via black
pip-tools==6.12.3
# via -r requirements-dev.in
platformdirs==3.2.0
platformdirs==3.1.1
# via
# black
# virtualenv

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/static/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -1,75 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2816 5005 c-56 -19 -102 -60 -127 -111 -22 -46 -22 -52 -22 -435 l0
-387 27 -46 c29 -50 74 -87 125 -105 23 -7 152 -11 395 -11 l361 0 -1 -247
c-1 -137 -2 -251 -3 -255 -1 -3 -352 -6 -781 -6 l-780 0 -1 32 c0 17 -1 68 -1
113 -1 98 -19 137 -70 150 -18 5 -248 9 -511 8 l-477 -1 -32 -31 -31 -31 -1
-118 -1 -119 -287 -2 c-188 -1 -293 -5 -307 -13 -33 -17 -49 -50 -55 -115 -4
-33 -9 -71 -11 -85 -3 -14 -7 -50 -10 -80 -3 -30 -7 -71 -10 -90 -5 -36 -11
-93 -20 -182 -5 -51 -10 -91 -20 -168 -2 -19 -7 -60 -10 -90 -3 -30 -8 -71
-10 -90 -9 -72 -15 -116 -20 -170 -3 -30 -7 -71 -10 -90 -6 -44 -17 -137 -20
-170 -6 -65 -16 -148 -21 -180 -2 -19 -7 -53 -9 -75 -2 -22 -7 -69 -10 -105
-4 -35 -8 -71 -10 -80 -2 -8 -6 -44 -9 -80 -4 -36 -9 -76 -11 -90 -16 -99 -20
-213 -20 -645 l0 -490 30 -54 c32 -58 89 -111 151 -138 37 -17 172 -18 2354
-19 2222 -1 2317 -1 2375 17 78 24 136 73 172 147 l28 57 0 485 c0 424 -5 582
-20 640 -2 8 -7 45 -10 82 -3 37 -7 75 -9 85 -2 10 -7 50 -11 88 -4 39 -9 86
-12 105 -2 19 -6 51 -8 70 -2 19 -6 60 -10 90 -10 92 -15 133 -20 185 -3 28
-8 61 -10 75 -2 14 -7 54 -10 90 -3 36 -8 76 -10 90 -2 14 -6 50 -10 80 -3 30
-7 69 -10 85 -2 17 -6 59 -10 95 -4 36 -8 76 -10 90 -2 14 -7 52 -10 85 -3 33
-8 76 -10 95 -3 19 -7 58 -10 85 -3 28 -10 82 -15 120 -5 39 -12 97 -15 130
-8 70 -15 86 -50 109 -24 16 -74 18 -543 19 l-517 2 0 253 -1 252 366 0 c363
0 365 0 416 24 54 25 87 60 110 116 19 44 21 760 2 822 -6 21 -27 55 -46 74
-70 73 -14 68 -939 70 -633 1 -840 -2 -867 -11z m1659 -196 c4 -5 8 -459 5
-666 l0 -33 -805 0 c-760 0 -805 1 -806 18 -4 70 0 672 4 679 7 10 1591 12
1602 2z m-2670 -1874 l0 -570 -357 0 -358 -1 -1 26 c-1 30 -1 1091 0 1105 1 6
127 10 359 10 l357 -1 0 -569z m-939 266 c20 -1 21 -3 21 -416 0 -228 -3 -415
-6 -416 -3 -1 -23 -2 -44 -4 -97 -8 -139 -99 -78 -167 l29 -33 648 0 c721 -1
695 -3 720 66 24 72 -29 134 -113 132 l-33 -1 -1 87 c-2 296 1 744 5 748 7 8
2666 6 2674 -1 3 -4 7 -12 8 -19 2 -14 15 -137 19 -182 2 -16 6 -48 8 -70 7
-48 15 -121 22 -190 3 -27 10 -84 15 -125 5 -41 12 -97 15 -125 3 -27 7 -66
10 -85 2 -19 7 -62 10 -95 4 -33 8 -73 10 -90 2 -16 6 -55 10 -85 3 -30 7 -66
10 -80 3 -14 7 -52 10 -85 3 -33 7 -71 9 -85 3 -14 8 -54 11 -90 4 -36 8 -76
10 -90 1 -14 6 -54 10 -90 4 -36 9 -73 10 -82 2 -9 6 -49 10 -90 3 -40 8 -81
11 -90 3 -10 2 -19 -3 -19 -25 -4 -4670 -4 -4681 -1 -7 2 -10 12 -8 21 3 9 7
45 11 81 3 36 8 76 10 90 2 14 7 52 10 85 7 70 15 139 20 175 2 14 7 56 10 95
4 38 8 77 10 87 2 9 6 48 10 85 3 37 8 79 10 93 3 14 7 52 10 85 3 33 8 71 10
85 2 14 7 54 11 90 3 36 7 72 9 80 2 8 6 44 9 80 8 76 12 106 26 215 5 44 12
105 15 135 3 30 7 69 9 85 2 17 7 64 11 106 8 85 12 95 39 100 15 3 313 3 412
0z m4051 -2456 c2 -359 1 -381 -16 -402 -11 -13 -30 -27 -42 -31 -29 -11
-4587 -9 -4610 1 -8 4 -23 21 -32 36 -15 26 -17 69 -17 403 l0 373 2358 0
2357 0 2 -380z"/>
<path d="M2620 2883 c-69 -28 -73 -139 -7 -174 27 -15 277 -20 325 -7 34 10
72 57 72 91 0 28 -28 75 -53 89 -31 17 -297 18 -337 1z"/>
<path d="M3289 2892 c-42 -5 -74 -46 -74 -97 0 -50 23 -81 69 -93 50 -13 291
-8 319 7 67 36 63 154 -6 177 -22 8 -258 12 -308 6z"/>
<path d="M3933 2892 c-34 -5 -73 -58 -73 -99 0 -38 44 -92 77 -94 107 -8 282
-3 305 8 63 31 77 102 29 155 l-28 32 -144 0 c-79 1 -154 0 -166 -2z"/>
<path d="M2553 2362 c-27 -4 -69 -46 -75 -75 -7 -37 11 -84 39 -103 24 -16 50
-18 195 -19 154 0 169 2 195 21 36 28 50 73 33 114 -24 57 -46 64 -214 64 -83
1 -161 0 -173 -2z"/>
<path d="M3262 2355 c-54 -23 -79 -83 -56 -131 26 -53 44 -58 222 -59 151 -2
166 0 195 19 40 27 55 74 37 116 -24 57 -45 63 -216 65 -97 1 -164 -3 -182
-10z"/>
<path d="M3991 2357 c-82 -21 -97 -142 -22 -178 23 -12 69 -15 197 -14 l167 1
28 32 c39 44 39 89 0 131 -16 17 -37 31 -48 32 -83 5 -299 3 -322 -4z"/>
<path d="M2414 1812 c-49 -39 -53 -107 -10 -151 26 -26 27 -26 204 -29 104 -2
189 1 204 7 52 20 77 84 52 136 -23 50 -46 55 -247 55 -157 0 -185 -3 -203
-18z"/>
<path d="M3230 1817 c-70 -35 -61 -151 13 -177 17 -5 104 -10 194 -10 184 0
215 8 238 63 18 43 10 77 -25 110 l-30 27 -183 0 c-126 0 -190 -4 -207 -13z"/>
<path d="M4027 1810 c-55 -43 -43 -140 19 -166 40 -17 338 -20 380 -4 69 27
87 108 35 161 l-29 29 -190 0 c-175 0 -191 -1 -215 -20z"/>
<path d="M1921 809 c-39 -8 -56 -22 -71 -58 -16 -39 -1 -89 34 -115 27 -21 36
-21 669 -22 685 -1 677 -1 709 48 23 35 15 98 -17 126 -14 12 -31 22 -38 23
-54 5 -1263 3 -1286 -2z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

View file

@ -1,18 +0,0 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/static/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff"
}

View file

@ -7,7 +7,6 @@
<title>Checkout</title>
<link href="{% static "vendor/bootstrap-5.2.3-dist/css/bootstrap.min.css" %}"
rel="stylesheet">
{% include "common/favicon.html" %}
<style>
body {
margin-bottom: 2em;

View file

@ -1,10 +0,0 @@
{% load static %}
<link rel="apple-touch-icon" sizes="180x180" href="{% static "icons/apple-touch-icon.png" %}">
<link rel="icon" type="image/png" sizes="32x32" href="{% static "icons/favicon-32x32.png" %}">
<link rel="icon" type="image/png" sizes="16x16" href="{% static "icons/favicon-16x16.png" %}">
<link rel="manifest" href="{% static "icons/site.webmanifest" %}">
<link rel="mask-icon" href="{% static "icons/safari-pinned-tab.svg" %}" color="#5bbad5">
<link rel="shortcut icon" href="{% static "icons/favicon.ico" %}">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="{% static "icons/browserconfig.xml" %}">
<meta name="theme-color" content="#ffffff">

View file

@ -75,16 +75,5 @@
"unit_price_cents": 290,
"display_order": 7
}
},
{
"model": "purchase.product",
"fields": {
"created_at": "2022-04-28T16:08:20.471Z",
"updated_at": "2022-04-28T16:09:34.003Z",
"name": "Tomme de ch\u00e8vre",
"image": "",
"unit_price_cents": 0,
"display_order": 8
}
}
]

View file

@ -1,16 +1,13 @@
from crispy_forms import layout
from crispy_forms.bootstrap import InlineRadios
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Div, Layout, Submit
from django import forms
from django.urls import reverse
from django.utils.translation import gettext as _
from purchase.layout import BasketItemField
from purchase.models import Basket, Product
PRICED_PREFIX = "product-"
UNPRICED_PREFIX = "unpriced_product-"
PREFIX = "product-"
class BasketForm(forms.ModelForm):
@ -27,12 +24,6 @@ class BasketForm(forms.ModelForm):
self.helper = FormHelper()
self.helper.form_class = "form-horizontal"
self.helper.add_input(Submit("submit", _("Save")))
self.helper.attrs = {
"hx_post": reverse("purchase:price_preview"),
"hx_trigger": "keyup delay:500ms,change delay:500ms",
"hx_target": "#price_preview",
"hx_swap": "innerHTML",
}
self.helper.layout = Layout()
products = {}
basket = kwargs.get("instance")
@ -40,8 +31,8 @@ class BasketForm(forms.ModelForm):
for item in basket.items.all():
products[item.product] = item.quantity
fields = []
for product in Product.objects.with_fixed_price():
field_name = f"{PRICED_PREFIX}{product.id}"
for product in Product.objects.all():
field_name = f"{PREFIX}{product.id}"
self.fields.update(
{
field_name: forms.IntegerField(
@ -52,21 +43,12 @@ class BasketForm(forms.ModelForm):
},
)
fields.append(BasketItemField(field_name, product=product))
total = 0
if basket:
total = basket.price / 100
self.helper.layout = Layout(
Div(
*fields,
css_class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-5 row-cols-xl-6 g-4",
css_id="products",
),
InlineRadios("payment_method"),
Div(
layout.HTML(f"Montant total : {total:.2f}"),
css_id="price_preview",
css_class="mb-2",
),
)
def save(self):
@ -74,8 +56,8 @@ class BasketForm(forms.ModelForm):
name: str
products = {product.id: product for product in Product.objects.all()}
for name, value in self.cleaned_data.items():
if name.startswith(PRICED_PREFIX):
product_id = int(name.removeprefix(PRICED_PREFIX))
if name.startswith(PREFIX):
product_id = int(name.removeprefix(PREFIX))
product = products[product_id]
if value > 0:
instance.items.update_or_create(

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-27 16:54+0200\n"
"POT-Creation-Date: 2022-04-27 22:58+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -18,97 +18,96 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: purchase/admin.py:19
#: purchase/admin.py:18
msgid "unit price"
msgstr ""
#: purchase/admin.py:23
#: purchase/admin.py:22
msgid "sold"
msgstr ""
#: purchase/admin.py:27 purchase/admin.py:40
#: purchase/admin.py:26 purchase/admin.py:39
msgid "turnover"
msgstr ""
#: purchase/admin.py:54 purchase/admin.py:71
#: purchase/admin.py:53 purchase/admin.py:70
msgid "price"
msgstr ""
#: purchase/forms.py:27
#: purchase/forms.py:23
msgid "Save"
msgstr ""
#: purchase/models.py:17
#: purchase/models.py:13
msgid "created at"
msgstr ""
#: purchase/models.py:18
#: purchase/models.py:14
msgid "updated at"
msgstr ""
#: purchase/models.py:46 purchase/models.py:93
#: purchase/models.py:42 purchase/models.py:83
msgid "name"
msgstr ""
#: purchase/models.py:51 purchase/models.py:193
#: purchase/models.py:47 purchase/models.py:167
msgid "payment method"
msgstr ""
#: purchase/models.py:52
#: purchase/models.py:48
msgid "payment methods"
msgstr ""
#: purchase/models.py:94
#: purchase/models.py:84
msgid "image"
msgstr ""
#: purchase/models.py:96 purchase/models.py:229
#: purchase/models.py:86 purchase/models.py:203
msgid "unit price (cents)"
msgstr ""
#: purchase/models.py:98
msgid ""
"Unit price in cents. Use zero to denote that the product has no fixed price."
#: purchase/models.py:86
msgid "unit price in cents"
msgstr ""
#: purchase/models.py:103
#: purchase/models.py:89
msgid "display order"
msgstr ""
#: purchase/models.py:110 purchase/models.py:219
#: purchase/models.py:96 purchase/models.py:193
msgid "product"
msgstr ""
#: purchase/models.py:111
#: purchase/models.py:97
msgid "products"
msgstr ""
#: purchase/models.py:199 purchase/models.py:225
#: purchase/models.py:173 purchase/models.py:199
msgid "basket"
msgstr ""
#: purchase/models.py:200
#: purchase/models.py:174
msgid "baskets"
msgstr ""
#: purchase/models.py:203
#: purchase/models.py:177
#, python-format
msgid "Basket #%(id)s"
msgstr ""
#: purchase/models.py:227
#: purchase/models.py:201
msgid "quantity"
msgstr ""
#: purchase/models.py:230
#: purchase/models.py:204
msgid "product's unit price in cents at the time of purchase"
msgstr ""
#: purchase/models.py:236
#: purchase/models.py:210
msgid "basket item"
msgstr ""
#: purchase/models.py:237
#: purchase/models.py:211
msgid "basket items"
msgstr ""
@ -117,17 +116,16 @@ msgstr ""
msgid "Are you sure you want to delete \"%(basket)s\"?"
msgstr ""
#: purchase/templates/purchase/basket_form.html:14
#: purchase/templates/purchase/basket_form.html:11
msgid "Missing payment method."
msgstr ""
#: purchase/templates/purchase/basket_form.html:17
#: purchase/templates/purchase/basket_form.html:52
#: purchase/templates/purchase/basket_form.html:14
msgid "New basket"
msgstr ""
#: purchase/templates/purchase/basket_form.html:36
msgid "Add product"
#: purchase/templates/purchase/basket_form.html:18
msgid "New"
msgstr ""
#: purchase/templates/purchase/basket_list.html:5
@ -146,45 +144,45 @@ msgid_plural "%(counter)s items"
msgstr[0] ""
msgstr[1] ""
#: purchase/templates/purchase/reports.html:9
#: purchase/templates/purchase/reports.html:11
msgid "Reports"
msgstr ""
#: purchase/templates/purchase/reports.html:10
#: purchase/templates/purchase/reports.html:12
msgid "General"
msgstr ""
#: purchase/templates/purchase/reports.html:12
#: purchase/templates/purchase/reports.html:14
msgid "Total turnover:"
msgstr ""
#: purchase/templates/purchase/reports.html:13
#: purchase/templates/purchase/reports.html:15
msgid "Average basket:"
msgstr ""
#: purchase/templates/purchase/reports.html:16
#: purchase/templates/purchase/reports.html:18
msgid "By day"
msgstr ""
#: purchase/templates/purchase/reports.html:17
#: purchase/templates/purchase/reports.html:19
#: purchase/templates/purchase/snippets/report_payment_methods.html:8
#: purchase/templates/purchase/snippets/report_products.html:8
msgid "Turnover"
msgstr ""
#: purchase/templates/purchase/reports.html:23
#: purchase/templates/purchase/reports.html:25
msgid "Average basket"
msgstr ""
#: purchase/templates/purchase/reports.html:32
#: purchase/templates/purchase/reports.html:34
msgid "Products"
msgstr ""
#: purchase/templates/purchase/reports.html:36
#: purchase/templates/purchase/reports.html:40
msgid "Turnover by payment method"
msgstr ""
#: purchase/templates/purchase/reports.html:39
#: purchase/templates/purchase/reports.html:43
msgid "Baskets without payment method"
msgstr ""
@ -209,42 +207,42 @@ msgid "Product"
msgstr ""
#: purchase/templates/purchase/snippets/report_products.html:7
#: purchase/views/reports.py:102 purchase/views/reports.py:116
#: purchase/views/reports.py:79 purchase/views/reports.py:93
msgid "# sold"
msgstr ""
#: purchase/views/basket.py:33
#: purchase/views/basket.py:15
msgid "Successfully created basket."
msgstr ""
#: purchase/views/basket.py:70
#: purchase/views/basket.py:30
msgid "Successfully updated basket."
msgstr ""
#: purchase/views/basket.py:116
#: purchase/views/basket.py:45
msgid "Basket successfully deleted."
msgstr ""
#: purchase/views/reports.py:64
#: purchase/views/reports.py:29
msgid "No sale to report"
msgstr ""
#: purchase/views/reports.py:93
#: purchase/views/reports.py:70
msgid "Sales by product"
msgstr ""
#: purchase/views/reports.py:108 purchase/views/reports.py:123
#: purchase/views/reports.py:85 purchase/views/reports.py:100
msgid "Turnover by product"
msgstr ""
#: purchase/views/reports.py:147
#: purchase/views/reports.py:122
msgid "Sales by hour"
msgstr ""
#: purchase/views/reports.py:158
#: purchase/views/reports.py:133
msgid "Basket count by hour"
msgstr ""
#: purchase/views/reports.py:166
#: purchase/views/reports.py:141
msgid "Turnover by hour"
msgstr ""

View file

@ -5,7 +5,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-27 16:54+0200\n"
"POT-Creation-Date: 2022-04-27 22:58+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -15,97 +15,96 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: purchase/admin.py:19
#: purchase/admin.py:18
msgid "unit price"
msgstr "prix unitaire"
#: purchase/admin.py:23
#: purchase/admin.py:22
msgid "sold"
msgstr "vendu"
#: purchase/admin.py:27 purchase/admin.py:40
#: purchase/admin.py:26 purchase/admin.py:39
msgid "turnover"
msgstr "chiffre d'affaires"
#: purchase/admin.py:54 purchase/admin.py:71
#: purchase/admin.py:53 purchase/admin.py:70
msgid "price"
msgstr "prix"
#: purchase/forms.py:27
#: purchase/forms.py:23
msgid "Save"
msgstr "Enregistrer"
#: purchase/models.py:17
#: purchase/models.py:13
msgid "created at"
msgstr "créé à"
#: purchase/models.py:18
#: purchase/models.py:14
msgid "updated at"
msgstr "mis à jour à"
#: purchase/models.py:46 purchase/models.py:93
#: purchase/models.py:42 purchase/models.py:83
msgid "name"
msgstr "nom"
#: purchase/models.py:51 purchase/models.py:193
#: purchase/models.py:47 purchase/models.py:167
msgid "payment method"
msgstr "moyen de paiement"
#: purchase/models.py:52
#: purchase/models.py:48
msgid "payment methods"
msgstr "moyens de paiement"
#: purchase/models.py:94
#: purchase/models.py:84
msgid "image"
msgstr "image"
#: purchase/models.py:96 purchase/models.py:229
#: purchase/models.py:86 purchase/models.py:203
msgid "unit price (cents)"
msgstr "prix unitaire (centimes)"
#: purchase/models.py:98
msgid ""
"Unit price in cents. Use zero to denote that the product has no fixed price."
msgstr "Prix unitaire en centimes. Utiliser zéro pour indiquer que le produit n'a pas de prix fixe."
#: purchase/models.py:86
msgid "unit price in cents"
msgstr "prix unitaire en centimes"
#: purchase/models.py:103
#: purchase/models.py:89
msgid "display order"
msgstr "ordre d'affichage"
#: purchase/models.py:110 purchase/models.py:219
#: purchase/models.py:96 purchase/models.py:193
msgid "product"
msgstr "produit"
#: purchase/models.py:111
#: purchase/models.py:97
msgid "products"
msgstr "produits"
#: purchase/models.py:199 purchase/models.py:225
#: purchase/models.py:173 purchase/models.py:199
msgid "basket"
msgstr "panier"
#: purchase/models.py:200
#: purchase/models.py:174
msgid "baskets"
msgstr "paniers"
#: purchase/models.py:203
#: purchase/models.py:177
#, python-format
msgid "Basket #%(id)s"
msgstr "Panier n°%(id)s"
#: purchase/models.py:227
#: purchase/models.py:201
msgid "quantity"
msgstr "quantité"
#: purchase/models.py:230
#: purchase/models.py:204
msgid "product's unit price in cents at the time of purchase"
msgstr "prix unitaire du produit en centimes au moment de l'achat"
#: purchase/models.py:236
#: purchase/models.py:210
msgid "basket item"
msgstr "article de panier"
#: purchase/models.py:237
#: purchase/models.py:211
msgid "basket items"
msgstr "articles de panier"
@ -114,18 +113,17 @@ msgstr "articles de panier"
msgid "Are you sure you want to delete \"%(basket)s\"?"
msgstr "Êtes-vous sûr de vouloir supprimer \"%(basket)s\" ?"
#: purchase/templates/purchase/basket_form.html:14
#: purchase/templates/purchase/basket_form.html:11
msgid "Missing payment method."
msgstr "Moyen de paiement manquant."
#: purchase/templates/purchase/basket_form.html:17
#: purchase/templates/purchase/basket_form.html:52
#: purchase/templates/purchase/basket_form.html:14
msgid "New basket"
msgstr "Nouveau panier"
#: purchase/templates/purchase/basket_form.html:36
msgid "Add product"
msgstr "Ajouter un produit"
#: purchase/templates/purchase/basket_form.html:18
msgid "New"
msgstr "Nouveau"
#: purchase/templates/purchase/basket_list.html:5
msgid "Baskets"
@ -143,45 +141,45 @@ msgid_plural "%(counter)s items"
msgstr[0] "1 article"
msgstr[1] "%(counter)s articles"
#: purchase/templates/purchase/reports.html:9
#: purchase/templates/purchase/reports.html:11
msgid "Reports"
msgstr "Rapports"
#: purchase/templates/purchase/reports.html:10
#: purchase/templates/purchase/reports.html:12
msgid "General"
msgstr "Général"
#: purchase/templates/purchase/reports.html:12
#: purchase/templates/purchase/reports.html:14
msgid "Total turnover:"
msgstr "Chiffre d'affaires total :"
#: purchase/templates/purchase/reports.html:13
#: purchase/templates/purchase/reports.html:15
msgid "Average basket:"
msgstr "Panier moyen :"
#: purchase/templates/purchase/reports.html:16
#: purchase/templates/purchase/reports.html:18
msgid "By day"
msgstr "Par jour"
#: purchase/templates/purchase/reports.html:17
#: purchase/templates/purchase/reports.html:19
#: purchase/templates/purchase/snippets/report_payment_methods.html:8
#: purchase/templates/purchase/snippets/report_products.html:8
msgid "Turnover"
msgstr "Chiffre d'affaires"
#: purchase/templates/purchase/reports.html:23
#: purchase/templates/purchase/reports.html:25
msgid "Average basket"
msgstr "Panier moyen"
#: purchase/templates/purchase/reports.html:32
#: purchase/templates/purchase/reports.html:34
msgid "Products"
msgstr "Produits"
#: purchase/templates/purchase/reports.html:36
#: purchase/templates/purchase/reports.html:40
msgid "Turnover by payment method"
msgstr "Chiffre d'affaires par moyen de paiement"
#: purchase/templates/purchase/reports.html:39
#: purchase/templates/purchase/reports.html:43
msgid "Baskets without payment method"
msgstr "Paniers sans moyen de paiement"
@ -206,42 +204,45 @@ msgid "Product"
msgstr "Produit"
#: purchase/templates/purchase/snippets/report_products.html:7
#: purchase/views/reports.py:102 purchase/views/reports.py:116
#: purchase/views/reports.py:79 purchase/views/reports.py:93
msgid "# sold"
msgstr "Nb. vendus"
#: purchase/views/basket.py:33
#: purchase/views/basket.py:15
msgid "Successfully created basket."
msgstr "Panier correctement créé."
#: purchase/views/basket.py:70
#: purchase/views/basket.py:30
msgid "Successfully updated basket."
msgstr "Panier correctement modifié."
#: purchase/views/basket.py:116
#: purchase/views/basket.py:45
msgid "Basket successfully deleted."
msgstr "Panier correctement supprimé."
#: purchase/views/reports.py:64
#: purchase/views/reports.py:29
msgid "No sale to report"
msgstr "Aucune vente à afficher"
#: purchase/views/reports.py:93
#: purchase/views/reports.py:70
msgid "Sales by product"
msgstr "Ventes par produit"
#: purchase/views/reports.py:108 purchase/views/reports.py:123
#: purchase/views/reports.py:85 purchase/views/reports.py:100
msgid "Turnover by product"
msgstr "Chiffre d'affaires par produit"
#: purchase/views/reports.py:147
#: purchase/views/reports.py:122
msgid "Sales by hour"
msgstr "Ventes par heure"
#: purchase/views/reports.py:158
#: purchase/views/reports.py:133
msgid "Basket count by hour"
msgstr "Nombre de paniers par heure"
#: purchase/views/reports.py:166
#: purchase/views/reports.py:141
msgid "Turnover by hour"
msgstr "Chiffre d'affaires par heure"
#~ msgid "Basket ID"
#~ msgstr "Id de panier"

View file

@ -36,7 +36,9 @@ class Command(BaseCommand):
methods_weights = [random.randint(1, 6) for _ in range(len(payment_methods))]
products_weights = [1 / product.display_order for product in products]
for _ in range(count):
method = random.choices(payment_methods, weights=methods_weights)[0]
method = None
if random.random() < 0.99: # noqa: PLR2004
method = random.choices(payment_methods, weights=methods_weights)[0]
basket = Basket.objects.create(payment_method=method)
items_in_basket = int(random.normalvariate(3, 2))
if items_in_basket > len(products):
@ -52,23 +54,13 @@ class Command(BaseCommand):
)
items = []
for product in selected_products:
if not product.has_fixed_price:
items.append(
BasketItem(
product=product,
basket=basket,
quantity=1,
unit_price_cents=random.randint(317, 514),
),
)
else:
items.append(
BasketItem(
product=product,
basket=basket,
quantity=random.randint(1, 3),
unit_price_cents=product.unit_price_cents,
),
)
items.append(
BasketItem(
product=product,
basket=basket,
quantity=random.randint(1, 3),
unit_price_cents=product.unit_price_cents,
),
)
BasketItem.objects.bulk_create(items)
return count

View file

@ -1,24 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-27 13:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("purchase", "0012_rename_value_cache_etag_cache_last_modified"),
]
operations = [
migrations.RemoveConstraint(
model_name="basketitem",
name="unique_product_per_basket",
),
migrations.AlterField(
model_name="product",
name="unit_price_cents",
field=models.PositiveIntegerField(
help_text="Unit price in cents. Use zero to denote that the product has no fixed price.",
verbose_name="unit price (cents)",
),
),
]

View file

@ -1,35 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-27 16:22
import django.db.models.deletion
from django.db import migrations, models
def delete_baskets_without_payment_method(apps, schema_editor):
Basket = apps.get_model("purchase", "Basket") # noqa: N806
Basket.objects.using(schema_editor.connection.alias).filter(
payment_method=None,
).delete()
class Migration(migrations.Migration):
dependencies = [
("purchase", "0013_remove_basketitem_unique_product_per_basket_and_more"),
]
operations = [
# Remove baskets with no payment method
migrations.RunPython(
delete_baskets_without_payment_method,
migrations.RunPython.noop,
),
migrations.AlterField(
model_name="basket",
name="payment_method",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="baskets",
to="purchase.paymentmethod",
verbose_name="payment method",
),
),
]

View file

@ -4,7 +4,7 @@ import hashlib
import uuid
from django.db import models
from django.db.models import Avg, Count, F, Sum
from django.db.models import Avg, Count, F, Sum, UniqueConstraint
from django.db.models.functions import Coalesce
from django.urls import reverse
from django.utils.translation import gettext
@ -77,12 +77,6 @@ class ProductQuerySet(models.QuerySet):
def with_sold(self):
return self.annotate(sold=Coalesce(Sum("basket_items__quantity"), 0))
def with_fixed_price(self):
return self.exclude(unit_price_cents=0)
def with_no_fixed_price(self):
return self.filter(unit_price_cents=0)
class ProductManager(models.Manager):
def get_by_natural_key(self, name):
@ -94,9 +88,7 @@ class Product(Model):
image = models.ImageField(null=True, blank=True, verbose_name=_("image"))
unit_price_cents = models.PositiveIntegerField(
verbose_name=_("unit price (cents)"),
help_text=_(
"Unit price in cents. Use zero to denote that the product has no fixed price.",
),
help_text=_("unit price in cents"),
)
display_order = models.PositiveIntegerField(
default=default_product_display_order,
@ -123,10 +115,6 @@ class Product(Model):
base=16,
)
@property
def has_fixed_price(self) -> bool:
return self.unit_price_cents > 0
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if not self.image:
@ -179,14 +167,17 @@ class BasketQuerySet(models.QuerySet):
def turnover(self) -> int:
return self.priced().aggregate(total=Sum("price"))["total"]
def no_payment_method(self) -> BasketQuerySet:
return self.filter(payment_method=None)
class Basket(Model):
payment_method = models.ForeignKey(
to=PaymentMethod,
on_delete=models.PROTECT,
related_name="baskets",
null=False,
blank=False,
null=True,
blank=True,
verbose_name=_("payment method"),
)
@ -232,6 +223,9 @@ class BasketItem(Model):
class Meta:
verbose_name = _("basket item")
verbose_name_plural = _("basket items")
constraints = [
UniqueConstraint("product", "basket", name="unique_product_per_basket"),
]
class Cache(SingletonModel):

View file

@ -1,25 +1,14 @@
window.incrementValue = function (id) {
const element = document.getElementById(id);
let value = parseInt(element.value);
let value = parseInt(document.getElementById(id).value);
value = isNaN(value) ? 0 : value;
value++;
element.value = value;
window.dispatchChanged(element);
document.getElementById(id).value = value;
};
window.decrementValue = function (id) {
const element = document.getElementById(id);
let value = parseInt(element.value);
let value = parseInt(document.getElementById(id).value);
value = isNaN(value) ? 0 : value;
value--;
value = value < 0 ? 0 : value;
element.value = value;
window.dispatchChanged(element);
};
window.dispatchChanged = function (element) {
const event = new Event("change", { bubbles: true });
element.dispatchEvent(event);
document.getElementById(id).value = value;
};

View file

@ -1,12 +1,13 @@
{% extends "common/base.html" %}
{% load i18n static crispy_forms_tags purchase django_htmx %}
{% load static %}
{% load i18n %}
{% load crispy_forms_tags purchase %}
{% block extrahead %}
<link rel="stylesheet" href="{% static "purchase/css/basket_form.css" %}">
{% endblock %}
{% block content %}
{% if basket %}
<h1>{{ basket }} <span id="basket-price" class="badge bg-secondary">{{ basket.price|currency }}</span></h1>
<h1>{{ basket }} <span class="badge bg-secondary">{{ basket.price|currency }}</span></h1>
<p class="metadata">
{{ basket.created_at }}
</p>
@ -14,54 +15,10 @@
<div class="alert alert-danger" role="alert">{% translate "Missing payment method." %}</div>
{% endif %}
{% else %}
<h1>{% translate "New basket" %} <span id="basket-price" class="badge bg-secondary d-none">{{ basket.price|currency }}</span></h1>
<h1>{% translate "New basket" %}</h1>
{% endif %}
{% crispy form %}
<div class="row">
<div class="col">
<form
hx-get="{% url "purchase:additional_unpriced_product" %}"
hx-target="#products"
hx-swap="beforeend"
>
<div class="input-group">
<select class="form-select" name="product_to_add" id="product_to_add">
{% for product in products %}
<option value="{{ product.pk }}">{{ product.name }}</option>
{% endfor %}
</select>
<button
class="btn btn-outline-secondary"
type="submit"
id="add_product"
>
{% translate "Add product" %}
</button>
</div>
</form>
</div>
</div>
{% for item in basket.items.all %}
{% if item.product.unit_price_cents == 0 %}
<input
type="hidden"
hx-get="{% url "purchase:additional_unpriced_product" %}?product_to_add={{ item.product.pk }}&value={{ item.unit_price_cents }}"
hx-trigger="load"
hx-target="#products"
hx-swap="beforeend"
>
{% endif %}
{% endfor %}
<div class="row mt-4">
<div class="col">
{% if basket %}
<a href="{% url "purchase:new" %}" class="btn btn-secondary">{% translate "New basket" %}</a>
{% endif %}
</div>
</div>
{% endblock %}
{% block extrascript %}
<script src="{% static 'vendor/htmx-1.8.6/htmx.min.js' %}" defer></script>
{% django_htmx_script %}
{% if basket %}
<a href="{% url "purchase:new" %}" class="btn btn-secondary">{% translate "New" %}</a>
{% endif %}
{% endblock %}

View file

@ -6,7 +6,7 @@
<div class="row row-cols-2 row-cols-xl-6 row-cols-lg-5 row-cols-md-4 row-cols-sm-3 g-4">
{% for basket in baskets %}
<div class="col">
<div class="card h-100">
<div class="card h-100 {% if not basket.payment_method %}bg-warning text-black{% endif %}">
<div class="card-body">
<h5 class="card-title">{% blocktranslate with basket_id=basket.id %}Basket #{{ basket_id }}{% endblocktranslate %}</h5>
<p class="card-text">

View file

@ -35,6 +35,9 @@
<h2>{% translate "Turnover by payment method" %}</h2>
{% include "purchase/snippets/report_payment_methods.html" %}
<h2>{% translate "Baskets without payment method" %}</h2>
{% include "purchase/snippets/report_no_payment_method.html" %}
{% endblock %}
{% block extrascript %}

View file

@ -15,7 +15,7 @@
<h4 class="card-title">{{ product.name }}</h4>
<div class="input-group">
<button class="btn btn-danger" type="button" onclick="decrementValue('{{ field.id_for_label }}')"><i class="fa-solid fa-minus"></i></button>
{% crispy_field field 'class' 'form-control' 'inputmode' 'numeric' %}
{% crispy_field field 'class' 'form-control' %}
<button class="btn btn-success {% if field.value %}border-white{% endif %}" type="button" onclick="incrementValue('{{ field.id_for_label }}')"><i class="fa-solid fa-plus"></i></button>
</div>
</div>

View file

@ -1,25 +0,0 @@
{% load crispy_forms_field %}
<div class="col">
<div class="card h-100 bg-success text-white" data-product-id="{{ product.pk }}">
{% if product.image %}
<img src="{{ product.image.url }}" class="card-img">
{% else %}
<div class="card-img product-img-placeholder"
style="background-color: hsl({{ product.color_hue }}, 60%, 80%)">
<span>
{{ product.name|slice:"1" }}
</span>
</div>
{% endif %}
<div class="card-body">
<h4 class="card-title">{{ product.name }}</h4>
<div class="input-group">
<input type="number" step="1" inputmode="numeric" min="0"
name="unpriced_product-{{ product.pk }}" class="numberinput form-control" required="" id="unpriced_id_product-{{ product.pk }}"
value="{{ value|default:0 }}"
>
<span class="input-group-text" id="basic-addon1">cts</span>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,21 @@
{% load i18n %}
{% load purchase %}
<table class="table table-hover table-sm">
<thead><tr>
<th scope="col">{% translate "Basket" %}</th>
<th scope="col">{% translate "Price" %}</th>
</tr></thead>
<tbody>
{% for basket in no_payment_method %}
<tr>
<th scope="row">
<a href="{% url "purchase:update" basket.id %}">
{{ basket }}
</a>
</th>
<td>{{ basket.price|currency }}</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -6,7 +6,6 @@ from pytest_django.live_server_helper import LiveServer
from selenium.webdriver import ActionChains, Keys
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.select import Select
from selenium.webdriver.support.wait import WebDriverWait
from common.models import User
@ -35,11 +34,6 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
ProductFactory(),
ProductFactory(),
]
unpriced_products = [
ProductFactory(unit_price_cents=0),
ProductFactory(unit_price_cents=0),
ProductFactory(unit_price_cents=0),
]
payment_methods = [
PaymentMethodFactory(),
PaymentMethodFactory(),
@ -91,45 +85,15 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
chain.double_click(quantity_input).perform()
quantity_input.send_keys("4")
# Add non-fixed priced product
select = Select(selenium.find_element(By.ID, "product_to_add"))
unpriced_product = unpriced_products[1]
select.select_by_value(str(unpriced_product.pk))
selenium.find_element(By.ID, "add_product").click()
selenium.find_element(By.ID, "add_product").click()
elements = selenium.find_elements(
By.CSS_SELECTOR,
f"[data-product-id='{unpriced_product.pk}']",
)
for elem in elements:
assert (
elem.find_element(By.CLASS_NAME, "card-title").text == unpriced_product.name
)
price_input = elements[0].find_element(By.CLASS_NAME, "numberinput")
chain = ActionChains(selenium)
chain.double_click(price_input).perform()
price_input.send_keys("237")
price_input = elements[1].find_element(By.CLASS_NAME, "numberinput")
chain = ActionChains(selenium)
chain.double_click(price_input).perform()
price_input.send_keys("401")
# Add payment method
selenium.find_element(By.TAG_NAME, "html").send_keys(Keys.END)
time.sleep(1)
selenium.find_element(
By.CSS_SELECTOR,
f'input[type="radio"][value="{payment_methods[1].pk}"]',
).click()
# Don't add payment method
# Save
selenium.find_element(By.ID, "submit-id-submit").click()
# Assert entries saved in DB (new basket with proper products)
assert Basket.objects.count() == 1
basket = Basket.objects.priced().first()
assert basket.payment_method == payment_methods[1]
assert basket.items.count() == 4
assert basket.payment_method is None
assert basket.items.count() == 2
assert basket.items.get(product=products[0]).quantity == 2
assert (
basket.items.get(product=products[0]).unit_price_cents
@ -140,14 +104,6 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
basket.items.get(product=products[1]).unit_price_cents
== products[1].unit_price_cents
)
unpriced_basket_items = basket.items.filter(product=unpriced_product).order_by(
"unit_price_cents",
)
assert len(unpriced_basket_items) == 2
assert unpriced_basket_items[0].quantity == 1
assert unpriced_basket_items[0].unit_price_cents == 237
assert unpriced_basket_items[1].quantity == 1
assert unpriced_basket_items[1].unit_price_cents == 401
# Assert redirected to basket update view
redirect_url = live_reverse(live_server, "purchase:update", pk=basket.pk)
@ -157,6 +113,10 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
created_message = selenium.find_element(By.CSS_SELECTOR, ".messages .alert-success")
assert created_message.text == "Panier correctement créé."
# Assert message in red for missing payment method
missing_payment = selenium.find_element(By.CSS_SELECTOR, ".alert.alert-danger")
assert missing_payment.text == "Moyen de paiement manquant."
# Assert ID, price, date & product quantities
# Selected products have a green background
title = selenium.find_element(By.TAG_NAME, "h1")
@ -181,12 +141,6 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
quantity = int(quantity_input.get_attribute("value"))
assert quantity == 0
elements = selenium.find_elements(
By.CSS_SELECTOR,
f"[data-product-id='{unpriced_product.pk}']",
)
assert len(elements) == 2, "Unpriced products should be displayed"
# Click on - on product 2
displayed_product = displayed_products[1]
displayed_product.find_element(By.CLASS_NAME, "btn-danger").click()
@ -196,6 +150,11 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
quantity = int(quantity_input.get_attribute("value"))
assert quantity == 3
# Add payment method
selenium.find_element(By.TAG_NAME, "html").send_keys(Keys.END)
time.sleep(1)
selenium.find_element(By.ID, f"id_payment_method_{payment_methods[1].pk}").click()
# Save
selenium.find_element(By.ID, "submit-id-submit").click()
@ -203,7 +162,7 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
assert Basket.objects.count() == 1
basket = Basket.objects.priced().first()
assert basket.payment_method == payment_methods[1]
assert basket.items.count() == 4
assert basket.items.count() == 2
assert basket.items.get(product=products[0]).quantity == 2
assert (
basket.items.get(product=products[0]).unit_price_cents
@ -214,14 +173,6 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
basket.items.get(product=products[1]).unit_price_cents
== products[1].unit_price_cents
)
unpriced_basket_items = basket.items.filter(product=unpriced_product).order_by(
"unit_price_cents",
)
assert len(unpriced_basket_items) == 2
assert unpriced_basket_items[0].quantity == 1
assert unpriced_basket_items[0].unit_price_cents == 237
assert unpriced_basket_items[1].quantity == 1
assert unpriced_basket_items[1].unit_price_cents == 401
# Assert redirected to same view
redirect_url = live_reverse(live_server, "purchase:update", pk=basket.pk)
@ -273,30 +224,34 @@ def test_baskets_list(live_server: LiveServer, selenium: WebDriver):
pk=basket_with_payment_method.pk,
)
with freezegun.freeze_time("2022-09-24 19:02:00+0200"):
another_basket = BasketWithItemsFactory()
another_basket = Basket.objects.priced().get(
pk=another_basket.pk,
basket_no_payment_method = BasketWithItemsFactory(payment_method=None)
basket_no_payment_method = Basket.objects.priced().get(
pk=basket_no_payment_method.pk,
)
# Login
url = reverse("purchase:list")
login(live_server, selenium, cashier, url)
# Assert first basket (last created) has yellow background
# Assert basket info displayed
displayed_baskets = selenium.find_elements(By.CSS_SELECTOR, ".card.h-100")
first_basket = displayed_baskets[0]
assert "bg-warning" in first_basket.get_attribute("class")
text = first_basket.text.replace("\n", " ")
assert f"{another_basket.pk} " in text
expected_articles_count = another_basket.items.count()
assert f"{basket_no_payment_method.pk} " in text
expected_articles_count = basket_no_payment_method.items.count()
assert f" {expected_articles_count} article" in text
expected_price = another_basket.price / 100
expected_price = basket_no_payment_method.price / 100
assert f" {expected_price:.2f}" in text
expected_payment_method = another_basket.payment_method.name
expected_payment_method = "-"
assert f" {expected_payment_method} " in text
assert "19:02" in text
# Assert second basket (first created) doesn't have yellow background
# Assert basket info displayed including payment method
second_basket = displayed_baskets[1]
assert "bg-warning" not in second_basket.get_attribute("class")
text = second_basket.text.replace("\n", " ")
assert f"{basket_with_payment_method.pk} " in text
expected_articles_count = basket_with_payment_method.items.count()
@ -315,7 +270,7 @@ def test_baskets_list(live_server: LiveServer, selenium: WebDriver):
# Assert object deleted in DB
assert Basket.objects.count() == 1
assert Basket.objects.first() == another_basket
assert Basket.objects.first() == basket_no_payment_method
# Assert redirected to list view
wait.until(
@ -330,7 +285,7 @@ def test_baskets_list(live_server: LiveServer, selenium: WebDriver):
redirect_url = live_reverse(
live_server,
"purchase:update",
pk=another_basket.pk,
pk=basket_no_payment_method.pk,
)
wait.until(lambda driver: driver.current_url == redirect_url)

View file

@ -1,27 +1,14 @@
from django.urls import path
from purchase.views import (
additional_unpriced_product,
delete_basket,
list_baskets,
new_basket,
price_preview,
update_basket,
)
from purchase.views import delete_basket, list_baskets, new_basket, update_basket
from purchase.views.reports import by_hour_plot_view, products_plots_view, reports
app_name = "purchase"
urlpatterns = [
path("", list_baskets, name="list"),
path("price_preview/", price_preview, name="price_preview"),
path("new/", new_basket, name="new"),
path("<int:pk>/update/", update_basket, name="update"),
path("<int:pk>/delete/", delete_basket, name="delete"),
path(
"additional_unpriced_product/",
additional_unpriced_product,
name="additional_unpriced_product",
),
path("reports/", reports, name="reports"),
# plots
path("reports/products_plots/", products_plots_view, name="products_plots"),

View file

@ -1,17 +1,3 @@
from .basket import (
additional_unpriced_product,
delete_basket,
list_baskets,
new_basket,
price_preview,
update_basket,
)
from .basket import delete_basket, list_baskets, new_basket, update_basket
__all__ = [
"new_basket",
"update_basket",
"delete_basket",
"list_baskets",
"additional_unpriced_product",
"price_preview",
]
__all__ = ["new_basket", "update_basket", "delete_basket", "list_baskets"]

View file

@ -1,33 +1,25 @@
import logging
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.datastructures import MultiValueDict
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import condition, require_http_methods
from django_htmx.http import trigger_client_event
from purchase.forms import PRICED_PREFIX, UNPRICED_PREFIX, BasketForm
from purchase.models import Basket, Product, reports_etag, reports_last_modified
logger = logging.getLogger(__name__)
from purchase.forms import BasketForm
from purchase.models import Basket, reports_etag, reports_last_modified
@require_http_methods(["GET", "POST"])
@permission_required("purchase.add_basket")
def new_basket(request: WSGIRequest) -> HttpResponse:
def new_basket(request: HttpRequest) -> HttpResponse:
if request.method == "POST":
form = BasketForm(request.POST)
if form.is_valid():
basket = form.save()
update_with_unpriced_products(basket, request.POST)
instance = form.save()
if request.user.has_perm("purchase.change_basket"):
url = basket.get_absolute_url()
url = instance.get_absolute_url()
else:
url = reverse("purchase:new")
messages.success(request, _("Successfully created basket."))
@ -35,79 +27,39 @@ def new_basket(request: WSGIRequest) -> HttpResponse:
else:
form = BasketForm()
return TemplateResponse(
request,
"purchase/basket_form.html",
{"form": form, "products": Product.objects.with_no_fixed_price()},
)
def update_with_unpriced_products(basket: Basket, post_data: MultiValueDict):
no_fixed_price = {
product.id: product for product in Product.objects.with_no_fixed_price()
}
basket.items.filter(product__in=no_fixed_price.values()).delete()
for product_id, product in no_fixed_price.items():
if prices := post_data.getlist(f"{UNPRICED_PREFIX}{product_id}"):
for price in map(int, prices):
if price:
basket.items.create(
product=product,
quantity=1,
unit_price_cents=price,
)
return TemplateResponse(request, "purchase/basket_form.html", {"form": form})
@require_http_methods(["GET", "POST"])
@permission_required("purchase.change_basket")
def update_basket(request: WSGIRequest, pk: int) -> HttpResponse:
def update_basket(request: HttpRequest, pk: int) -> HttpResponse:
basket = get_object_or_404(Basket.objects.priced(), pk=pk)
if request.method == "POST":
form = BasketForm(request.POST, instance=basket)
if form.is_valid():
basket = form.save()
update_with_unpriced_products(basket, request.POST)
messages.success(request, _("Successfully updated basket."))
return redirect(basket.get_absolute_url())
else:
form = BasketForm(instance=basket)
response = render(
return TemplateResponse(
request,
"purchase/basket_form.html",
{
"form": form,
"basket": basket,
"products": Product.objects.with_no_fixed_price(),
},
)
trigger_client_event(response, "load-unpriced", after="swap")
return response
@require_http_methods(["GET"])
def additional_unpriced_product(request: WSGIRequest) -> HttpResponse:
product_id = request.GET.get("product_to_add")
value = request.GET.get("value", 0)
product = get_object_or_404(Product.objects.with_no_fixed_price(), pk=product_id)
context = {"product": product, "value": value}
return render(
request,
"purchase/snippets/basket_unpriced_item.html",
context,
{"form": form, "basket": basket},
)
@permission_required("purchase.view_basket")
@condition(etag_func=reports_etag, last_modified_func=reports_last_modified)
def list_baskets(request: WSGIRequest) -> HttpResponse:
def list_baskets(request: HttpRequest) -> HttpResponse:
context = {"baskets": Basket.objects.priced().order_by("-id")}
return TemplateResponse(request, "purchase/basket_list.html", context)
@require_http_methods(["GET", "POST"])
@permission_required("purchase.delete_basket")
def delete_basket(request: WSGIRequest, pk: int) -> HttpResponse:
def delete_basket(request: HttpRequest, pk: int) -> HttpResponse:
basket = get_object_or_404(Basket, pk=pk)
if request.method == "GET":
context = {"basket": basket}
@ -115,21 +67,3 @@ def delete_basket(request: WSGIRequest, pk: int) -> HttpResponse:
basket.delete()
messages.success(request, _("Basket successfully deleted."))
return redirect("purchase:list")
@require_http_methods(["POST"])
@permission_required("purchase.add_basket")
def price_preview(request: WSGIRequest) -> HttpResponse:
total = 0
for name in request.POST:
if name.startswith(PRICED_PREFIX):
product_id = name[len(PRICED_PREFIX) :]
product = get_object_or_404(Product, pk=product_id)
total += product.unit_price_cents * int(request.POST.get(name, 0))
elif name.startswith(UNPRICED_PREFIX):
total += sum(map(int, request.POST.getlist(name)))
total = f"{total/100:.2f}"
return HttpResponse(
f'<span hx-swap-oob="true" id="basket-price" class="badge bg-secondary">{total}</span>Montant total : {total}',
)

View file

@ -79,6 +79,7 @@ def reports(request):
"average_basket_by_day": average_basket_by_day,
"products": products,
"payment_methods": PaymentMethod.objects.with_turnover().with_sold(),
"no_payment_method": Basket.objects.no_payment_method().priced(),
}
return TemplateResponse(request, template_name, context)