Add reports by day

This commit is contained in:
Gabriel Augendre 2022-04-26 18:19:53 +02:00
parent bc80ca060b
commit 51ff1afdb8
8 changed files with 228 additions and 165 deletions

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-25 23:04+0200\n"
"POT-Creation-Date: 2022-04-26 18:17+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,44 +18,44 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/common/templates/403.html:4
#: common/templates/403.html:4
msgid "Permission denied"
msgstr ""
#: src/common/templates/403.html:5
#: common/templates/403.html:5
msgid "You're not allowed to access this page."
msgstr ""
#: src/common/templates/404.html:4
#: common/templates/404.html:4
msgid "Page not found"
msgstr ""
#: src/common/templates/404.html:5
#: common/templates/404.html:5
msgid "We tried and tried but couldn't find the page you're looking for."
msgstr ""
#: src/common/templates/500.html:11
#: common/templates/500.html:11
msgid "Server error (500)"
msgstr ""
#: src/common/templates/500.html:12
#: common/templates/500.html:12
msgid ""
"There's an error on our end. Don't worry though, our engineers are already "
"working to fix it!"
msgstr ""
#: src/common/templates/common/navbar.html:12
#: common/templates/common/navbar.html:12
msgid "New basket"
msgstr ""
#: src/common/templates/common/navbar.html:17
#: common/templates/common/navbar.html:17
msgid "Baskets"
msgstr ""
#: src/common/templates/common/navbar.html:20
#: common/templates/common/navbar.html:20
msgid "Reports"
msgstr ""
#: src/common/templates/common/navbar.html:25
#: common/templates/common/navbar.html:25
msgid "Admin"
msgstr ""

View file

@ -5,7 +5,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-25 23:04+0200\n"
"POT-Creation-Date: 2022-04-26 18:17+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,29 +15,29 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: src/common/templates/403.html:4
#: common/templates/403.html:4
msgid "Permission denied"
msgstr "Permission refusée"
#: src/common/templates/403.html:5
#: common/templates/403.html:5
msgid "You're not allowed to access this page."
msgstr "Vous n'avez pas le droit d'accéder à cette page"
#: src/common/templates/404.html:4
#: common/templates/404.html:4
msgid "Page not found"
msgstr "Page non trouvée"
#: src/common/templates/404.html:5
#: common/templates/404.html:5
msgid "We tried and tried but couldn't find the page you're looking for."
msgstr ""
"Nous avons cherché partout mais nous n'avons pas trouvé la page que vous "
"cherchiez."
#: src/common/templates/500.html:11
#: common/templates/500.html:11
msgid "Server error (500)"
msgstr "Erreur serveur (500)"
#: src/common/templates/500.html:12
#: common/templates/500.html:12
msgid ""
"There's an error on our end. Don't worry though, our engineers are already "
"working to fix it!"
@ -45,18 +45,18 @@ msgstr ""
"Il y a une erreur de notre côté. Ne vous inquiétez pas, nos ingénieurs "
"travaillent déjà à sa résolution !"
#: src/common/templates/common/navbar.html:12
#: common/templates/common/navbar.html:12
msgid "New basket"
msgstr "Nouveau panier"
#: src/common/templates/common/navbar.html:17
#: common/templates/common/navbar.html:17
msgid "Baskets"
msgstr "Paniers"
#: src/common/templates/common/navbar.html:20
#: common/templates/common/navbar.html:20
msgid "Reports"
msgstr "Rapports"
#: src/common/templates/common/navbar.html:25
#: common/templates/common/navbar.html:25
msgid "Admin"
msgstr "Admin"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-25 23:04+0200\n"
"POT-Creation-Date: 2022-04-26 18:17+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,193 +18,202 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/purchase/admin.py:18
#: purchase/admin.py:18
msgid "unit price"
msgstr ""
#: src/purchase/admin.py:22
#: purchase/admin.py:22
msgid "sold"
msgstr ""
#: src/purchase/admin.py:26 src/purchase/admin.py:39
#: purchase/admin.py:26 purchase/admin.py:39
msgid "turnover"
msgstr ""
#: src/purchase/admin.py:53 src/purchase/admin.py:70
#: purchase/admin.py:53 purchase/admin.py:70
msgid "price"
msgstr ""
#: src/purchase/forms.py:23
#: purchase/forms.py:23
msgid "Save"
msgstr ""
#: src/purchase/models.py:10
#: purchase/models.py:13
msgid "created at"
msgstr ""
#: src/purchase/models.py:11
#: purchase/models.py:14
msgid "updated at"
msgstr ""
#: src/purchase/models.py:31 src/purchase/models.py:58
#: purchase/models.py:37 purchase/models.py:66
msgid "name"
msgstr ""
#: src/purchase/models.py:36 src/purchase/models.py:128
#: purchase/models.py:42 purchase/models.py:149
msgid "payment method"
msgstr ""
#: src/purchase/models.py:37
#: purchase/models.py:43
msgid "payment methods"
msgstr ""
#: src/purchase/models.py:59
#: purchase/models.py:67
msgid "image"
msgstr ""
#: src/purchase/models.py:61
#: purchase/models.py:69
msgid "unit price (cents)"
msgstr ""
#: src/purchase/models.py:61
#: purchase/models.py:69
msgid "unit price in cents"
msgstr ""
#: src/purchase/models.py:64
#: purchase/models.py:72
msgid "display order"
msgstr ""
#: src/purchase/models.py:71 src/purchase/models.py:154
#: purchase/models.py:79 purchase/models.py:177
msgid "product"
msgstr ""
#: src/purchase/models.py:72
#: purchase/models.py:80
msgid "products"
msgstr ""
#: src/purchase/models.py:134 src/purchase/models.py:160
#: purchase/models.py:155 purchase/models.py:183
msgid "basket"
msgstr ""
#: src/purchase/models.py:135
#: purchase/models.py:156
msgid "baskets"
msgstr ""
#: src/purchase/models.py:138
#: purchase/models.py:159
#, python-format
msgid "Basket #%(id)s"
msgstr ""
#: src/purchase/models.py:162
#: purchase/models.py:185
msgid "quantity"
msgstr ""
#: src/purchase/models.py:167
#: purchase/models.py:190
msgid "basket item"
msgstr ""
#: src/purchase/models.py:168
#: purchase/models.py:191
msgid "basket items"
msgstr ""
#: src/purchase/templates/purchase/basket_confirm_delete.html:7
#: purchase/templates/purchase/basket_confirm_delete.html:7
#, python-format
msgid "Are you sure you want to delete \"%(basket)s\"?"
msgstr ""
#: src/purchase/templates/purchase/basket_form.html:11
#: purchase/templates/purchase/basket_form.html:11
msgid "Missing payment method."
msgstr ""
#: src/purchase/templates/purchase/basket_form.html:14
#: purchase/templates/purchase/basket_form.html:14
msgid "New basket"
msgstr ""
#: src/purchase/templates/purchase/basket_form.html:18
#: purchase/templates/purchase/basket_form.html:18
msgid "New"
msgstr ""
#: src/purchase/templates/purchase/basket_list.html:5
#: purchase/templates/purchase/basket_list.html:5
msgid "Baskets"
msgstr ""
#: src/purchase/templates/purchase/basket_list.html:11
#: purchase/templates/purchase/basket_list.html:11
#, python-format
msgid "Basket #%(basket_id)s"
msgstr ""
#: src/purchase/templates/purchase/basket_list.html:13
#: purchase/templates/purchase/basket_list.html:13
#, python-format
msgid "1 item"
msgid_plural "%(counter)s items"
msgstr[0] ""
msgstr[1] ""
#: src/purchase/templates/purchase/reports.html:6
#: purchase/templates/purchase/reports.html:6
msgid "Reports"
msgstr ""
#: src/purchase/templates/purchase/reports.html:7
#: purchase/templates/purchase/reports.html:7
msgid "General"
msgstr ""
#: src/purchase/templates/purchase/reports.html:9
#: purchase/templates/purchase/reports.html:9
msgid "Total turnover:"
msgstr ""
#: src/purchase/templates/purchase/reports.html:10
#: purchase/templates/purchase/reports.html:10
msgid "Average basket:"
msgstr ""
#: src/purchase/templates/purchase/reports.html:13
msgid "Products"
#: purchase/templates/purchase/reports.html:13
msgid "By day"
msgstr ""
#: src/purchase/templates/purchase/reports.html:16
msgid "Turnover by payment method"
msgstr ""
#: src/purchase/templates/purchase/reports.html:19
msgid "Baskets without payment method"
msgstr ""
#: src/purchase/templates/purchase/snippets/report_no_payment_method.html:6
msgid "Basket ID"
msgstr ""
#: src/purchase/templates/purchase/snippets/report_no_payment_method.html:7
msgid "Price"
msgstr ""
#: src/purchase/templates/purchase/snippets/report_payment_methods.html:6
msgid "Payment method"
msgstr ""
#: src/purchase/templates/purchase/snippets/report_payment_methods.html:7
msgid "# baskets"
msgstr ""
#: src/purchase/templates/purchase/snippets/report_payment_methods.html:8
#: src/purchase/templates/purchase/snippets/report_products.html:8
#: purchase/templates/purchase/reports.html:14
#: purchase/templates/purchase/snippets/report_payment_methods.html:8
#: purchase/templates/purchase/snippets/report_products.html:8
msgid "Turnover"
msgstr ""
#: src/purchase/templates/purchase/snippets/report_products.html:6
#: purchase/templates/purchase/reports.html:20
msgid "Average basket"
msgstr ""
#: purchase/templates/purchase/reports.html:27
msgid "Products"
msgstr ""
#: purchase/templates/purchase/reports.html:30
msgid "Turnover by payment method"
msgstr ""
#: purchase/templates/purchase/reports.html:33
msgid "Baskets without payment method"
msgstr ""
#: purchase/templates/purchase/snippets/report_no_payment_method.html:6
msgid "Basket ID"
msgstr ""
#: purchase/templates/purchase/snippets/report_no_payment_method.html:7
msgid "Price"
msgstr ""
#: purchase/templates/purchase/snippets/report_payment_methods.html:6
msgid "Payment method"
msgstr ""
#: purchase/templates/purchase/snippets/report_payment_methods.html:7
msgid "# baskets"
msgstr ""
#: purchase/templates/purchase/snippets/report_products.html:6
msgid "Product"
msgstr ""
#: src/purchase/templates/purchase/snippets/report_products.html:7
#: purchase/templates/purchase/snippets/report_products.html:7
msgid "# sold"
msgstr ""
#: src/purchase/views/basket.py:15
#: purchase/views/basket.py:15
msgid "Successfully created basket."
msgstr ""
#: src/purchase/views/basket.py:30
#: purchase/views/basket.py:30
msgid "Successfully updated basket."
msgstr ""
#: src/purchase/views/basket.py:45
#: purchase/views/basket.py:45
msgid "Basket successfully deleted."
msgstr ""

View file

@ -5,7 +5,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-25 23:04+0200\n"
"POT-Creation-Date: 2022-04-26 18:17+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,194 +15,203 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: src/purchase/admin.py:18
#: purchase/admin.py:18
msgid "unit price"
msgstr "prix unitaire"
#: src/purchase/admin.py:22
#: purchase/admin.py:22
msgid "sold"
msgstr "vendu"
#: src/purchase/admin.py:26 src/purchase/admin.py:39
#: purchase/admin.py:26 purchase/admin.py:39
msgid "turnover"
msgstr "chiffre d'affaires"
#: src/purchase/admin.py:53 src/purchase/admin.py:70
#: purchase/admin.py:53 purchase/admin.py:70
msgid "price"
msgstr "prix"
#: src/purchase/forms.py:23
#: purchase/forms.py:23
msgid "Save"
msgstr "Enregistrer"
#: src/purchase/models.py:10
#: purchase/models.py:13
msgid "created at"
msgstr "créé à"
#: src/purchase/models.py:11
#: purchase/models.py:14
msgid "updated at"
msgstr "mis à jour à"
#: src/purchase/models.py:31 src/purchase/models.py:58
#: purchase/models.py:37 purchase/models.py:66
msgid "name"
msgstr "nom"
#: src/purchase/models.py:36 src/purchase/models.py:128
#: purchase/models.py:42 purchase/models.py:149
msgid "payment method"
msgstr "moyen de paiement"
#: src/purchase/models.py:37
#: purchase/models.py:43
msgid "payment methods"
msgstr "moyens de paiement"
#: src/purchase/models.py:59
#: purchase/models.py:67
msgid "image"
msgstr "image"
#: src/purchase/models.py:61
#: purchase/models.py:69
msgid "unit price (cents)"
msgstr "prix unitaire (centimes)"
#: src/purchase/models.py:61
#: purchase/models.py:69
msgid "unit price in cents"
msgstr "prix unitaire en centimes"
#: src/purchase/models.py:64
#: purchase/models.py:72
msgid "display order"
msgstr "ordre d'affichage"
#: src/purchase/models.py:71 src/purchase/models.py:154
#: purchase/models.py:79 purchase/models.py:177
msgid "product"
msgstr "produit"
#: src/purchase/models.py:72
#: purchase/models.py:80
msgid "products"
msgstr "produits"
#: src/purchase/models.py:134 src/purchase/models.py:160
#: purchase/models.py:155 purchase/models.py:183
msgid "basket"
msgstr "panier"
#: src/purchase/models.py:135
#: purchase/models.py:156
msgid "baskets"
msgstr "paniers"
#: src/purchase/models.py:138
#: purchase/models.py:159
#, python-format
msgid "Basket #%(id)s"
msgstr "Panier n°%(id)s"
#: src/purchase/models.py:162
#: purchase/models.py:185
msgid "quantity"
msgstr "quantité"
#: src/purchase/models.py:167
#: purchase/models.py:190
msgid "basket item"
msgstr "article de panier"
#: src/purchase/models.py:168
#: purchase/models.py:191
msgid "basket items"
msgstr "articles de panier"
#: src/purchase/templates/purchase/basket_confirm_delete.html:7
#: purchase/templates/purchase/basket_confirm_delete.html:7
#, python-format
msgid "Are you sure you want to delete \"%(basket)s\"?"
msgstr "Êtes-vous sûr de vouloir supprimer \"%(basket)s\" ?"
#: src/purchase/templates/purchase/basket_form.html:11
#: purchase/templates/purchase/basket_form.html:11
msgid "Missing payment method."
msgstr "Moyen de paiement manquant."
#: src/purchase/templates/purchase/basket_form.html:14
#: purchase/templates/purchase/basket_form.html:14
msgid "New basket"
msgstr "Nouveau panier"
#: src/purchase/templates/purchase/basket_form.html:18
#: purchase/templates/purchase/basket_form.html:18
msgid "New"
msgstr "Nouveau"
#: src/purchase/templates/purchase/basket_list.html:5
#: purchase/templates/purchase/basket_list.html:5
msgid "Baskets"
msgstr "Paniers"
#: src/purchase/templates/purchase/basket_list.html:11
#: purchase/templates/purchase/basket_list.html:11
#, python-format
msgid "Basket #%(basket_id)s"
msgstr "Panier n°%(basket_id)s"
#: src/purchase/templates/purchase/basket_list.html:13
#: purchase/templates/purchase/basket_list.html:13
#, python-format
msgid "1 item"
msgid_plural "%(counter)s items"
msgstr[0] "1 article"
msgstr[1] "%(counter)s articles"
#: src/purchase/templates/purchase/reports.html:6
#: purchase/templates/purchase/reports.html:6
msgid "Reports"
msgstr "Rapports"
#: src/purchase/templates/purchase/reports.html:7
#: purchase/templates/purchase/reports.html:7
msgid "General"
msgstr "Général"
#: src/purchase/templates/purchase/reports.html:9
#: purchase/templates/purchase/reports.html:9
msgid "Total turnover:"
msgstr "Chiffre d'affaires total :"
#: src/purchase/templates/purchase/reports.html:10
#: purchase/templates/purchase/reports.html:10
msgid "Average basket:"
msgstr "Panier moyen :"
#: src/purchase/templates/purchase/reports.html:13
msgid "Products"
msgstr "Produits"
#: purchase/templates/purchase/reports.html:13
msgid "By day"
msgstr "Par jour"
#: src/purchase/templates/purchase/reports.html:16
msgid "Turnover by payment method"
msgstr "Chiffre d'affaires par moyen de paiement"
#: src/purchase/templates/purchase/reports.html:19
msgid "Baskets without payment method"
msgstr "Paniers sans moyen de paiement"
#: src/purchase/templates/purchase/snippets/report_no_payment_method.html:6
msgid "Basket ID"
msgstr "Id de panier"
#: src/purchase/templates/purchase/snippets/report_no_payment_method.html:7
msgid "Price"
msgstr "Prix"
#: src/purchase/templates/purchase/snippets/report_payment_methods.html:6
msgid "Payment method"
msgstr "Moyen de paiement"
#: src/purchase/templates/purchase/snippets/report_payment_methods.html:7
msgid "# baskets"
msgstr "Nb. de paniers"
#: src/purchase/templates/purchase/snippets/report_payment_methods.html:8
#: src/purchase/templates/purchase/snippets/report_products.html:8
#: purchase/templates/purchase/reports.html:14
#: purchase/templates/purchase/snippets/report_payment_methods.html:8
#: purchase/templates/purchase/snippets/report_products.html:8
msgid "Turnover"
msgstr "Chiffre d'affaires"
#: src/purchase/templates/purchase/snippets/report_products.html:6
#: purchase/templates/purchase/reports.html:20
msgid "Average basket"
msgstr "Panier moyen"
#: purchase/templates/purchase/reports.html:27
msgid "Products"
msgstr "Produits"
#: purchase/templates/purchase/reports.html:30
msgid "Turnover by payment method"
msgstr "Chiffre d'affaires par moyen de paiement"
#: purchase/templates/purchase/reports.html:33
msgid "Baskets without payment method"
msgstr "Paniers sans moyen de paiement"
#: purchase/templates/purchase/snippets/report_no_payment_method.html:6
msgid "Basket ID"
msgstr "Id de panier"
#: purchase/templates/purchase/snippets/report_no_payment_method.html:7
msgid "Price"
msgstr "Prix"
#: purchase/templates/purchase/snippets/report_payment_methods.html:6
msgid "Payment method"
msgstr "Moyen de paiement"
#: purchase/templates/purchase/snippets/report_payment_methods.html:7
msgid "# baskets"
msgstr "Nb. de paniers"
#: purchase/templates/purchase/snippets/report_products.html:6
msgid "Product"
msgstr "Produit"
#: src/purchase/templates/purchase/snippets/report_products.html:7
#: purchase/templates/purchase/snippets/report_products.html:7
msgid "# sold"
msgstr "Nb. vendus"
#: src/purchase/views/basket.py:15
#: purchase/views/basket.py:15
msgid "Successfully created basket."
msgstr "Panier correctement créé."
#: src/purchase/views/basket.py:30
#: purchase/views/basket.py:30
msgid "Successfully updated basket."
msgstr "Panier correctement modifié."
#: src/purchase/views/basket.py:45
#: purchase/views/basket.py:45
msgid "Basket successfully deleted."
msgstr "Panier correctement supprimé."

View file

@ -1,5 +1,7 @@
from __future__ import annotations
from django.db import models
from django.db.models import Count, F, Sum
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
@ -117,14 +119,23 @@ class Product(Model):
class BasketQuerySet(models.QuerySet):
def priced(self):
def priced(self) -> BasketQuerySet:
return self.annotate(
price=Coalesce(
Sum(F("items__quantity") * F("items__product__unit_price_cents")), 0
)
)
def no_payment_method(self):
def average_basket(self) -> float:
return self.priced().aggregate(avg=Avg("price"))["avg"]
def by_date(self, date) -> BasketQuerySet:
return self.filter(created_at__date=date)
def turnover(self) -> int:
return self.priced().aggregate(total=Sum("price"))["total"]
def no_payment_method(self) -> BasketQuerySet:
return self.filter(payment_method=None)

View file

@ -6,10 +6,24 @@
<h1>{% translate "Reports" %}</h1>
<h2>{% translate "General" %}</h2>
<ul>
<li>{% translate "Total turnover:" %} {{ total|currency }}</li>
<li>{% translate "Total turnover:" %} {{ turnover|currency }}</li>
<li>{% translate "Average basket:" %} {{ average_basket|currency }}</li>
</ul>
<h3>{% translate "By day" %}</h3>
<h4>{% translate "Turnover" %}</h4>
<ul>
{% for date, turnover in turnover_by_day.items %}
<li>{{ date }} : {{ turnover|currency }}</li>
{% endfor %}
</ul>
<h4>{% translate "Average basket" %}</h4>
<ul>
{% for date, average in average_basket_by_day.items %}
<li>{{ date }} : {{ average|currency }}</li>
{% endfor %}
</ul>
<h2>{% translate "Products" %}</h2>
{% include "purchase/snippets/report_products.html" %}

View file

@ -11,12 +11,20 @@ class ReportsView(ProtectedViewsMixin, TemplateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
dates = Basket.objects.values_list("created_at__date", flat=True).distinct()
average_basket_by_day = {
date: Basket.objects.by_date(date).average_basket() for date in dates
}
turnover_by_day = {
date: Basket.objects.by_date(date).turnover() for date in dates
}
context.update(
{
"total": Basket.objects.priced().aggregate(total=Sum("price"))["total"],
"average_basket": Basket.objects.priced().aggregate(avg=Avg("price"))[
"avg"
],
"turnover": Basket.objects.turnover(),
"turnover_by_day": turnover_by_day,
"average_basket": Basket.objects.average_basket(),
"average_basket_by_day": average_basket_by_day,
"products": Product.objects.with_turnover().with_sold(),
"payment_methods": PaymentMethod.objects.with_turnover().with_sold(),
"no_payment_method": Basket.objects.no_payment_method().priced(),

View file

@ -20,6 +20,18 @@ COMPOSE_BUILD_FILE = BASE_DIR / "docker-compose-build.yaml"
COMPOSE_BUILD_ENV = {"COMPOSE_FILE": COMPOSE_BUILD_FILE}
@task
def makemessages(ctx: Context) -> None:
with ctx.cd(SRC_DIR):
ctx.run("./manage.py makemessages -l en -l fr", pty=True, echo=True)
@task
def compilemessages(ctx: Context) -> None:
with ctx.cd(SRC_DIR):
ctx.run("./manage.py compilemessages -l en -l fr", pty=True, echo=True)
@task
def test(ctx: Context) -> None:
with ctx.cd(SRC_DIR):