From 500d82f1662eaa2a12d6ed1c787d04004734725e Mon Sep 17 00:00:00 2001 From: imperosol Date: Fri, 7 Mar 2025 02:03:22 +0100 Subject: [PATCH] add pages to manage returnable products --- core/static/core/components/card.scss | 8 ++ core/templates/core/delete_confirm.jinja | 9 ++- core/templates/core/user_tools.jinja | 5 ++ core/views/mixins.py | 24 +++--- counter/forms.py | 20 +++++ .../templates/counter/returnable_list.jinja | 67 ++++++++++++++++ counter/urls.py | 27 ++++++- counter/views/admin.py | 76 ++++++++++++++++++- counter/views/mixins.py | 5 ++ locale/fr/LC_MESSAGES/django.po | 29 ++++++- 10 files changed, 251 insertions(+), 19 deletions(-) create mode 100644 counter/templates/counter/returnable_list.jinja diff --git a/core/static/core/components/card.scss b/core/static/core/components/card.scss index c8e59098..941b32a5 100644 --- a/core/static/core/components/card.scss +++ b/core/static/core/components/card.scss @@ -55,6 +55,14 @@ width: 80%; } + .card-top-left { + position: absolute; + top: 10px; + right: 10px; + padding: 10px; + text-align: center; + } + .card-content { color: black; display: flex; diff --git a/core/templates/core/delete_confirm.jinja b/core/templates/core/delete_confirm.jinja index 3a393b67..6ae8a1b2 100644 --- a/core/templates/core/delete_confirm.jinja +++ b/core/templates/core/delete_confirm.jinja @@ -10,10 +10,17 @@ {% block nav %} {% endblock %} +{# if the template context has the `object_name` variable, + then this one will be used, + instead of the result of `str(object)` #} +{% if object and not object_name %} + {% set object_name=object %} +{% endif %} + {% block content %}

{% trans %}Delete confirmation{% endtrans %}

{% csrf_token %} -

{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}

+

{% trans name=object_name %}Are you sure you want to delete "{{ name }}"?{% endtrans %}

diff --git a/core/templates/core/user_tools.jinja b/core/templates/core/user_tools.jinja index 4c9b9462..e1e8e581 100644 --- a/core/templates/core/user_tools.jinja +++ b/core/templates/core/user_tools.jinja @@ -62,6 +62,11 @@ {% trans %}Product types management{% endtrans %} +
  • + + {% trans %}Returnable products management{% endtrans %} + +
  • {% trans %}Cash register summaries{% endtrans %} diff --git a/core/views/mixins.py b/core/views/mixins.py index 9687f5d9..e5b445d6 100644 --- a/core/views/mixins.py +++ b/core/views/mixins.py @@ -1,3 +1,5 @@ +from typing import ClassVar + from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.views import View @@ -6,20 +8,24 @@ from django.views import View class TabedViewMixin(View): """Basic functions for displaying tabs in the template.""" + current_tab: ClassVar[str | None] = None + list_of_tabs: ClassVar[list | None] = None + tabs_title: ClassVar[str | None] = None + def get_tabs_title(self): - if hasattr(self, "tabs_title"): - return self.tabs_title - raise ImproperlyConfigured("tabs_title is required") + if not self.tabs_title: + raise ImproperlyConfigured("tabs_title is required") + return self.tabs_title def get_current_tab(self): - if hasattr(self, "current_tab"): - return self.current_tab - raise ImproperlyConfigured("current_tab is required") + if not self.current_tab: + raise ImproperlyConfigured("current_tab is required") + return self.current_tab def get_list_of_tabs(self): - if hasattr(self, "list_of_tabs"): - return self.list_of_tabs - raise ImproperlyConfigured("list_of_tabs is required") + if not self.list_of_tabs: + raise ImproperlyConfigured("list_of_tabs is required") + return self.list_of_tabs def get_context_data(self, **kwargs): kwargs = super().get_context_data(**kwargs) diff --git a/counter/forms.py b/counter/forms.py index 59762920..a480e182 100644 --- a/counter/forms.py +++ b/counter/forms.py @@ -17,6 +17,7 @@ from counter.models import ( Eticket, Product, Refilling, + ReturnableProduct, StudentCard, ) from counter.widgets.select import ( @@ -213,6 +214,25 @@ class ProductEditForm(forms.ModelForm): return ret +class ReturnableProductForm(forms.ModelForm): + class Meta: + model = ReturnableProduct + fields = ["product", "returned_product", "max_return"] + widgets = { + "product": AutoCompleteSelectProduct(), + "returned_product": AutoCompleteSelectProduct(), + } + + def save(self, commit: bool = True) -> ReturnableProduct: # noqa FBT + instance: ReturnableProduct = super().save(commit=commit) + if commit: + # This is expensive, but we don't have a task queue to offload it. + # Hopefully, creations and updates of returnable products + # occur very rarely + instance.update_balances() + return instance + + class CashSummaryFormBase(forms.Form): begin_date = forms.DateTimeField( label=_("Begin date"), widget=SelectDateTime, required=False diff --git a/counter/templates/counter/returnable_list.jinja b/counter/templates/counter/returnable_list.jinja new file mode 100644 index 00000000..cc107a30 --- /dev/null +++ b/counter/templates/counter/returnable_list.jinja @@ -0,0 +1,67 @@ +{% extends "core/base.jinja" %} + +{% block title %} + {% trans %}Returnable products{% endtrans %} +{% endblock %} + +{% block additional_js %} +{% endblock %} + +{% block additional_css %} + + + + +{% endblock %} + +{% block content %} +

    {% trans %}Returnable products{% endtrans %}

    + {% if user.has_perm("counter.add_returnableproduct") %} +
    + {% trans %}New returnable product{% endtrans %} + + {% endif %} + +{% endblock content %} diff --git a/counter/urls.py b/counter/urls.py index 885b4b14..c07205cb 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -30,6 +30,10 @@ from counter.views.admin import ( ProductTypeEditView, ProductTypeListView, RefillingDeleteView, + ReturnableProductCreateView, + ReturnableProductDeleteView, + ReturnableProductListView, + ReturnableProductUpdateView, SellingDeleteView, ) from counter.views.auth import counter_login, counter_logout @@ -51,10 +55,7 @@ from counter.views.home import ( CounterMain, ) from counter.views.invoice import InvoiceCallView -from counter.views.student_card import ( - StudentCardDeleteView, - StudentCardFormView, -) +from counter.views.student_card import StudentCardDeleteView, StudentCardFormView urlpatterns = [ path("/", CounterMain.as_view(), name="details"), @@ -129,6 +130,24 @@ urlpatterns = [ ProductTypeEditView.as_view(), name="product_type_edit", ), + path( + "admin/returnable/", ReturnableProductListView.as_view(), name="returnable_list" + ), + path( + "admin/returnable/create/", + ReturnableProductCreateView.as_view(), + name="create_returnable", + ), + path( + "admin/returnable//", + ReturnableProductUpdateView.as_view(), + name="edit_returnable", + ), + path( + "admin/returnable/delete//", + ReturnableProductDeleteView.as_view(), + name="delete_returnable", + ), path("admin/eticket/list/", EticketListView.as_view(), name="eticket_list"), path("admin/eticket/new/", EticketCreateView.as_view(), name="new_eticket"), path( diff --git a/counter/views/admin.py b/counter/views/admin.py index ffe81ea0..76e4e512 100644 --- a/counter/views/admin.py +++ b/counter/views/admin.py @@ -15,19 +15,28 @@ from datetime import timedelta from django.conf import settings +from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.exceptions import PermissionDenied from django.forms import CheckboxSelectMultiple from django.forms.models import modelform_factory from django.shortcuts import get_object_or_404 from django.urls import reverse, reverse_lazy from django.utils import timezone +from django.utils.translation import gettext as _ from django.views.generic import DetailView, ListView, TemplateView from django.views.generic.edit import CreateView, DeleteView, UpdateView from core.auth.mixins import CanEditMixin, CanViewMixin from core.utils import get_semester_code, get_start_of_semester -from counter.forms import CounterEditForm, ProductEditForm -from counter.models import Counter, Product, ProductType, Refilling, Selling +from counter.forms import CounterEditForm, ProductEditForm, ReturnableProductForm +from counter.models import ( + Counter, + Product, + ProductType, + Refilling, + ReturnableProduct, + Selling, +) from counter.utils import is_logged_in_counter from counter.views.mixins import CounterAdminMixin, CounterAdminTabsMixin @@ -146,6 +155,69 @@ class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): current_tab = "products" +class ReturnableProductListView( + CounterAdminTabsMixin, PermissionRequiredMixin, ListView +): + model = ReturnableProduct + queryset = model.objects.select_related("product", "returned_product") + template_name = "counter/returnable_list.jinja" + current_tab = "returnable_products" + permission_required = "counter.view_returnableproduct" + + +class ReturnableProductCreateView( + CounterAdminTabsMixin, PermissionRequiredMixin, CreateView +): + form_class = ReturnableProductForm + template_name = "core/create.jinja" + current_tab = "returnable_products" + success_url = reverse_lazy("counter:returnable_list") + permission_required = "counter.add_returnableproduct" + + +class ReturnableProductUpdateView( + CounterAdminTabsMixin, PermissionRequiredMixin, UpdateView +): + model = ReturnableProduct + pk_url_kwarg = "returnable_id" + queryset = model.objects.select_related("product", "returned_product") + form_class = ReturnableProductForm + template_name = "core/edit.jinja" + current_tab = "returnable_products" + success_url = reverse_lazy("counter:returnable_list") + permission_required = "counter.change_returnableproduct" + + def get_context_data(self, **kwargs): + return super().get_context_data(**kwargs) | { + "object_name": _("returnable product : %(returnable)s -> %(returned)s") + % { + "returnable": self.object.product.name, + "returned": self.object.returned_product.name, + } + } + + +class ReturnableProductDeleteView( + CounterAdminTabsMixin, PermissionRequiredMixin, DeleteView +): + model = ReturnableProduct + pk_url_kwarg = "returnable_id" + queryset = model.objects.select_related("product", "returned_product") + template_name = "core/delete_confirm.jinja" + current_tab = "returnable_products" + success_url = reverse_lazy("counter:returnable_list") + permission_required = "counter.delete_returnableproduct" + + def get_context_data(self, **kwargs): + return super().get_context_data(**kwargs) | { + "object_name": _("returnable product : %(returnable)s -> %(returned)s") + % { + "returnable": self.object.product.name, + "returned": self.object.returned_product.name, + } + } + + class RefillingDeleteView(DeleteView): """Delete a refilling (for the admins).""" diff --git a/counter/views/mixins.py b/counter/views/mixins.py index 5c01392a..90bd1291 100644 --- a/counter/views/mixins.py +++ b/counter/views/mixins.py @@ -98,6 +98,11 @@ class CounterAdminTabsMixin(TabedViewMixin): "slug": "product_types", "name": _("Product types"), }, + { + "url": reverse_lazy("counter:returnable_list"), + "slug": "returnable_products", + "name": _("Returnable products"), + }, { "url": reverse_lazy("counter:cash_summary_list"), "slug": "cash_summary", diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 429d2090..891ecfda 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-03-06 16:50+0100\n" +"POT-Creation-Date: 2025-03-06 22:40+0100\n" "PO-Revision-Date: 2016-07-18\n" "Last-Translator: Maréchal \n" @@ -3199,6 +3199,10 @@ msgstr "Gestion des produits" msgid "Product types management" msgstr "Gestion des types de produit" +#: core/templates/core/user_tools.jinja +msgid "Returnable products management" +msgstr "Gestion des consignes" + #: core/templates/core/user_tools.jinja #: counter/templates/counter/cash_summary_list.jinja counter/views/mixins.py msgid "Cash register summaries" @@ -3712,7 +3716,7 @@ msgstr "produit consigné" #: counter/models.py msgid "returned product" -msgstr "produits déconsignés" +msgstr "produit déconsigné" #: counter/models.py msgid "maximum returns" @@ -3724,12 +3728,18 @@ msgid "" "bought them." msgstr "" "Le nombre maximum d'articles qu'un client peut déconsigner sans les avoir " -"acheté." +"achetés." #: counter/models.py msgid "returnable products" msgstr "produits consignés" +#: counter/models.py +msgid "The returnable product cannot be the same as the returned one" +msgstr "" +"Le produit consigné ne peut pas être le même " +"que le produit de déconsigne" + #: counter/templates/counter/activity.jinja #, python-format msgid "%(counter_name)s activity" @@ -4097,6 +4107,14 @@ msgstr "Il n'y a pas de types de produit dans ce site web." msgid "Seller" msgstr "Vendeur" +#: counter/templates/counter/returnable_list.jinja counter/views/mixins.py +msgid "Returnable products" +msgstr "Produits consignés" + +#: counter/templates/counter/returnable_list.jinja +msgid "Returned product" +msgstr "Produit déconsigné" + #: counter/templates/counter/stats.jinja #, python-format msgid "%(counter_name)s stats" @@ -4129,6 +4147,11 @@ msgstr "Temps" msgid "Top 100 barman %(counter_name)s (all semesters)" msgstr "Top 100 barman %(counter_name)s (tous les semestres)" +#: counter/views/admin.py +#, python-format +msgid "returnable product : %(returnable)s -> %(returned)s" +msgstr "produit consigné : %(returnable)s -> %(returned)s" + #: counter/views/cash.py msgid "10 cents" msgstr "10 centimes"