mirror of
https://github.com/ae-utbm/sith.git
synced 2026-03-13 15:15:03 +00:00
use new price system in the eboutic
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
export {};
|
||||
|
||||
interface BasketItem {
|
||||
id: number;
|
||||
priceId: number;
|
||||
name: string;
|
||||
quantity: number;
|
||||
// biome-ignore lint/style/useNamingConvention: the python code is snake_case
|
||||
unit_price: number;
|
||||
unitPrice: number;
|
||||
}
|
||||
|
||||
// increment the key number if the data schema of the cached basket changes
|
||||
const BASKET_CACHE_KEY = "basket1";
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("basket", (lastPurchaseTime?: number) => ({
|
||||
basket: [] as BasketItem[],
|
||||
@@ -30,24 +32,24 @@ document.addEventListener("alpine:init", () => {
|
||||
// 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")
|
||||
.getElementById("#id_form-TOTAL_FORMS")
|
||||
.setAttribute(":value", "basket.length");
|
||||
},
|
||||
|
||||
loadBasket(): BasketItem[] {
|
||||
if (localStorage.basket === undefined) {
|
||||
if (localStorage.getItem(BASKET_CACHE_KEY) === null) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
return JSON.parse(localStorage.basket);
|
||||
return JSON.parse(localStorage.getItem(BASKET_CACHE_KEY));
|
||||
} catch (_err) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
saveBasket() {
|
||||
localStorage.basket = JSON.stringify(this.basket);
|
||||
localStorage.basketTimestamp = Date.now();
|
||||
localStorage.setItem(BASKET_CACHE_KEY, JSON.stringify(this.basket));
|
||||
localStorage.setItem("basketTimestamp", Date.now().toString());
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -56,7 +58,7 @@ document.addEventListener("alpine:init", () => {
|
||||
*/
|
||||
getTotal() {
|
||||
return this.basket.reduce(
|
||||
(acc: number, item: BasketItem) => acc + item.quantity * item.unit_price,
|
||||
(acc: number, item: BasketItem) => acc + item.quantity * item.unitPrice,
|
||||
0,
|
||||
);
|
||||
},
|
||||
@@ -74,7 +76,7 @@ document.addEventListener("alpine:init", () => {
|
||||
* @param itemId the id of the item to remove
|
||||
*/
|
||||
remove(itemId: number) {
|
||||
const index = this.basket.findIndex((e: BasketItem) => e.id === itemId);
|
||||
const index = this.basket.findIndex((e: BasketItem) => e.priceId === itemId);
|
||||
|
||||
if (index < 0) {
|
||||
return;
|
||||
@@ -83,7 +85,7 @@ document.addEventListener("alpine:init", () => {
|
||||
|
||||
if (this.basket[index].quantity === 0) {
|
||||
this.basket = this.basket.filter(
|
||||
(e: BasketItem) => e.id !== this.basket[index].id,
|
||||
(e: BasketItem) => e.priceId !== this.basket[index].id,
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -104,11 +106,10 @@ document.addEventListener("alpine:init", () => {
|
||||
*/
|
||||
createItem(id: number, name: string, price: number): BasketItem {
|
||||
const newItem = {
|
||||
id,
|
||||
priceId: id,
|
||||
name,
|
||||
quantity: 0,
|
||||
// biome-ignore lint/style/useNamingConvention: the python code is snake_case
|
||||
unit_price: price,
|
||||
unitPrice: price,
|
||||
} as BasketItem;
|
||||
|
||||
this.basket.push(newItem);
|
||||
@@ -125,7 +126,7 @@ document.addEventListener("alpine:init", () => {
|
||||
* @param price The unit price of the product
|
||||
*/
|
||||
addFromCatalog(id: number, name: string, price: number) {
|
||||
let item = this.basket.find((e: BasketItem) => e.id === id);
|
||||
let item = this.basket.find((e: BasketItem) => e.priceId === id);
|
||||
|
||||
// if the item is not in the basket, we create it
|
||||
// else we add + 1 to it
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
<tbody>
|
||||
{% for item in basket.items.all() %}
|
||||
<tr>
|
||||
<td>{{ item.product_name }}</td>
|
||||
<td>{{ item.label }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{{ item.product_unit_price }} €</td>
|
||||
<td>{{ item.unit_price }} €</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -51,15 +51,15 @@
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<template x-for="(item, index) in Object.values(basket)" :key="item.id">
|
||||
<template x-for="(item, index) in Object.values(basket)" :key="item.priceId">
|
||||
<li class="item-row" x-show="item.quantity > 0">
|
||||
<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.priceId)"></i>
|
||||
<span x-text="item.quantity"></span>
|
||||
<i class="fa fa-plus" @click="add(item)"></i>
|
||||
</div>
|
||||
<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.unitPrice * item.quantity).toFixed(2) + ' €'"></span>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
@@ -71,9 +71,9 @@
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
:value="item.id"
|
||||
:id="`id_form-${index}-id`"
|
||||
:name="`form-${index}-id`"
|
||||
:value="item.priceId"
|
||||
:id="`id_form-${index}-price_id`"
|
||||
:name="`form-${index}-price_id`"
|
||||
required
|
||||
readonly
|
||||
>
|
||||
@@ -116,45 +116,40 @@
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for priority_groups in products|groupby('order') %}
|
||||
{% for category, items in priority_groups.list|groupby('category') %}
|
||||
{% if items|count > 0 %}
|
||||
{% for prices in categories %}
|
||||
{% set category = prices[0].product.product_type %}
|
||||
<section>
|
||||
{# I would have wholeheartedly directly used the header element instead
|
||||
but it has already been made messy in core/style.scss #}
|
||||
<div class="category-header">
|
||||
<h3>{{ category }}</h3>
|
||||
{% if items[0].category_comment %}
|
||||
<p><i>{{ items[0].category_comment }}</i></p>
|
||||
<h3>{{ category.name }}</h3>
|
||||
{% if category.comment %}
|
||||
<p><i>{{ category.comment }}</i></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="product-group">
|
||||
{% for p in items %}
|
||||
{% for price in prices %}
|
||||
<button
|
||||
id="{{ p.id }}"
|
||||
id="{{ price.id }}"
|
||||
class="card product-button clickable shadow"
|
||||
:class="{selected: basket.some((i) => i.id === {{ p.id }})}"
|
||||
@click='addFromCatalog({{ p.id }}, {{ p.name|tojson }}, {{ p.selling_price }})'
|
||||
:class="{selected: basket.some((i) => i.priceId === {{ price.id }})}"
|
||||
@click='addFromCatalog({{ price.id }}, {{ price.full_label|tojson }}, {{ price.amount }})'
|
||||
>
|
||||
{% if p.icon %}
|
||||
{% if price.product.icon %}
|
||||
<img
|
||||
class="card-image"
|
||||
src="{{ p.icon.url }}"
|
||||
alt="image de {{ p.name }}"
|
||||
src="{{ price.product.icon.url }}"
|
||||
alt="image de {{ price.full_label }}"
|
||||
>
|
||||
{% else %}
|
||||
<i class="fa-regular fa-image fa-2x card-image"></i>
|
||||
{% endif %}
|
||||
<div class="card-content">
|
||||
<h4 class="card-title">{{ p.name }}</h4>
|
||||
<p>{{ p.selling_price }} €</p>
|
||||
<h4 class="card-title">{{ price.full_label }}</h4>
|
||||
<p>{{ price.amount }} €</p>
|
||||
</div>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>{% trans %}There are no items available for sale{% endtrans %}</p>
|
||||
{% endfor %}
|
||||
|
||||
@@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import contextlib
|
||||
import itertools
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -48,11 +49,11 @@ from django_countries.fields import Country
|
||||
|
||||
from core.auth.mixins import CanViewMixin
|
||||
from core.views.mixins import FragmentMixin, UseFragmentsMixin
|
||||
from counter.forms import BaseBasketForm, BasketProductForm, BillingInfoForm
|
||||
from counter.forms import BaseBasketForm, BasketItemForm, BillingInfoForm
|
||||
from counter.models import (
|
||||
BillingInfo,
|
||||
Customer,
|
||||
Product,
|
||||
Price,
|
||||
Refilling,
|
||||
Selling,
|
||||
get_eboutic,
|
||||
@@ -63,7 +64,7 @@ from eboutic.models import (
|
||||
BillingInfoState,
|
||||
Invoice,
|
||||
InvoiceItem,
|
||||
get_eboutic_products,
|
||||
get_eboutic_prices,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -78,7 +79,7 @@ class BaseEbouticBasketForm(BaseBasketForm):
|
||||
|
||||
|
||||
EbouticBasketForm = forms.formset_factory(
|
||||
BasketProductForm, formset=BaseEbouticBasketForm, absolute_max=None, min_num=1
|
||||
BasketItemForm, formset=BaseEbouticBasketForm, absolute_max=None, min_num=1
|
||||
)
|
||||
|
||||
|
||||
@@ -88,7 +89,6 @@ class EbouticMainView(LoginRequiredMixin, FormView):
|
||||
The purchasable products are those of the eboutic which
|
||||
belong to a category of products of a product category
|
||||
(orphan products are inaccessible).
|
||||
|
||||
"""
|
||||
|
||||
template_name = "eboutic/eboutic_main.jinja"
|
||||
@@ -99,7 +99,7 @@ class EbouticMainView(LoginRequiredMixin, FormView):
|
||||
kwargs["form_kwargs"] = {
|
||||
"customer": self.customer,
|
||||
"counter": get_eboutic(),
|
||||
"allowed_products": {product.id: product for product in self.products},
|
||||
"allowed_prices": {price.id: price for price in self.prices},
|
||||
}
|
||||
return kwargs
|
||||
|
||||
@@ -110,19 +110,22 @@ class EbouticMainView(LoginRequiredMixin, FormView):
|
||||
|
||||
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()
|
||||
BasketItem.objects.bulk_create(
|
||||
[
|
||||
BasketItem.from_price(
|
||||
form.price, form.cleaned_data["quantity"], self.basket
|
||||
)
|
||||
for form in formset
|
||||
]
|
||||
)
|
||||
return super().form_valid(formset)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("eboutic:checkout", kwargs={"basket_id": self.basket.id})
|
||||
|
||||
@cached_property
|
||||
def products(self) -> list[Product]:
|
||||
return get_eboutic_products(self.request.user)
|
||||
def prices(self) -> list[Price]:
|
||||
return get_eboutic_prices(self.request.user)
|
||||
|
||||
@cached_property
|
||||
def customer(self) -> Customer:
|
||||
@@ -130,7 +133,12 @@ class EbouticMainView(LoginRequiredMixin, FormView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["products"] = self.products
|
||||
context["categories"] = [
|
||||
list(i[1])
|
||||
for i in itertools.groupby(
|
||||
self.prices, key=lambda p: p.product.product_type_id
|
||||
)
|
||||
]
|
||||
context["customer_amount"] = self.request.user.account_balance
|
||||
|
||||
purchases = (
|
||||
@@ -267,11 +275,8 @@ class EbouticPayWithSith(CanViewMixin, SingleObjectMixin, View):
|
||||
def post(self, request, *args, **kwargs):
|
||||
basket = self.get_object()
|
||||
refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
||||
if basket.items.filter(type_id=refilling).exists():
|
||||
messages.error(
|
||||
self.request,
|
||||
_("You can't buy a refilling with sith money"),
|
||||
)
|
||||
if basket.items.filter(product__product_type_id=refilling).exists():
|
||||
messages.error(self.request, _("You can't buy a refilling with sith money"))
|
||||
return redirect("eboutic:payment_result", "failure")
|
||||
|
||||
eboutic = get_eboutic()
|
||||
@@ -326,22 +331,23 @@ class EtransactionAutoAnswer(View):
|
||||
raise SuspiciousOperation(
|
||||
"Basket total and amount do not match"
|
||||
)
|
||||
i = Invoice()
|
||||
i.user = b.user
|
||||
i.payment_method = "CARD"
|
||||
i.save()
|
||||
for it in b.items.all():
|
||||
i = Invoice.objects.create(user=b.user)
|
||||
InvoiceItem.objects.bulk_create(
|
||||
[
|
||||
InvoiceItem(
|
||||
invoice=i,
|
||||
product_id=it.product_id,
|
||||
product_name=it.product_name,
|
||||
type_id=it.type_id,
|
||||
product_unit_price=it.product_unit_price,
|
||||
quantity=it.quantity,
|
||||
).save()
|
||||
product_id=item.product_id,
|
||||
label=item.label,
|
||||
unit_price=item.unit_price,
|
||||
quantity=item.quantity,
|
||||
)
|
||||
for item in b.items.all()
|
||||
]
|
||||
)
|
||||
i.validate()
|
||||
b.delete()
|
||||
except Exception as e:
|
||||
sentry_sdk.capture_exception(e)
|
||||
return HttpResponse(
|
||||
"Basket processing failed with error: " + repr(e), status=500
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user