7 Commits

Author SHA1 Message Date
thomas girod
71ed7cdf7d Merge pull request #1289 from ae-utbm/product_history
Product history
2026-02-17 22:05:59 +01:00
imperosol
43768171a1 show creation date on Product update page 2026-02-17 22:05:34 +01:00
imperosol
0eccb4a5b5 Add created_at and updated_at to Product model 2026-02-17 22:05:19 +01:00
thomas girod
e7584c8c83 Merge pull request #1299 from ae-utbm/populate-more
add ban generation to populate_more
2026-02-17 22:04:18 +01:00
thomas girod
ac06de4f55 Merge pull request #1300 from ae-utbm/csv-typo
fix: typo
2026-02-17 12:30:35 +01:00
imperosol
e2fca3e6d2 fix: typo 2026-02-14 15:22:18 +01:00
imperosol
2138783bde add ban generation to populate_more 2026-02-14 15:14:45 +01:00
9 changed files with 128 additions and 22 deletions

View File

@@ -35,7 +35,7 @@ TODO : rewrite the pagination used in this template an Alpine one
{% csrf_token %} {% csrf_token %}
{{ form }} {{ form }}
<p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p> <p><input type="submit" value="{% trans %}Show{% endtrans %}" /></p>
<p><input type="submit" value="{% trans %}Download as cvs{% endtrans %}" formaction="{{ url('club:sellings_csv', club_id=object.id) }}"/></p> <p><input type="submit" value="{% trans %}Download as CSV{% endtrans %}" formaction="{{ url('club:sellings_csv', club_id=object.id) }}"/></p>
</form> </form>
<p> <p>
{% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/> {% trans %}Quantity: {% endtrans %}{{ total_quantity }} {% trans %}units{% endtrans %}<br/>

View File

@@ -12,7 +12,7 @@ from django.utils.timezone import localdate, make_aware, now
from faker import Faker from faker import Faker
from club.models import Club, Membership from club.models import Club, Membership
from core.models import Group, User from core.models import Group, User, UserBan
from counter.models import ( from counter.models import (
Counter, Counter,
Customer, Customer,
@@ -40,6 +40,7 @@ class Command(BaseCommand):
self.stdout.write("Creating users...") self.stdout.write("Creating users...")
users = self.create_users() users = self.create_users()
self.create_bans(random.sample(users, k=len(users) // 200)) # 0.5% of users
subscribers = random.sample(users, k=int(0.8 * len(users))) subscribers = random.sample(users, k=int(0.8 * len(users)))
self.stdout.write("Creating subscriptions...") self.stdout.write("Creating subscriptions...")
self.create_subscriptions(subscribers) self.create_subscriptions(subscribers)
@@ -88,6 +89,8 @@ class Command(BaseCommand):
self.stdout.write("Done") self.stdout.write("Done")
def create_users(self) -> list[User]: def create_users(self) -> list[User]:
# Create a single password hash for all users to make it faster.
# It's insecure as hell, but it's ok since it's only for dev purposes.
password = make_password("plop") password = make_password("plop")
users = [ users = [
User( User(
@@ -114,14 +117,33 @@ class Command(BaseCommand):
public_group.users.add(*users) public_group.users.add(*users)
return users return users
def create_bans(self, users: list[User]):
ban_groups = [
settings.SITH_GROUP_BANNED_COUNTER_ID,
settings.SITH_GROUP_BANNED_SUBSCRIPTION_ID,
settings.SITH_GROUP_BANNED_ALCOHOL_ID,
]
UserBan.objects.bulk_create(
[
UserBan(
user=user,
ban_group_id=i,
reason=self.faker.sentence(),
expires_at=make_aware(self.faker.future_datetime("+1y")),
)
for user in users
for i in random.sample(ban_groups, k=random.randint(1, len(ban_groups)))
]
)
def create_subscriptions(self, users: list[User]): def create_subscriptions(self, users: list[User]):
def prepare_subscription(_user: User, start_date: date) -> Subscription: def prepare_subscription(_user: User, start_date: date) -> Subscription:
payment_method = random.choice(settings.SITH_SUBSCRIPTION_PAYMENT_METHOD)[0] payment_method = random.choice(settings.SITH_SUBSCRIPTION_PAYMENT_METHOD)[0]
duration = random.randint(1, 4) duration = random.randint(1, 4)
sub = Subscription(member=_user, payment_method=payment_method) s = Subscription(member=_user, payment_method=payment_method)
sub.subscription_start = sub.compute_start(d=start_date, duration=duration) s.subscription_start = s.compute_start(d=start_date, duration=duration)
sub.subscription_end = sub.compute_end(duration) s.subscription_end = s.compute_end(duration)
return sub return s
subscriptions = [] subscriptions = []
customers = [] customers = []

View File

@@ -39,8 +39,9 @@ class ProductAdmin(SearchModelAdmin):
"code", "code",
"product_type", "product_type",
"selling_price", "selling_price",
"profit",
"archived", "archived",
"created_at",
"updated_at",
) )
list_select_related = ("product_type",) list_select_related = ("product_type",)
search_fields = ("name", "code") search_fields = ("name", "code")

View File

@@ -0,0 +1,67 @@
# Generated by Django 5.2.8 on 2026-02-10 15:40
from operator import attrgetter
import django.utils.timezone
from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import OuterRef, Subquery
from counter.models import Selling
def apply_product_history_dates(apps: StateApps, schema_editor):
"""Approximate a posteriori the value of created_at and updated_at."""
Product = apps.get_model("counter", "Product")
sales_subquery = Selling.objects.filter(product=OuterRef("pk")).values("date")
# for products that have an associated sale, we set the creation date
# to the one of the first sale, and the update date to the one of the last sale
products = list(
Product.objects.exclude(sellings=None)
.annotate(
new_created_at=Subquery(sales_subquery.order_by("date")[:1]),
new_updated_at=Subquery(sales_subquery.order_by("-date")[:1]),
)
.only("id")
)
for product in products:
product.created_at = product.new_created_at
product.updated_at = product.new_updated_at
# For the remaining products (those without sale),
# they are given the creation and update date of the previous product having sales.
products_without_sale = list(Product.objects.filter(sellings=None).only("id"))
for product in products_without_sale:
previous_product = max(
(p for p in products if p.id < product.id), key=attrgetter("id")
)
product.created_at = previous_product.created_at
product.updated_at = previous_product.updated_at
products.extend(products_without_sale)
Product.objects.bulk_update(products, fields=["created_at", "updated_at"])
class Migration(migrations.Migration):
dependencies = [("counter", "0035_remove_selling_is_validated_and_more")]
operations = [
migrations.AddField(
model_name="product",
name="created_at",
field=models.DateTimeField(
auto_now_add=True,
default=django.utils.timezone.now,
verbose_name="created at",
),
preserve_default=False,
),
migrations.AddField(
model_name="product",
name="updated_at",
field=models.DateTimeField(auto_now=True, verbose_name="updated at"),
),
migrations.RunPython(
apply_product_history_dates, reverse_code=migrations.RunPython.noop
),
]

View File

@@ -399,6 +399,8 @@ class Product(models.Model):
Group, related_name="products", verbose_name=_("buying groups"), blank=True Group, related_name="products", verbose_name=_("buying groups"), blank=True
) )
archived = models.BooleanField(_("archived"), default=False) archived = models.BooleanField(_("archived"), default=False)
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
updated_at = models.DateTimeField(_("updated at"), auto_now=True)
class Meta: class Meta:
verbose_name = _("product") verbose_name = _("product")

View File

@@ -3,6 +3,8 @@
{% block content %} {% block content %}
{% if object %} {% if object %}
<h2>{% trans name=object %}Edit product {{ name }}{% endtrans %}</h2> <h2>{% trans name=object %}Edit product {{ name }}{% endtrans %}</h2>
<p><i>{% trans %}Creation date{% endtrans %} : {{ object.created_at|date }}</i></p>
<p><i>{% trans %}Last update{% endtrans %} : {{ object.updated_at|date }}</i></p>
{% else %} {% else %}
<h2>{% trans %}Product creation{% endtrans %}</h2> <h2>{% trans %}Product creation{% endtrans %}</h2>
{% endif %} {% endif %}

View File

@@ -89,7 +89,7 @@
:disabled="csvLoading" :disabled="csvLoading"
:aria-busy="csvLoading" :aria-busy="csvLoading"
> >
{% trans %}Download as cvs{% endtrans %} <i class="fa fa-file-arrow-down"></i> {% trans %}Download as CSV{% endtrans %} <i class="fa fa-file-arrow-down"></i>
</button> </button>
</div> </div>

View File

@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-08 16:14+0100\n" "POT-Creation-Date: 2026-02-14 15:21+0100\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -388,7 +388,7 @@ msgstr "Montrer"
#: club/templates/club/club_sellings.jinja #: club/templates/club/club_sellings.jinja
#: counter/templates/counter/product_list.jinja #: counter/templates/counter/product_list.jinja
msgid "Download as cvs" msgid "Download as CSV"
msgstr "Télécharger en CSV" msgstr "Télécharger en CSV"
#: club/templates/club/club_sellings.jinja #: club/templates/club/club_sellings.jinja
@@ -1566,7 +1566,7 @@ msgstr "Visiteur"
msgid "ban type" msgid "ban type"
msgstr "type de ban" msgstr "type de ban"
#: core/models.py #: core/models.py counter/models.py
msgid "created at" msgid "created at"
msgstr "créé le" msgstr "créé le"
@@ -3109,6 +3109,10 @@ msgstr "groupe d'achat"
msgid "archived" msgid "archived"
msgstr "archivé" msgstr "archivé"
#: counter/models.py
msgid "updated at"
msgstr "mis à jour le"
#: counter/models.py #: counter/models.py
msgid "product" msgid "product"
msgstr "produit" msgstr "produit"
@@ -3664,6 +3668,14 @@ msgstr ""
msgid "Edit product %(name)s" msgid "Edit product %(name)s"
msgstr "Édition du produit %(name)s" msgstr "Édition du produit %(name)s"
#: counter/templates/counter/product_form.jinja
msgid "Creation date"
msgstr "Date de création"
#: counter/templates/counter/product_form.jinja
msgid "Last update"
msgstr "Dernière mise à jour"
#: counter/templates/counter/product_form.jinja #: counter/templates/counter/product_form.jinja
msgid "Product creation" msgid "Product creation"
msgstr "Création de produit" msgstr "Création de produit"
@@ -3951,8 +3963,8 @@ msgid ""
"inconvenience." "inconvenience."
msgstr "" msgstr ""
"Les paiements par carte bancaire sont actuellement désactivés sur l'eboutic. " "Les paiements par carte bancaire sont actuellement désactivés sur l'eboutic. "
"Vous pouvez cependant toujours recharger votre compte dans un des lieux de vie de l'AE. " "Vous pouvez cependant toujours recharger votre compte dans un des lieux de "
"Veuillez nous excuser pour le désagrément." "vie de l'AE. Veuillez nous excuser pour le désagrément."
#: eboutic/templates/eboutic/eboutic_checkout.jinja #: eboutic/templates/eboutic/eboutic_checkout.jinja
msgid "" msgid ""
@@ -4121,8 +4133,8 @@ msgstr "Les candidatures sont fermées pour cette élection"
#: election/templates/election/election_detail.jinja #: election/templates/election/election_detail.jinja
msgid "Candidate pictures won't display for privacy reasons." msgid "Candidate pictures won't display for privacy reasons."
msgstr "" msgstr ""
"La photo du candidat ne s'affiche pas pour " "La photo du candidat ne s'affiche pas pour des raisons de respect de la vie "
"des raisons de respect de la vie privée." "privée."
#: election/templates/election/election_detail.jinja #: election/templates/election/election_detail.jinja
msgid "Polls close " msgid "Polls close "

14
uv.lock generated
View File

@@ -775,11 +775,11 @@ wheels = [
[[package]] [[package]]
name = "filelock" name = "filelock"
version = "3.21.2" version = "3.20.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/73/71/74364ff065ca78914d8bd90b312fe78ddc5e11372d38bc9cb7104f887ce1/filelock-3.21.2.tar.gz", hash = "sha256:cfd218cfccf8b947fce7837da312ec3359d10ef2a47c8602edd59e0bacffb708", size = 31486, upload-time = "2026-02-13T01:27:15.223Z" } sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/98/73/3a18f1e1276810e81477c431009b55eeccebbd7301d28a350b77aacf3c33/filelock-3.21.2-py3-none-any.whl", hash = "sha256:d6cd4dbef3e1bb63bc16500fc5aa100f16e405bbff3fb4231711851be50c1560", size = 21479, upload-time = "2026-02-13T01:27:13.611Z" }, { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
] ]
[[package]] [[package]]
@@ -2380,16 +2380,16 @@ wheels = [
[[package]] [[package]]
name = "virtualenv" name = "virtualenv"
version = "20.36.1" version = "20.35.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "distlib" }, { name = "distlib" },
{ name = "filelock" }, { name = "filelock" },
{ name = "platformdirs" }, { name = "platformdirs" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" },
] ]
[[package]] [[package]]
@@ -2446,4 +2446,4 @@ dependencies = [
{ name = "django-haystack" }, { name = "django-haystack" },
{ name = "filelock" }, { name = "filelock" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/a3/db/c6219763f6c4519cdaae812e60fd9471a7805de2b39d912931e45575c8e6/xapian-haystack-3.1.0.tar.gz", hash = "sha256:9f9ab90bf450bf6699d164594d569243aafb6c9f0990a16855f55a1d16bc09c6", size = 37887, upload-time = "2023-03-19T11:58:37.035Z" } sdist = { url = "https://files.pythonhosted.org/packages/a3/db/c6219763f6c4519cdaae812e60fd9471a7805de2b39d912931e45575c8e6/xapian-haystack-3.1.0.tar.gz", hash = "sha256:9f9ab90bf450bf6699d164594d569243aafb6c9f0990a16855f55a1d16bc09c6", size = 37887, upload-time = "2023-03-19T11:58:37.035Z" }