From 1978658b9c3a3493987b961ed20f2ff044b2ba97 Mon Sep 17 00:00:00 2001 From: Sli Date: Fri, 21 Feb 2025 14:50:07 +0100 Subject: [PATCH] Allow transactions on counter when an user has recorded too many products as long as he doesn't record more --- .../bundled/counter/counter-click-index.ts | 249 +++++++++--------- counter/templates/counter/counter_click.jinja | 16 +- counter/tests/test_counter.py | 36 +++ counter/views/click.py | 5 + 4 files changed, 171 insertions(+), 135 deletions(-) diff --git a/counter/static/bundled/counter/counter-click-index.ts b/counter/static/bundled/counter/counter-click-index.ts index 7ab8de1d..b2b63c80 100644 --- a/counter/static/bundled/counter/counter-click-index.ts +++ b/counter/static/bundled/counter/counter-click-index.ts @@ -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, - errors: [], - customerBalance: config.customerBalance, - codeField: undefined, - alertMessage: { - content: "", - show: false, - timeout: null, - }, +document.addEventListener("alpine:init", () => { + Alpine.data("counter", (config: CounterConfig) => ({ + basket: {} as Record, + 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(); + }, + })); }); $(() => { diff --git a/counter/templates/counter/counter_click.jinja b/counter/templates/counter/counter_click.jinja index 82707e2e..38c43637 100644 --- a/counter/templates/counter/counter_click.jinja +++ b/counter/templates/counter/counter_click.jinja @@ -27,7 +27,13 @@ {% block content %}

{{ counter }}

-
+
@@ -256,13 +262,7 @@ {%- endfor -%} ]; window.addEventListener("DOMContentLoaded", () => { - loadCounter({ - customerBalance: {{ customer.amount }}, - products: products, - customerId: {{ customer.pk }}, - formInitial: formInitial, - cancelUrl: "{{ cancel_url }}", - }); + loadCounter(); }); {% endblock script %} \ No newline at end of file diff --git a/counter/tests/test_counter.py b/counter/tests/test_counter.py index 27ce62bc..d50bb6c4 100644 --- a/counter/tests/test_counter.py +++ b/counter/tests/test_counter.py @@ -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 diff --git a/counter/views/click.py b/counter/views/click.py index eb6f8e28..46bf8e62 100644 --- a/counter/views/click.py +++ b/counter/views/click.py @@ -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"))