From dc4e01ff9c61f7e2e5b37082aed6bad938d0900e Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 14 Sep 2025 01:36:46 +0200 Subject: [PATCH] feat: automatic product archiving --- counter/forms.py | 87 +++++++++++++++++--- counter/models.py | 3 +- counter/tasks.py | 12 +++ counter/templates/counter/product_form.jinja | 31 +++++++ counter/views/admin.py | 4 +- locale/fr/LC_MESSAGES/django.po | 38 +++++++++ 6 files changed, 160 insertions(+), 15 deletions(-) create mode 100644 counter/tasks.py create mode 100644 counter/templates/counter/product_form.jinja 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 94f80d2c..a17fd309 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..34ea1a9c --- /dev/null +++ b/counter/tasks.py @@ -0,0 +1,12 @@ +# Create your tasks here + +from celery import shared_task + +from counter.models import Product + + +@shared_task +def archive_product(product_id: int): + product = Product.objects.get(id=product_id) + product.archived = True + product.save() 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 f4bbbb45..83f66bb4 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -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 @@ -2929,6 +2930,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, " @@ -3581,6 +3596,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"