mirror of
https://github.com/ae-utbm/sith.git
synced 2025-03-09 14:57:08 +00:00
add pages to manage returnable products
This commit is contained in:
parent
9148cbf206
commit
500d82f166
@ -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;
|
||||
|
@ -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();">
|
||||
|
@ -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 %}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
67
counter/templates/counter/returnable_list.jinja
Normal file
67
counter/templates/counter/returnable_list.jinja
Normal 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 %}
|
@ -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(
|
||||
|
@ -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)."""
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user