Allow transactions on counter when an user has recorded too many products as long as he doesn't record more

This commit is contained in:
Antoine Bartuccio 2025-02-21 14:50:07 +01:00
parent a96b374ad7
commit 1978658b9c
4 changed files with 171 additions and 135 deletions

View File

@ -1,146 +1,141 @@
import { exportToHtml } from "#core:utils/globals";
import { BasketItem } from "#counter:counter/basket";
import type { CounterConfig, ErrorMessage } from "#counter:counter/types";
import type { CounterProductSelect } from "./components/counter-product-select-index";
exportToHtml("loadCounter", (config: CounterConfig) => {
document.addEventListener("alpine:init", () => {
Alpine.data("counter", () => ({
basket: {} as Record<string, BasketItem>,
errors: [],
customerBalance: config.customerBalance,
codeField: undefined,
alertMessage: {
content: "",
show: false,
timeout: null,
},
document.addEventListener("alpine:init", () => {
Alpine.data("counter", (config: CounterConfig) => ({
basket: {} as Record<string, BasketItem>,
errors: [],
customerBalance: config.customerBalance,
codeField: null as CounterProductSelect | null,
alertMessage: {
content: "",
show: false,
timeout: null,
},
init() {
// Fill the basket with the initial data
for (const entry of config.formInitial) {
if (entry.id !== undefined && entry.quantity !== undefined) {
this.addToBasket(entry.id, entry.quantity);
this.basket[entry.id].errors = entry.errors ?? [];
}
init() {
// Fill the basket with the initial data
for (const entry of config.formInitial) {
if (entry.id !== undefined && entry.quantity !== undefined) {
this.addToBasket(entry.id, entry.quantity);
this.basket[entry.id].errors = entry.errors ?? [];
}
}
this.codeField = this.$refs.codeField;
this.codeField.widget.focus();
this.codeField = this.$refs.codeField;
this.codeField.widget.focus();
// It's quite tricky to manually apply attributes to the management part
// of a formset so we dynamically apply it here
this.$refs.basketManagementForm
.querySelector("#id_form-TOTAL_FORMS")
.setAttribute(":value", "getBasketSize()");
},
// It's quite tricky to manually apply attributes to the management part
// of a formset so we dynamically apply it here
this.$refs.basketManagementForm
.querySelector("#id_form-TOTAL_FORMS")
.setAttribute(":value", "getBasketSize()");
},
removeFromBasket(id: string) {
removeFromBasket(id: string) {
delete this.basket[id];
},
addToBasket(id: string, quantity: number): ErrorMessage {
const item: BasketItem =
this.basket[id] || new BasketItem(config.products[id], 0);
const oldQty = item.quantity;
item.quantity += quantity;
if (item.quantity <= 0) {
delete this.basket[id];
},
addToBasket(id: string, quantity: number): ErrorMessage {
const item: BasketItem =
this.basket[id] || new BasketItem(config.products[id], 0);
const oldQty = item.quantity;
item.quantity += quantity;
if (item.quantity <= 0) {
delete this.basket[id];
return "";
}
this.basket[id] = item;
if (this.sumBasket() > this.customerBalance) {
item.quantity = oldQty;
if (item.quantity === 0) {
delete this.basket[id];
}
return gettext("Not enough money");
}
return "";
},
}
getBasketSize() {
return Object.keys(this.basket).length;
},
this.basket[id] = item;
sumBasket() {
if (this.getBasketSize() === 0) {
return 0;
if (this.sumBasket() > this.customerBalance) {
item.quantity = oldQty;
if (item.quantity === 0) {
delete this.basket[id];
}
const total = Object.values(this.basket).reduce(
(acc: number, cur: BasketItem) => acc + cur.sum(),
0,
) as number;
return total;
},
return gettext("Not enough money");
}
showAlertMessage(message: string) {
if (this.alertMessage.timeout !== null) {
clearTimeout(this.alertMessage.timeout);
return "";
},
getBasketSize() {
return Object.keys(this.basket).length;
},
sumBasket() {
if (this.getBasketSize() === 0) {
return 0;
}
const total = Object.values(this.basket).reduce(
(acc: number, cur: BasketItem) => acc + cur.sum(),
0,
) as number;
return total;
},
showAlertMessage(message: string) {
if (this.alertMessage.timeout !== null) {
clearTimeout(this.alertMessage.timeout);
}
this.alertMessage.content = message;
this.alertMessage.show = true;
this.alertMessage.timeout = setTimeout(() => {
this.alertMessage.show = false;
this.alertMessage.timeout = null;
}, 2000);
},
addToBasketWithMessage(id: string, quantity: number) {
const message = this.addToBasket(id, quantity);
if (message.length > 0) {
this.showAlertMessage(message);
}
},
onRefillingSuccess(event: CustomEvent) {
if (event.type !== "htmx:after-request" || event.detail.failed) {
return;
}
this.customerBalance += Number.parseFloat(
(event.detail.target.querySelector("#id_amount") as HTMLInputElement).value,
);
document.getElementById("selling-accordion").click();
this.codeField.widget.focus();
},
finish() {
if (this.getBasketSize() === 0) {
this.showAlertMessage(gettext("You can't send an empty basket."));
return;
}
this.$refs.basketForm.submit();
},
cancel() {
location.href = config.cancelUrl;
},
handleCode() {
const [quantity, code] = this.codeField.getSelectedProduct() as [number, string];
if (this.codeField.getOperationCodes().includes(code.toUpperCase())) {
if (code === "ANN") {
this.cancel();
}
this.alertMessage.content = message;
this.alertMessage.show = true;
this.alertMessage.timeout = setTimeout(() => {
this.alertMessage.show = false;
this.alertMessage.timeout = null;
}, 2000);
},
addToBasketWithMessage(id: string, quantity: number) {
const message = this.addToBasket(id, quantity);
if (message.length > 0) {
this.showAlertMessage(message);
if (code === "FIN") {
this.finish();
}
},
onRefillingSuccess(event: CustomEvent) {
if (event.type !== "htmx:after-request" || event.detail.failed) {
return;
}
this.customerBalance += Number.parseFloat(
(event.detail.target.querySelector("#id_amount") as HTMLInputElement).value,
);
document.getElementById("selling-accordion").click();
this.codeField.widget.focus();
},
finish() {
if (this.getBasketSize() === 0) {
this.showAlertMessage(gettext("You can't send an empty basket."));
return;
}
this.$refs.basketForm.submit();
},
cancel() {
location.href = config.cancelUrl;
},
handleCode() {
const [quantity, code] = this.codeField.getSelectedProduct() as [
number,
string,
];
if (this.codeField.getOperationCodes().includes(code.toUpperCase())) {
if (code === "ANN") {
this.cancel();
}
if (code === "FIN") {
this.finish();
}
} else {
this.addToBasketWithMessage(code, quantity);
}
this.codeField.widget.clear();
this.codeField.widget.focus();
},
}));
});
} else {
this.addToBasketWithMessage(code, quantity);
}
this.codeField.widget.clear();
this.codeField.widget.focus();
},
}));
});
$(() => {

View File

@ -27,7 +27,13 @@
{% block content %}
<h4>{{ counter }}</h4>
<div id="bar-ui" x-data="counter">
<div id="bar-ui" x-data="counter({
customerBalance: {{ customer.amount }},
products: products,
customerId: {{ customer.pk }},
formInitial: formInitial,
cancelUrl: '{{ cancel_url }}',
})">
<noscript>
<p class="important">Javascript is required for the counter UI.</p>
</noscript>
@ -256,13 +262,7 @@
{%- endfor -%}
];
window.addEventListener("DOMContentLoaded", () => {
loadCounter({
customerBalance: {{ customer.amount }},
products: products,
customerId: {{ customer.pk }},
formInitial: formInitial,
cancelUrl: "{{ cancel_url }}",
});
loadCounter();
});
</script>
{% endblock script %}

View File

@ -681,6 +681,42 @@ class TestCounterClick(TestFullClickBase):
-3 - settings.SITH_ECOCUP_LIMIT
)
def test_recordings_when_negative(self):
self.refill_user(
self.customer,
self.cons.selling_price * 3 + Decimal(self.beer.selling_price),
)
self.customer.customer.recorded_products = settings.SITH_ECOCUP_LIMIT * -10
self.customer.customer.save()
self.login_in_bar(self.barmen)
assert (
self.submit_basket(
self.customer,
[BasketItem(self.dcons.id, 1)],
).status_code
== 200
)
assert self.updated_amount(
self.customer
) == self.cons.selling_price * 3 + Decimal(self.beer.selling_price)
assert (
self.submit_basket(
self.customer,
[BasketItem(self.cons.id, 3)],
).status_code
== 302
)
assert self.updated_amount(self.customer) == Decimal(self.beer.selling_price)
assert (
self.submit_basket(
self.customer,
[BasketItem(self.beer.id, 1)],
).status_code
== 302
)
assert self.updated_amount(self.customer) == 0
class TestCounterStats(TestCase):
@classmethod

View File

@ -126,6 +126,11 @@ class BaseBasketForm(BaseFormSet):
if form.product.is_unrecord_product:
self.total_recordings += form.cleaned_data["quantity"]
# We don't want to block an user that have negative recordings
# if he isn't recording anything or reducing it's recording count
if self.total_recordings <= 0:
return
if not customer.can_record_more(self.total_recordings):
raise ValidationError(_("This user have reached his recording limit"))