mirror of
https://github.com/ae-utbm/sith.git
synced 2026-03-14 07:35:00 +00:00
use new price system in counters
This commit is contained in:
@@ -464,48 +464,47 @@ class CloseCustomerAccountForm(forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BasketProductForm(forms.Form):
|
class BasketItemForm(forms.Form):
|
||||||
quantity = forms.IntegerField(min_value=1, required=True)
|
quantity = forms.IntegerField(min_value=1, required=True)
|
||||||
id = forms.IntegerField(min_value=0, required=True)
|
price_id = forms.IntegerField(min_value=0, required=True)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
customer: Customer,
|
customer: Customer,
|
||||||
counter: Counter,
|
counter: Counter,
|
||||||
allowed_products: dict[int, Product],
|
allowed_prices: dict[int, Price],
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
self.customer = customer # Used by formset
|
self.customer = customer # Used by formset
|
||||||
self.counter = counter # Used by formset
|
self.counter = counter # Used by formset
|
||||||
self.allowed_products = allowed_products
|
self.allowed_prices = allowed_prices
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def clean_id(self):
|
def clean_price_id(self):
|
||||||
data = self.cleaned_data["id"]
|
data = self.cleaned_data["price_id"]
|
||||||
|
|
||||||
# We store self.product so we can use it later on the formset validation
|
# We store self.price so we can use it later on the formset validation
|
||||||
# And also in the global clean
|
# And also in the global clean
|
||||||
self.product = self.allowed_products.get(data, None)
|
self.price = self.allowed_prices.get(data, None)
|
||||||
if self.product is None:
|
if self.price is None:
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
_("The selected product isn't available for this user")
|
_("The selected product isn't available for this user")
|
||||||
)
|
)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
if len(self.errors) > 0:
|
if len(self.errors) > 0:
|
||||||
return
|
return cleaned_data
|
||||||
|
|
||||||
# Compute prices
|
# Compute prices
|
||||||
cleaned_data["bonus_quantity"] = 0
|
cleaned_data["bonus_quantity"] = 0
|
||||||
if self.product.tray:
|
if self.price.product.tray:
|
||||||
cleaned_data["bonus_quantity"] = math.floor(
|
cleaned_data["bonus_quantity"] = math.floor(
|
||||||
cleaned_data["quantity"] / Product.QUANTITY_FOR_TRAY_PRICE
|
cleaned_data["quantity"] / Product.QUANTITY_FOR_TRAY_PRICE
|
||||||
)
|
)
|
||||||
cleaned_data["total_price"] = self.product.price * (
|
cleaned_data["total_price"] = self.price.amount * (
|
||||||
cleaned_data["quantity"] - cleaned_data["bonus_quantity"]
|
cleaned_data["quantity"] - cleaned_data["bonus_quantity"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -529,8 +528,8 @@ class BaseBasketForm(forms.BaseFormSet):
|
|||||||
raise forms.ValidationError(_("Submitted basket is invalid"))
|
raise forms.ValidationError(_("Submitted basket is invalid"))
|
||||||
|
|
||||||
def _check_product_are_unique(self):
|
def _check_product_are_unique(self):
|
||||||
product_ids = {form.cleaned_data["id"] for form in self.forms}
|
price_ids = {form.cleaned_data["price_id"] for form in self.forms}
|
||||||
if len(product_ids) != len(self.forms):
|
if len(price_ids) != len(self.forms):
|
||||||
raise forms.ValidationError(_("Duplicated product entries."))
|
raise forms.ValidationError(_("Duplicated product entries."))
|
||||||
|
|
||||||
def _check_enough_money(self, counter: Counter, customer: Customer):
|
def _check_enough_money(self, counter: Counter, customer: Customer):
|
||||||
@@ -540,10 +539,9 @@ class BaseBasketForm(forms.BaseFormSet):
|
|||||||
|
|
||||||
def _check_recorded_products(self, customer: Customer):
|
def _check_recorded_products(self, customer: Customer):
|
||||||
"""Check for, among other things, ecocups and pitchers"""
|
"""Check for, among other things, ecocups and pitchers"""
|
||||||
items = {
|
items = defaultdict(int)
|
||||||
form.cleaned_data["id"]: form.cleaned_data["quantity"]
|
for form in self.forms:
|
||||||
for form in self.forms
|
items[form.price.product_id] += form.cleaned_data["quantity"]
|
||||||
}
|
|
||||||
ids = list(items.keys())
|
ids = list(items.keys())
|
||||||
returnables = list(
|
returnables = list(
|
||||||
ReturnableProduct.objects.filter(
|
ReturnableProduct.objects.filter(
|
||||||
@@ -569,7 +567,7 @@ class BaseBasketForm(forms.BaseFormSet):
|
|||||||
|
|
||||||
|
|
||||||
BasketForm = forms.formset_factory(
|
BasketForm = forms.formset_factory(
|
||||||
BasketProductForm, formset=BaseBasketForm, absolute_max=None, min_num=1
|
BasketItemForm, formset=BaseBasketForm, absolute_max=None, min_num=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -784,36 +784,14 @@ class Counter(models.Model):
|
|||||||
# but they share the same primary key
|
# but they share the same primary key
|
||||||
return self.type == "BAR" and any(b.pk == customer.pk for b in self.barmen_list)
|
return self.type == "BAR" and any(b.pk == customer.pk for b in self.barmen_list)
|
||||||
|
|
||||||
def get_products_for(self, customer: Customer) -> list[Product]:
|
def get_prices_for(self, customer: Customer) -> list[Price]:
|
||||||
"""
|
return list(
|
||||||
Get all allowed products for the provided customer on this counter
|
Price.objects.filter(product__counters=self)
|
||||||
Prices will be annotated
|
.for_user(customer.user)
|
||||||
"""
|
.select_related("product", "product__product_type")
|
||||||
|
.prefetch_related("groups")
|
||||||
products = (
|
|
||||||
self.products.filter(archived=False)
|
|
||||||
.select_related("product_type")
|
|
||||||
.prefetch_related("buying_groups")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only include age appropriate products
|
|
||||||
age = customer.user.age
|
|
||||||
if customer.user.is_banned_alcohol:
|
|
||||||
age = min(age, 17)
|
|
||||||
products = products.filter(limit_age__lte=age)
|
|
||||||
|
|
||||||
# Compute special price for customer if he is a barmen on that bar
|
|
||||||
if self.customer_is_barman(customer):
|
|
||||||
products = products.annotate(price=F("special_selling_price"))
|
|
||||||
else:
|
|
||||||
products = products.annotate(price=F("selling_price"))
|
|
||||||
|
|
||||||
return [
|
|
||||||
product
|
|
||||||
for product in products.all()
|
|
||||||
if product.can_be_sold_to(customer.user)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class RefillingQuerySet(models.QuerySet):
|
class RefillingQuerySet(models.QuerySet):
|
||||||
def annotate_total(self) -> Self:
|
def annotate_total(self) -> Self:
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { Product } from "#counter:counter/types.ts";
|
import type { Product } from "#counter:counter/types";
|
||||||
|
|
||||||
export class BasketItem {
|
export class BasketItem {
|
||||||
quantity: number;
|
quantity: number;
|
||||||
product: Product;
|
product: Product;
|
||||||
quantityForTrayPrice: number;
|
|
||||||
errors: string[];
|
errors: string[];
|
||||||
|
|
||||||
constructor(product: Product, quantity: number) {
|
constructor(product: Product, quantity: number) {
|
||||||
@@ -20,6 +19,6 @@ export class BasketItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sum(): number {
|
sum(): number {
|
||||||
return (this.quantity - this.getBonusQuantity()) * this.product.price;
|
return (this.quantity - this.getBonusQuantity()) * this.product.price.amount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { AlertMessage } from "#core:utils/alert-message.ts";
|
import { AlertMessage } from "#core:utils/alert-message";
|
||||||
import { BasketItem } from "#counter:counter/basket.ts";
|
import { BasketItem } from "#counter:counter/basket";
|
||||||
import type {
|
import type {
|
||||||
CounterConfig,
|
CounterConfig,
|
||||||
ErrorMessage,
|
ErrorMessage,
|
||||||
ProductFormula,
|
ProductFormula,
|
||||||
} from "#counter:counter/types.ts";
|
} from "#counter:counter/types";
|
||||||
import type { CounterProductSelect } from "./components/counter-product-select-index.ts";
|
import type { CounterProductSelect } from "./components/counter-product-select-index";
|
||||||
|
|
||||||
document.addEventListener("alpine:init", () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
Alpine.data("counter", (config: CounterConfig) => ({
|
Alpine.data("counter", (config: CounterConfig) => ({
|
||||||
|
|||||||
8
counter/static/bundled/counter/types.d.ts
vendored
8
counter/static/bundled/counter/types.d.ts
vendored
@@ -21,11 +21,15 @@ export interface CounterConfig {
|
|||||||
cancelUrl: string;
|
cancelUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Price {
|
||||||
|
id: number;
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Product {
|
export interface Product {
|
||||||
id: string;
|
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
price: number;
|
price: Price;
|
||||||
hasTrayPrice: boolean;
|
hasTrayPrice: boolean;
|
||||||
quantityForTrayPrice: number;
|
quantityForTrayPrice: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block additional_css %}
|
{% block additional_css %}
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static('counter/css/counter-click.scss') }}" defer></link>
|
<link rel="stylesheet" href="{{ static('counter/css/counter-click.scss') }}">
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static('bundled/core/components/ajax-select-index.css') }}" defer></link>
|
<link rel="stylesheet" href="{{ static('bundled/core/components/ajax-select-index.css') }}">
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static('core/components/ajax-select.scss') }}" defer></link>
|
<link rel="stylesheet" href="{{ static('core/components/ajax-select.scss') }}">
|
||||||
<link rel="stylesheet" type="text/css" href="{{ static('core/components/tabs.scss') }}" defer></link>
|
<link rel="stylesheet" href="{{ static('core/components/tabs.scss') }}">
|
||||||
<link rel="stylesheet" href="{{ static("core/components/card.scss") }}">
|
<link rel="stylesheet" href="{{ static("core/components/card.scss") }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -65,10 +65,10 @@
|
|||||||
<option value="FIN">{% trans %}Confirm (FIN){% endtrans %}</option>
|
<option value="FIN">{% trans %}Confirm (FIN){% endtrans %}</option>
|
||||||
<option value="ANN">{% trans %}Cancel (ANN){% endtrans %}</option>
|
<option value="ANN">{% trans %}Cancel (ANN){% endtrans %}</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
{%- for category in categories.keys() -%}
|
{%- for category, prices in categories.items() -%}
|
||||||
<optgroup label="{{ category }}">
|
<optgroup label="{{ category }}">
|
||||||
{%- for product in categories[category] -%}
|
{%- for price in prices -%}
|
||||||
<option value="{{ product.id }}">{{ product }}</option>
|
<option value="{{ price.id }}">{{ price.full_label }}</option>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
@@ -103,24 +103,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li x-show="getBasketSize() === 0">{% trans %}This basket is empty{% endtrans %}</li>
|
<li x-show="getBasketSize() === 0">{% trans %}This basket is empty{% endtrans %}</li>
|
||||||
<template x-for="(item, index) in Object.values(basket)" :key="item.product.id">
|
<template x-for="(item, index) in Object.values(basket)" :key="item.product.price.id">
|
||||||
<li>
|
<li>
|
||||||
<template x-for="error in item.errors">
|
<template x-for="error in item.errors">
|
||||||
<div class="alert alert-red" x-text="error">
|
<div class="alert alert-red" x-text="error">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<button @click.prevent="addToBasket(item.product.id, -1)">-</button>
|
<button @click.prevent="addToBasket(item.product.price.id, -1)">-</button>
|
||||||
<span class="quantity" x-text="item.quantity"></span>
|
<span class="quantity" x-text="item.quantity"></span>
|
||||||
<button @click.prevent="addToBasket(item.product.id, 1)">+</button>
|
<button @click.prevent="addToBasket(item.product.price.id, 1)">+</button>
|
||||||
|
|
||||||
<span x-text="item.product.name"></span> :
|
<span x-text="item.product.name"></span> :
|
||||||
<span x-text="item.sum().toLocaleString(undefined, { minimumFractionDigits: 2 })">€</span>
|
<span x-text="item.sum().toLocaleString(undefined, { minimumFractionDigits: 2 })">€</span>
|
||||||
<span x-show="item.getBonusQuantity() > 0" x-text="`${item.getBonusQuantity()} x P`"></span>
|
<span x-show="item.getBonusQuantity() > 0"
|
||||||
|
x-text="`${item.getBonusQuantity()} x P`"></span>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="remove-item"
|
class="remove-item"
|
||||||
@click.prevent="removeFromBasket(item.product.id)"
|
@click.prevent="removeFromBasket(item.product.price.id)"
|
||||||
><i class="fa fa-trash-can delete-action"></i></button>
|
><i class="fa fa-trash-can delete-action"></i></button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@@ -133,9 +134,9 @@
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
:value="item.product.id"
|
:value="item.product.price.id"
|
||||||
:id="`id_form-${index}-id`"
|
:id="`id_form-${index}-price_id`"
|
||||||
:name="`form-${index}-id`"
|
:name="`form-${index}-price_id`"
|
||||||
required
|
required
|
||||||
readonly
|
readonly
|
||||||
>
|
>
|
||||||
@@ -207,24 +208,24 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<ui-tab-group>
|
<ui-tab-group>
|
||||||
{% for category in categories.keys() -%}
|
{% for category, prices in categories.items() -%}
|
||||||
<ui-tab title="{{ category }}" {% if loop.index == 1 -%}active{%- endif -%}>
|
<ui-tab title="{{ category }}" {% if loop.index == 1 -%}active{%- endif -%}>
|
||||||
<h5 class="margin-bottom">{{ category }}</h5>
|
<h5 class="margin-bottom">{{ category }}</h5>
|
||||||
<div class="row gap-2x">
|
<div class="row gap-2x">
|
||||||
{% for product in categories[category] -%}
|
{% for price in prices -%}
|
||||||
<button class="card shadow" @click="addToBasket('{{ product.id }}', 1)">
|
<button class="card shadow" @click="addToBasket('{{ price.id }}', 1)">
|
||||||
<img
|
<img
|
||||||
class="card-image"
|
class="card-image"
|
||||||
alt="image de {{ product.name }}"
|
alt="image de {{ price.full_label }}"
|
||||||
{% if product.icon %}
|
{% if price.product.icon %}
|
||||||
src="{{ product.icon.url }}"
|
src="{{ price.product.icon.url }}"
|
||||||
{% else %}
|
{% else %}
|
||||||
src="{{ static('core/img/na.gif') }}"
|
src="{{ static('core/img/na.gif') }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
/>
|
/>
|
||||||
<span class="card-content">
|
<span class="card-content">
|
||||||
<strong class="card-title">{{ product.name }}</strong>
|
<strong class="card-title">{{ price.full_label }}</strong>
|
||||||
<p>{{ product.price }} €<br>{{ product.code }}</p>
|
<p>{{ price.amount }} €<br>{{ price.product.code }}</p>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
@@ -241,13 +242,12 @@
|
|||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
const products = {
|
const products = {
|
||||||
{%- for product in products -%}
|
{%- for price in products -%}
|
||||||
{{ product.id }}: {
|
{{ price.id }}: {
|
||||||
id: "{{ product.id }}",
|
price: { id: "{{ price.id }}", amount: {{ price.amount }} },
|
||||||
name: "{{ product.name }}",
|
name: "{{ price.full_label }}",
|
||||||
price: {{ product.price }},
|
hasTrayPrice: {{ price.product.tray | tojson }},
|
||||||
hasTrayPrice: {{ product.tray | tojson }},
|
quantityForTrayPrice: {{ price.product.QUANTITY_FOR_TRAY_PRICE }},
|
||||||
quantityForTrayPrice: {{ product.QUANTITY_FOR_TRAY_PRICE }},
|
|
||||||
},
|
},
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class CounterClick(
|
|||||||
kwargs["form_kwargs"] = {
|
kwargs["form_kwargs"] = {
|
||||||
"customer": self.customer,
|
"customer": self.customer,
|
||||||
"counter": self.object,
|
"counter": self.object,
|
||||||
"allowed_products": {product.id: product for product in self.products},
|
"allowed_prices": {price.id: price for price in self.products},
|
||||||
}
|
}
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ class CounterClick(
|
|||||||
):
|
):
|
||||||
return redirect(obj) # Redirect to counter
|
return redirect(obj) # Redirect to counter
|
||||||
|
|
||||||
self.products = obj.get_products_for(self.customer)
|
self.products = obj.get_prices_for(self.customer)
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
@@ -121,32 +121,31 @@ class CounterClick(
|
|||||||
# This is important because some items have a negative price
|
# This is important because some items have a negative price
|
||||||
# Negative priced items gives money to the customer and should
|
# Negative priced items gives money to the customer and should
|
||||||
# be processed first so that we don't throw a not enough money error
|
# be processed first so that we don't throw a not enough money error
|
||||||
for form in sorted(formset, key=lambda form: form.product.price):
|
for form in sorted(formset, key=lambda form: form.price.amount):
|
||||||
self.request.session["last_basket"].append(
|
self.request.session["last_basket"].append(
|
||||||
f"{form.cleaned_data['quantity']} x {form.product.name}"
|
f"{form.cleaned_data['quantity']} x {form.price.full_label}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
common_kwargs = {
|
||||||
|
"product": form.price.product,
|
||||||
|
"club_id": form.price.product.club_id,
|
||||||
|
"counter": self.object,
|
||||||
|
"seller": operator,
|
||||||
|
"customer": self.customer,
|
||||||
|
}
|
||||||
Selling(
|
Selling(
|
||||||
label=form.product.name,
|
**common_kwargs,
|
||||||
product=form.product,
|
label=form.price.full_label,
|
||||||
club=form.product.club,
|
unit_price=form.price.amount,
|
||||||
counter=self.object,
|
|
||||||
unit_price=form.product.price,
|
|
||||||
quantity=form.cleaned_data["quantity"]
|
quantity=form.cleaned_data["quantity"]
|
||||||
- form.cleaned_data["bonus_quantity"],
|
- form.cleaned_data["bonus_quantity"],
|
||||||
seller=operator,
|
|
||||||
customer=self.customer,
|
|
||||||
).save()
|
).save()
|
||||||
if form.cleaned_data["bonus_quantity"] > 0:
|
if form.cleaned_data["bonus_quantity"] > 0:
|
||||||
Selling(
|
Selling(
|
||||||
label=f"{form.product.name} (Plateau)",
|
**common_kwargs,
|
||||||
product=form.product,
|
label=f"{form.price.full_label} (Plateau)",
|
||||||
club=form.product.club,
|
|
||||||
counter=self.object,
|
|
||||||
unit_price=0,
|
unit_price=0,
|
||||||
quantity=form.cleaned_data["bonus_quantity"],
|
quantity=form.cleaned_data["bonus_quantity"],
|
||||||
seller=operator,
|
|
||||||
customer=self.customer,
|
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
self.customer.update_returnable_balance()
|
self.customer.update_returnable_balance()
|
||||||
@@ -212,9 +211,8 @@ class CounterClick(
|
|||||||
result__in=self.products
|
result__in=self.products
|
||||||
).prefetch_related("products")
|
).prefetch_related("products")
|
||||||
kwargs["categories"] = defaultdict(list)
|
kwargs["categories"] = defaultdict(list)
|
||||||
for product in kwargs["products"]:
|
for price in kwargs["products"]:
|
||||||
if product.product_type:
|
kwargs["categories"][price.product.product_type].append(price)
|
||||||
kwargs["categories"][product.product_type].append(product)
|
|
||||||
kwargs["customer"] = self.customer
|
kwargs["customer"] = self.customer
|
||||||
kwargs["cancel_url"] = self.get_success_url()
|
kwargs["cancel_url"] = self.get_success_url()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user