mirror of
https://github.com/ae-utbm/sith.git
synced 2026-03-13 15:15:03 +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)
|
||||
id = forms.IntegerField(min_value=0, required=True)
|
||||
price_id = forms.IntegerField(min_value=0, required=True)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
customer: Customer,
|
||||
counter: Counter,
|
||||
allowed_products: dict[int, Product],
|
||||
allowed_prices: dict[int, Price],
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.customer = customer # Used by formset
|
||||
self.counter = counter # Used by formset
|
||||
self.allowed_products = allowed_products
|
||||
self.allowed_prices = allowed_prices
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_id(self):
|
||||
data = self.cleaned_data["id"]
|
||||
def clean_price_id(self):
|
||||
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
|
||||
self.product = self.allowed_products.get(data, None)
|
||||
if self.product is None:
|
||||
self.price = self.allowed_prices.get(data, None)
|
||||
if self.price 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
|
||||
return cleaned_data
|
||||
|
||||
# Compute prices
|
||||
cleaned_data["bonus_quantity"] = 0
|
||||
if self.product.tray:
|
||||
if self.price.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["total_price"] = self.price.amount * (
|
||||
cleaned_data["quantity"] - cleaned_data["bonus_quantity"]
|
||||
)
|
||||
|
||||
@@ -529,8 +528,8 @@ class BaseBasketForm(forms.BaseFormSet):
|
||||
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):
|
||||
price_ids = {form.cleaned_data["price_id"] for form in self.forms}
|
||||
if len(price_ids) != len(self.forms):
|
||||
raise forms.ValidationError(_("Duplicated product entries."))
|
||||
|
||||
def _check_enough_money(self, counter: Counter, customer: Customer):
|
||||
@@ -540,10 +539,9 @@ class BaseBasketForm(forms.BaseFormSet):
|
||||
|
||||
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
|
||||
}
|
||||
items = defaultdict(int)
|
||||
for form in self.forms:
|
||||
items[form.price.product_id] += form.cleaned_data["quantity"]
|
||||
ids = list(items.keys())
|
||||
returnables = list(
|
||||
ReturnableProduct.objects.filter(
|
||||
@@ -569,7 +567,7 @@ class BaseBasketForm(forms.BaseFormSet):
|
||||
|
||||
|
||||
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
|
||||
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]:
|
||||
"""
|
||||
Get all allowed products for the provided customer on this counter
|
||||
Prices will be annotated
|
||||
"""
|
||||
|
||||
products = (
|
||||
self.products.filter(archived=False)
|
||||
.select_related("product_type")
|
||||
.prefetch_related("buying_groups")
|
||||
def get_prices_for(self, customer: Customer) -> list[Price]:
|
||||
return list(
|
||||
Price.objects.filter(product__counters=self)
|
||||
.for_user(customer.user)
|
||||
.select_related("product", "product__product_type")
|
||||
.prefetch_related("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):
|
||||
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 {
|
||||
quantity: number;
|
||||
product: Product;
|
||||
quantityForTrayPrice: number;
|
||||
errors: string[];
|
||||
|
||||
constructor(product: Product, quantity: number) {
|
||||
@@ -20,6 +19,6 @@ export class BasketItem {
|
||||
}
|
||||
|
||||
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 { BasketItem } from "#counter:counter/basket.ts";
|
||||
import { AlertMessage } from "#core:utils/alert-message";
|
||||
import { BasketItem } from "#counter:counter/basket";
|
||||
import type {
|
||||
CounterConfig,
|
||||
ErrorMessage,
|
||||
ProductFormula,
|
||||
} from "#counter:counter/types.ts";
|
||||
import type { CounterProductSelect } from "./components/counter-product-select-index.ts";
|
||||
} from "#counter:counter/types";
|
||||
import type { CounterProductSelect } from "./components/counter-product-select-index";
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
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;
|
||||
}
|
||||
|
||||
interface Price {
|
||||
id: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
price: number;
|
||||
price: Price;
|
||||
hasTrayPrice: boolean;
|
||||
quantityForTrayPrice: number;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_css %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ static('counter/css/counter-click.scss') }}" defer></link>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static('bundled/core/components/ajax-select-index.css') }}" defer></link>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static('core/components/ajax-select.scss') }}" defer></link>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static('core/components/tabs.scss') }}" defer></link>
|
||||
<link rel="stylesheet" href="{{ static('counter/css/counter-click.scss') }}">
|
||||
<link rel="stylesheet" href="{{ static('bundled/core/components/ajax-select-index.css') }}">
|
||||
<link rel="stylesheet" href="{{ static('core/components/ajax-select.scss') }}">
|
||||
<link rel="stylesheet" href="{{ static('core/components/tabs.scss') }}">
|
||||
<link rel="stylesheet" href="{{ static("core/components/card.scss") }}">
|
||||
{% endblock %}
|
||||
|
||||
@@ -65,10 +65,10 @@
|
||||
<option value="FIN">{% trans %}Confirm (FIN){% endtrans %}</option>
|
||||
<option value="ANN">{% trans %}Cancel (ANN){% endtrans %}</option>
|
||||
</optgroup>
|
||||
{%- for category in categories.keys() -%}
|
||||
{%- for category, prices in categories.items() -%}
|
||||
<optgroup label="{{ category }}">
|
||||
{%- for product in categories[category] -%}
|
||||
<option value="{{ product.id }}">{{ product }}</option>
|
||||
{%- for price in prices -%}
|
||||
<option value="{{ price.id }}">{{ price.full_label }}</option>
|
||||
{%- endfor -%}
|
||||
</optgroup>
|
||||
{%- endfor -%}
|
||||
@@ -103,24 +103,25 @@
|
||||
</div>
|
||||
<ul>
|
||||
<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>
|
||||
<template x-for="error in item.errors">
|
||||
<div class="alert alert-red" x-text="error">
|
||||
</div>
|
||||
</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>
|
||||
<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.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
|
||||
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>
|
||||
|
||||
<input
|
||||
@@ -133,9 +134,9 @@
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
:value="item.product.id"
|
||||
:id="`id_form-${index}-id`"
|
||||
:name="`form-${index}-id`"
|
||||
:value="item.product.price.id"
|
||||
:id="`id_form-${index}-price_id`"
|
||||
:name="`form-${index}-price_id`"
|
||||
required
|
||||
readonly
|
||||
>
|
||||
@@ -207,24 +208,24 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<ui-tab-group>
|
||||
{% for category in categories.keys() -%}
|
||||
{% for category, prices in categories.items() -%}
|
||||
<ui-tab title="{{ category }}" {% if loop.index == 1 -%}active{%- endif -%}>
|
||||
<h5 class="margin-bottom">{{ category }}</h5>
|
||||
<div class="row gap-2x">
|
||||
{% for product in categories[category] -%}
|
||||
<button class="card shadow" @click="addToBasket('{{ product.id }}', 1)">
|
||||
{% for price in prices -%}
|
||||
<button class="card shadow" @click="addToBasket('{{ price.id }}', 1)">
|
||||
<img
|
||||
class="card-image"
|
||||
alt="image de {{ product.name }}"
|
||||
{% if product.icon %}
|
||||
src="{{ product.icon.url }}"
|
||||
alt="image de {{ price.full_label }}"
|
||||
{% if price.product.icon %}
|
||||
src="{{ price.product.icon.url }}"
|
||||
{% else %}
|
||||
src="{{ static('core/img/na.gif') }}"
|
||||
{% endif %}
|
||||
/>
|
||||
<span class="card-content">
|
||||
<strong class="card-title">{{ product.name }}</strong>
|
||||
<p>{{ product.price }} €<br>{{ product.code }}</p>
|
||||
<strong class="card-title">{{ price.full_label }}</strong>
|
||||
<p>{{ price.amount }} €<br>{{ price.product.code }}</p>
|
||||
</span>
|
||||
</button>
|
||||
{%- endfor %}
|
||||
@@ -241,13 +242,12 @@
|
||||
{{ super() }}
|
||||
<script>
|
||||
const products = {
|
||||
{%- for product in products -%}
|
||||
{{ product.id }}: {
|
||||
id: "{{ product.id }}",
|
||||
name: "{{ product.name }}",
|
||||
price: {{ product.price }},
|
||||
hasTrayPrice: {{ product.tray | tojson }},
|
||||
quantityForTrayPrice: {{ product.QUANTITY_FOR_TRAY_PRICE }},
|
||||
{%- for price in products -%}
|
||||
{{ price.id }}: {
|
||||
price: { id: "{{ price.id }}", amount: {{ price.amount }} },
|
||||
name: "{{ price.full_label }}",
|
||||
hasTrayPrice: {{ price.product.tray | tojson }},
|
||||
quantityForTrayPrice: {{ price.product.QUANTITY_FOR_TRAY_PRICE }},
|
||||
},
|
||||
{%- endfor -%}
|
||||
};
|
||||
|
||||
@@ -73,7 +73,7 @@ class CounterClick(
|
||||
kwargs["form_kwargs"] = {
|
||||
"customer": self.customer,
|
||||
"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
|
||||
|
||||
@@ -103,7 +103,7 @@ class CounterClick(
|
||||
):
|
||||
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)
|
||||
|
||||
@@ -121,32 +121,31 @@ class CounterClick(
|
||||
# This is important because some items have a negative price
|
||||
# Negative priced items gives money to the customer and should
|
||||
# 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(
|
||||
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(
|
||||
label=form.product.name,
|
||||
product=form.product,
|
||||
club=form.product.club,
|
||||
counter=self.object,
|
||||
unit_price=form.product.price,
|
||||
**common_kwargs,
|
||||
label=form.price.full_label,
|
||||
unit_price=form.price.amount,
|
||||
quantity=form.cleaned_data["quantity"]
|
||||
- form.cleaned_data["bonus_quantity"],
|
||||
seller=operator,
|
||||
customer=self.customer,
|
||||
).save()
|
||||
if form.cleaned_data["bonus_quantity"] > 0:
|
||||
Selling(
|
||||
label=f"{form.product.name} (Plateau)",
|
||||
product=form.product,
|
||||
club=form.product.club,
|
||||
counter=self.object,
|
||||
**common_kwargs,
|
||||
label=f"{form.price.full_label} (Plateau)",
|
||||
unit_price=0,
|
||||
quantity=form.cleaned_data["bonus_quantity"],
|
||||
seller=operator,
|
||||
customer=self.customer,
|
||||
).save()
|
||||
|
||||
self.customer.update_returnable_balance()
|
||||
@@ -212,9 +211,8 @@ class CounterClick(
|
||||
result__in=self.products
|
||||
).prefetch_related("products")
|
||||
kwargs["categories"] = defaultdict(list)
|
||||
for product in kwargs["products"]:
|
||||
if product.product_type:
|
||||
kwargs["categories"][product.product_type].append(product)
|
||||
for price in kwargs["products"]:
|
||||
kwargs["categories"][price.product.product_type].append(price)
|
||||
kwargs["customer"] = self.customer
|
||||
kwargs["cancel_url"] = self.get_success_url()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user