use new price system in counters

This commit is contained in:
imperosol
2026-03-02 17:51:15 +01:00
parent a019707d4a
commit c2dfbc8bec
7 changed files with 84 additions and 107 deletions

View File

@@ -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
)

View File

@@ -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:

View File

@@ -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;
}
}

View File

@@ -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) => ({

View File

@@ -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;
}

View File

@@ -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 -%}
};

View File

@@ -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()