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