Sith/counter/views.py

1665 lines
61 KiB
Python
Raw Normal View History

#
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
#
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
#
# You can find the source code of the website at https://github.com/ae-utbm/sith3
#
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
# SEE : https://raw.githubusercontent.com/ae-utbm/sith3/master/LICENSE
# OR WITHIN THE LOCAL FILE "LICENSE"
#
#
import json
2024-06-24 11:07:36 +00:00
import re
from datetime import datetime, timedelta
2024-07-18 15:33:14 +00:00
from datetime import timezone as tz
2024-06-24 11:07:36 +00:00
from http import HTTPStatus
from urllib.parse import parse_qs
2024-06-24 11:07:36 +00:00
from django import forms
from django.conf import settings
from django.contrib.auth.decorators import login_required
2024-06-24 11:07:36 +00:00
from django.core.exceptions import PermissionDenied
from django.db import DataError, transaction
from django.db.models import F
2024-06-24 11:07:36 +00:00
from django.forms import CheckboxSelectMultiple
from django.forms.models import modelform_factory
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
2017-06-12 07:47:24 +00:00
from django.shortcuts import get_object_or_404
2024-06-24 11:07:36 +00:00
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST
2024-06-24 11:07:36 +00:00
from django.views.generic import DetailView, ListView, RedirectView, TemplateView
2017-04-03 09:57:28 +00:00
from django.views.generic.base import View
2018-10-04 19:29:19 +00:00
from django.views.generic.edit import (
CreateView,
DeleteView,
FormMixin,
FormView,
2024-06-24 11:07:36 +00:00
ProcessFormView,
UpdateView,
2018-10-04 19:29:19 +00:00
)
2016-03-28 12:54:35 +00:00
2024-06-24 11:07:36 +00:00
from accounting.models import CurrencyField
from core.models import User
2024-06-24 11:07:36 +00:00
from core.utils import get_semester_code, get_start_of_semester
from core.views import CanEditMixin, CanViewMixin, TabedViewMixin
from core.views.forms import LoginForm
from counter.forms import (
BillingInfoForm,
CashSummaryFormBase,
2024-06-24 11:07:36 +00:00
CounterEditForm,
EticketForm,
2024-06-24 11:07:36 +00:00
GetUserForm,
ProductEditForm,
RefillForm,
StudentCardForm,
)
2018-10-04 19:29:19 +00:00
from counter.models import (
2024-06-24 11:07:36 +00:00
BillingInfo,
CashRegisterSummary,
CashRegisterSummaryItem,
2018-10-04 19:29:19 +00:00
Counter,
Customer,
2024-06-24 11:07:36 +00:00
Eticket,
2018-10-04 19:29:19 +00:00
Product,
ProductType,
2024-06-24 11:07:36 +00:00
Refilling,
Selling,
StudentCard,
2018-10-04 19:29:19 +00:00
)
2016-03-28 12:54:35 +00:00
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class CounterAdminMixin(View):
2024-07-12 07:34:16 +00:00
"""Protect counter admin section."""
2018-10-04 19:29:19 +00:00
2017-04-03 13:00:13 +00:00
edit_group = [settings.SITH_GROUP_COUNTER_ADMIN_ID]
edit_club = []
2017-04-03 11:50:28 +00:00
def _test_group(self, user):
for grp_id in self.edit_group:
if user.is_in_group(pk=grp_id):
2017-04-03 11:50:28 +00:00
return True
return False
2017-04-03 13:00:13 +00:00
def _test_club(self, user):
for c in self.edit_club:
if c.can_be_edited_by(user):
return True
return False
2017-04-03 09:57:28 +00:00
def dispatch(self, request, *args, **kwargs):
2018-10-04 19:29:19 +00:00
if not (
request.user.is_root
or self._test_group(request.user)
or self._test_club(request.user)
):
2017-04-03 09:57:28 +00:00
raise PermissionDenied
2024-06-27 12:46:43 +00:00
return super().dispatch(request, *args, **kwargs)
2017-04-03 09:57:28 +00:00
2017-06-12 07:47:24 +00:00
class StudentCardDeleteView(DeleteView, CanEditMixin):
2024-07-12 07:34:16 +00:00
"""View used to delete a card from a user."""
model = StudentCard
template_name = "core/delete_confirm.jinja"
pk_url_kwarg = "card_id"
def dispatch(self, request, *args, **kwargs):
self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"])
2024-06-27 12:46:43 +00:00
return super().dispatch(request, *args, **kwargs)
def get_success_url(self, **kwargs):
return reverse_lazy(
"core:user_prefs", kwargs={"user_id": self.customer.user.pk}
)
2016-09-28 09:07:32 +00:00
class CounterTabsMixin(TabedViewMixin):
def get_tabs_title(self):
2018-10-04 19:29:19 +00:00
if hasattr(self.object, "stock_owner"):
2017-01-03 13:50:49 +00:00
return self.object.stock_owner.counter
else:
return self.object
2017-06-12 07:47:24 +00:00
2016-09-28 09:07:32 +00:00
def get_list_of_tabs(self):
tab_list = []
2018-10-04 19:29:19 +00:00
tab_list.append(
{
"url": reverse_lazy(
"counter:details",
kwargs={
"counter_id": (
self.object.stock_owner.counter.id
if hasattr(self.object, "stock_owner")
else self.object.id
)
2018-10-04 19:29:19 +00:00
},
),
"slug": "counter",
"name": _("Counter"),
}
)
if (
self.object.stock_owner.counter.type
if hasattr(self.object, "stock_owner")
else self.object.type == "BAR"
):
tab_list.append(
{
"url": reverse_lazy(
"counter:cash_summary",
kwargs={
"counter_id": (
self.object.stock_owner.counter.id
if hasattr(self.object, "stock_owner")
else self.object.id
)
2018-10-04 19:29:19 +00:00
},
),
"slug": "cash_summary",
"name": _("Cash summary"),
}
)
tab_list.append(
{
"url": reverse_lazy(
"counter:last_ops",
kwargs={
"counter_id": (
self.object.stock_owner.counter.id
if hasattr(self.object, "stock_owner")
else self.object.id
)
2018-10-04 19:29:19 +00:00
},
),
"slug": "last_ops",
"name": _("Last operations"),
}
)
2017-05-01 14:53:30 +00:00
try:
2018-10-04 19:29:19 +00:00
tab_list.append(
{
"url": reverse_lazy(
"stock:take_items",
kwargs={
"stock_id": (
self.object.stock.id
if hasattr(self.object, "stock")
else self.object.stock_owner.id
)
2018-10-04 19:29:19 +00:00
},
),
"slug": "take_items_from_stock",
"name": _("Take items from stock"),
}
)
2017-06-12 07:47:24 +00:00
except:
pass # The counter just have no stock
2016-09-28 09:07:32 +00:00
return tab_list
2017-06-12 07:47:24 +00:00
2018-10-04 19:29:19 +00:00
class CounterMain(
CounterTabsMixin, CanViewMixin, DetailView, ProcessFormView, FormMixin
):
2024-07-12 07:34:16 +00:00
"""The public (barman) view."""
2018-10-04 19:29:19 +00:00
2016-03-28 12:54:35 +00:00
model = Counter
2018-10-04 19:29:19 +00:00
template_name = "counter/counter_main.jinja"
2016-03-28 12:54:35 +00:00
pk_url_kwarg = "counter_id"
2018-10-04 19:29:19 +00:00
form_class = (
2019-11-04 12:46:09 +00:00
GetUserForm # Form to enter a client code and get the corresponding user id
)
2016-09-28 09:07:32 +00:00
current_tab = "counter"
2016-03-29 08:30:24 +00:00
def post(self, request, *args, **kwargs):
self.object = self.get_object()
2018-10-04 19:29:19 +00:00
if self.object.type == "BAR" and not (
"counter_token" in self.request.session.keys()
and self.request.session["counter_token"] == self.object.token
): # Check the token to avoid the bar to be stolen
return HttpResponseRedirect(
reverse_lazy(
"counter:details",
args=self.args,
kwargs={"counter_id": self.object.id},
)
+ "?bad_location"
)
2024-06-27 12:46:43 +00:00
return super().post(request, *args, **kwargs)
2016-04-12 08:00:47 +00:00
def get_context_data(self, **kwargs):
2024-07-12 07:34:16 +00:00
"""We handle here the login form for the barman."""
2018-10-04 19:29:19 +00:00
if self.request.method == "POST":
self.object = self.get_object()
2016-09-12 15:34:33 +00:00
self.object.update_activity()
2024-06-27 12:46:43 +00:00
kwargs = super().get_context_data(**kwargs)
2018-10-04 19:29:19 +00:00
kwargs["login_form"] = LoginForm()
kwargs["login_form"].fields["username"].widget.attrs["autofocus"] = True
kwargs[
"login_form"
].cleaned_data = {} # add_error fails if there are no cleaned_data
2016-08-20 14:09:46 +00:00
if "credentials" in self.request.GET:
2018-10-04 19:29:19 +00:00
kwargs["login_form"].add_error(None, _("Bad credentials"))
if "sellers" in self.request.GET:
2018-10-04 19:29:19 +00:00
kwargs["login_form"].add_error(None, _("User is not barman"))
kwargs["form"] = self.get_form()
kwargs["form"].cleaned_data = {} # same as above
if "bad_location" in self.request.GET:
2018-10-04 19:29:19 +00:00
kwargs["form"].add_error(
None, _("Bad location, someone is already logged in somewhere else")
)
if self.object.type == "BAR":
kwargs["barmen"] = self.object.get_barmen_list()
elif self.request.user.is_authenticated:
2018-10-04 19:29:19 +00:00
kwargs["barmen"] = [self.request.user]
if "last_basket" in self.request.session.keys():
kwargs["last_basket"] = self.request.session.pop("last_basket")
kwargs["last_customer"] = self.request.session.pop("last_customer")
kwargs["last_total"] = self.request.session.pop("last_total")
kwargs["new_customer_amount"] = self.request.session.pop(
"new_customer_amount"
)
2016-04-15 09:50:31 +00:00
return kwargs
def form_valid(self, form):
2024-07-12 07:34:16 +00:00
"""We handle here the redirection, passing the user id of the asked customer."""
2018-10-04 19:29:19 +00:00
self.kwargs["user_id"] = form.cleaned_data["user_id"]
2024-06-27 12:46:43 +00:00
return super().form_valid(form)
def get_success_url(self):
2018-10-04 19:29:19 +00:00
return reverse_lazy("counter:click", args=self.args, kwargs=self.kwargs)
2017-06-12 07:47:24 +00:00
2017-01-04 18:39:37 +00:00
class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
2024-07-12 07:34:16 +00:00
"""The click view
This is a detail view not to have to worry about loading the counter
2024-07-12 07:34:16 +00:00
Everything is made by hand in the post method.
2016-04-15 09:50:31 +00:00
"""
2018-10-04 19:29:19 +00:00
model = Counter
2018-10-04 19:29:19 +00:00
template_name = "counter/counter_click.jinja"
2016-04-15 09:50:31 +00:00
pk_url_kwarg = "counter_id"
2016-09-28 09:07:32 +00:00
current_tab = "counter"
def render_to_response(self, *args, **kwargs):
if self.is_ajax(self.request):
response = {"errors": []}
status = HTTPStatus.OK
if self.request.session["too_young"]:
response["errors"].append(_("Too young for that product"))
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
if self.request.session["not_allowed"]:
response["errors"].append(_("Not allowed for that product"))
status = HTTPStatus.FORBIDDEN
if self.request.session["no_age"]:
response["errors"].append(_("No date of birth provided"))
status = HTTPStatus.UNAVAILABLE_FOR_LEGAL_REASONS
if self.request.session["not_enough"]:
response["errors"].append(_("Not enough money"))
status = HTTPStatus.PAYMENT_REQUIRED
if len(response["errors"]) > 1:
status = HTTPStatus.BAD_REQUEST
response["basket"] = self.request.session["basket"]
return JsonResponse(response, status=status)
else: # Standard HTML page
return super().render_to_response(*args, **kwargs)
def dispatch(self, request, *args, **kwargs):
2018-10-04 19:29:19 +00:00
self.customer = get_object_or_404(Customer, user__id=self.kwargs["user_id"])
2017-04-03 08:41:36 +00:00
obj = self.get_object()
if not self.customer.can_buy:
raise Http404
2017-04-03 08:41:36 +00:00
if obj.type == "BAR":
2018-10-04 19:29:19 +00:00
if (
not (
"counter_token" in request.session.keys()
and request.session["counter_token"] == obj.token
)
or len(obj.get_barmen_list()) < 1
):
return HttpResponseRedirect(
reverse_lazy("counter:details", kwargs={"counter_id": obj.id})
)
2017-04-03 08:41:36 +00:00
else:
if not request.user.is_authenticated:
2017-04-03 08:41:36 +00:00
raise PermissionDenied
2024-06-27 12:46:43 +00:00
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
2024-07-12 07:34:16 +00:00
"""Simple get view."""
2018-10-04 19:29:19 +00:00
if "basket" not in request.session.keys(): # Init the basket session entry
request.session["basket"] = {}
request.session["basket_total"] = 0
request.session["not_enough"] = False # Reset every variable
request.session["too_young"] = False
request.session["not_allowed"] = False
request.session["no_age"] = False
self.refill_form = None
2024-06-27 12:46:43 +00:00
ret = super().get(request, *args, **kwargs)
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
2018-10-04 19:29:19 +00:00
self.object.type == "BAR" and len(self.object.get_barmen_list()) < 1
): # Check that at least one barman is logged in
2017-06-12 07:47:24 +00:00
ret = self.cancel(request) # Otherwise, go to main view
return ret
2016-04-15 09:50:31 +00:00
def post(self, request, *args, **kwargs):
2024-07-12 07:34:16 +00:00
"""Handle the many possibilities of the post request."""
self.object = self.get_object()
self.refill_form = None
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
2018-10-04 19:29:19 +00:00
self.object.type == "BAR" and len(self.object.get_barmen_list()) < 1
): # Check that at least one barman is logged in
return self.cancel(request)
2018-10-04 19:29:19 +00:00
if self.object.type == "BAR" and not (
"counter_token" in self.request.session.keys()
and self.request.session["counter_token"] == self.object.token
): # Also check the token to avoid the bar to be stolen
return HttpResponseRedirect(
reverse_lazy(
"counter:details",
args=self.args,
kwargs={"counter_id": self.object.id},
)
+ "?bad_location"
)
if "basket" not in request.session.keys():
request.session["basket"] = {}
request.session["basket_total"] = 0
request.session["not_enough"] = False # Reset every variable
request.session["too_young"] = False
request.session["not_allowed"] = False
request.session["no_age"] = False
2018-10-18 23:21:57 +00:00
request.session["not_valid_student_card_uid"] = False
2016-07-22 11:34:34 +00:00
if self.object.type != "BAR":
self.operator = request.user
elif self.customer_is_barman():
2016-07-22 11:34:34 +00:00
self.operator = self.customer.user
else:
2016-08-06 10:37:36 +00:00
self.operator = self.object.get_random_barman()
action = self.request.POST.get("action", None)
if action is None:
action = parse_qs(request.body.decode()).get("action", [""])[0]
if action == "add_product":
self.add_product(request)
elif action == "add_student_card":
2018-10-18 23:21:57 +00:00
self.add_student_card(request)
elif action == "del_product":
self.del_product(request)
elif action == "refill":
2016-06-26 18:07:29 +00:00
self.refill(request)
elif action == "code":
2016-06-26 17:30:28 +00:00
return self.parse_code(request)
elif action == "cancel":
return self.cancel(request)
elif action == "finish":
return self.finish(request)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def customer_is_barman(self) -> bool:
barmen = self.object.barmen_list
return self.object.type == "BAR" and self.customer.user in barmen
def get_product(self, pid):
return Product.objects.filter(pk=int(pid)).first()
def get_price(self, pid):
p = self.get_product(pid)
if self.customer_is_barman():
price = p.special_selling_price
else:
price = p.selling_price
return price
def sum_basket(self, request):
total = 0
2024-07-08 10:34:06 +00:00
for infos in request.session["basket"].values():
2018-10-04 19:29:19 +00:00
total += infos["price"] * infos["qty"]
return total / 100
2016-08-20 14:09:46 +00:00
def get_total_quantity_for_pid(self, request, pid):
pid = str(pid)
try:
2018-10-04 19:29:19 +00:00
return (
request.session["basket"][pid]["qty"]
+ request.session["basket"][pid]["bonus_qty"]
)
2016-08-20 14:09:46 +00:00
except:
return 0
2017-07-21 19:39:49 +00:00
def compute_record_product(self, request, product=None):
recorded = 0
2018-10-04 19:29:19 +00:00
basket = request.session["basket"]
2017-07-21 19:39:49 +00:00
if product:
if product.is_record_product:
recorded -= 1
elif product.is_unrecord_product:
recorded += 1
for p in basket:
bproduct = self.get_product(str(p))
if bproduct.is_record_product:
2018-10-04 19:29:19 +00:00
recorded -= basket[p]["qty"]
2017-07-21 19:39:49 +00:00
elif bproduct.is_unrecord_product:
2018-10-04 19:29:19 +00:00
recorded += basket[p]["qty"]
2017-07-21 19:39:49 +00:00
return recorded
def is_record_product_ok(self, request, product):
return self.customer.can_record_more(
2018-10-04 19:29:19 +00:00
self.compute_record_product(request, product)
)
2017-07-21 19:39:49 +00:00
@staticmethod
def is_ajax(request):
# when using the fetch API, the django request.POST dict is empty
# this is but a wretched contrivance which strive to replace
# the deprecated django is_ajax() method
# and which must be replaced as soon as possible
# by a proper separation between the api endpoints of the counter
return len(request.POST) == 0 and len(request.body) != 0
2017-06-12 07:47:24 +00:00
def add_product(self, request, q=1, p=None):
2024-07-12 07:34:16 +00:00
"""Add a product to the basket
q is the quantity passed as integer
2024-07-12 07:34:16 +00:00
p is the product id, passed as an integer.
"""
pid = p or parse_qs(request.body.decode())["product_id"][0]
2016-07-16 14:48:56 +00:00
pid = str(pid)
price = self.get_price(pid)
total = self.sum_basket(request)
product: Product = self.get_product(pid)
user: User = self.customer.user
buying_groups = list(product.buying_groups.values_list("pk", flat=True))
can_buy = len(buying_groups) == 0 or any(
user.is_in_group(pk=group_id) for group_id in buying_groups
)
if not can_buy:
2018-10-04 19:29:19 +00:00
request.session["not_allowed"] = True
return False
2017-06-12 07:47:24 +00:00
bq = 0 # Bonus quantity, for trays
2018-10-04 19:29:19 +00:00
if (
product.tray
): # Handle the tray to adjust the quantity q to add and the bonus quantity bq
2016-08-20 14:09:46 +00:00
total_qty_mod_6 = self.get_total_quantity_for_pid(request, pid) % 6
2017-06-12 07:47:24 +00:00
bq = int((total_qty_mod_6 + q) / 6) # Integer division
2016-08-20 14:09:46 +00:00
q -= bq
2018-10-04 19:29:19 +00:00
if self.customer.amount < (
total + round(q * float(price), 2)
): # Check for enough money
request.session["not_enough"] = True
2016-06-26 17:30:28 +00:00
return False
2018-10-04 19:29:19 +00:00
if product.is_unrecord_product and not self.is_record_product_ok(
request, product
):
request.session["not_allowed"] = True
2017-07-21 19:39:49 +00:00
return False
if product.limit_age >= 18 and not user.date_of_birth:
2018-10-04 19:29:19 +00:00
request.session["no_age"] = True
2016-09-01 14:55:43 +00:00
return False
if product.limit_age >= 18 and user.is_banned_alcohol:
2018-10-04 19:29:19 +00:00
request.session["not_allowed"] = True
2016-10-15 00:33:38 +00:00
return False
if user.is_banned_counter:
2018-10-04 19:29:19 +00:00
request.session["not_allowed"] = True
return False
2018-10-04 19:29:19 +00:00
if (
user.date_of_birth and self.customer.user.get_age() < product.limit_age
2018-10-04 19:29:19 +00:00
): # Check if affordable
request.session["too_young"] = True
return False
2018-10-04 19:29:19 +00:00
if pid in request.session["basket"]: # Add if already in basket
request.session["basket"][pid]["qty"] += q
request.session["basket"][pid]["bonus_qty"] += bq
2017-06-12 07:47:24 +00:00
else: # or create if not
2018-10-04 19:29:19 +00:00
request.session["basket"][pid] = {
"qty": q,
"price": int(price * 100),
"bonus_qty": bq,
}
request.session.modified = True
2016-06-26 17:30:28 +00:00
return True
2018-10-18 23:21:57 +00:00
def add_student_card(self, request):
2024-07-12 07:34:16 +00:00
"""Add a new student card on the customer account."""
2018-10-18 23:21:57 +00:00
uid = request.POST["student_card_uid"]
uid = str(uid)
if not StudentCard.is_valid(uid):
2018-10-18 23:21:57 +00:00
request.session["not_valid_student_card_uid"] = True
return False
if not (
self.object.type == "BAR"
and "counter_token" in request.session.keys()
and request.session["counter_token"] == self.object.token
and len(self.object.get_barmen_list()) > 0
):
raise PermissionDenied
StudentCard(customer=self.customer, uid=uid).save()
2018-10-18 23:21:57 +00:00
return True
def del_product(self, request):
2024-07-12 07:34:16 +00:00
"""Delete a product from the basket."""
pid = parse_qs(request.body.decode())["product_id"][0]
2016-08-20 14:09:46 +00:00
product = self.get_product(pid)
2018-10-04 19:29:19 +00:00
if pid in request.session["basket"]:
if (
product.tray
and (self.get_total_quantity_for_pid(request, pid) % 6 == 0)
and request.session["basket"][pid]["bonus_qty"]
):
request.session["basket"][pid]["bonus_qty"] -= 1
2016-08-20 14:09:46 +00:00
else:
2018-10-04 19:29:19 +00:00
request.session["basket"][pid]["qty"] -= 1
if request.session["basket"][pid]["qty"] <= 0:
del request.session["basket"][pid]
request.session.modified = True
2016-06-26 17:30:28 +00:00
def parse_code(self, request):
2024-07-12 07:34:16 +00:00
"""Parse the string entered by the barman.
This can be of two forms :
2024-07-12 07:34:16 +00:00
- `<str>`, where the string is the code of the product
- `<int>X<str>`, where the integer is the quantity and str the code.
"""
string = parse_qs(request.body.decode()).get("code", [""])[0].upper()
if string == "FIN":
2016-06-26 17:30:28 +00:00
return self.finish(request)
elif string == "ANN":
2016-06-26 17:30:28 +00:00
return self.cancel(request)
regex = re.compile(r"^((?P<nb>[0-9]+)X)?(?P<code>[A-Z0-9]+)$")
m = regex.match(string)
if m is not None:
2018-10-04 19:29:19 +00:00
nb = m.group("nb")
code = m.group("code")
nb = int(nb) if nb is not None else 1
2016-07-22 11:34:34 +00:00
p = self.object.products.filter(code=code).first()
2016-06-26 17:30:28 +00:00
if p is not None:
self.add_product(request, nb, p.id)
2016-06-26 17:30:28 +00:00
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
def finish(self, request):
2024-07-12 07:34:16 +00:00
"""Finish the click session, and validate the basket."""
2016-07-21 23:19:04 +00:00
with transaction.atomic():
2018-10-04 19:29:19 +00:00
request.session["last_basket"] = []
2017-07-27 14:53:53 +00:00
if self.sum_basket(request) > self.customer.amount:
raise DataError(_("You have not enough money to buy all the basket"))
2018-10-04 19:29:19 +00:00
for pid, infos in request.session["basket"].items():
2016-07-21 23:19:04 +00:00
# This duplicates code for DB optimization (prevent to load many times the same object)
p = Product.objects.filter(pk=pid).first()
if self.customer_is_barman():
2016-07-21 23:19:04 +00:00
uprice = p.special_selling_price
else:
uprice = p.selling_price
2018-10-04 19:29:19 +00:00
request.session["last_basket"].append(
"%d x %s" % (infos["qty"] + infos["bonus_qty"], p.name)
)
s = Selling(
label=p.name,
product=p,
club=p.club,
counter=self.object,
unit_price=uprice,
quantity=infos["qty"],
seller=self.operator,
customer=self.customer,
)
2016-07-21 23:19:04 +00:00
s.save()
2018-10-04 19:29:19 +00:00
if infos["bonus_qty"]:
s = Selling(
label=p.name + " (Plateau)",
product=p,
club=p.club,
counter=self.object,
unit_price=0,
quantity=infos["bonus_qty"],
seller=self.operator,
customer=self.customer,
)
2016-08-20 14:09:46 +00:00
s.save()
2017-07-21 19:39:49 +00:00
self.customer.recorded_products -= self.compute_record_product(request)
self.customer.save()
2018-10-04 19:29:19 +00:00
request.session["last_customer"] = self.customer.user.get_display_name()
request.session["last_total"] = "%0.2f" % self.sum_basket(request)
request.session["new_customer_amount"] = str(self.customer.amount)
del request.session["basket"]
2016-07-21 23:19:04 +00:00
request.session.modified = True
2018-10-04 19:29:19 +00:00
kwargs = {"counter_id": self.object.id}
return HttpResponseRedirect(
reverse_lazy("counter:details", args=self.args, kwargs=kwargs)
)
def cancel(self, request):
2024-07-12 07:34:16 +00:00
"""Cancel the click session."""
2018-10-04 19:29:19 +00:00
kwargs = {"counter_id": self.object.id}
request.session.pop("basket", None)
return HttpResponseRedirect(
reverse_lazy("counter:details", args=self.args, kwargs=kwargs)
)
2016-06-26 18:07:29 +00:00
def refill(self, request):
2024-07-12 07:34:16 +00:00
"""Refill the customer's account."""
if not self.object.can_refill():
2017-04-03 08:41:36 +00:00
raise PermissionDenied
form = RefillForm(request.POST)
if form.is_valid():
form.instance.counter = self.object
form.instance.operator = self.operator
form.instance.customer = self.customer
form.instance.save()
else:
self.refill_form = form
2016-06-26 18:07:29 +00:00
def get_context_data(self, **kwargs):
2024-07-12 07:34:16 +00:00
"""Add customer to the context."""
2024-06-27 12:46:43 +00:00
kwargs = super().get_context_data(**kwargs)
products = self.object.products.select_related("product_type")
if self.customer_is_barman():
products = products.annotate(price=F("special_selling_price"))
else:
products = products.annotate(price=F("selling_price"))
kwargs["products"] = products
kwargs["categories"] = {}
for product in kwargs["products"]:
if product.product_type:
kwargs["categories"].setdefault(product.product_type, []).append(
product
)
2018-10-04 19:29:19 +00:00
kwargs["customer"] = self.customer
kwargs["basket_total"] = self.sum_basket(self.request)
kwargs["refill_form"] = self.refill_form or RefillForm()
2018-10-18 23:21:57 +00:00
kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE
2022-04-20 12:01:33 +00:00
kwargs["barmens_can_refill"] = self.object.can_refill()
return kwargs
2017-06-12 07:47:24 +00:00
2016-03-31 08:36:00 +00:00
class CounterLogin(RedirectView):
2024-07-12 07:34:16 +00:00
"""Handle the login of a barman.
2016-04-12 08:00:47 +00:00
Logged barmen are stored in the Permanency model
2016-04-12 08:00:47 +00:00
"""
2018-10-04 19:29:19 +00:00
2016-03-31 08:36:00 +00:00
permanent = False
2017-06-12 07:47:24 +00:00
2016-04-12 08:00:47 +00:00
def post(self, request, *args, **kwargs):
2024-07-12 07:34:16 +00:00
"""Register the logged user as barman for this counter."""
2018-10-04 19:29:19 +00:00
self.counter_id = kwargs["counter_id"]
self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
2016-08-31 00:43:49 +00:00
form = LoginForm(request, data=request.POST)
2016-08-20 14:09:46 +00:00
self.errors = []
2016-03-31 08:36:00 +00:00
if form.is_valid():
2018-10-04 19:29:19 +00:00
user = User.objects.filter(username=form.cleaned_data["username"]).first()
if (
user in self.counter.sellers.all()
and not user in self.counter.get_barmen_list()
):
if len(self.counter.get_barmen_list()) <= 0:
self.counter.gen_token()
2018-10-04 19:29:19 +00:00
request.session["counter_token"] = self.counter.token
self.counter.add_barman(user)
2016-08-20 14:09:46 +00:00
else:
self.errors += ["sellers"]
2016-04-12 08:00:47 +00:00
else:
2016-08-20 14:09:46 +00:00
self.errors += ["credentials"]
2024-06-27 12:46:43 +00:00
return super().post(request, *args, **kwargs)
2016-04-12 08:00:47 +00:00
def get_redirect_url(self, *args, **kwargs):
2018-10-04 19:29:19 +00:00
return (
reverse_lazy("counter:details", args=args, kwargs=kwargs)
+ "?"
+ "&".join(self.errors)
)
2017-06-12 07:47:24 +00:00
2016-04-12 08:00:47 +00:00
class CounterLogout(RedirectView):
permanent = False
2017-06-12 07:47:24 +00:00
2016-04-12 08:00:47 +00:00
def post(self, request, *args, **kwargs):
2024-07-12 07:34:16 +00:00
"""Unregister the user from the barman."""
2018-10-04 19:29:19 +00:00
self.counter = Counter.objects.filter(id=kwargs["counter_id"]).first()
user = User.objects.filter(id=request.POST["user_id"]).first()
self.counter.del_barman(user)
2024-06-27 12:46:43 +00:00
return super().post(request, *args, **kwargs)
2016-04-12 08:00:47 +00:00
def get_redirect_url(self, *args, **kwargs):
2018-10-04 19:29:19 +00:00
return reverse_lazy("counter:details", args=args, kwargs=kwargs)
2016-03-31 08:36:00 +00:00
2017-06-12 07:47:24 +00:00
# Counter admin views
2016-09-28 09:07:32 +00:00
class CounterAdminTabsMixin(TabedViewMixin):
tabs_title = _("Counter administration")
list_of_tabs = [
2018-10-04 19:29:19 +00:00
{"url": reverse_lazy("stock:list"), "slug": "stocks", "name": _("Stocks")},
2017-06-12 07:47:24 +00:00
{
2018-10-04 19:29:19 +00:00
"url": reverse_lazy("counter:admin_list"),
"slug": "counters",
"name": _("Counters"),
2017-06-12 07:47:24 +00:00
},
{
2018-10-04 19:29:19 +00:00
"url": reverse_lazy("counter:product_list"),
"slug": "products",
"name": _("Products"),
2017-06-12 07:47:24 +00:00
},
{
2018-10-04 19:29:19 +00:00
"url": reverse_lazy("counter:product_list_archived"),
"slug": "archive",
"name": _("Archived products"),
2017-06-12 07:47:24 +00:00
},
{
2018-10-04 19:29:19 +00:00
"url": reverse_lazy("counter:producttype_list"),
"slug": "product_types",
"name": _("Product types"),
2017-06-12 07:47:24 +00:00
},
{
2018-10-04 19:29:19 +00:00
"url": reverse_lazy("counter:cash_summary_list"),
"slug": "cash_summary",
"name": _("Cash register summaries"),
2017-06-12 07:47:24 +00:00
},
{
2018-10-04 19:29:19 +00:00
"url": reverse_lazy("counter:invoices_call"),
"slug": "invoices_call",
"name": _("Invoices call"),
2017-06-12 07:47:24 +00:00
},
{
2018-10-04 19:29:19 +00:00
"url": reverse_lazy("counter:eticket_list"),
"slug": "etickets",
"name": _("Etickets"),
2017-06-12 07:47:24 +00:00
},
]
2016-09-28 09:07:32 +00:00
class CounterListView(CounterAdminTabsMixin, CanViewMixin, ListView):
2024-07-12 07:34:16 +00:00
"""A list view for the admins."""
2018-10-04 19:29:19 +00:00
2016-03-31 08:36:00 +00:00
model = Counter
2018-10-04 19:29:19 +00:00
template_name = "counter/counter_list.jinja"
current_tab = "counters"
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class CounterEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
2024-07-12 07:34:16 +00:00
"""Edit a counter's main informations (for the counter's manager)."""
2018-10-04 19:29:19 +00:00
model = Counter
form_class = CounterEditForm
pk_url_kwarg = "counter_id"
2018-10-04 19:29:19 +00:00
template_name = "core/edit.jinja"
current_tab = "counters"
2017-04-03 11:50:28 +00:00
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
2017-04-03 13:00:13 +00:00
self.edit_club.append(obj.club)
2024-06-27 12:46:43 +00:00
return super().dispatch(request, *args, **kwargs)
2017-04-03 11:50:28 +00:00
def get_success_url(self):
2018-10-04 19:29:19 +00:00
return reverse_lazy("counter:admin", kwargs={"counter_id": self.object.id})
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class CounterEditPropView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
2024-07-12 07:34:16 +00:00
"""Edit a counter's main informations (for the counter's admin)."""
2018-10-04 19:29:19 +00:00
2016-03-29 08:30:24 +00:00
model = Counter
2018-10-04 19:29:19 +00:00
form_class = modelform_factory(Counter, fields=["name", "club", "type"])
2016-03-29 08:30:24 +00:00
pk_url_kwarg = "counter_id"
2018-10-04 19:29:19 +00:00
template_name = "core/edit.jinja"
current_tab = "counters"
2016-03-29 08:30:24 +00:00
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class CounterCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
2024-07-12 07:34:16 +00:00
"""Create a counter (for the admins)."""
2018-10-04 19:29:19 +00:00
2016-03-29 08:30:24 +00:00
model = Counter
2018-10-04 19:29:19 +00:00
form_class = modelform_factory(
Counter,
fields=["name", "club", "type", "products"],
widgets={"products": CheckboxSelectMultiple},
)
template_name = "core/create.jinja"
current_tab = "counters"
2016-03-29 08:30:24 +00:00
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class CounterDeleteView(CounterAdminTabsMixin, CounterAdminMixin, DeleteView):
2024-07-12 07:34:16 +00:00
"""Delete a counter (for the admins)."""
2018-10-04 19:29:19 +00:00
2016-03-29 08:30:24 +00:00
model = Counter
pk_url_kwarg = "counter_id"
2018-10-04 19:29:19 +00:00
template_name = "core/delete_confirm.jinja"
success_url = reverse_lazy("counter:admin_list")
current_tab = "counters"
2016-03-31 08:36:00 +00:00
2018-10-04 19:29:19 +00:00
2016-07-27 15:23:02 +00:00
# Product management
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class ProductTypeListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
2024-07-12 07:34:16 +00:00
"""A list view for the admins."""
2018-10-04 19:29:19 +00:00
2016-07-27 18:05:45 +00:00
model = ProductType
2018-10-04 19:29:19 +00:00
template_name = "counter/producttype_list.jinja"
current_tab = "product_types"
2016-07-27 18:05:45 +00:00
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class ProductTypeCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
2024-07-12 07:34:16 +00:00
"""A create view for the admins."""
2018-10-04 19:29:19 +00:00
2016-07-27 18:05:45 +00:00
model = ProductType
2022-11-16 19:41:24 +00:00
fields = ["name", "description", "comment", "icon", "priority"]
2018-10-04 19:29:19 +00:00
template_name = "core/create.jinja"
current_tab = "products"
2016-07-27 18:05:45 +00:00
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
2024-07-12 07:34:16 +00:00
"""An edit view for the admins."""
2018-10-04 19:29:19 +00:00
2016-07-27 18:05:45 +00:00
model = ProductType
2018-10-04 19:29:19 +00:00
template_name = "core/edit.jinja"
2022-11-16 19:41:24 +00:00
fields = ["name", "description", "comment", "icon", "priority"]
2016-07-27 18:05:45 +00:00
pk_url_kwarg = "type_id"
current_tab = "products"
2016-07-27 18:05:45 +00:00
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class ProductArchivedListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
2024-07-12 07:34:16 +00:00
"""A list view for the admins."""
2018-10-04 19:29:19 +00:00
model = Product
2018-10-04 19:29:19 +00:00
template_name = "counter/product_list.jinja"
queryset = Product.objects.filter(archived=True)
2018-10-04 19:29:19 +00:00
ordering = ["name"]
current_tab = "archive"
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
2024-07-12 07:34:16 +00:00
"""A list view for the admins."""
2018-10-04 19:29:19 +00:00
2016-07-27 15:23:02 +00:00
model = Product
2018-10-04 19:29:19 +00:00
template_name = "counter/product_list.jinja"
queryset = Product.objects.filter(archived=False)
2018-10-04 19:29:19 +00:00
ordering = ["name"]
current_tab = "products"
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
2024-07-12 07:34:16 +00:00
"""A create view for the admins."""
2018-10-04 19:29:19 +00:00
2016-07-27 15:23:02 +00:00
model = Product
form_class = ProductEditForm
2018-10-04 19:29:19 +00:00
template_name = "core/create.jinja"
current_tab = "products"
2016-07-27 15:23:02 +00:00
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class ProductEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
2024-07-12 07:34:16 +00:00
"""An edit view for the admins."""
2018-10-04 19:29:19 +00:00
2016-07-27 15:23:02 +00:00
model = Product
form_class = ProductEditForm
2016-07-27 15:23:02 +00:00
pk_url_kwarg = "product_id"
2018-10-04 19:29:19 +00:00
template_name = "core/edit.jinja"
current_tab = "products"
2016-08-18 19:06:10 +00:00
2017-06-12 07:47:24 +00:00
2016-09-28 09:07:32 +00:00
class RefillingDeleteView(DeleteView):
2024-07-12 07:34:16 +00:00
"""Delete a refilling (for the admins)."""
2018-10-04 19:29:19 +00:00
2016-08-18 19:06:10 +00:00
model = Refilling
pk_url_kwarg = "refilling_id"
2018-10-04 19:29:19 +00:00
template_name = "core/delete_confirm.jinja"
2016-08-18 19:06:10 +00:00
2016-09-28 09:07:32 +00:00
def dispatch(self, request, *args, **kwargs):
2024-07-12 07:34:16 +00:00
"""We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
2016-09-28 09:07:32 +00:00
self.object = self.get_object()
2018-10-04 19:29:19 +00:00
if (
timezone.now() - self.object.date
<= timedelta(minutes=settings.SITH_LAST_OPERATIONS_LIMIT)
and "counter_token" in request.session.keys()
and request.session["counter_token"]
and Counter.objects.filter( # check if not null for counters that have no token set
token=request.session["counter_token"]
).exists()
):
self.success_url = reverse(
"counter:details", kwargs={"counter_id": self.object.counter.id}
)
2024-06-27 12:46:43 +00:00
return super().dispatch(request, *args, **kwargs)
2016-09-28 09:07:32 +00:00
elif self.object.is_owned_by(request.user):
2018-10-04 19:29:19 +00:00
self.success_url = reverse(
"core:user_account", kwargs={"user_id": self.object.customer.user.id}
)
2024-06-27 12:46:43 +00:00
return super().dispatch(request, *args, **kwargs)
2016-09-28 09:07:32 +00:00
raise PermissionDenied
2016-08-18 19:06:10 +00:00
2017-06-12 07:47:24 +00:00
2016-09-28 09:07:32 +00:00
class SellingDeleteView(DeleteView):
2024-07-12 07:34:16 +00:00
"""Delete a selling (for the admins)."""
2018-10-04 19:29:19 +00:00
2016-08-18 19:06:10 +00:00
model = Selling
pk_url_kwarg = "selling_id"
2018-10-04 19:29:19 +00:00
template_name = "core/delete_confirm.jinja"
2016-08-18 19:06:10 +00:00
2016-09-28 09:07:32 +00:00
def dispatch(self, request, *args, **kwargs):
2024-07-12 07:34:16 +00:00
"""We have here a very particular right handling, we can't inherit from CanEditPropMixin."""
2016-09-28 09:07:32 +00:00
self.object = self.get_object()
2018-10-04 19:29:19 +00:00
if (
timezone.now() - self.object.date
<= timedelta(minutes=settings.SITH_LAST_OPERATIONS_LIMIT)
and "counter_token" in request.session.keys()
and request.session["counter_token"]
and Counter.objects.filter( # check if not null for counters that have no token set
token=request.session["counter_token"]
).exists()
):
self.success_url = reverse(
"counter:details", kwargs={"counter_id": self.object.counter.id}
)
2024-06-27 12:46:43 +00:00
return super().dispatch(request, *args, **kwargs)
2016-09-28 09:07:32 +00:00
elif self.object.is_owned_by(request.user):
2018-10-04 19:29:19 +00:00
self.success_url = reverse(
"core:user_account", kwargs={"user_id": self.object.customer.user.id}
)
2024-06-27 12:46:43 +00:00
return super().dispatch(request, *args, **kwargs)
2016-09-28 09:07:32 +00:00
raise PermissionDenied
2016-08-18 19:06:10 +00:00
2018-10-04 19:29:19 +00:00
2016-08-26 18:57:04 +00:00
# Cash register summaries
2017-06-12 07:47:24 +00:00
2016-08-26 18:57:04 +00:00
class CashRegisterSummaryForm(forms.Form):
2024-07-12 07:34:16 +00:00
"""Provide the cash summary form."""
2018-10-04 19:29:19 +00:00
2017-03-01 09:21:57 +00:00
ten_cents = forms.IntegerField(label=_("10 cents"), required=False, min_value=0)
twenty_cents = forms.IntegerField(label=_("20 cents"), required=False, min_value=0)
fifty_cents = forms.IntegerField(label=_("50 cents"), required=False, min_value=0)
one_euro = forms.IntegerField(label=_("1 euro"), required=False, min_value=0)
two_euros = forms.IntegerField(label=_("2 euros"), required=False, min_value=0)
five_euros = forms.IntegerField(label=_("5 euros"), required=False, min_value=0)
ten_euros = forms.IntegerField(label=_("10 euros"), required=False, min_value=0)
twenty_euros = forms.IntegerField(label=_("20 euros"), required=False, min_value=0)
fifty_euros = forms.IntegerField(label=_("50 euros"), required=False, min_value=0)
2018-10-04 19:29:19 +00:00
hundred_euros = forms.IntegerField(
label=_("100 euros"), required=False, min_value=0
)
check_1_value = forms.DecimalField(
label=_("Check amount"), required=False, min_value=0
)
check_1_quantity = forms.IntegerField(
label=_("Check quantity"), required=False, min_value=0
)
check_2_value = forms.DecimalField(
label=_("Check amount"), required=False, min_value=0
)
check_2_quantity = forms.IntegerField(
label=_("Check quantity"), required=False, min_value=0
)
check_3_value = forms.DecimalField(
label=_("Check amount"), required=False, min_value=0
)
check_3_quantity = forms.IntegerField(
label=_("Check quantity"), required=False, min_value=0
)
check_4_value = forms.DecimalField(
label=_("Check amount"), required=False, min_value=0
)
check_4_quantity = forms.IntegerField(
label=_("Check quantity"), required=False, min_value=0
)
check_5_value = forms.DecimalField(
label=_("Check amount"), required=False, min_value=0
)
check_5_quantity = forms.IntegerField(
label=_("Check quantity"), required=False, min_value=0
)
2016-08-26 18:57:04 +00:00
comment = forms.CharField(label=_("Comment"), required=False)
emptied = forms.BooleanField(label=_("Emptied"), required=False)
def __init__(self, *args, **kwargs):
2018-10-04 19:29:19 +00:00
instance = kwargs.pop("instance", None)
2024-06-27 12:46:43 +00:00
super().__init__(*args, **kwargs)
if instance:
2018-10-04 19:29:19 +00:00
self.fields["ten_cents"].initial = (
instance.ten_cents.quantity if instance.ten_cents else 0
)
self.fields["twenty_cents"].initial = (
instance.twenty_cents.quantity if instance.twenty_cents else 0
)
self.fields["fifty_cents"].initial = (
instance.fifty_cents.quantity if instance.fifty_cents else 0
)
self.fields["one_euro"].initial = (
instance.one_euro.quantity if instance.one_euro else 0
)
self.fields["two_euros"].initial = (
instance.two_euros.quantity if instance.two_euros else 0
)
self.fields["five_euros"].initial = (
instance.five_euros.quantity if instance.five_euros else 0
)
self.fields["ten_euros"].initial = (
instance.ten_euros.quantity if instance.ten_euros else 0
)
self.fields["twenty_euros"].initial = (
instance.twenty_euros.quantity if instance.twenty_euros else 0
)
self.fields["fifty_euros"].initial = (
instance.fifty_euros.quantity if instance.fifty_euros else 0
)
self.fields["hundred_euros"].initial = (
instance.hundred_euros.quantity if instance.hundred_euros else 0
)
self.fields["check_1_quantity"].initial = (
instance.check_1.quantity if instance.check_1 else 0
)
self.fields["check_2_quantity"].initial = (
instance.check_2.quantity if instance.check_2 else 0
)
self.fields["check_3_quantity"].initial = (
instance.check_3.quantity if instance.check_3 else 0
)
self.fields["check_4_quantity"].initial = (
instance.check_4.quantity if instance.check_4 else 0
)
self.fields["check_5_quantity"].initial = (
instance.check_5.quantity if instance.check_5 else 0
)
self.fields["check_1_value"].initial = (
instance.check_1.value if instance.check_1 else 0
)
self.fields["check_2_value"].initial = (
instance.check_2.value if instance.check_2 else 0
)
self.fields["check_3_value"].initial = (
instance.check_3.value if instance.check_3 else 0
)
self.fields["check_4_value"].initial = (
instance.check_4.value if instance.check_4 else 0
)
self.fields["check_5_value"].initial = (
instance.check_5.value if instance.check_5 else 0
)
self.fields["comment"].initial = instance.comment
self.fields["emptied"].initial = instance.emptied
self.instance = instance
else:
self.instance = None
def save(self, counter=None):
2016-08-26 18:57:04 +00:00
cd = self.cleaned_data
summary = self.instance or CashRegisterSummary(
2018-10-04 19:29:19 +00:00
counter=counter, user=counter.get_random_barman()
2017-06-12 07:47:24 +00:00
)
2018-10-04 19:29:19 +00:00
summary.comment = cd["comment"]
summary.emptied = cd["emptied"]
2016-08-26 18:57:04 +00:00
summary.save()
summary.items.all().delete()
2016-08-26 18:57:04 +00:00
# Cash
2018-10-04 19:29:19 +00:00
if cd["ten_cents"]:
CashRegisterSummaryItem(
cash_summary=summary, value=0.1, quantity=cd["ten_cents"]
).save()
if cd["twenty_cents"]:
CashRegisterSummaryItem(
cash_summary=summary, value=0.2, quantity=cd["twenty_cents"]
).save()
if cd["fifty_cents"]:
CashRegisterSummaryItem(
cash_summary=summary, value=0.5, quantity=cd["fifty_cents"]
).save()
if cd["one_euro"]:
CashRegisterSummaryItem(
cash_summary=summary, value=1, quantity=cd["one_euro"]
).save()
if cd["two_euros"]:
CashRegisterSummaryItem(
cash_summary=summary, value=2, quantity=cd["two_euros"]
).save()
if cd["five_euros"]:
CashRegisterSummaryItem(
cash_summary=summary, value=5, quantity=cd["five_euros"]
).save()
if cd["ten_euros"]:
CashRegisterSummaryItem(
cash_summary=summary, value=10, quantity=cd["ten_euros"]
).save()
if cd["twenty_euros"]:
CashRegisterSummaryItem(
cash_summary=summary, value=20, quantity=cd["twenty_euros"]
).save()
if cd["fifty_euros"]:
CashRegisterSummaryItem(
cash_summary=summary, value=50, quantity=cd["fifty_euros"]
).save()
if cd["hundred_euros"]:
CashRegisterSummaryItem(
cash_summary=summary, value=100, quantity=cd["hundred_euros"]
).save()
2016-08-26 18:57:04 +00:00
# Checks
2018-10-04 19:29:19 +00:00
if cd["check_1_quantity"]:
CashRegisterSummaryItem(
cash_summary=summary,
value=cd["check_1_value"],
quantity=cd["check_1_quantity"],
is_check=True,
2018-10-04 19:29:19 +00:00
).save()
if cd["check_2_quantity"]:
CashRegisterSummaryItem(
cash_summary=summary,
value=cd["check_2_value"],
quantity=cd["check_2_quantity"],
is_check=True,
2018-10-04 19:29:19 +00:00
).save()
if cd["check_3_quantity"]:
CashRegisterSummaryItem(
cash_summary=summary,
value=cd["check_3_value"],
quantity=cd["check_3_quantity"],
is_check=True,
2018-10-04 19:29:19 +00:00
).save()
if cd["check_4_quantity"]:
CashRegisterSummaryItem(
cash_summary=summary,
value=cd["check_4_value"],
quantity=cd["check_4_quantity"],
is_check=True,
2018-10-04 19:29:19 +00:00
).save()
if cd["check_5_quantity"]:
CashRegisterSummaryItem(
cash_summary=summary,
value=cd["check_5_value"],
quantity=cd["check_5_quantity"],
is_check=True,
2018-10-04 19:29:19 +00:00
).save()
2016-08-26 18:57:04 +00:00
if summary.items.count() < 1:
summary.delete()
2017-06-12 07:47:24 +00:00
2016-09-28 09:07:32 +00:00
class CounterLastOperationsView(CounterTabsMixin, CanViewMixin, DetailView):
2024-07-12 07:34:16 +00:00
"""Provide the last operations to allow barmen to delete them."""
2018-10-04 19:29:19 +00:00
2016-09-28 09:07:32 +00:00
model = Counter
pk_url_kwarg = "counter_id"
2018-10-04 19:29:19 +00:00
template_name = "counter/last_ops.jinja"
2016-09-28 09:07:32 +00:00
current_tab = "last_ops"
def dispatch(self, request, *args, **kwargs):
2024-07-12 07:34:16 +00:00
"""We have here again a very particular right handling."""
2016-09-28 09:07:32 +00:00
self.object = self.get_object()
2018-10-04 19:29:19 +00:00
if (
self.object.get_barmen_list()
and "counter_token" in request.session.keys()
and request.session["counter_token"]
and Counter.objects.filter( # check if not null for counters that have no token set
token=request.session["counter_token"]
).exists()
):
2024-06-27 12:46:43 +00:00
return super().dispatch(request, *args, **kwargs)
2018-10-04 19:29:19 +00:00
return HttpResponseRedirect(
reverse("counter:details", kwargs={"counter_id": self.object.id})
+ "?bad_location"
)
2016-09-28 09:07:32 +00:00
def get_context_data(self, **kwargs):
2024-07-12 07:34:16 +00:00
"""Add form to the context."""
2024-06-27 12:46:43 +00:00
kwargs = super().get_context_data(**kwargs)
2018-10-04 19:29:19 +00:00
threshold = timezone.now() - timedelta(
minutes=settings.SITH_LAST_OPERATIONS_LIMIT
)
kwargs["last_refillings"] = self.object.refillings.filter(
date__gte=threshold
).order_by("-id")[:20]
kwargs["last_sellings"] = self.object.sellings.filter(
date__gte=threshold
).order_by("-id")[:20]
2016-09-28 09:07:32 +00:00
return kwargs
2017-06-12 07:47:24 +00:00
2016-09-28 09:07:32 +00:00
class CounterCashSummaryView(CounterTabsMixin, CanViewMixin, DetailView):
2024-07-12 07:34:16 +00:00
"""Provide the cash summary form."""
2018-10-04 19:29:19 +00:00
2016-08-26 18:57:04 +00:00
model = Counter
pk_url_kwarg = "counter_id"
2018-10-04 19:29:19 +00:00
template_name = "counter/cash_register_summary.jinja"
2016-09-28 09:07:32 +00:00
current_tab = "cash_summary"
def dispatch(self, request, *args, **kwargs):
2024-07-12 07:34:16 +00:00
"""We have here again a very particular right handling."""
2016-09-28 09:07:32 +00:00
self.object = self.get_object()
2018-10-04 19:29:19 +00:00
if (
self.object.get_barmen_list()
and "counter_token" in request.session.keys()
and request.session["counter_token"]
and Counter.objects.filter( # check if not null for counters that have no token set
token=request.session["counter_token"]
).exists()
):
2024-06-27 12:46:43 +00:00
return super().dispatch(request, *args, **kwargs)
2018-10-04 19:29:19 +00:00
return HttpResponseRedirect(
reverse("counter:details", kwargs={"counter_id": self.object.id})
+ "?bad_location"
)
2016-08-26 18:57:04 +00:00
def get(self, request, *args, **kwargs):
self.object = self.get_object()
self.form = CashRegisterSummaryForm()
2024-06-27 12:46:43 +00:00
return super().get(request, *args, **kwargs)
2016-08-26 18:57:04 +00:00
def post(self, request, *args, **kwargs):
self.object = self.get_object()
self.form = CashRegisterSummaryForm(request.POST)
if self.form.is_valid():
self.form.save(self.object)
return HttpResponseRedirect(self.get_success_url())
2024-06-27 12:46:43 +00:00
return super().get(request, *args, **kwargs)
2016-08-26 18:57:04 +00:00
def get_success_url(self):
2018-10-04 19:29:19 +00:00
return reverse_lazy("counter:details", kwargs={"counter_id": self.object.id})
2016-08-26 18:57:04 +00:00
def get_context_data(self, **kwargs):
2024-07-12 07:34:16 +00:00
"""Add form to the context."""
2024-06-27 12:46:43 +00:00
kwargs = super().get_context_data(**kwargs)
2018-10-04 19:29:19 +00:00
kwargs["form"] = self.form
2016-08-26 18:57:04 +00:00
return kwargs
2016-09-12 15:34:33 +00:00
2017-06-12 07:47:24 +00:00
2016-09-12 15:34:33 +00:00
class CounterActivityView(DetailView):
2024-07-12 07:34:16 +00:00
"""Show the bar activity."""
2018-10-04 19:29:19 +00:00
2016-09-12 15:34:33 +00:00
model = Counter
pk_url_kwarg = "counter_id"
2018-10-04 19:29:19 +00:00
template_name = "counter/activity.jinja"
2016-09-12 15:34:33 +00:00
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class CounterStatView(DetailView, CounterAdminMixin):
2024-07-12 07:34:16 +00:00
"""Show the bar stats."""
2018-10-04 19:29:19 +00:00
2016-09-15 09:07:03 +00:00
model = Counter
pk_url_kwarg = "counter_id"
2018-10-04 19:29:19 +00:00
template_name = "counter/stats.jinja"
2016-09-15 09:07:03 +00:00
def get_context_data(self, **kwargs):
2024-07-12 07:34:16 +00:00
"""Add stats to the context."""
counter: Counter = self.object
semester_start = get_start_of_semester()
office_hours = counter.get_top_barmen()
2024-06-27 12:46:43 +00:00
kwargs = super().get_context_data(**kwargs)
kwargs.update(
{
"counter": counter,
"current_semester": get_semester_code(),
"total_sellings": counter.get_total_sales(since=semester_start),
"top_customers": counter.get_top_customers(since=semester_start)[:100],
"top_barman": office_hours[:100],
"top_barman_semester": (
office_hours.filter(start__gt=semester_start)[:100]
),
}
2018-10-04 19:29:19 +00:00
)
2016-09-15 09:07:03 +00:00
return kwargs
def dispatch(self, request, *args, **kwargs):
2016-09-27 14:44:12 +00:00
try:
2024-06-27 12:46:43 +00:00
return super().dispatch(request, *args, **kwargs)
except PermissionDenied:
2018-10-04 19:29:19 +00:00
if (
request.user.is_root
or request.user.is_board_member
or self.get_object().is_owned_by(request.user)
2018-10-04 19:29:19 +00:00
):
return super(CanEditMixin, self).dispatch(request, *args, **kwargs)
raise PermissionDenied
2017-06-12 07:47:24 +00:00
class CashSummaryEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
2024-07-12 07:34:16 +00:00
"""Edit cash summaries."""
2018-10-04 19:29:19 +00:00
model = CashRegisterSummary
2018-10-04 19:29:19 +00:00
template_name = "counter/cash_register_summary.jinja"
context_object_name = "cashsummary"
pk_url_kwarg = "cashsummary_id"
form_class = CashRegisterSummaryForm
current_tab = "cash_summary"
def get_success_url(self):
2018-10-04 19:29:19 +00:00
return reverse("counter:cash_summary_list")
2016-09-15 09:07:03 +00:00
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class CashSummaryListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
2024-07-12 07:34:16 +00:00
"""Display a list of cash summaries."""
2018-10-04 19:29:19 +00:00
2016-09-13 00:04:49 +00:00
model = CashRegisterSummary
2018-10-04 19:29:19 +00:00
template_name = "counter/cash_summary_list.jinja"
2016-09-13 00:04:49 +00:00
context_object_name = "cashsummary_list"
current_tab = "cash_summary"
2018-10-04 19:29:19 +00:00
queryset = CashRegisterSummary.objects.all().order_by("-date")
2017-03-13 22:32:06 +00:00
paginate_by = settings.SITH_COUNTER_CASH_SUMMARY_LENGTH
2016-09-13 00:04:49 +00:00
def get_context_data(self, **kwargs):
2024-07-12 07:34:16 +00:00
"""Add sums to the context."""
2024-06-27 12:46:43 +00:00
kwargs = super().get_context_data(**kwargs)
form = CashSummaryFormBase(self.request.GET)
2018-10-04 19:29:19 +00:00
kwargs["form"] = form
kwargs["summaries_sums"] = {}
kwargs["refilling_sums"] = {}
2016-09-13 00:04:49 +00:00
for c in Counter.objects.filter(type="BAR").all():
refillings = Refilling.objects.filter(counter=c)
cashredistersummaries = CashRegisterSummary.objects.filter(counter=c)
2018-10-04 19:29:19 +00:00
if form.is_valid() and form.cleaned_data["begin_date"]:
refillings = refillings.filter(
date__gte=form.cleaned_data["begin_date"]
)
cashredistersummaries = cashredistersummaries.filter(
date__gte=form.cleaned_data["begin_date"]
)
2016-09-13 00:04:49 +00:00
else:
2018-10-04 19:29:19 +00:00
last_summary = (
CashRegisterSummary.objects.filter(counter=c, emptied=True)
.order_by("-date")
.first()
)
if last_summary:
2016-10-18 12:48:47 +00:00
refillings = refillings.filter(date__gt=last_summary.date)
2018-10-04 19:29:19 +00:00
cashredistersummaries = cashredistersummaries.filter(
date__gt=last_summary.date
)
else:
2018-10-04 19:29:19 +00:00
refillings = refillings.filter(
2024-07-18 15:33:14 +00:00
date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc)
2018-10-04 19:29:19 +00:00
) # My birth date should be old enough
cashredistersummaries = cashredistersummaries.filter(
2024-07-18 15:33:14 +00:00
date__gte=datetime(year=1994, month=5, day=17, tzinfo=tz.utc)
2018-10-04 19:29:19 +00:00
)
if form.is_valid() and form.cleaned_data["end_date"]:
refillings = refillings.filter(date__lte=form.cleaned_data["end_date"])
cashredistersummaries = cashredistersummaries.filter(
date__lte=form.cleaned_data["end_date"]
)
kwargs["summaries_sums"][c.name] = sum(
[s.get_total() for s in cashredistersummaries.all()]
)
kwargs["refilling_sums"][c.name] = sum([s.amount for s in refillings.all()])
2016-09-13 00:04:49 +00:00
return kwargs
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class InvoiceCallView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView):
2018-10-04 19:29:19 +00:00
template_name = "counter/invoices_call.jinja"
current_tab = "invoices_call"
2016-09-29 16:17:44 +00:00
def get_context_data(self, **kwargs):
2024-07-12 07:34:16 +00:00
"""Add sums to the context."""
2024-06-27 12:46:43 +00:00
kwargs = super().get_context_data(**kwargs)
2018-10-04 19:29:19 +00:00
kwargs["months"] = Selling.objects.datetimes("date", "month", order="DESC")
2024-07-18 15:33:14 +00:00
if "month" in self.request.GET:
2018-10-04 19:29:19 +00:00
start_date = datetime.strptime(self.request.GET["month"], "%Y-%m")
2024-07-18 15:33:14 +00:00
else:
2018-10-04 19:29:19 +00:00
start_date = datetime(
year=timezone.now().year,
month=(timezone.now().month + 10) % 12 + 1,
day=1,
)
2024-07-18 15:33:14 +00:00
start_date = start_date.replace(tzinfo=tz.utc)
2018-10-04 19:29:19 +00:00
end_date = (start_date + timedelta(days=32)).replace(
day=1, hour=0, minute=0, microsecond=0
)
2024-06-24 11:07:36 +00:00
from django.db.models import Case, F, Sum, When
2018-10-04 19:29:19 +00:00
kwargs["sum_cb"] = sum(
[
r.amount
for r in Refilling.objects.filter(
payment_method="CARD",
is_validated=True,
date__gte=start_date,
date__lte=end_date,
)
]
)
kwargs["sum_cb"] += sum(
[
s.quantity * s.unit_price
for s in Selling.objects.filter(
payment_method="CARD",
is_validated=True,
date__gte=start_date,
date__lte=end_date,
)
]
)
kwargs["start_date"] = start_date
kwargs["sums"] = (
Selling.objects.values("club__name")
.annotate(
selling_sum=Sum(
Case(
When(
date__gte=start_date,
date__lt=end_date,
then=F("unit_price") * F("quantity"),
),
output_field=CurrencyField(),
)
)
)
.exclude(selling_sum=None)
.order_by("-selling_sum")
)
2016-09-29 16:17:44 +00:00
return kwargs
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class EticketListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
2024-07-12 07:34:16 +00:00
"""A list view for the admins."""
2018-10-04 19:29:19 +00:00
2016-10-03 17:30:05 +00:00
model = Eticket
2018-10-04 19:29:19 +00:00
template_name = "counter/eticket_list.jinja"
ordering = ["id"]
2016-10-03 17:30:05 +00:00
current_tab = "etickets"
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class EticketCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView):
2024-07-12 07:34:16 +00:00
"""Create an eticket."""
2018-10-04 19:29:19 +00:00
2016-10-03 17:30:05 +00:00
model = Eticket
2018-10-04 19:29:19 +00:00
template_name = "core/create.jinja"
2016-10-03 17:30:05 +00:00
form_class = EticketForm
current_tab = "etickets"
2017-06-12 07:47:24 +00:00
2017-04-04 13:45:02 +00:00
class EticketEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView):
2024-07-12 07:34:16 +00:00
"""Edit an eticket."""
2018-10-04 19:29:19 +00:00
2016-10-03 17:30:05 +00:00
model = Eticket
2018-10-04 19:29:19 +00:00
template_name = "core/edit.jinja"
2016-10-03 17:30:05 +00:00
form_class = EticketForm
pk_url_kwarg = "eticket_id"
current_tab = "etickets"
2017-06-12 07:47:24 +00:00
2016-10-03 17:30:05 +00:00
class EticketPDFView(CanViewMixin, DetailView):
2024-07-12 07:34:16 +00:00
"""Display the PDF of an eticket."""
2018-10-04 19:29:19 +00:00
2016-10-03 17:30:05 +00:00
model = Selling
pk_url_kwarg = "selling_id"
def get(self, request, *args, **kwargs):
from reportlab.graphics import renderPDF
2024-06-24 11:07:36 +00:00
from reportlab.graphics.barcode.qr import QrCodeWidget
from reportlab.graphics.shapes import Drawing
from reportlab.lib.units import cm
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas
2018-10-04 19:29:19 +00:00
if not (
hasattr(self.object, "product") and hasattr(self.object.product, "eticket")
):
raise Http404
2016-10-03 17:30:05 +00:00
eticket = self.object.product.eticket
user = self.object.customer.user
2018-10-04 19:29:19 +00:00
code = "%s %s %s %s" % (
self.object.customer.user.id,
self.object.product.id,
self.object.id,
self.object.quantity,
)
2016-10-03 17:30:05 +00:00
code += " " + eticket.get_hash(code)[:8].upper()
2018-10-04 19:29:19 +00:00
response = HttpResponse(content_type="application/pdf")
response["Content-Disposition"] = 'filename="eticket.pdf"'
2016-10-03 17:30:05 +00:00
p = canvas.Canvas(response)
p.setTitle("Eticket")
im = ImageReader("core/static/core/img/eticket.jpg")
width, height = im.getSize()
size = max(width, height)
width = 8 * cm * width / size
height = 8 * cm * height / size
p.drawImage(im, 10 * cm, 25 * cm, width, height)
if eticket.banner:
im = ImageReader(eticket.banner)
width, height = im.getSize()
size = max(width, height)
width = 6 * cm * width / size
height = 6 * cm * height / size
p.drawImage(im, 1 * cm, 25 * cm, width, height)
if user.profile_pict:
im = ImageReader(user.profile_pict.file)
width, height = im.getSize()
size = max(width, height)
width = 150 * width / size
height = 150 * height / size
p.drawImage(im, 10.5 * cm - width / 2, 16 * cm, width, height)
if eticket.event_title:
p.setFont("Helvetica-Bold", 20)
p.drawCentredString(10.5 * cm, 23.6 * cm, eticket.event_title)
if eticket.event_date:
p.setFont("Helvetica-Bold", 16)
2018-10-04 19:29:19 +00:00
p.drawCentredString(
10.5 * cm, 22.6 * cm, eticket.event_date.strftime("%d %b %Y")
) # FIXME with a locale
2016-10-03 17:30:05 +00:00
p.setFont("Helvetica-Bold", 14)
2018-10-04 19:29:19 +00:00
p.drawCentredString(
10.5 * cm,
15 * cm,
"%s : %d %s"
% (user.get_display_name(), self.object.quantity, str(_("people(s)"))),
)
2016-10-03 17:30:05 +00:00
p.setFont("Courier-Bold", 14)
qrcode = QrCodeWidget(code)
bounds = qrcode.getBounds()
width = bounds[2] - bounds[0]
height = bounds[3] - bounds[1]
2018-10-04 19:29:19 +00:00
d = Drawing(260, 260, transform=[260.0 / width, 0, 0, 260.0 / height, 0, 0])
2016-10-03 17:30:05 +00:00
d.add(qrcode)
renderPDF.draw(d, p, 10.5 * cm - 130, 6.1 * cm)
p.drawCentredString(10.5 * cm, 6 * cm, code)
2016-12-18 23:08:17 +00:00
partners = ImageReader("core/static/core/img/partners.png")
width, height = partners.getSize()
size = max(width, height)
width = width * 2 / 3
height = height * 2 / 3
p.drawImage(partners, 0 * cm, 0 * cm, width, height)
2016-10-03 17:30:05 +00:00
p.showPage()
p.save()
return response
2017-09-02 13:05:36 +00:00
class CounterRefillingListView(CounterAdminTabsMixin, CounterAdminMixin, ListView):
2024-07-12 07:34:16 +00:00
"""List of refillings on a counter."""
2018-10-04 19:29:19 +00:00
2017-09-02 13:05:36 +00:00
model = Refilling
2018-10-04 19:29:19 +00:00
template_name = "counter/refilling_list.jinja"
2017-09-02 13:05:36 +00:00
current_tab = "counters"
paginate_by = 30
def dispatch(self, request, *args, **kwargs):
2018-10-04 19:29:19 +00:00
self.counter = get_object_or_404(Counter, pk=kwargs["counter_id"])
2017-09-02 13:05:36 +00:00
self.queryset = Refilling.objects.filter(counter__id=self.counter.id)
2024-06-27 12:46:43 +00:00
return super().dispatch(request, *args, **kwargs)
2017-09-02 13:05:36 +00:00
def get_context_data(self, **kwargs):
2024-06-27 12:46:43 +00:00
kwargs = super().get_context_data(**kwargs)
2018-10-04 19:29:19 +00:00
kwargs["counter"] = self.counter
2017-09-02 13:05:36 +00:00
return kwargs
class StudentCardFormView(FormView):
2024-07-12 07:34:16 +00:00
"""Add a new student card."""
form_class = StudentCardForm
template_name = "core/create.jinja"
def dispatch(self, request, *args, **kwargs):
self.customer = get_object_or_404(Customer, pk=kwargs["customer_id"])
if not StudentCard.can_create(self.customer, request.user):
raise PermissionDenied
2024-06-27 12:46:43 +00:00
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
data = form.clean()
res = super(FormView, self).form_valid(form)
StudentCard(customer=self.customer, uid=data["uid"]).save()
return res
def get_success_url(self, **kwargs):
return reverse_lazy(
"core:user_prefs", kwargs={"user_id": self.customer.user.pk}
)
2024-06-27 12:57:40 +00:00
def __manage_billing_info_req(request, user_id, *, delete_if_fail=False):
data = json.loads(request.body)
form = BillingInfoForm(data)
if not form.is_valid():
if delete_if_fail:
Customer.objects.get(user__id=user_id).billing_infos.delete()
errors = [
{"field": str(form.fields[k].label), "messages": v}
for k, v in form.errors.items()
]
content = json.dumps({"errors": errors})
return HttpResponse(status=400, content=content)
if form.is_valid():
infos = Customer.objects.get(user__id=user_id).billing_infos
for field in form.fields:
infos.__dict__[field] = form[field].value()
infos.save()
content = json.dumps({"errors": None})
return HttpResponse(status=200, content=content)
@login_required
@require_POST
def create_billing_info(request, user_id):
user = request.user
if user.id != user_id and not user.has_perm("counter:add_billinginfo"):
raise PermissionDenied()
user = get_object_or_404(User, pk=user_id)
2022-11-23 11:23:17 +00:00
customer, _ = Customer.get_or_create(user)
BillingInfo.objects.create(customer=customer)
2024-06-27 12:57:40 +00:00
return __manage_billing_info_req(request, user_id, delete_if_fail=True)
@login_required
@require_POST
def edit_billing_info(request, user_id):
user = request.user
if user.id != user_id and not user.has_perm("counter:change_billinginfo"):
raise PermissionDenied()
user = get_object_or_404(User, pk=user_id)
if not hasattr(user, "customer"):
raise Http404
if not hasattr(user.customer, "billing_infos"):
raise Http404
return __manage_billing_info_req(request, user_id)