mirror of
https://github.com/ae-utbm/sith.git
synced 2025-05-19 16:57:28 +00:00
Don't use cookies for processing eboutic baskets
This commit is contained in:
parent
4fa83d0667
commit
262ed7eb4c
112
counter/forms.py
112
counter/forms.py
@ -1,4 +1,7 @@
|
|||||||
|
import math
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||||
|
|
||||||
@ -261,3 +264,112 @@ class CloseCustomerAccountForm(forms.Form):
|
|||||||
widget=AutoCompleteSelectUser,
|
widget=AutoCompleteSelectUser,
|
||||||
queryset=User.objects.all(),
|
queryset=User.objects.all(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductForm(forms.Form):
|
||||||
|
quantity = forms.IntegerField(min_value=1, required=True)
|
||||||
|
id = forms.IntegerField(min_value=0, required=True)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
customer: Customer,
|
||||||
|
counter: Counter,
|
||||||
|
allowed_products: dict[int, Product],
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
self.customer = customer # Used by formset
|
||||||
|
self.counter = counter # Used by formset
|
||||||
|
self.allowed_products = allowed_products
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean_id(self):
|
||||||
|
data = self.cleaned_data["id"]
|
||||||
|
|
||||||
|
# We store self.product so we can use it later on the formset validation
|
||||||
|
# And also in the global clean
|
||||||
|
self.product = self.allowed_products.get(data, None)
|
||||||
|
if self.product is None:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_("The selected product isn't available for this user")
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
if len(self.errors) > 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Compute prices
|
||||||
|
cleaned_data["bonus_quantity"] = 0
|
||||||
|
if self.product.tray:
|
||||||
|
cleaned_data["bonus_quantity"] = math.floor(
|
||||||
|
cleaned_data["quantity"] / Product.QUANTITY_FOR_TRAY_PRICE
|
||||||
|
)
|
||||||
|
cleaned_data["total_price"] = self.product.price * (
|
||||||
|
cleaned_data["quantity"] - cleaned_data["bonus_quantity"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class BaseBasketForm(forms.BaseFormSet):
|
||||||
|
def clean(self):
|
||||||
|
self.forms = [form for form in self.forms if form.cleaned_data != {}]
|
||||||
|
|
||||||
|
if len(self.forms) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._check_forms_have_errors()
|
||||||
|
self._check_product_are_unique()
|
||||||
|
self._check_recorded_products(self[0].customer)
|
||||||
|
self._check_enough_money(self[0].counter, self[0].customer)
|
||||||
|
|
||||||
|
def _check_forms_have_errors(self):
|
||||||
|
if any(len(form.errors) > 0 for form in self):
|
||||||
|
raise forms.ValidationError(_("Submitted basket is invalid"))
|
||||||
|
|
||||||
|
def _check_product_are_unique(self):
|
||||||
|
product_ids = {form.cleaned_data["id"] for form in self.forms}
|
||||||
|
if len(product_ids) != len(self.forms):
|
||||||
|
raise forms.ValidationError(_("Duplicated product entries."))
|
||||||
|
|
||||||
|
def _check_enough_money(self, counter: Counter, customer: Customer):
|
||||||
|
self.total_price = sum([data["total_price"] for data in self.cleaned_data])
|
||||||
|
if self.total_price > customer.amount:
|
||||||
|
raise forms.ValidationError(_("Not enough money"))
|
||||||
|
|
||||||
|
def _check_recorded_products(self, customer: Customer):
|
||||||
|
"""Check for, among other things, ecocups and pitchers"""
|
||||||
|
items = {
|
||||||
|
form.cleaned_data["id"]: form.cleaned_data["quantity"]
|
||||||
|
for form in self.forms
|
||||||
|
}
|
||||||
|
ids = list(items.keys())
|
||||||
|
returnables = list(
|
||||||
|
ReturnableProduct.objects.filter(
|
||||||
|
Q(product_id__in=ids) | Q(returned_product_id__in=ids)
|
||||||
|
).annotate_balance_for(customer)
|
||||||
|
)
|
||||||
|
limit_reached = []
|
||||||
|
for returnable in returnables:
|
||||||
|
returnable.balance += items.get(returnable.product_id, 0)
|
||||||
|
for returnable in returnables:
|
||||||
|
dcons = items.get(returnable.returned_product_id, 0)
|
||||||
|
returnable.balance -= dcons
|
||||||
|
if dcons and returnable.balance < -returnable.max_return:
|
||||||
|
limit_reached.append(returnable.returned_product)
|
||||||
|
if limit_reached:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
_(
|
||||||
|
"This user have reached his recording limit "
|
||||||
|
"for the following products : %s"
|
||||||
|
)
|
||||||
|
% ", ".join([str(p) for p in limit_reached])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
BasketForm = forms.formset_factory(
|
||||||
|
ProductForm, formset=BaseBasketForm, absolute_max=None, min_num=1
|
||||||
|
)
|
||||||
|
@ -12,23 +12,14 @@
|
|||||||
# OR WITHIN THE LOCAL FILE "LICENSE"
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
import math
|
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.forms import (
|
|
||||||
BaseFormSet,
|
|
||||||
Form,
|
|
||||||
IntegerField,
|
|
||||||
ValidationError,
|
|
||||||
formset_factory,
|
|
||||||
)
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
from django.shortcuts import get_object_or_404, redirect, resolve_url
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from ninja.main import HttpRequest
|
from ninja.main import HttpRequest
|
||||||
@ -36,11 +27,10 @@ from ninja.main import HttpRequest
|
|||||||
from core.auth.mixins import CanViewMixin
|
from core.auth.mixins import CanViewMixin
|
||||||
from core.models import User
|
from core.models import User
|
||||||
from core.views.mixins import FragmentMixin, UseFragmentsMixin
|
from core.views.mixins import FragmentMixin, UseFragmentsMixin
|
||||||
from counter.forms import RefillForm
|
from counter.forms import BasketForm, RefillForm
|
||||||
from counter.models import (
|
from counter.models import (
|
||||||
Counter,
|
Counter,
|
||||||
Customer,
|
Customer,
|
||||||
Product,
|
|
||||||
ReturnableProduct,
|
ReturnableProduct,
|
||||||
Selling,
|
Selling,
|
||||||
)
|
)
|
||||||
@ -57,113 +47,6 @@ def get_operator(request: HttpRequest, counter: Counter, customer: Customer) ->
|
|||||||
return counter.get_random_barman()
|
return counter.get_random_barman()
|
||||||
|
|
||||||
|
|
||||||
class ProductForm(Form):
|
|
||||||
quantity = IntegerField(min_value=1)
|
|
||||||
id = IntegerField(min_value=0)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
customer: Customer,
|
|
||||||
counter: Counter,
|
|
||||||
allowed_products: dict[int, Product],
|
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
self.customer = customer # Used by formset
|
|
||||||
self.counter = counter # Used by formset
|
|
||||||
self.allowed_products = allowed_products
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def clean_id(self):
|
|
||||||
data = self.cleaned_data["id"]
|
|
||||||
|
|
||||||
# We store self.product so we can use it later on the formset validation
|
|
||||||
# And also in the global clean
|
|
||||||
self.product = self.allowed_products.get(data, None)
|
|
||||||
if self.product is None:
|
|
||||||
raise ValidationError(
|
|
||||||
_("The selected product isn't available for this user")
|
|
||||||
)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
cleaned_data = super().clean()
|
|
||||||
if len(self.errors) > 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Compute prices
|
|
||||||
cleaned_data["bonus_quantity"] = 0
|
|
||||||
if self.product.tray:
|
|
||||||
cleaned_data["bonus_quantity"] = math.floor(
|
|
||||||
cleaned_data["quantity"] / Product.QUANTITY_FOR_TRAY_PRICE
|
|
||||||
)
|
|
||||||
cleaned_data["total_price"] = self.product.price * (
|
|
||||||
cleaned_data["quantity"] - cleaned_data["bonus_quantity"]
|
|
||||||
)
|
|
||||||
|
|
||||||
return cleaned_data
|
|
||||||
|
|
||||||
|
|
||||||
class BaseBasketForm(BaseFormSet):
|
|
||||||
def clean(self):
|
|
||||||
if len(self.forms) == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._check_forms_have_errors()
|
|
||||||
self._check_product_are_unique()
|
|
||||||
self._check_recorded_products(self[0].customer)
|
|
||||||
self._check_enough_money(self[0].counter, self[0].customer)
|
|
||||||
|
|
||||||
def _check_forms_have_errors(self):
|
|
||||||
if any(len(form.errors) > 0 for form in self):
|
|
||||||
raise ValidationError(_("Submitted basket is invalid"))
|
|
||||||
|
|
||||||
def _check_product_are_unique(self):
|
|
||||||
product_ids = {form.cleaned_data["id"] for form in self.forms}
|
|
||||||
if len(product_ids) != len(self.forms):
|
|
||||||
raise ValidationError(_("Duplicated product entries."))
|
|
||||||
|
|
||||||
def _check_enough_money(self, counter: Counter, customer: Customer):
|
|
||||||
self.total_price = sum([data["total_price"] for data in self.cleaned_data])
|
|
||||||
if self.total_price > customer.amount:
|
|
||||||
raise ValidationError(_("Not enough money"))
|
|
||||||
|
|
||||||
def _check_recorded_products(self, customer: Customer):
|
|
||||||
"""Check for, among other things, ecocups and pitchers"""
|
|
||||||
items = {
|
|
||||||
form.cleaned_data["id"]: form.cleaned_data["quantity"]
|
|
||||||
for form in self.forms
|
|
||||||
}
|
|
||||||
ids = list(items.keys())
|
|
||||||
returnables = list(
|
|
||||||
ReturnableProduct.objects.filter(
|
|
||||||
Q(product_id__in=ids) | Q(returned_product_id__in=ids)
|
|
||||||
).annotate_balance_for(customer)
|
|
||||||
)
|
|
||||||
limit_reached = []
|
|
||||||
for returnable in returnables:
|
|
||||||
returnable.balance += items.get(returnable.product_id, 0)
|
|
||||||
for returnable in returnables:
|
|
||||||
dcons = items.get(returnable.returned_product_id, 0)
|
|
||||||
returnable.balance -= dcons
|
|
||||||
if dcons and returnable.balance < -returnable.max_return:
|
|
||||||
limit_reached.append(returnable.returned_product)
|
|
||||||
if limit_reached:
|
|
||||||
raise ValidationError(
|
|
||||||
_(
|
|
||||||
"This user have reached his recording limit "
|
|
||||||
"for the following products : %s"
|
|
||||||
)
|
|
||||||
% ", ".join([str(p) for p in limit_reached])
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
BasketForm = formset_factory(
|
|
||||||
ProductForm, formset=BaseBasketForm, absolute_max=None, min_num=1
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CounterClick(
|
class CounterClick(
|
||||||
CounterTabsMixin, UseFragmentsMixin, CanViewMixin, SingleObjectMixin, FormView
|
CounterTabsMixin, UseFragmentsMixin, CanViewMixin, SingleObjectMixin, FormView
|
||||||
):
|
):
|
||||||
|
128
eboutic/forms.py
128
eboutic/forms.py
@ -1,128 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright 2022
|
|
||||||
# - Maréchal <thgirod@hotmail.com
|
|
||||||
#
|
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
|
||||||
# http://ae.utbm.fr.
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify it under
|
|
||||||
# the terms of the GNU General Public License a published by the Free Software
|
|
||||||
# Foundation; either version 3 of the License, or (at your option) any later
|
|
||||||
# version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
||||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
||||||
# details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License along with
|
|
||||||
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
|
||||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
from functools import cached_property
|
|
||||||
from urllib.parse import unquote
|
|
||||||
|
|
||||||
from django.http import HttpRequest
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from pydantic import ValidationError
|
|
||||||
|
|
||||||
from eboutic.models import get_eboutic_products
|
|
||||||
from eboutic.schemas import PurchaseItemList, PurchaseItemSchema
|
|
||||||
|
|
||||||
|
|
||||||
class BasketForm:
|
|
||||||
"""Class intended to perform checks on the request sended to the server when
|
|
||||||
the user submits his basket from /eboutic/.
|
|
||||||
|
|
||||||
Because it must check an unknown number of fields, coming from a cookie
|
|
||||||
and needing some databases checks to be performed, inheriting from forms.Form
|
|
||||||
or using formset would have been likely to end in a big ball of wibbly-wobbly hacky stuff.
|
|
||||||
Thus this class is a pure standalone and performs its operations by its own means.
|
|
||||||
However, it still tries to share some similarities with a standard django Form.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
::
|
|
||||||
|
|
||||||
def my_view(request):
|
|
||||||
form = BasketForm(request)
|
|
||||||
form.clean()
|
|
||||||
if form.is_valid():
|
|
||||||
# perform operations
|
|
||||||
else:
|
|
||||||
errors = form.get_error_messages()
|
|
||||||
|
|
||||||
# return the cookie that was in the request, but with all
|
|
||||||
# incorrects elements removed
|
|
||||||
cookie = form.get_cleaned_cookie()
|
|
||||||
|
|
||||||
You can also use a little shortcut by directly calling `form.is_valid()`
|
|
||||||
without calling `form.clean()`. In this case, the latter method shall be
|
|
||||||
implicitly called.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, request: HttpRequest):
|
|
||||||
self.user = request.user
|
|
||||||
self.cookies = request.COOKIES
|
|
||||||
self.error_messages = set()
|
|
||||||
self.correct_items = []
|
|
||||||
|
|
||||||
def clean(self) -> None:
|
|
||||||
"""Perform all the checks, but return nothing.
|
|
||||||
To know if the form is valid, the `is_valid()` method must be used.
|
|
||||||
|
|
||||||
The form shall be considered as valid if it meets all the following conditions :
|
|
||||||
- it contains a "basket_items" key in the cookies of the request given in the constructor
|
|
||||||
- this cookie is a list of objects formatted this way : `[{'id': <int>, 'quantity': <int>,
|
|
||||||
'name': <str>, 'unit_price': <float>}, ...]`. The order of the fields in each object does not matter
|
|
||||||
- all the ids are positive integers
|
|
||||||
- all the ids refer to products available in the EBOUTIC
|
|
||||||
- all the ids refer to products the user is allowed to buy
|
|
||||||
- all the quantities are positive integers
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
basket = PurchaseItemList.validate_json(
|
|
||||||
unquote(self.cookies.get("basket_items", "[]"))
|
|
||||||
)
|
|
||||||
except ValidationError:
|
|
||||||
self.error_messages.add(_("The request was badly formatted."))
|
|
||||||
return
|
|
||||||
if len(basket) == 0:
|
|
||||||
self.error_messages.add(_("Your basket is empty."))
|
|
||||||
return
|
|
||||||
existing_ids = {product.id for product in get_eboutic_products(self.user)}
|
|
||||||
for item in basket:
|
|
||||||
# check a product with this id does exist
|
|
||||||
if item.product_id in existing_ids:
|
|
||||||
self.correct_items.append(item)
|
|
||||||
else:
|
|
||||||
self.error_messages.add(
|
|
||||||
_(
|
|
||||||
"%(name)s : this product does not exist or may no longer be available."
|
|
||||||
)
|
|
||||||
% {"name": item.name}
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
# this function does not return anything.
|
|
||||||
# instead, it fills a set containing the collected error messages
|
|
||||||
# an empty set means that no error was seen thus everything is ok
|
|
||||||
# and the form is valid.
|
|
||||||
# a non-empty set means there was at least one error thus
|
|
||||||
# the form is invalid
|
|
||||||
|
|
||||||
def is_valid(self) -> bool:
|
|
||||||
"""Return True if the form is correct else False.
|
|
||||||
|
|
||||||
If the `clean()` method has not been called beforehand, call it.
|
|
||||||
"""
|
|
||||||
if not self.error_messages and not self.correct_items:
|
|
||||||
self.clean()
|
|
||||||
return not self.error_messages
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def errors(self) -> list[str]:
|
|
||||||
return list(self.error_messages)
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def cleaned_data(self) -> list[PurchaseItemSchema]:
|
|
||||||
return self.correct_items
|
|
@ -40,6 +40,7 @@ def get_eboutic_products(user: User) -> list[Product]:
|
|||||||
.annotate(order=F("product_type__order"))
|
.annotate(order=F("product_type__order"))
|
||||||
.annotate(category=F("product_type__name"))
|
.annotate(category=F("product_type__name"))
|
||||||
.annotate(category_comment=F("product_type__comment"))
|
.annotate(category_comment=F("product_type__comment"))
|
||||||
|
.annotate(price=F("selling_price"))
|
||||||
.prefetch_related("buying_groups") # <-- used in `Product.can_be_sold_to`
|
.prefetch_related("buying_groups") # <-- used in `Product.can_be_sold_to`
|
||||||
)
|
)
|
||||||
return [p for p in products if p.can_be_sold_to(user)]
|
return [p for p in products if p.can_be_sold_to(user)]
|
||||||
@ -84,6 +85,9 @@ class Basket(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user}'s basket ({self.items.all().count()} items)"
|
return f"{self.user}'s basket ({self.items.all().count()} items)"
|
||||||
|
|
||||||
|
def can_be_viewed_by(self, user):
|
||||||
|
return self.user == user
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def contains_refilling_item(self) -> bool:
|
def contains_refilling_item(self) -> bool:
|
||||||
return self.items.filter(
|
return self.items.filter(
|
||||||
@ -139,7 +143,7 @@ class Basket(models.Model):
|
|||||||
club=product.club,
|
club=product.club,
|
||||||
product=product,
|
product=product,
|
||||||
seller=seller,
|
seller=seller,
|
||||||
customer=self.user.customer,
|
customer=Customer.get_or_create(self.user)[0],
|
||||||
unit_price=item.product_unit_price,
|
unit_price=item.product_unit_price,
|
||||||
quantity=item.quantity,
|
quantity=item.quantity,
|
||||||
payment_method=payment_method,
|
payment_method=payment_method,
|
||||||
|
@ -8,55 +8,44 @@ interface BasketItem {
|
|||||||
unit_price: number;
|
unit_price: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BASKET_ITEMS_COOKIE_NAME: string = "basket_items";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for a cookie by name
|
|
||||||
* @param name Name of the cookie to get
|
|
||||||
* @returns the value of the cookie or null if it does not exist, undefined if not found
|
|
||||||
*/
|
|
||||||
function getCookie(name: string): string | null | undefined {
|
|
||||||
if (!document.cookie || document.cookie.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const found = document.cookie
|
|
||||||
.split(";")
|
|
||||||
.map((c) => c.trim())
|
|
||||||
.find((c) => c.startsWith(`${name}=`));
|
|
||||||
|
|
||||||
return found === undefined ? undefined : decodeURIComponent(found.split("=")[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the basket items from the associated cookie
|
|
||||||
* @returns the items in the basket
|
|
||||||
*/
|
|
||||||
function getStartingItems(): BasketItem[] {
|
|
||||||
const cookie = getCookie(BASKET_ITEMS_COOKIE_NAME);
|
|
||||||
if (!cookie) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// Django cookie backend converts `,` to `\054`
|
|
||||||
let parsed = JSON.parse(cookie.replace(/\\054/g, ","));
|
|
||||||
if (typeof parsed === "string") {
|
|
||||||
// In some conditions, a second parsing is needed
|
|
||||||
parsed = JSON.parse(parsed);
|
|
||||||
}
|
|
||||||
const res = Array.isArray(parsed) ? parsed : [];
|
|
||||||
return res.filter((i) => !!document.getElementById(i.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("alpine:init", () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
Alpine.data("basket", () => ({
|
Alpine.data("basket", () => ({
|
||||||
items: getStartingItems() as BasketItem[],
|
basket: [] as BasketItem[],
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.basket = this.loadBasket();
|
||||||
|
this.$watch("basket", () => {
|
||||||
|
this.saveBasket();
|
||||||
|
});
|
||||||
|
|
||||||
|
// It's quite tricky to manually apply attributes to the management part
|
||||||
|
// of a formset so we dynamically apply it here
|
||||||
|
this.$refs.basketManagementForm
|
||||||
|
.querySelector("#id_form-TOTAL_FORMS")
|
||||||
|
.setAttribute(":value", "basket.length");
|
||||||
|
},
|
||||||
|
|
||||||
|
loadBasket(): BasketItem[] {
|
||||||
|
if (localStorage.basket === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.basket);
|
||||||
|
} catch (_err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveBasket() {
|
||||||
|
localStorage.basket = JSON.stringify(this.basket);
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the total price of the basket
|
* Get the total price of the basket
|
||||||
* @returns {number} The total price of the basket
|
* @returns {number} The total price of the basket
|
||||||
*/
|
*/
|
||||||
getTotal() {
|
getTotal() {
|
||||||
return this.items.reduce(
|
return this.basket.reduce(
|
||||||
(acc: number, item: BasketItem) => acc + item.quantity * item.unit_price,
|
(acc: number, item: BasketItem) => acc + item.quantity * item.unit_price,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
@ -68,7 +57,6 @@ document.addEventListener("alpine:init", () => {
|
|||||||
*/
|
*/
|
||||||
add(item: BasketItem) {
|
add(item: BasketItem) {
|
||||||
item.quantity++;
|
item.quantity++;
|
||||||
this.setCookies();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,39 +64,25 @@ document.addEventListener("alpine:init", () => {
|
|||||||
* @param itemId the id of the item to remove
|
* @param itemId the id of the item to remove
|
||||||
*/
|
*/
|
||||||
remove(itemId: number) {
|
remove(itemId: number) {
|
||||||
const index = this.items.findIndex((e: BasketItem) => e.id === itemId);
|
const index = this.basket.findIndex((e: BasketItem) => e.id === itemId);
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.items[index].quantity -= 1;
|
this.basket[index].quantity -= 1;
|
||||||
|
|
||||||
if (this.items[index].quantity === 0) {
|
if (this.basket[index].quantity === 0) {
|
||||||
this.items = this.items.filter(
|
this.basket = this.basket.filter(
|
||||||
(e: BasketItem) => e.id !== this.items[index].id,
|
(e: BasketItem) => e.id !== this.basket[index].id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.setCookies();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove all the items from the basket & cleans the catalog CSS classes
|
* Remove all the basket from the basket & cleans the catalog CSS classes
|
||||||
*/
|
*/
|
||||||
clearBasket() {
|
clearBasket() {
|
||||||
this.items = [];
|
this.basket = [];
|
||||||
this.setCookies();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the cookie in the browser with the basket items
|
|
||||||
* ! the cookie survives an hour
|
|
||||||
*/
|
|
||||||
setCookies() {
|
|
||||||
if (this.items.length === 0) {
|
|
||||||
document.cookie = `${BASKET_ITEMS_COOKIE_NAME}=;Max-Age=0`;
|
|
||||||
} else {
|
|
||||||
document.cookie = `${BASKET_ITEMS_COOKIE_NAME}=${encodeURIComponent(JSON.stringify(this.items))};Max-Age=3600`;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,7 +101,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
unit_price: price,
|
unit_price: price,
|
||||||
} as BasketItem;
|
} as BasketItem;
|
||||||
|
|
||||||
this.items.push(newItem);
|
this.basket.push(newItem);
|
||||||
this.add(newItem);
|
this.add(newItem);
|
||||||
|
|
||||||
return newItem;
|
return newItem;
|
||||||
@ -141,7 +115,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
* @param price The unit price of the product
|
* @param price The unit price of the product
|
||||||
*/
|
*/
|
||||||
addFromCatalog(id: number, name: string, price: number) {
|
addFromCatalog(id: number, name: string, price: number) {
|
||||||
let item = this.items.find((e: BasketItem) => e.id === id);
|
let item = this.basket.find((e: BasketItem) => e.id === id);
|
||||||
|
|
||||||
// if the item is not in the basket, we create it
|
// if the item is not in the basket, we create it
|
||||||
// else we add + 1 to it
|
// else we add + 1 to it
|
||||||
|
@ -24,10 +24,16 @@
|
|||||||
<div id="eboutic" x-data="basket">
|
<div id="eboutic" x-data="basket">
|
||||||
<div id="basket">
|
<div id="basket">
|
||||||
<h3>Panier</h3>
|
<h3>Panier</h3>
|
||||||
{% if errors %}
|
<form method="post" action="">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div x-ref="basketManagementForm">
|
||||||
|
{{ form.management_form }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if form.non_form_errors() %}
|
||||||
<div class="alert alert-red">
|
<div class="alert alert-red">
|
||||||
<div class="alert-main">
|
<div class="alert-main">
|
||||||
{% for error in errors %}
|
{% for error in form.non_form_errors() %}
|
||||||
<p style="margin: 0">{{ error }}</p>
|
<p style="margin: 0">{{ error }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@ -43,7 +49,8 @@
|
|||||||
<strong>{{ "%0.2f"|format(customer_amount) }} €</strong>
|
<strong>{{ "%0.2f"|format(customer_amount) }} €</strong>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<template x-for="item in items" :key="item.id">
|
|
||||||
|
<template x-for="(item, index) in Object.values(basket)">
|
||||||
<li class="item-row" x-show="item.quantity > 0">
|
<li class="item-row" x-show="item.quantity > 0">
|
||||||
<div class="item-quantity">
|
<div class="item-quantity">
|
||||||
<i class="fa fa-minus fa-xs" @click="remove(item.id)"></i>
|
<i class="fa fa-minus fa-xs" @click="remove(item.id)"></i>
|
||||||
@ -52,6 +59,24 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="item-name" x-text="item.name"></span>
|
<span class="item-name" x-text="item.name"></span>
|
||||||
<span class="item-price" x-text="(item.unit_price * item.quantity).toFixed(2) + ' €'"></span>
|
<span class="item-price" x-text="(item.unit_price * item.quantity).toFixed(2) + ' €'"></span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
:value="item.quantity"
|
||||||
|
:id="`id_form-${index}-quantity`"
|
||||||
|
:name="`form-${index}-quantity`"
|
||||||
|
required
|
||||||
|
readonly
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
:value="item.id"
|
||||||
|
:id="`id_form-${index}-id`"
|
||||||
|
:name="`form-${index}-id`"
|
||||||
|
required
|
||||||
|
readonly
|
||||||
|
>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
{# Total price #}
|
{# Total price #}
|
||||||
@ -61,11 +86,10 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="catalog-buttons">
|
<div class="catalog-buttons">
|
||||||
<button @click="clearBasket()" class="btn btn-grey">
|
<button @click.prevent="clearBasket()" class="btn btn-grey">
|
||||||
<i class="fa fa-trash"></i>
|
<i class="fa fa-trash"></i>
|
||||||
{% trans %}Clear{% endtrans %}
|
{% trans %}Clear{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
<form method="get" action="{{ url('eboutic:command') }}">
|
|
||||||
<button class="btn btn-blue">
|
<button class="btn btn-blue">
|
||||||
<i class="fa fa-check"></i>
|
<i class="fa fa-check"></i>
|
||||||
<input type="submit" value="{% trans %}Validate{% endtrans %}"/>
|
<input type="submit" value="{% trans %}Validate{% endtrans %}"/>
|
||||||
@ -158,7 +182,7 @@
|
|||||||
<button
|
<button
|
||||||
id="{{ p.id }}"
|
id="{{ p.id }}"
|
||||||
class="card product-button clickable shadow"
|
class="card product-button clickable shadow"
|
||||||
:class="{selected: items.some((i) => i.id === {{ p.id }})}"
|
:class="{selected: basket.some((i) => i.id === {{ p.id }})}"
|
||||||
@click='addFromCatalog({{ p.id }}, {{ p.name|tojson }}, {{ p.selling_price }})'
|
@click='addFromCatalog({{ p.id }}, {{ p.name|tojson }}, {{ p.selling_price }})'
|
||||||
>
|
>
|
||||||
{% if p.icon %}
|
{% if p.icon %}
|
||||||
|
@ -82,9 +82,8 @@
|
|||||||
{% elif basket.total > user.account_balance %}
|
{% elif basket.total > user.account_balance %}
|
||||||
<p>{% trans %}AE account payment disabled because you do not have enough money remaining.{% endtrans %}</p>
|
<p>{% trans %}AE account payment disabled because you do not have enough money remaining.{% endtrans %}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="post" action="{{ url('eboutic:pay_with_sith') }}" name="sith-pay-form">
|
<form method="post" action="{{ url('eboutic:pay_with_sith', basket_id=basket.id) }}" name="sith-pay-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="pay_with_sith_account">
|
|
||||||
<input class="btn btn-blue" type="submit" value="{% trans %}Pay with Sith account{% endtrans %}"/>
|
<input class="btn btn-blue" type="submit" value="{% trans %}Pay with Sith account{% endtrans %}"/>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -28,10 +28,10 @@ from eboutic.converters import PaymentResultConverter
|
|||||||
from eboutic.views import (
|
from eboutic.views import (
|
||||||
BillingInfoFormFragment,
|
BillingInfoFormFragment,
|
||||||
EbouticCommand,
|
EbouticCommand,
|
||||||
|
EbouticCreateBasket,
|
||||||
|
EbouticPayWithSith,
|
||||||
EtransactionAutoAnswer,
|
EtransactionAutoAnswer,
|
||||||
EurokPartnerFragment,
|
EurokPartnerFragment,
|
||||||
eboutic_main,
|
|
||||||
pay_with_sith,
|
|
||||||
payment_result,
|
payment_result,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,10 +39,12 @@ register_converter(PaymentResultConverter, "res")
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Subscription views
|
# Subscription views
|
||||||
path("", eboutic_main, name="main"),
|
path("", EbouticCreateBasket.as_view(), name="main"),
|
||||||
path("command/", EbouticCommand.as_view(), name="command"),
|
path("command/<int:basket_id>", EbouticCommand.as_view(), name="command"),
|
||||||
path("billing-infos/", BillingInfoFormFragment.as_view(), name="billing_infos"),
|
path("billing-infos/", BillingInfoFormFragment.as_view(), name="billing_infos"),
|
||||||
path("pay/sith/", pay_with_sith, name="pay_with_sith"),
|
path(
|
||||||
|
"pay/sith/<int:basket_id>", EbouticPayWithSith.as_view(), name="pay_with_sith"
|
||||||
|
),
|
||||||
path("pay/<res:result>/", payment_result, name="payment_result"),
|
path("pay/<res:result>/", payment_result, name="payment_result"),
|
||||||
path("eurok/", EurokPartnerFragment.as_view(), name="eurok"),
|
path("eurok/", EurokPartnerFragment.as_view(), name="eurok"),
|
||||||
path(
|
path(
|
||||||
|
160
eboutic/views.py
160
eboutic/views.py
@ -35,21 +35,21 @@ from django.contrib.auth.mixins import (
|
|||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.db import DatabaseError, transaction
|
from django.db import DatabaseError, transaction
|
||||||
|
from django.db.models.fields import forms
|
||||||
from django.db.utils import cached_property
|
from django.db.utils import cached_property
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.decorators.http import require_GET, require_POST
|
from django.views.decorators.http import require_GET
|
||||||
from django.views.generic import TemplateView, UpdateView, View
|
from django.views.generic import DetailView, FormView, TemplateView, UpdateView, View
|
||||||
|
from django.views.generic.edit import SingleObjectMixin
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
|
|
||||||
from core.auth.mixins import IsSubscriberMixin
|
from core.auth.mixins import CanViewMixin, IsSubscriberMixin
|
||||||
from core.views.mixins import FragmentMixin, UseFragmentsMixin
|
from core.views.mixins import FragmentMixin, UseFragmentsMixin
|
||||||
from counter.forms import BillingInfoForm
|
from counter.forms import BaseBasketForm, BillingInfoForm, ProductForm
|
||||||
from counter.models import BillingInfo, Counter, Customer, Product
|
from counter.models import BillingInfo, Counter, Customer, Product
|
||||||
from eboutic.forms import BasketForm
|
|
||||||
from eboutic.models import (
|
from eboutic.models import (
|
||||||
Basket,
|
Basket,
|
||||||
BasketItem,
|
BasketItem,
|
||||||
@ -58,39 +58,69 @@ from eboutic.models import (
|
|||||||
InvoiceItem,
|
InvoiceItem,
|
||||||
get_eboutic_products,
|
get_eboutic_products,
|
||||||
)
|
)
|
||||||
from eboutic.schemas import PurchaseItemList, PurchaseItemSchema
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
||||||
from django.utils.html import SafeString
|
from django.utils.html import SafeString
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class BaseEbouticBasketForm(BaseBasketForm):
|
||||||
@require_GET
|
def _check_enough_money(self, *args, **kwargs):
|
||||||
def eboutic_main(request: HttpRequest) -> HttpResponse:
|
# Disable money check
|
||||||
"""Main view of the eboutic application.
|
...
|
||||||
|
|
||||||
Return an Http response whose content is of type text/html.
|
|
||||||
The latter represents the page from which a user can see
|
EbouticBasketForm = forms.formset_factory(
|
||||||
the catalogue of products that he can buy and fill
|
ProductForm, formset=BaseEbouticBasketForm, absolute_max=None, min_num=1
|
||||||
his shopping cart.
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EbouticCreateBasket(LoginRequiredMixin, FormView):
|
||||||
|
"""Main view of the eboutic application.
|
||||||
|
|
||||||
The purchasable products are those of the eboutic which
|
The purchasable products are those of the eboutic which
|
||||||
belong to a category of products of a product category
|
belong to a category of products of a product category
|
||||||
(orphan products are inaccessible).
|
(orphan products are inaccessible).
|
||||||
|
|
||||||
If the session contains a key-value pair that associates "errors"
|
|
||||||
with a list of strings, this pair is removed from the session
|
|
||||||
and its value displayed to the user when the page is rendered.
|
|
||||||
"""
|
"""
|
||||||
errors = request.session.pop("errors", None)
|
|
||||||
products = get_eboutic_products(request.user)
|
template_name = "eboutic/eboutic_main.jinja"
|
||||||
context = {
|
form_class = EbouticBasketForm
|
||||||
"errors": errors,
|
|
||||||
"products": products,
|
def get_form_kwargs(self):
|
||||||
"customer_amount": request.user.account_balance,
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs["form_kwargs"] = {
|
||||||
|
"customer": Customer.get_or_create(self.request.user)[0],
|
||||||
|
"counter": Counter.objects.get(type="EBOUTIC"),
|
||||||
|
"allowed_products": {product.id: product for product in self.products},
|
||||||
}
|
}
|
||||||
return render(request, "eboutic/eboutic_main.jinja", context)
|
return kwargs
|
||||||
|
|
||||||
|
def form_valid(self, formset):
|
||||||
|
if len(formset) == 0:
|
||||||
|
return self.form_invalid(formset)
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
self.basket = Basket.objects.create(user=self.request.user)
|
||||||
|
for form in formset:
|
||||||
|
BasketItem.from_product(
|
||||||
|
form.product, form.cleaned_data["quantity"], self.basket
|
||||||
|
).save()
|
||||||
|
self.basket.save()
|
||||||
|
return super().form_valid(formset)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse("eboutic:command", kwargs={"basket_id": self.basket.id})
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def products(self) -> list[Product]:
|
||||||
|
return get_eboutic_products(self.request.user)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context["products"] = self.products
|
||||||
|
context["customer_amount"] = self.request.user.account_balance
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
@require_GET
|
@require_GET
|
||||||
@ -166,48 +196,15 @@ class BillingInfoFormFragment(
|
|||||||
return self.request.path
|
return self.request.path
|
||||||
|
|
||||||
|
|
||||||
class EbouticCommand(LoginRequiredMixin, UseFragmentsMixin, TemplateView):
|
class EbouticCommand(CanViewMixin, UseFragmentsMixin, DetailView):
|
||||||
|
model = Basket
|
||||||
|
pk_url_kwarg = "basket_id"
|
||||||
|
context_object_name = "basket"
|
||||||
template_name = "eboutic/eboutic_makecommand.jinja"
|
template_name = "eboutic/eboutic_makecommand.jinja"
|
||||||
basket: Basket
|
|
||||||
fragments = {
|
fragments = {
|
||||||
"billing_infos_form": BillingInfoFormFragment,
|
"billing_infos_form": BillingInfoFormFragment,
|
||||||
}
|
}
|
||||||
|
|
||||||
@method_decorator(login_required)
|
|
||||||
def post(self, request, *args, **kwargs):
|
|
||||||
return redirect("eboutic:main")
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs):
|
|
||||||
form = BasketForm(request)
|
|
||||||
if not form.is_valid():
|
|
||||||
request.session["errors"] = form.errors
|
|
||||||
request.session.modified = True
|
|
||||||
res = redirect("eboutic:main")
|
|
||||||
res.set_cookie(
|
|
||||||
"basket_items",
|
|
||||||
PurchaseItemList.dump_json(form.cleaned_data, by_alias=True).decode(),
|
|
||||||
path="/eboutic",
|
|
||||||
)
|
|
||||||
return res
|
|
||||||
basket = Basket.from_session(request.session)
|
|
||||||
if basket is not None:
|
|
||||||
basket.items.all().delete()
|
|
||||||
else:
|
|
||||||
basket = Basket.objects.create(user=request.user)
|
|
||||||
request.session["basket_id"] = basket.id
|
|
||||||
request.session.modified = True
|
|
||||||
|
|
||||||
items: list[PurchaseItemSchema] = form.cleaned_data
|
|
||||||
pks = {item.product_id for item in items}
|
|
||||||
products = {p.pk: p for p in Product.objects.filter(pk__in=pks)}
|
|
||||||
db_items = []
|
|
||||||
for pk in pks:
|
|
||||||
quantity = sum(i.quantity for i in items if i.product_id == pk)
|
|
||||||
db_items.append(BasketItem.from_product(products[pk], quantity, basket))
|
|
||||||
BasketItem.objects.bulk_create(db_items)
|
|
||||||
self.basket = basket
|
|
||||||
return super().get(request)
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
if hasattr(self.request.user, "customer"):
|
if hasattr(self.request.user, "customer"):
|
||||||
@ -215,32 +212,32 @@ class EbouticCommand(LoginRequiredMixin, UseFragmentsMixin, TemplateView):
|
|||||||
kwargs["customer_amount"] = customer.amount
|
kwargs["customer_amount"] = customer.amount
|
||||||
else:
|
else:
|
||||||
kwargs["customer_amount"] = None
|
kwargs["customer_amount"] = None
|
||||||
kwargs["basket"] = self.basket
|
|
||||||
kwargs["billing_infos"] = {}
|
kwargs["billing_infos"] = {}
|
||||||
|
|
||||||
with contextlib.suppress(BillingInfo.DoesNotExist):
|
with contextlib.suppress(BillingInfo.DoesNotExist):
|
||||||
kwargs["billing_infos"] = json.dumps(
|
kwargs["billing_infos"] = json.dumps(
|
||||||
dict(self.basket.get_e_transaction_data())
|
dict(self.object.get_e_transaction_data())
|
||||||
)
|
)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class EbouticPayWithSith(CanViewMixin, SingleObjectMixin, View):
|
||||||
@require_POST
|
http_method_names = ["post"]
|
||||||
def pay_with_sith(request):
|
model = Basket
|
||||||
basket = Basket.from_session(request.session)
|
pk_url_kwarg = "basket_id"
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
basket = self.get_object()
|
||||||
refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
||||||
if basket is None or basket.items.filter(type_id=refilling).exists():
|
if basket.items.filter(type_id=refilling).exists():
|
||||||
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
_("You can't buy a refilling with sith money"),
|
||||||
|
)
|
||||||
return redirect("eboutic:main")
|
return redirect("eboutic:main")
|
||||||
c = Customer.objects.filter(user__id=basket.user_id).first()
|
|
||||||
if c is None:
|
|
||||||
return redirect("eboutic:main")
|
|
||||||
if c.amount < basket.total:
|
|
||||||
res = redirect("eboutic:payment_result", "failure")
|
|
||||||
res.delete_cookie("basket_items", "/eboutic")
|
|
||||||
return res
|
|
||||||
eboutic = Counter.objects.get(type="EBOUTIC")
|
eboutic = Counter.objects.get(type="EBOUTIC")
|
||||||
sales = basket.generate_sales(eboutic, c.user, "SITH_ACCOUNT")
|
sales = basket.generate_sales(eboutic, basket.user, "SITH_ACCOUNT")
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# Selling.save has some important business logic in it.
|
# Selling.save has some important business logic in it.
|
||||||
@ -248,8 +245,7 @@ def pay_with_sith(request):
|
|||||||
for sale in sales:
|
for sale in sales:
|
||||||
sale.save()
|
sale.save()
|
||||||
basket.delete()
|
basket.delete()
|
||||||
request.session.pop("basket_id", None)
|
return redirect("eboutic:payment_result", "success")
|
||||||
res = redirect("eboutic:payment_result", "success")
|
|
||||||
except DatabaseError as e:
|
except DatabaseError as e:
|
||||||
with sentry_sdk.push_scope() as scope:
|
with sentry_sdk.push_scope() as scope:
|
||||||
scope.user = {"username": request.user.username}
|
scope.user = {"username": request.user.username}
|
||||||
@ -257,9 +253,7 @@ def pay_with_sith(request):
|
|||||||
sentry_sdk.capture_message(
|
sentry_sdk.capture_message(
|
||||||
f"Erreur le {datetime.now()} dans eboutic.pay_with_sith"
|
f"Erreur le {datetime.now()} dans eboutic.pay_with_sith"
|
||||||
)
|
)
|
||||||
res = redirect("eboutic:payment_result", "failure")
|
return redirect("eboutic:payment_result", "failure")
|
||||||
res.delete_cookie("basket_items", "/eboutic")
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
class EtransactionAutoAnswer(View):
|
class EtransactionAutoAnswer(View):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user