add pages to manage returnable products

This commit is contained in:
imperosol 2025-03-07 02:03:22 +01:00
parent 9148cbf206
commit 500d82f166
10 changed files with 251 additions and 19 deletions

View File

@ -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;

View File

@ -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 %}
<h2>{% trans %}Delete confirmation{% endtrans %}</h2>
<form action="" method="post">{% csrf_token %}
<p>{% trans obj=object %}Are you sure you want to delete "{{ obj }}"?{% endtrans %}</p>
<p>{% trans name=object_name %}Are you sure you want to delete "{{ name }}"?{% endtrans %}</p>
<input type="submit" value="{% trans %}Confirm{% endtrans %}" />
</form>
<form method="GET" action="javascript:history.back();">

View File

@ -62,6 +62,11 @@
{% trans %}Product types management{% endtrans %}
</a>
</li>
<li>
<a href="{{ url("counter:returnable_list") }}">
{% trans %}Returnable products management{% endtrans %}
</a>
</li>
<li>
<a href="{{ url('counter:cash_summary_list') }}">
{% trans %}Cash register summaries{% endtrans %}

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,67 @@
{% extends "core/base.jinja" %}
{% block title %}
{% trans %}Returnable products{% endtrans %}
{% endblock %}
{% block additional_js %}
{% endblock %}
{% block additional_css %}
<link rel="stylesheet" href="{{ static("core/components/card.scss") }}">
<link rel="stylesheet" href="{{ static("counter/css/admin.scss") }}">
<link rel="stylesheet" href="{{ static("bundled/core/components/ajax-select-index.css") }}">
<link rel="stylesheet" href="{{ static("core/components/ajax-select.scss") }}">
{% endblock %}
{% block content %}
<h3 class="margin-bottom">{% trans %}Returnable products{% endtrans %}</h3>
{% if user.has_perm("counter.add_returnableproduct") %}
<a href="{{ url('counter:create_returnable') }}" class="btn btn-blue margin-bottom">
{% trans %}New returnable product{% endtrans %} <i class="fa fa-plus"></i>
</a>
{% endif %}
<div class="product-group">
{% for returnable in object_list %}
{% if user.has_perm("counter.change_returnableproduct") %}
<a
class="card card-row shadow clickable"
href="{{ url("counter:edit_returnable", returnable_id=returnable.id) }}"
>
{% else %}
<div class="card card-row shadow">
{% endif %}
{% if returnable.product.icon %}
<img
class="card-image"
src="{{ returnable.product.icon.url }}"
alt="{{ returnable.product.name }}"
>
{% else %}
<i class="fa-regular fa-image fa-2x card-image"></i>
{% endif %}
<div class="card-content">
<strong class="card-title">{{ returnable.product }}</strong>
<p>{% trans %}Returned product{% endtrans %} : {{ returnable.returned_product }}</p>
</div>
{% if user.has_perm("counter.delete_returnableproduct") %}
<button
x-data
class="btn btn-red btn-no-text card-top-left"
@click.prevent="document.location.href = '{{ url("counter:delete_returnable", returnable_id=returnable.id) }}'"
>
{# The delete link is a button with a JS event listener
instead of a proper <a> element,
because the enclosing card is already a <a>,
and HTML forbids nested <a> #}
<i class="fa fa-trash"></i>
</button>
{% endif %}
{% if user.has_perm("counter.change_returnableproduct") %}
</a>
{% else %}
</div>
{% endif %}
{% endfor %}
</div>
{% endblock content %}

View File

@ -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("<int:counter_id>/", 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/<int:returnable_id>/",
ReturnableProductUpdateView.as_view(),
name="edit_returnable",
),
path(
"admin/returnable/delete/<int:returnable_id>/",
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(

View File

@ -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)."""

View File

@ -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",

View File

@ -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 <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\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"