mirror of
https://github.com/ae-utbm/sith.git
synced 2026-06-13 11:39:25 +00:00
max amount for counter refills
This commit is contained in:
+10
-5
@@ -6,6 +6,7 @@ from datetime import date, datetime, timezone
|
|||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.forms import BaseModelFormSet
|
from django.forms import BaseModelFormSet
|
||||||
@@ -168,18 +169,19 @@ class RefillForm(forms.ModelForm):
|
|||||||
|
|
||||||
error_css_class = "error"
|
error_css_class = "error"
|
||||||
required_css_class = "required"
|
required_css_class = "required"
|
||||||
amount = forms.FloatField(
|
|
||||||
min_value=0, widget=forms.NumberInput(attrs={"class": "focus"})
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Refilling
|
model = Refilling
|
||||||
fields = ["amount", "payment_method"]
|
fields = ["amount", "payment_method"]
|
||||||
widgets = {"payment_method": forms.RadioSelect}
|
widgets = {"payment_method": forms.RadioSelect}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(
|
||||||
|
self, *args, counter: Counter, operator: User, customer: Customer, **kwargs
|
||||||
|
):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
max_value = settings.SITH_ACCOUNT_MAX_MONEY - customer.amount
|
||||||
|
# server-side max_value validation is done by Refilling.clean
|
||||||
|
self.fields["amount"].widget.attrs["max"] = max_value
|
||||||
self.fields["payment_method"].choices = (
|
self.fields["payment_method"].choices = (
|
||||||
method
|
method
|
||||||
for method in self.fields["payment_method"].choices
|
for method in self.fields["payment_method"].choices
|
||||||
@@ -187,6 +189,9 @@ class RefillForm(forms.ModelForm):
|
|||||||
)
|
)
|
||||||
if self.fields["payment_method"].initial not in self.allowed_refilling_methods:
|
if self.fields["payment_method"].initial not in self.allowed_refilling_methods:
|
||||||
self.fields["payment_method"].initial = self.allowed_refilling_methods[0]
|
self.fields["payment_method"].initial = self.allowed_refilling_methods[0]
|
||||||
|
self.instance.counter = counter
|
||||||
|
self.instance.operator = operator
|
||||||
|
self.instance.customer = customer
|
||||||
|
|
||||||
|
|
||||||
class CounterEditForm(forms.ModelForm):
|
class CounterEditForm(forms.ModelForm):
|
||||||
|
|||||||
@@ -881,6 +881,14 @@ class Refilling(models.Model):
|
|||||||
return False
|
return False
|
||||||
return user.is_owner(self.counter) and self.payment_method != "CARD"
|
return user.is_owner(self.counter) and self.payment_method != "CARD"
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
if self.amount + self.customer.amount > settings.SITH_ACCOUNT_MAX_MONEY:
|
||||||
|
raise ValidationError(
|
||||||
|
_("There cannot be more than %(money)d€ on an AE account")
|
||||||
|
% {"money": settings.SITH_ACCOUNT_MAX_MONEY}
|
||||||
|
)
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
self.customer.amount -= self.amount
|
self.customer.amount -= self.amount
|
||||||
self.customer.save()
|
self.customer.save()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const productParsingRegex = /^(\d+x)?(.*)/i;
|
|||||||
const codeParsingRegex = / \((\w+)\)$/;
|
const codeParsingRegex = / \((\w+)\)$/;
|
||||||
|
|
||||||
function parseProduct(query: string): [number, string] {
|
function parseProduct(query: string): [number, string] {
|
||||||
const parsed = productParsingRegex.exec(query);
|
const parsed = productParsingRegex.exec(query) as RegExpExecArray;
|
||||||
return [Number.parseInt(parsed[1] || "1", 10), parsed[2]];
|
return [Number.parseInt(parsed[1] || "1", 10), parsed[2]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { BasketItem } from "#counter:counter/basket";
|
|||||||
import type {
|
import type {
|
||||||
CounterConfig,
|
CounterConfig,
|
||||||
CounterItem,
|
CounterItem,
|
||||||
ErrorMessage,
|
|
||||||
ProductFormula,
|
ProductFormula,
|
||||||
} from "#counter:counter/types";
|
} from "#counter:counter/types";
|
||||||
import type { CounterProductSelect } from "./components/counter-product-select-index";
|
import type { CounterProductSelect } from "./components/counter-product-select-index";
|
||||||
@@ -24,7 +23,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.codeField = this.$refs.codeField;
|
this.codeField = this.$refs.codeField as CounterProductSelect;
|
||||||
this.codeField.widget.hook("after", "onOptionSelect", () => {
|
this.codeField.widget.hook("after", "onOptionSelect", () => {
|
||||||
this.handleCode();
|
this.handleCode();
|
||||||
});
|
});
|
||||||
@@ -34,14 +33,14 @@ document.addEventListener("alpine:init", () => {
|
|||||||
// 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")
|
.querySelector("#id_form-TOTAL_FORMS")
|
||||||
.setAttribute(":value", "getBasketSize()");
|
?.setAttribute(":value", "getBasketSize()");
|
||||||
},
|
},
|
||||||
|
|
||||||
removeFromBasket(id: string) {
|
removeFromBasket(id: string) {
|
||||||
delete this.basket[id];
|
delete this.basket[id];
|
||||||
},
|
},
|
||||||
|
|
||||||
addToBasket(id: string, quantity: number): ErrorMessage {
|
addToBasket(id: string, quantity: number) {
|
||||||
const item: BasketItem =
|
const item: BasketItem =
|
||||||
this.basket[id] || new BasketItem(config.products[id], 0);
|
this.basket[id] || new BasketItem(config.products[id], 0);
|
||||||
|
|
||||||
@@ -50,7 +49,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
|
|
||||||
if (item.quantity <= 0) {
|
if (item.quantity <= 0) {
|
||||||
delete this.basket[id];
|
delete this.basket[id];
|
||||||
return "";
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.basket[id] = item;
|
this.basket[id] = item;
|
||||||
@@ -72,7 +71,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
const products = new Set(
|
const products = new Set(
|
||||||
Object.values(this.basket).map((item: BasketItem) => item.product.productId),
|
Object.values(this.basket).map((item: BasketItem) => item.product.productId),
|
||||||
);
|
);
|
||||||
const formula: ProductFormula = config.formulas.find((f: ProductFormula) => {
|
const formula = config.formulas.find((f: ProductFormula) => {
|
||||||
return f.products.every((p: number) => products.has(p));
|
return f.products.every((p: number) => products.has(p));
|
||||||
});
|
});
|
||||||
if (formula === undefined) {
|
if (formula === undefined) {
|
||||||
@@ -80,9 +79,13 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
// Now that the formula is found, remove the items composing it from the basket
|
// Now that the formula is found, remove the items composing it from the basket
|
||||||
for (const product of formula.products) {
|
for (const product of formula.products) {
|
||||||
const key = Object.entries(this.basket).find(
|
const item = Object.entries(this.basket).find(
|
||||||
([_, i]: [string, BasketItem]) => i.product.productId === product,
|
([_, i]: [string, BasketItem]) => i.product.productId === product,
|
||||||
)[0];
|
);
|
||||||
|
if (item === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = item[0];
|
||||||
this.basket[key].quantity -= 1;
|
this.basket[key].quantity -= 1;
|
||||||
if (this.basket[key].quantity <= 0) {
|
if (this.basket[key].quantity <= 0) {
|
||||||
this.removeFromBasket(key);
|
this.removeFromBasket(key);
|
||||||
@@ -92,7 +95,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
const result = Object.values(config.products)
|
const result = Object.values(config.products)
|
||||||
.filter((item: CounterItem) => item.productId === formula.result)
|
.filter((item: CounterItem) => item.productId === formula.result)
|
||||||
.reduce((acc, curr) => (acc.price.amount < curr.price.amount ? acc : curr));
|
.reduce((acc, curr) => (acc.price.amount < curr.price.amount ? acc : curr));
|
||||||
this.addToBasket(result.price.id, 1);
|
this.addToBasket(result.price.id.toString(), 1);
|
||||||
this.alertMessage.display(
|
this.alertMessage.display(
|
||||||
interpolate(
|
interpolate(
|
||||||
gettext("Formula %(formula)s applied"),
|
gettext("Formula %(formula)s applied"),
|
||||||
@@ -119,14 +122,18 @@ document.addEventListener("alpine:init", () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onRefillingSuccess(event: CustomEvent) {
|
onRefillingSuccess(event: CustomEvent) {
|
||||||
if (event.type !== "htmx:after-request" || event.detail.failed) {
|
if (
|
||||||
|
event.type !== "htmx:after-swap" ||
|
||||||
|
event.detail.failed ||
|
||||||
|
event.detail.elt.querySelector(".errorlist")
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.customerBalance += Number.parseFloat(
|
this.customerBalance += Number.parseFloat(
|
||||||
(event.detail.target.querySelector("#id_amount") as HTMLInputElement).value,
|
(event.detail.target.querySelector("#id_amount") as HTMLInputElement).value,
|
||||||
);
|
);
|
||||||
document.getElementById("selling-accordion").setAttribute("open", "");
|
document.getElementById("selling-accordion")?.setAttribute("open", "");
|
||||||
this.codeField.widget.focus();
|
this.codeField?.widget.focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
finish() {
|
finish() {
|
||||||
@@ -136,7 +143,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$refs.basketForm.submit();
|
(this.$refs.basketForm as HTMLFormElement).submit();
|
||||||
},
|
},
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
@@ -144,6 +151,8 @@ document.addEventListener("alpine:init", () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleCode() {
|
handleCode() {
|
||||||
|
if (!this.codeField) throw Error("Unexpected null codeField.");
|
||||||
|
|
||||||
const [quantity, code] = this.codeField.getSelectedProduct() as [number, string];
|
const [quantity, code] = this.codeField.getSelectedProduct() as [number, string];
|
||||||
|
|
||||||
if (this.codeField.getOperationCodes().includes(code.toUpperCase())) {
|
if (this.codeField.getOperationCodes().includes(code.toUpperCase())) {
|
||||||
|
|||||||
@@ -182,7 +182,7 @@
|
|||||||
{% if refilling_fragment %}
|
{% if refilling_fragment %}
|
||||||
<div
|
<div
|
||||||
class="accordion-content"
|
class="accordion-content"
|
||||||
@htmx:after-request="onRefillingSuccess"
|
@htmx:after-swap="onRefillingSuccess"
|
||||||
>
|
>
|
||||||
{{ refilling_fragment }}
|
{{ refilling_fragment }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+18
-13
@@ -24,7 +24,7 @@ from django.shortcuts import get_object_or_404, redirect, resolve_url
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import FormView
|
from django.views.generic import CreateView, FormView
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from ninja.main import HttpRequest
|
from ninja.main import HttpRequest
|
||||||
|
|
||||||
@@ -32,7 +32,14 @@ from core.auth.mixins import CanViewMixin
|
|||||||
from core.models import User
|
from core.models import User
|
||||||
from core.views.mixins import FragmentMixin, UseFragmentsMixin
|
from core.views.mixins import FragmentMixin, UseFragmentsMixin
|
||||||
from counter.forms import BasketForm, RefillForm
|
from counter.forms import BasketForm, RefillForm
|
||||||
from counter.models import Counter, Customer, ProductFormula, ReturnableProduct, Selling
|
from counter.models import (
|
||||||
|
Counter,
|
||||||
|
Customer,
|
||||||
|
ProductFormula,
|
||||||
|
Refilling,
|
||||||
|
ReturnableProduct,
|
||||||
|
Selling,
|
||||||
|
)
|
||||||
from counter.utils import is_logged_in_counter
|
from counter.utils import is_logged_in_counter
|
||||||
from counter.views.mixins import CounterTabsMixin
|
from counter.views.mixins import CounterTabsMixin
|
||||||
from counter.views.student_card import StudentCardFormFragment
|
from counter.views.student_card import StudentCardFormFragment
|
||||||
@@ -219,9 +226,10 @@ class CounterClick(
|
|||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class RefillingCreateView(FragmentMixin, FormView):
|
class RefillingCreateView(FragmentMixin, CreateView):
|
||||||
"""This is a fragment only view which integrates with counter_click.jinja"""
|
"""This is a fragment only view which integrates with counter_click.jinja"""
|
||||||
|
|
||||||
|
model = Refilling
|
||||||
form_class = RefillForm
|
form_class = RefillForm
|
||||||
template_name = "counter/fragments/create_refill.jinja"
|
template_name = "counter/fragments/create_refill.jinja"
|
||||||
|
|
||||||
@@ -242,23 +250,20 @@ class RefillingCreateView(FragmentMixin, FormView):
|
|||||||
):
|
):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
self.operator = get_operator(request, self.counter, self.customer)
|
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def render_fragment(self, request, **kwargs) -> SafeString:
|
def render_fragment(self, request, **kwargs) -> SafeString:
|
||||||
self.customer = kwargs.pop("customer")
|
self.customer = kwargs.pop("customer")
|
||||||
self.counter = kwargs.pop("counter")
|
self.counter = kwargs.pop("counter")
|
||||||
|
self.object = None
|
||||||
return super().render_fragment(request, **kwargs)
|
return super().render_fragment(request, **kwargs)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def get_form_kwargs(self):
|
||||||
res = super().form_valid(form)
|
return super().get_form_kwargs() | {
|
||||||
form.clean()
|
"counter": self.counter,
|
||||||
form.instance.counter = self.counter
|
"operator": get_operator(self.request, self.counter, self.customer),
|
||||||
form.instance.operator = self.operator
|
"customer": self.customer,
|
||||||
form.instance.customer = self.customer
|
}
|
||||||
form.instance.save()
|
|
||||||
return res
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
|
|||||||
Reference in New Issue
Block a user