1 Commits

Author SHA1 Message Date
imperosol
5274f1f0f0 feat: automatic product archiving 2025-09-14 01:36:46 +02:00
6 changed files with 164 additions and 22 deletions

View File

@@ -1,13 +1,19 @@
import math import math
from django import forms from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Q from django.db.models import Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import ClockedSchedule, PeriodicTask
from phonenumber_field.widgets import RegionalPhoneNumberWidget from phonenumber_field.widgets import RegionalPhoneNumberWidget
from club.widgets.ajax_select import AutoCompleteSelectClub from club.widgets.ajax_select import AutoCompleteSelectClub
from core.models import User from core.models import User
from core.views.forms import NFCTextInput, SelectDate, SelectDateTime from core.views.forms import (
NFCTextInput,
SelectDate,
SelectDateTime,
)
from core.views.widgets.ajax_select import ( from core.views.widgets.ajax_select import (
AutoCompleteSelect, AutoCompleteSelect,
AutoCompleteSelectMultipleGroup, AutoCompleteSelectMultipleGroup,
@@ -158,6 +164,66 @@ class CounterEditForm(forms.ModelForm):
} }
class ProductArchiveForm(forms.Form):
"""Form for automatic product archiving."""
enabled = forms.BooleanField(
label=_("Enabled"),
widget=forms.CheckboxInput(attrs={"class": "switch"}),
required=False,
)
archive_at = forms.DateTimeField(
label=_("Date and time of archiving"), widget=SelectDateTime, required=False
)
def __init__(self, *args, product: Product, **kwargs):
self.product = product
self.instance = PeriodicTask.objects.filter(
task="counter.tasks.archive_product", args=f"[{product.id}]"
).first()
super().__init__(*args, **kwargs)
if self.instance:
self.fields["enabled"].initial = self.instance.enabled
self.fields["archive_at"].initial = self.instance.clocked.clocked_time
def clean(self):
cleaned_data = super().clean()
if cleaned_data["enabled"] is True and cleaned_data["archive_at"] is None:
raise ValidationError(
_(
"Automatic archiving cannot be enabled "
"without providing a archiving date."
)
)
def save(self):
if not self.changed_data:
return
if not self.instance:
PeriodicTask.objects.create(
task="counter.tasks.archive_product",
args=f"[{self.product.id}]",
name=f"Archive product {self.product}",
clocked=ClockedSchedule.objects.create(
clocked_time=self.cleaned_data["archive_at"]
),
enabled=self.cleaned_data["enabled"],
one_off=True,
)
return
if (
"archive_at" in self.changed_data
and self.cleaned_data["archive_at"] is None
):
self.instance.delete()
elif "archive_at" in self.changed_data:
self.instance.clocked.clocked_time = self.cleaned_data["archive_at"]
self.instance.clocked.save()
self.instance.enabled = self.cleaned_data["enabled"]
self.instance.save()
return self.instance
class ProductEditForm(forms.ModelForm): class ProductEditForm(forms.ModelForm):
error_css_class = "error" error_css_class = "error"
required_css_class = "required" required_css_class = "required"
@@ -199,22 +265,19 @@ class ProductEditForm(forms.ModelForm):
queryset=Counter.objects.all(), queryset=Counter.objects.all(),
) )
def __init__(self, *args, **kwargs): def __init__(self, *args, instance=None, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, instance=instance, **kwargs)
if self.instance.id: if self.instance.id:
self.fields["counters"].initial = self.instance.counters.all() self.fields["counters"].initial = self.instance.counters.all()
self.archive_form = ProductArchiveForm(*args, product=self.instance, **kwargs)
def is_valid(self):
return super().is_valid() and self.archive_form.is_valid()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
ret = super().save(*args, **kwargs) ret = super().save(*args, **kwargs)
if self.fields["counters"].initial: self.instance.counters.set(self.cleaned_data["counters"])
# Remove the product from all counter it was added to self.archive_form.save()
# It will then only be added to selected counters
for counter in self.fields["counters"].initial:
counter.products.remove(self.instance)
counter.save()
for counter in self.cleaned_data["counters"]:
counter.products.add(self.instance)
counter.save()
return ret return ret

View File

@@ -445,7 +445,8 @@ class Product(models.Model):
buying_groups = list(self.buying_groups.all()) buying_groups = list(self.buying_groups.all())
if not buying_groups: if not buying_groups:
return True return True
return any(user.is_in_group(pk=group.id) for group in buying_groups) res = any(user.is_in_group(pk=group.id) for group in buying_groups)
return res
@property @property
def profit(self): def profit(self):

13
counter/tasks.py Normal file
View File

@@ -0,0 +1,13 @@
# Create your tasks here
from celery import shared_task
from counter.models import Product
@shared_task
def archive_product(product_id):
product = Product.objects.get(id=product_id)
product.archived = True
product.save()
product.counters.clear()

View File

@@ -0,0 +1,31 @@
{% extends "core/base.jinja" %}
{% block content %}
{% if object %}
<h2>{% trans name=object %}Edit product {{ name }}{% endtrans %}</h2>
{% else %}
<h2>{% trans %}Product creation{% endtrans %}</h2>
{% endif %}
<form method="post">
{% csrf_token %}
{{ form.as_p() }}
<br />
<h3>{% trans %}Automatic archiving{% endtrans %}</h3>
<p>
<em>
{%- trans trimmed -%}
Automatic archiving allows you to mark a product as archived
and remove it from all its counters at a specified time and date.
{%- endtrans -%}
</em>
</p>
<fieldset x-data="{enabled: {{ form.archive_form.enabled.initial|tojson }}}">
{{ form.archive_form.as_p() }}
</fieldset>
<p><input type="submit" value="{% trans %}Save{% endtrans %}" /></p>
</form>
{% endblock %}

View File

@@ -147,7 +147,7 @@ class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
model = Product model = Product
form_class = ProductEditForm form_class = ProductEditForm
template_name = "core/create.jinja" template_name = "counter/product_form.jinja"
current_tab = "products" current_tab = "products"
@@ -157,7 +157,7 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
model = Product model = Product
form_class = ProductEditForm form_class = ProductEditForm
pk_url_kwarg = "product_id" pk_url_kwarg = "product_id"
template_name = "core/edit.jinja" template_name = "counter/product_form.jinja"
current_tab = "products" current_tab = "products"

View File

@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-01 18:18+0200\n" "POT-Creation-Date: 2025-09-14 01:35+0200\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"
@@ -561,6 +561,7 @@ msgstr ""
#: core/templates/core/user_godfathers_tree.jinja #: core/templates/core/user_godfathers_tree.jinja
#: core/templates/core/user_preferences.jinja #: core/templates/core/user_preferences.jinja
#: counter/templates/counter/cash_register_summary.jinja #: counter/templates/counter/cash_register_summary.jinja
#: counter/templates/counter/product_form.jinja
#: forum/templates/forum/reply.jinja #: forum/templates/forum/reply.jinja
#: subscription/templates/subscription/fragments/creation_form.jinja #: subscription/templates/subscription/fragments/creation_form.jinja
#: trombi/templates/trombi/comment.jinja #: trombi/templates/trombi/comment.jinja
@@ -1713,8 +1714,8 @@ msgid ""
"AE UTBM is a voluntary organisation run by UTBM students. It organises " "AE UTBM is a voluntary organisation run by UTBM students. It organises "
"student life at UTBM and manages its student facilities." "student life at UTBM and manages its student facilities."
msgstr "" msgstr ""
"L'AE UTBM est une association bénévole gérée par les étudiants de " "L'AE UTBM est une association bénévole gérée par les étudiants de l'UTBM. "
"l'UTBM. Elle organise la vie étudiante de l'UTBM et gère ses lieux de vie." "Elle organise la vie étudiante de l'UTBM et gère ses lieux de vie."
#: core/templates/core/base/footer.jinja core/templates/core/base/navbar.jinja #: core/templates/core/base/footer.jinja core/templates/core/base/navbar.jinja
msgid "Contacts" msgid "Contacts"
@@ -2157,10 +2158,6 @@ msgstr ""
msgid "Page history" msgid "Page history"
msgstr "Historique de la page" msgstr "Historique de la page"
#: core/templates/core/page_list.jinja
msgid "There is no page in this website."
msgstr "Il n'y a pas de page sur ce site web."
#: core/templates/core/page_prop.jinja #: core/templates/core/page_prop.jinja
msgid "Page properties" msgid "Page properties"
msgstr "Propriétés de la page" msgstr "Propriétés de la page"
@@ -2896,6 +2893,20 @@ msgstr "Cet UID est invalide"
msgid "User not found" msgid "User not found"
msgstr "Utilisateur non trouvé" msgstr "Utilisateur non trouvé"
#: counter/forms.py
msgid "Enabled"
msgstr "Activé"
#: counter/forms.py
msgid "Date and time of archiving"
msgstr "Date et heure de l'archivage"
#: counter/forms.py
msgid ""
"Automatic archiving cannot be enabled without providing a archiving date."
msgstr ""
"L'archivage automatique ne peut pas activé sans fournir une date d'archivage."
#: counter/forms.py #: counter/forms.py
msgid "" msgid ""
"Describe the product. If it's an event's click, give some insights about it, " "Describe the product. If it's an event's click, give some insights about it, "
@@ -3548,6 +3559,29 @@ msgstr ""
"votre cotisation. Si vous ne renouvelez pas votre cotisation, il n'y aura " "votre cotisation. Si vous ne renouvelez pas votre cotisation, il n'y aura "
"aucune conséquence autre que le retrait de l'argent de votre compte." "aucune conséquence autre que le retrait de l'argent de votre compte."
#: counter/templates/counter/product_form.jinja
#, python-format
msgid "Edit product %(name)s"
msgstr "Édition du produit %(name)s"
#: counter/templates/counter/product_form.jinja
#, fuzzy
#| msgid "Product state"
msgid "Product creation"
msgstr "Etat du produit"
#: counter/templates/counter/product_form.jinja
msgid "Automatic archiving"
msgstr "Archivage automatique"
#: counter/templates/counter/product_form.jinja
msgid ""
"Automatic archiving allows you to mark a product as archived and remove it "
"from all its counters at a specified time and date."
msgstr ""
"L'archivage automatique permet de marquer un produit comme archivé et de le "
"retirer de tous ses comptoirs à une heure et une date voulues."
#: counter/templates/counter/product_list.jinja #: counter/templates/counter/product_list.jinja
msgid "Product list" msgid "Product list"
msgstr "Liste des produits" msgstr "Liste des produits"