diff --git a/counter/forms.py b/counter/forms.py index c7df1d9d..4d299701 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -1,13 +1,19 @@ import math from django import forms +from django.core.exceptions import ValidationError from django.db.models import Q from django.utils.translation import gettext_lazy as _ +from django_celery_beat.models import ClockedSchedule, PeriodicTask from phonenumber_field.widgets import RegionalPhoneNumberWidget from club.widgets.ajax_select import AutoCompleteSelectClub 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 ( AutoCompleteSelect, 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): error_css_class = "error" required_css_class = "required" @@ -199,22 +265,19 @@ class ProductEditForm(forms.ModelForm): queryset=Counter.objects.all(), ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, *args, instance=None, **kwargs): + super().__init__(*args, instance=instance, **kwargs) if self.instance.id: 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): ret = super().save(*args, **kwargs) - if self.fields["counters"].initial: - # Remove the product from all counter it was added to - # 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() + self.instance.counters.set(self.cleaned_data["counters"]) + self.archive_form.save() return ret diff --git a/counter/models.py b/counter/models.py index d2b9a927..c1181de7 100644 --- a/counter/models.py +++ b/counter/models.py @@ -445,7 +445,8 @@ class Product(models.Model): buying_groups = list(self.buying_groups.all()) if not buying_groups: 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 def profit(self): diff --git a/counter/tasks.py b/counter/tasks.py new file mode 100644 index 00000000..6c6a46b7 --- /dev/null +++ b/counter/tasks.py @@ -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() diff --git a/counter/templates/counter/product_form.jinja b/counter/templates/counter/product_form.jinja new file mode 100644 index 00000000..f4980f51 --- /dev/null +++ b/counter/templates/counter/product_form.jinja @@ -0,0 +1,31 @@ +{% extends "core/base.jinja" %} + +{% block content %} + {% if object %} +

{% trans name=object %}Edit product {{ name }}{% endtrans %}

+ {% else %} +

{% trans %}Product creation{% endtrans %}

+ {% endif %} +
+ {% csrf_token %} + {{ form.as_p() }} + +
+ +

{% trans %}Automatic archiving{% endtrans %}

+ +

+ + {%- 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 -%} + +

+ +
+ {{ form.archive_form.as_p() }} +
+

+
+{% endblock %} \ No newline at end of file diff --git a/counter/views/admin.py b/counter/views/admin.py index ddd7a40e..7c1d4727 100644 --- a/counter/views/admin.py +++ b/counter/views/admin.py @@ -147,7 +147,7 @@ class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): model = Product form_class = ProductEditForm - template_name = "core/create.jinja" + template_name = "counter/product_form.jinja" current_tab = "products" @@ -157,7 +157,7 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): model = Product form_class = ProductEditForm pk_url_kwarg = "product_id" - template_name = "core/edit.jinja" + template_name = "counter/product_form.jinja" current_tab = "products" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index a638339e..443e45db 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "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" "Last-Translator: Maréchal \n" @@ -561,6 +561,7 @@ msgstr "" #: core/templates/core/user_godfathers_tree.jinja #: core/templates/core/user_preferences.jinja #: counter/templates/counter/cash_register_summary.jinja +#: counter/templates/counter/product_form.jinja #: forum/templates/forum/reply.jinja #: subscription/templates/subscription/fragments/creation_form.jinja #: trombi/templates/trombi/comment.jinja @@ -1713,8 +1714,8 @@ msgid "" "AE UTBM is a voluntary organisation run by UTBM students. It organises " "student life at UTBM and manages its student facilities." msgstr "" -"L'AE UTBM est une association bénévole gérée par les étudiants de " -"l'UTBM. Elle organise la vie étudiante de l'UTBM et gère ses lieux de vie." +"L'AE UTBM est une association bénévole gérée par les étudiants de l'UTBM. " +"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 msgid "Contacts" @@ -2157,10 +2158,6 @@ msgstr "" msgid "Page history" 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 msgid "Page properties" msgstr "Propriétés de la page" @@ -2896,6 +2893,20 @@ msgstr "Cet UID est invalide" msgid "User not found" 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 msgid "" "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 " "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 msgid "Product list" msgstr "Liste des produits"