Compare commits
16 Commits
072c762ad0
...
90787707d1
Author | SHA1 | Date |
---|---|---|
Crocmagnon | 90787707d1 | |
Gabriel Augendre | e3bb57396b | |
Gabriel Augendre | 0f0e4f854a | |
Gabriel Augendre | 1dec9c0bb6 | |
Gabriel Augendre | ee57139597 | |
Gabriel Augendre | f060df9219 | |
Gabriel Augendre | a219eeae84 | |
Gabriel Augendre | 2d952e1a63 | |
Gabriel Augendre | f8e7647af1 | |
Gabriel Augendre | 755d896f1c | |
Gabriel Augendre | 9b381a0e18 | |
Gabriel Augendre | 3de1c13995 | |
Gabriel Augendre | edc0cb5f0a | |
Gabriel Augendre | 885ec52864 | |
Gabriel Augendre | f3cd4201b2 | |
Gabriel Augendre | 56c2c7045d |
|
@ -48,7 +48,7 @@ factory-boy==3.2.1
|
|||
# via -r requirements-dev.in
|
||||
faker==18.3.1
|
||||
# via factory-boy
|
||||
filelock==3.10.4
|
||||
filelock==3.10.7
|
||||
# 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.1.1
|
||||
platformdirs==3.2.0
|
||||
# via
|
||||
# black
|
||||
# virtualenv
|
||||
|
|
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/static/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
After Width: | Height: | Size: 667 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 3.3 KiB |
|
@ -0,0 +1,75 @@
|
|||
<?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>
|
After Width: | Height: | Size: 4.7 KiB |
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"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"
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
<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;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{% 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">
|
|
@ -75,5 +75,16 @@
|
|||
"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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
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
|
||||
|
||||
PREFIX = "product-"
|
||||
PRICED_PREFIX = "product-"
|
||||
UNPRICED_PREFIX = "unpriced_product-"
|
||||
|
||||
|
||||
class BasketForm(forms.ModelForm):
|
||||
|
@ -24,6 +27,12 @@ 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")
|
||||
|
@ -31,8 +40,8 @@ class BasketForm(forms.ModelForm):
|
|||
for item in basket.items.all():
|
||||
products[item.product] = item.quantity
|
||||
fields = []
|
||||
for product in Product.objects.all():
|
||||
field_name = f"{PREFIX}{product.id}"
|
||||
for product in Product.objects.with_fixed_price():
|
||||
field_name = f"{PRICED_PREFIX}{product.id}"
|
||||
self.fields.update(
|
||||
{
|
||||
field_name: forms.IntegerField(
|
||||
|
@ -43,12 +52,21 @@ 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):
|
||||
|
@ -56,8 +74,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(PREFIX):
|
||||
product_id = int(name.removeprefix(PREFIX))
|
||||
if name.startswith(PRICED_PREFIX):
|
||||
product_id = int(name.removeprefix(PRICED_PREFIX))
|
||||
product = products[product_id]
|
||||
if value > 0:
|
||||
instance.items.update_or_create(
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-04-27 22:58+0200\n"
|
||||
"POT-Creation-Date: 2023-03-27 16:54+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,96 +18,97 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: purchase/admin.py:18
|
||||
#: purchase/admin.py:19
|
||||
msgid "unit price"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/admin.py:22
|
||||
#: purchase/admin.py:23
|
||||
msgid "sold"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/admin.py:26 purchase/admin.py:39
|
||||
#: purchase/admin.py:27 purchase/admin.py:40
|
||||
msgid "turnover"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/admin.py:53 purchase/admin.py:70
|
||||
#: purchase/admin.py:54 purchase/admin.py:71
|
||||
msgid "price"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/forms.py:23
|
||||
#: purchase/forms.py:27
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:13
|
||||
#: purchase/models.py:17
|
||||
msgid "created at"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:14
|
||||
#: purchase/models.py:18
|
||||
msgid "updated at"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:42 purchase/models.py:83
|
||||
#: purchase/models.py:46 purchase/models.py:93
|
||||
msgid "name"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:47 purchase/models.py:167
|
||||
#: purchase/models.py:51 purchase/models.py:193
|
||||
msgid "payment method"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:48
|
||||
#: purchase/models.py:52
|
||||
msgid "payment methods"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:84
|
||||
#: purchase/models.py:94
|
||||
msgid "image"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:86 purchase/models.py:203
|
||||
#: purchase/models.py:96 purchase/models.py:229
|
||||
msgid "unit price (cents)"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:86
|
||||
msgid "unit price in cents"
|
||||
#: purchase/models.py:98
|
||||
msgid ""
|
||||
"Unit price in cents. Use zero to denote that the product has no fixed price."
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:89
|
||||
#: purchase/models.py:103
|
||||
msgid "display order"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:96 purchase/models.py:193
|
||||
#: purchase/models.py:110 purchase/models.py:219
|
||||
msgid "product"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:97
|
||||
#: purchase/models.py:111
|
||||
msgid "products"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:173 purchase/models.py:199
|
||||
#: purchase/models.py:199 purchase/models.py:225
|
||||
msgid "basket"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:174
|
||||
#: purchase/models.py:200
|
||||
msgid "baskets"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:177
|
||||
#: purchase/models.py:203
|
||||
#, python-format
|
||||
msgid "Basket #%(id)s"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:201
|
||||
#: purchase/models.py:227
|
||||
msgid "quantity"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:204
|
||||
#: purchase/models.py:230
|
||||
msgid "product's unit price in cents at the time of purchase"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:210
|
||||
#: purchase/models.py:236
|
||||
msgid "basket item"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/models.py:211
|
||||
#: purchase/models.py:237
|
||||
msgid "basket items"
|
||||
msgstr ""
|
||||
|
||||
|
@ -116,16 +117,17 @@ msgstr ""
|
|||
msgid "Are you sure you want to delete \"%(basket)s\"?"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/templates/purchase/basket_form.html:11
|
||||
#: purchase/templates/purchase/basket_form.html:14
|
||||
msgid "Missing payment method."
|
||||
msgstr ""
|
||||
|
||||
#: purchase/templates/purchase/basket_form.html:14
|
||||
#: purchase/templates/purchase/basket_form.html:17
|
||||
#: purchase/templates/purchase/basket_form.html:52
|
||||
msgid "New basket"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/templates/purchase/basket_form.html:18
|
||||
msgid "New"
|
||||
#: purchase/templates/purchase/basket_form.html:36
|
||||
msgid "Add product"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/templates/purchase/basket_list.html:5
|
||||
|
@ -144,45 +146,45 @@ msgid_plural "%(counter)s items"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
#: purchase/templates/purchase/reports.html:11
|
||||
#: purchase/templates/purchase/reports.html:9
|
||||
msgid "Reports"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/templates/purchase/reports.html:12
|
||||
#: purchase/templates/purchase/reports.html:10
|
||||
msgid "General"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/templates/purchase/reports.html:14
|
||||
#: purchase/templates/purchase/reports.html:12
|
||||
msgid "Total turnover:"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/templates/purchase/reports.html:15
|
||||
#: purchase/templates/purchase/reports.html:13
|
||||
msgid "Average basket:"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/templates/purchase/reports.html:18
|
||||
#: purchase/templates/purchase/reports.html:16
|
||||
msgid "By day"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/templates/purchase/reports.html:19
|
||||
#: purchase/templates/purchase/reports.html:17
|
||||
#: 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:25
|
||||
#: purchase/templates/purchase/reports.html:23
|
||||
msgid "Average basket"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/templates/purchase/reports.html:34
|
||||
#: purchase/templates/purchase/reports.html:32
|
||||
msgid "Products"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/templates/purchase/reports.html:40
|
||||
#: purchase/templates/purchase/reports.html:36
|
||||
msgid "Turnover by payment method"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/templates/purchase/reports.html:43
|
||||
#: purchase/templates/purchase/reports.html:39
|
||||
msgid "Baskets without payment method"
|
||||
msgstr ""
|
||||
|
||||
|
@ -207,42 +209,42 @@ msgid "Product"
|
|||
msgstr ""
|
||||
|
||||
#: purchase/templates/purchase/snippets/report_products.html:7
|
||||
#: purchase/views/reports.py:79 purchase/views/reports.py:93
|
||||
#: purchase/views/reports.py:102 purchase/views/reports.py:116
|
||||
msgid "# sold"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/views/basket.py:15
|
||||
#: purchase/views/basket.py:33
|
||||
msgid "Successfully created basket."
|
||||
msgstr ""
|
||||
|
||||
#: purchase/views/basket.py:30
|
||||
#: purchase/views/basket.py:70
|
||||
msgid "Successfully updated basket."
|
||||
msgstr ""
|
||||
|
||||
#: purchase/views/basket.py:45
|
||||
#: purchase/views/basket.py:116
|
||||
msgid "Basket successfully deleted."
|
||||
msgstr ""
|
||||
|
||||
#: purchase/views/reports.py:29
|
||||
#: purchase/views/reports.py:64
|
||||
msgid "No sale to report"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/views/reports.py:70
|
||||
#: purchase/views/reports.py:93
|
||||
msgid "Sales by product"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/views/reports.py:85 purchase/views/reports.py:100
|
||||
#: purchase/views/reports.py:108 purchase/views/reports.py:123
|
||||
msgid "Turnover by product"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/views/reports.py:122
|
||||
#: purchase/views/reports.py:147
|
||||
msgid "Sales by hour"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/views/reports.py:133
|
||||
#: purchase/views/reports.py:158
|
||||
msgid "Basket count by hour"
|
||||
msgstr ""
|
||||
|
||||
#: purchase/views/reports.py:141
|
||||
#: purchase/views/reports.py:166
|
||||
msgid "Turnover by hour"
|
||||
msgstr ""
|
||||
|
|
|
@ -5,7 +5,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-04-27 22:58+0200\n"
|
||||
"POT-Creation-Date: 2023-03-27 16:54+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,96 +15,97 @@ msgstr ""
|
|||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: purchase/admin.py:18
|
||||
#: purchase/admin.py:19
|
||||
msgid "unit price"
|
||||
msgstr "prix unitaire"
|
||||
|
||||
#: purchase/admin.py:22
|
||||
#: purchase/admin.py:23
|
||||
msgid "sold"
|
||||
msgstr "vendu"
|
||||
|
||||
#: purchase/admin.py:26 purchase/admin.py:39
|
||||
#: purchase/admin.py:27 purchase/admin.py:40
|
||||
msgid "turnover"
|
||||
msgstr "chiffre d'affaires"
|
||||
|
||||
#: purchase/admin.py:53 purchase/admin.py:70
|
||||
#: purchase/admin.py:54 purchase/admin.py:71
|
||||
msgid "price"
|
||||
msgstr "prix"
|
||||
|
||||
#: purchase/forms.py:23
|
||||
#: purchase/forms.py:27
|
||||
msgid "Save"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
#: purchase/models.py:13
|
||||
#: purchase/models.py:17
|
||||
msgid "created at"
|
||||
msgstr "créé à"
|
||||
|
||||
#: purchase/models.py:14
|
||||
#: purchase/models.py:18
|
||||
msgid "updated at"
|
||||
msgstr "mis à jour à"
|
||||
|
||||
#: purchase/models.py:42 purchase/models.py:83
|
||||
#: purchase/models.py:46 purchase/models.py:93
|
||||
msgid "name"
|
||||
msgstr "nom"
|
||||
|
||||
#: purchase/models.py:47 purchase/models.py:167
|
||||
#: purchase/models.py:51 purchase/models.py:193
|
||||
msgid "payment method"
|
||||
msgstr "moyen de paiement"
|
||||
|
||||
#: purchase/models.py:48
|
||||
#: purchase/models.py:52
|
||||
msgid "payment methods"
|
||||
msgstr "moyens de paiement"
|
||||
|
||||
#: purchase/models.py:84
|
||||
#: purchase/models.py:94
|
||||
msgid "image"
|
||||
msgstr "image"
|
||||
|
||||
#: purchase/models.py:86 purchase/models.py:203
|
||||
#: purchase/models.py:96 purchase/models.py:229
|
||||
msgid "unit price (cents)"
|
||||
msgstr "prix unitaire (centimes)"
|
||||
|
||||
#: purchase/models.py:86
|
||||
msgid "unit price in cents"
|
||||
msgstr "prix unitaire en 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:89
|
||||
#: purchase/models.py:103
|
||||
msgid "display order"
|
||||
msgstr "ordre d'affichage"
|
||||
|
||||
#: purchase/models.py:96 purchase/models.py:193
|
||||
#: purchase/models.py:110 purchase/models.py:219
|
||||
msgid "product"
|
||||
msgstr "produit"
|
||||
|
||||
#: purchase/models.py:97
|
||||
#: purchase/models.py:111
|
||||
msgid "products"
|
||||
msgstr "produits"
|
||||
|
||||
#: purchase/models.py:173 purchase/models.py:199
|
||||
#: purchase/models.py:199 purchase/models.py:225
|
||||
msgid "basket"
|
||||
msgstr "panier"
|
||||
|
||||
#: purchase/models.py:174
|
||||
#: purchase/models.py:200
|
||||
msgid "baskets"
|
||||
msgstr "paniers"
|
||||
|
||||
#: purchase/models.py:177
|
||||
#: purchase/models.py:203
|
||||
#, python-format
|
||||
msgid "Basket #%(id)s"
|
||||
msgstr "Panier n°%(id)s"
|
||||
|
||||
#: purchase/models.py:201
|
||||
#: purchase/models.py:227
|
||||
msgid "quantity"
|
||||
msgstr "quantité"
|
||||
|
||||
#: purchase/models.py:204
|
||||
#: purchase/models.py:230
|
||||
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:210
|
||||
#: purchase/models.py:236
|
||||
msgid "basket item"
|
||||
msgstr "article de panier"
|
||||
|
||||
#: purchase/models.py:211
|
||||
#: purchase/models.py:237
|
||||
msgid "basket items"
|
||||
msgstr "articles de panier"
|
||||
|
||||
|
@ -113,17 +114,18 @@ 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:11
|
||||
#: purchase/templates/purchase/basket_form.html:14
|
||||
msgid "Missing payment method."
|
||||
msgstr "Moyen de paiement manquant."
|
||||
|
||||
#: purchase/templates/purchase/basket_form.html:14
|
||||
#: purchase/templates/purchase/basket_form.html:17
|
||||
#: purchase/templates/purchase/basket_form.html:52
|
||||
msgid "New basket"
|
||||
msgstr "Nouveau panier"
|
||||
|
||||
#: purchase/templates/purchase/basket_form.html:18
|
||||
msgid "New"
|
||||
msgstr "Nouveau"
|
||||
#: purchase/templates/purchase/basket_form.html:36
|
||||
msgid "Add product"
|
||||
msgstr "Ajouter un produit"
|
||||
|
||||
#: purchase/templates/purchase/basket_list.html:5
|
||||
msgid "Baskets"
|
||||
|
@ -141,45 +143,45 @@ msgid_plural "%(counter)s items"
|
|||
msgstr[0] "1 article"
|
||||
msgstr[1] "%(counter)s articles"
|
||||
|
||||
#: purchase/templates/purchase/reports.html:11
|
||||
#: purchase/templates/purchase/reports.html:9
|
||||
msgid "Reports"
|
||||
msgstr "Rapports"
|
||||
|
||||
#: purchase/templates/purchase/reports.html:12
|
||||
#: purchase/templates/purchase/reports.html:10
|
||||
msgid "General"
|
||||
msgstr "Général"
|
||||
|
||||
#: purchase/templates/purchase/reports.html:14
|
||||
#: purchase/templates/purchase/reports.html:12
|
||||
msgid "Total turnover:"
|
||||
msgstr "Chiffre d'affaires total :"
|
||||
|
||||
#: purchase/templates/purchase/reports.html:15
|
||||
#: purchase/templates/purchase/reports.html:13
|
||||
msgid "Average basket:"
|
||||
msgstr "Panier moyen :"
|
||||
|
||||
#: purchase/templates/purchase/reports.html:18
|
||||
#: purchase/templates/purchase/reports.html:16
|
||||
msgid "By day"
|
||||
msgstr "Par jour"
|
||||
|
||||
#: purchase/templates/purchase/reports.html:19
|
||||
#: purchase/templates/purchase/reports.html:17
|
||||
#: 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:25
|
||||
#: purchase/templates/purchase/reports.html:23
|
||||
msgid "Average basket"
|
||||
msgstr "Panier moyen"
|
||||
|
||||
#: purchase/templates/purchase/reports.html:34
|
||||
#: purchase/templates/purchase/reports.html:32
|
||||
msgid "Products"
|
||||
msgstr "Produits"
|
||||
|
||||
#: purchase/templates/purchase/reports.html:40
|
||||
#: purchase/templates/purchase/reports.html:36
|
||||
msgid "Turnover by payment method"
|
||||
msgstr "Chiffre d'affaires par moyen de paiement"
|
||||
|
||||
#: purchase/templates/purchase/reports.html:43
|
||||
#: purchase/templates/purchase/reports.html:39
|
||||
msgid "Baskets without payment method"
|
||||
msgstr "Paniers sans moyen de paiement"
|
||||
|
||||
|
@ -204,45 +206,42 @@ msgid "Product"
|
|||
msgstr "Produit"
|
||||
|
||||
#: purchase/templates/purchase/snippets/report_products.html:7
|
||||
#: purchase/views/reports.py:79 purchase/views/reports.py:93
|
||||
#: purchase/views/reports.py:102 purchase/views/reports.py:116
|
||||
msgid "# sold"
|
||||
msgstr "Nb. vendus"
|
||||
|
||||
#: purchase/views/basket.py:15
|
||||
#: purchase/views/basket.py:33
|
||||
msgid "Successfully created basket."
|
||||
msgstr "Panier correctement créé."
|
||||
|
||||
#: purchase/views/basket.py:30
|
||||
#: purchase/views/basket.py:70
|
||||
msgid "Successfully updated basket."
|
||||
msgstr "Panier correctement modifié."
|
||||
|
||||
#: purchase/views/basket.py:45
|
||||
#: purchase/views/basket.py:116
|
||||
msgid "Basket successfully deleted."
|
||||
msgstr "Panier correctement supprimé."
|
||||
|
||||
#: purchase/views/reports.py:29
|
||||
#: purchase/views/reports.py:64
|
||||
msgid "No sale to report"
|
||||
msgstr "Aucune vente à afficher"
|
||||
|
||||
#: purchase/views/reports.py:70
|
||||
#: purchase/views/reports.py:93
|
||||
msgid "Sales by product"
|
||||
msgstr "Ventes par produit"
|
||||
|
||||
#: purchase/views/reports.py:85 purchase/views/reports.py:100
|
||||
#: purchase/views/reports.py:108 purchase/views/reports.py:123
|
||||
msgid "Turnover by product"
|
||||
msgstr "Chiffre d'affaires par produit"
|
||||
|
||||
#: purchase/views/reports.py:122
|
||||
#: purchase/views/reports.py:147
|
||||
msgid "Sales by hour"
|
||||
msgstr "Ventes par heure"
|
||||
|
||||
#: purchase/views/reports.py:133
|
||||
#: purchase/views/reports.py:158
|
||||
msgid "Basket count by hour"
|
||||
msgstr "Nombre de paniers par heure"
|
||||
|
||||
#: purchase/views/reports.py:141
|
||||
#: purchase/views/reports.py:166
|
||||
msgid "Turnover by hour"
|
||||
msgstr "Chiffre d'affaires par heure"
|
||||
|
||||
#~ msgid "Basket ID"
|
||||
#~ msgstr "Id de panier"
|
||||
|
|
|
@ -36,9 +36,7 @@ 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 = None
|
||||
if random.random() < 0.99: # noqa: PLR2004
|
||||
method = random.choices(payment_methods, weights=methods_weights)[0]
|
||||
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):
|
||||
|
@ -54,13 +52,23 @@ class Command(BaseCommand):
|
|||
)
|
||||
items = []
|
||||
for product in selected_products:
|
||||
items.append(
|
||||
BasketItem(
|
||||
product=product,
|
||||
basket=basket,
|
||||
quantity=random.randint(1, 3),
|
||||
unit_price_cents=product.unit_price_cents,
|
||||
),
|
||||
)
|
||||
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,
|
||||
),
|
||||
)
|
||||
BasketItem.objects.bulk_create(items)
|
||||
return count
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# 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)",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,35 @@
|
|||
# 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",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -4,7 +4,7 @@ import hashlib
|
|||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Avg, Count, F, Sum, UniqueConstraint
|
||||
from django.db.models import Avg, Count, F, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext
|
||||
|
@ -77,6 +77,12 @@ 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):
|
||||
|
@ -88,7 +94,9 @@ 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"),
|
||||
help_text=_(
|
||||
"Unit price in cents. Use zero to denote that the product has no fixed price.",
|
||||
),
|
||||
)
|
||||
display_order = models.PositiveIntegerField(
|
||||
default=default_product_display_order,
|
||||
|
@ -115,6 +123,10 @@ 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:
|
||||
|
@ -167,17 +179,14 @@ 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=True,
|
||||
blank=True,
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name=_("payment method"),
|
||||
)
|
||||
|
||||
|
@ -223,9 +232,6 @@ 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):
|
||||
|
|
|
@ -1,14 +1,25 @@
|
|||
window.incrementValue = function (id) {
|
||||
let value = parseInt(document.getElementById(id).value);
|
||||
const element = document.getElementById(id);
|
||||
let value = parseInt(element.value);
|
||||
value = isNaN(value) ? 0 : value;
|
||||
value++;
|
||||
document.getElementById(id).value = value;
|
||||
element.value = value;
|
||||
|
||||
window.dispatchChanged(element);
|
||||
};
|
||||
|
||||
window.decrementValue = function (id) {
|
||||
let value = parseInt(document.getElementById(id).value);
|
||||
const element = document.getElementById(id);
|
||||
let value = parseInt(element.value);
|
||||
value = isNaN(value) ? 0 : value;
|
||||
value--;
|
||||
value = value < 0 ? 0 : value;
|
||||
document.getElementById(id).value = value;
|
||||
element.value = value;
|
||||
|
||||
window.dispatchChanged(element);
|
||||
};
|
||||
|
||||
window.dispatchChanged = function (element) {
|
||||
const event = new Event("change", { bubbles: true });
|
||||
element.dispatchEvent(event);
|
||||
};
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
{% extends "common/base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_tags purchase %}
|
||||
{% load i18n static crispy_forms_tags purchase django_htmx %}
|
||||
|
||||
{% block extrahead %}
|
||||
<link rel="stylesheet" href="{% static "purchase/css/basket_form.css" %}">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% if basket %}
|
||||
<h1>{{ basket }} <span class="badge bg-secondary">{{ basket.price|currency }}</span></h1>
|
||||
<h1>{{ basket }} <span id="basket-price" class="badge bg-secondary">{{ basket.price|currency }}</span></h1>
|
||||
<p class="metadata">
|
||||
{{ basket.created_at }}
|
||||
</p>
|
||||
|
@ -15,10 +14,54 @@
|
|||
<div class="alert alert-danger" role="alert">{% translate "Missing payment method." %}</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<h1>{% translate "New basket" %}</h1>
|
||||
<h1>{% translate "New basket" %} <span id="basket-price" class="badge bg-secondary d-none">{{ basket.price|currency }}</span></h1>
|
||||
{% endif %}
|
||||
{% crispy form %}
|
||||
{% if basket %}
|
||||
<a href="{% url "purchase:new" %}" class="btn btn-secondary">{% translate "New" %}</a>
|
||||
{% endif %}
|
||||
<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 %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -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 {% if not basket.payment_method %}bg-warning text-black{% endif %}">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{% blocktranslate with basket_id=basket.id %}Basket #{{ basket_id }}{% endblocktranslate %}</h5>
|
||||
<p class="card-text">
|
||||
|
|
|
@ -35,9 +35,6 @@
|
|||
|
||||
<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 %}
|
||||
|
|
|
@ -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' %}
|
||||
{% crispy_field field 'class' 'form-control' 'inputmode' 'numeric' %}
|
||||
<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>
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
{% 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>
|
|
@ -1,21 +0,0 @@
|
|||
{% 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>
|
|
@ -6,6 +6,7 @@ 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
|
||||
|
@ -34,6 +35,11 @@ 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(),
|
||||
|
@ -85,15 +91,45 @@ def test_cashier_create_and_update_basket( # noqa: PLR0915
|
|||
chain.double_click(quantity_input).perform()
|
||||
quantity_input.send_keys("4")
|
||||
|
||||
# Don't add payment method
|
||||
# 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()
|
||||
|
||||
# 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 is None
|
||||
assert basket.items.count() == 2
|
||||
assert basket.payment_method == payment_methods[1]
|
||||
assert basket.items.count() == 4
|
||||
assert basket.items.get(product=products[0]).quantity == 2
|
||||
assert (
|
||||
basket.items.get(product=products[0]).unit_price_cents
|
||||
|
@ -104,6 +140,14 @@ 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)
|
||||
|
@ -113,10 +157,6 @@ 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")
|
||||
|
@ -141,6 +181,12 @@ 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()
|
||||
|
@ -150,11 +196,6 @@ 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()
|
||||
|
||||
|
@ -162,7 +203,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() == 2
|
||||
assert basket.items.count() == 4
|
||||
assert basket.items.get(product=products[0]).quantity == 2
|
||||
assert (
|
||||
basket.items.get(product=products[0]).unit_price_cents
|
||||
|
@ -173,6 +214,14 @@ 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)
|
||||
|
@ -224,34 +273,30 @@ 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"):
|
||||
basket_no_payment_method = BasketWithItemsFactory(payment_method=None)
|
||||
basket_no_payment_method = Basket.objects.priced().get(
|
||||
pk=basket_no_payment_method.pk,
|
||||
another_basket = BasketWithItemsFactory()
|
||||
another_basket = Basket.objects.priced().get(
|
||||
pk=another_basket.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"n°{basket_no_payment_method.pk} " in text
|
||||
expected_articles_count = basket_no_payment_method.items.count()
|
||||
assert f"n°{another_basket.pk} " in text
|
||||
expected_articles_count = another_basket.items.count()
|
||||
assert f" {expected_articles_count} article" in text
|
||||
expected_price = basket_no_payment_method.price / 100
|
||||
expected_price = another_basket.price / 100
|
||||
assert f" {expected_price:.2f}€" in text
|
||||
expected_payment_method = "-"
|
||||
expected_payment_method = another_basket.payment_method.name
|
||||
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"n°{basket_with_payment_method.pk} " in text
|
||||
expected_articles_count = basket_with_payment_method.items.count()
|
||||
|
@ -270,7 +315,7 @@ def test_baskets_list(live_server: LiveServer, selenium: WebDriver):
|
|||
|
||||
# Assert object deleted in DB
|
||||
assert Basket.objects.count() == 1
|
||||
assert Basket.objects.first() == basket_no_payment_method
|
||||
assert Basket.objects.first() == another_basket
|
||||
|
||||
# Assert redirected to list view
|
||||
wait.until(
|
||||
|
@ -285,7 +330,7 @@ def test_baskets_list(live_server: LiveServer, selenium: WebDriver):
|
|||
redirect_url = live_reverse(
|
||||
live_server,
|
||||
"purchase:update",
|
||||
pk=basket_no_payment_method.pk,
|
||||
pk=another_basket.pk,
|
||||
)
|
||||
wait.until(lambda driver: driver.current_url == redirect_url)
|
||||
|
||||
|
|
|
@ -1,14 +1,27 @@
|
|||
from django.urls import path
|
||||
|
||||
from purchase.views import delete_basket, list_baskets, new_basket, update_basket
|
||||
from purchase.views import (
|
||||
additional_unpriced_product,
|
||||
delete_basket,
|
||||
list_baskets,
|
||||
new_basket,
|
||||
price_preview,
|
||||
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"),
|
||||
|
|
|
@ -1,3 +1,17 @@
|
|||
from .basket import delete_basket, list_baskets, new_basket, update_basket
|
||||
from .basket import (
|
||||
additional_unpriced_product,
|
||||
delete_basket,
|
||||
list_baskets,
|
||||
new_basket,
|
||||
price_preview,
|
||||
update_basket,
|
||||
)
|
||||
|
||||
__all__ = ["new_basket", "update_basket", "delete_basket", "list_baskets"]
|
||||
__all__ = [
|
||||
"new_basket",
|
||||
"update_basket",
|
||||
"delete_basket",
|
||||
"list_baskets",
|
||||
"additional_unpriced_product",
|
||||
"price_preview",
|
||||
]
|
||||
|
|
|
@ -1,25 +1,33 @@
|
|||
import logging
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
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 BasketForm
|
||||
from purchase.models import Basket, reports_etag, reports_last_modified
|
||||
from purchase.forms import PRICED_PREFIX, UNPRICED_PREFIX, BasketForm
|
||||
from purchase.models import Basket, Product, reports_etag, reports_last_modified
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
@permission_required("purchase.add_basket")
|
||||
def new_basket(request: HttpRequest) -> HttpResponse:
|
||||
def new_basket(request: WSGIRequest) -> HttpResponse:
|
||||
if request.method == "POST":
|
||||
form = BasketForm(request.POST)
|
||||
if form.is_valid():
|
||||
instance = form.save()
|
||||
basket = form.save()
|
||||
update_with_unpriced_products(basket, request.POST)
|
||||
if request.user.has_perm("purchase.change_basket"):
|
||||
url = instance.get_absolute_url()
|
||||
url = basket.get_absolute_url()
|
||||
else:
|
||||
url = reverse("purchase:new")
|
||||
messages.success(request, _("Successfully created basket."))
|
||||
|
@ -27,39 +35,79 @@ def new_basket(request: HttpRequest) -> HttpResponse:
|
|||
else:
|
||||
form = BasketForm()
|
||||
|
||||
return TemplateResponse(request, "purchase/basket_form.html", {"form": form})
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
@permission_required("purchase.change_basket")
|
||||
def update_basket(request: HttpRequest, pk: int) -> HttpResponse:
|
||||
def update_basket(request: WSGIRequest, 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)
|
||||
|
||||
return TemplateResponse(
|
||||
response = render(
|
||||
request,
|
||||
"purchase/basket_form.html",
|
||||
{"form": form, "basket": basket},
|
||||
{
|
||||
"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,
|
||||
)
|
||||
|
||||
|
||||
@permission_required("purchase.view_basket")
|
||||
@condition(etag_func=reports_etag, last_modified_func=reports_last_modified)
|
||||
def list_baskets(request: HttpRequest) -> HttpResponse:
|
||||
def list_baskets(request: WSGIRequest) -> 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: HttpRequest, pk: int) -> HttpResponse:
|
||||
def delete_basket(request: WSGIRequest, pk: int) -> HttpResponse:
|
||||
basket = get_object_or_404(Basket, pk=pk)
|
||||
if request.method == "GET":
|
||||
context = {"basket": basket}
|
||||
|
@ -67,3 +115,21 @@ def delete_basket(request: HttpRequest, 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}',
|
||||
)
|
||||
|
|
|
@ -79,7 +79,6 @@ 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)
|
||||
|
||||
|
|