mirror of
https://github.com/ae-utbm/sith.git
synced 2024-12-22 07:41:14 +00:00
Merge pull request #923 from ae-utbm/counter-click-step-2
Casser counter click step 2 : separate refilling from counter clicks with fragments
This commit is contained in:
commit
6416de237f
@ -24,6 +24,12 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
PAYMENT_METHOD = [
|
||||||
|
("CHECK", _("Check")),
|
||||||
|
("CASH", _("Cash")),
|
||||||
|
("CARD", _("Credit card")),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class CounterConfig(AppConfig):
|
class CounterConfig(AppConfig):
|
||||||
name = "counter"
|
name = "counter"
|
||||||
|
@ -111,6 +111,8 @@ class GetUserForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class RefillForm(forms.ModelForm):
|
class RefillForm(forms.ModelForm):
|
||||||
|
allowed_refilling_methods = ["CASH", "CARD"]
|
||||||
|
|
||||||
error_css_class = "error"
|
error_css_class = "error"
|
||||||
required_css_class = "required"
|
required_css_class = "required"
|
||||||
amount = forms.FloatField(
|
amount = forms.FloatField(
|
||||||
@ -120,6 +122,21 @@ class RefillForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Refilling
|
model = Refilling
|
||||||
fields = ["amount", "payment_method", "bank"]
|
fields = ["amount", "payment_method", "bank"]
|
||||||
|
widgets = {"payment_method": forms.RadioSelect}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields["payment_method"].choices = (
|
||||||
|
method
|
||||||
|
for method in self.fields["payment_method"].choices
|
||||||
|
if method[0] 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]
|
||||||
|
|
||||||
|
if "CHECK" not in self.allowed_refilling_methods:
|
||||||
|
del self.fields["bank"]
|
||||||
|
|
||||||
|
|
||||||
class CounterEditForm(forms.ModelForm):
|
class CounterEditForm(forms.ModelForm):
|
||||||
|
22
counter/migrations/0027_alter_refilling_payment_method.py
Normal file
22
counter/migrations/0027_alter_refilling_payment_method.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 4.2.17 on 2024-12-15 22:21
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("counter", "0026_alter_studentcard_customer"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="refilling",
|
||||||
|
name="payment_method",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[("CHECK", "Check"), ("CASH", "Cash"), ("CARD", "Credit card")],
|
||||||
|
default="CARD",
|
||||||
|
max_length=255,
|
||||||
|
verbose_name="payment method",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -42,7 +42,8 @@ from club.models import Club
|
|||||||
from core.fields import ResizedImageField
|
from core.fields import ResizedImageField
|
||||||
from core.models import Group, Notification, User
|
from core.models import Group, Notification, User
|
||||||
from core.utils import get_start_of_semester
|
from core.utils import get_start_of_semester
|
||||||
from sith.settings import SITH_COUNTER_OFFICES, SITH_MAIN_CLUB
|
from counter.apps import PAYMENT_METHOD
|
||||||
|
from sith.settings import SITH_MAIN_CLUB
|
||||||
from subscription.models import Subscription
|
from subscription.models import Subscription
|
||||||
|
|
||||||
|
|
||||||
@ -558,9 +559,6 @@ class Counter(models.Model):
|
|||||||
"""Show if the counter authorize the refilling with physic money."""
|
"""Show if the counter authorize the refilling with physic money."""
|
||||||
if self.type != "BAR":
|
if self.type != "BAR":
|
||||||
return False
|
return False
|
||||||
if self.id in SITH_COUNTER_OFFICES:
|
|
||||||
# If the counter is either 'AE' or 'BdF', refills are authorized
|
|
||||||
return True
|
|
||||||
# at least one of the barmen is in the AE board
|
# at least one of the barmen is in the AE board
|
||||||
ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"])
|
ae = Club.objects.get(unix_name=SITH_MAIN_CLUB["unix_name"])
|
||||||
return any(ae.get_membership_for(barman) for barman in self.barmen_list)
|
return any(ae.get_membership_for(barman) for barman in self.barmen_list)
|
||||||
@ -650,6 +648,14 @@ class Counter(models.Model):
|
|||||||
)
|
)
|
||||||
)["total"]
|
)["total"]
|
||||||
|
|
||||||
|
def customer_is_barman(self, customer: Customer | User) -> bool:
|
||||||
|
"""Check if this counter is a `bar` and if the customer is currently logged in.
|
||||||
|
This is useful to compute special prices."""
|
||||||
|
|
||||||
|
# Customer and User are two different tables,
|
||||||
|
# but they share the same primary key
|
||||||
|
return self.type == "BAR" and any(b.pk == customer.pk for b in self.barmen_list)
|
||||||
|
|
||||||
|
|
||||||
class RefillingQuerySet(models.QuerySet):
|
class RefillingQuerySet(models.QuerySet):
|
||||||
def annotate_total(self) -> Self:
|
def annotate_total(self) -> Self:
|
||||||
@ -688,8 +694,8 @@ class Refilling(models.Model):
|
|||||||
payment_method = models.CharField(
|
payment_method = models.CharField(
|
||||||
_("payment method"),
|
_("payment method"),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
choices=settings.SITH_COUNTER_PAYMENT_METHOD,
|
choices=PAYMENT_METHOD,
|
||||||
default="CASH",
|
default="CARD",
|
||||||
)
|
)
|
||||||
bank = models.CharField(
|
bank = models.CharField(
|
||||||
_("bank"), max_length=255, choices=settings.SITH_COUNTER_BANK, default="OTHER"
|
_("bank"), max_length=255, choices=settings.SITH_COUNTER_BANK, default="OTHER"
|
||||||
|
129
counter/static/bundled/counter/counter-click-index.ts
Normal file
129
counter/static/bundled/counter/counter-click-index.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { exportToHtml } from "#core:utils/globals";
|
||||||
|
|
||||||
|
interface CounterConfig {
|
||||||
|
csrfToken: string;
|
||||||
|
clickApiUrl: string;
|
||||||
|
sessionBasket: Record<number, BasketItem>;
|
||||||
|
customerBalance: number;
|
||||||
|
customerId: number;
|
||||||
|
}
|
||||||
|
interface BasketItem {
|
||||||
|
// biome-ignore lint/style/useNamingConvention: talking with python
|
||||||
|
bonus_qty: number;
|
||||||
|
price: number;
|
||||||
|
qty: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
exportToHtml("loadCounter", (config: CounterConfig) => {
|
||||||
|
document.addEventListener("alpine:init", () => {
|
||||||
|
Alpine.data("counter", () => ({
|
||||||
|
basket: config.sessionBasket,
|
||||||
|
errors: [],
|
||||||
|
customerBalance: config.customerBalance,
|
||||||
|
|
||||||
|
sumBasket() {
|
||||||
|
if (!this.basket || Object.keys(this.basket).length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const total = Object.values(this.basket).reduce(
|
||||||
|
(acc: number, cur: BasketItem) => acc + cur.qty * cur.price,
|
||||||
|
0,
|
||||||
|
) as number;
|
||||||
|
return total / 100;
|
||||||
|
},
|
||||||
|
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleCode(event: SubmitEvent) {
|
||||||
|
const code = (
|
||||||
|
$(event.target).find("#code_field").val() as string
|
||||||
|
).toUpperCase();
|
||||||
|
if (["FIN", "ANN"].includes(code)) {
|
||||||
|
$(event.target).submit();
|
||||||
|
} else {
|
||||||
|
await this.handleAction(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleAction(event: SubmitEvent) {
|
||||||
|
const payload = $(event.target).serialize();
|
||||||
|
const request = new Request(config.clickApiUrl, {
|
||||||
|
method: "POST",
|
||||||
|
body: payload,
|
||||||
|
headers: {
|
||||||
|
// biome-ignore lint/style/useNamingConvention: this goes into http headers
|
||||||
|
Accept: "application/json",
|
||||||
|
"X-CSRFToken": config.csrfToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const response = await fetch(request);
|
||||||
|
const json = await response.json();
|
||||||
|
this.basket = json.basket;
|
||||||
|
this.errors = json.errors;
|
||||||
|
$("form.code_form #code_field").val("").focus();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
tags: string;
|
||||||
|
}
|
||||||
|
declare global {
|
||||||
|
const productsAutocomplete: Product[];
|
||||||
|
}
|
||||||
|
|
||||||
|
$(() => {
|
||||||
|
/* Autocompletion in the code field */
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery
|
||||||
|
const codeField: any = $("#code_field");
|
||||||
|
|
||||||
|
let quantity = "";
|
||||||
|
codeField.autocomplete({
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery
|
||||||
|
select: (event: any, ui: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
codeField.val(quantity + ui.item.value);
|
||||||
|
},
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery
|
||||||
|
focus: (event: any, ui: any) => {
|
||||||
|
event.preventDefault();
|
||||||
|
codeField.val(quantity + ui.item.value);
|
||||||
|
},
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery
|
||||||
|
source: (request: any, response: any) => {
|
||||||
|
// biome-ignore lint/performance/useTopLevelRegex: performance impact is minimal
|
||||||
|
const res = /^(\d+x)?(.*)/i.exec(request.term);
|
||||||
|
quantity = res[1] || "";
|
||||||
|
const search = res[2];
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery
|
||||||
|
const matcher = new RegExp(($ as any).ui.autocomplete.escapeRegex(search), "i");
|
||||||
|
response(
|
||||||
|
$.grep(productsAutocomplete, (value: Product) => {
|
||||||
|
return matcher.test(value.tags);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Accordion UI between basket and refills */
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery
|
||||||
|
($("#click_form") as any).accordion({
|
||||||
|
heightStyle: "content",
|
||||||
|
activate: () => $(".focus").focus(),
|
||||||
|
});
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery
|
||||||
|
($("#products") as any).tabs();
|
||||||
|
|
||||||
|
codeField.focus();
|
||||||
|
});
|
@ -1,86 +0,0 @@
|
|||||||
document.addEventListener("alpine:init", () => {
|
|
||||||
Alpine.data("counter", () => ({
|
|
||||||
// biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja
|
|
||||||
basket: sessionBasket,
|
|
||||||
errors: [],
|
|
||||||
|
|
||||||
sumBasket() {
|
|
||||||
if (!this.basket || Object.keys(this.basket).length === 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const total = Object.values(this.basket).reduce(
|
|
||||||
(acc, cur) => acc + cur.qty * cur.price,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
return total / 100;
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleCode(event) {
|
|
||||||
const code = $(event.target).find("#code_field").val().toUpperCase();
|
|
||||||
if (["FIN", "ANN"].includes(code)) {
|
|
||||||
$(event.target).submit();
|
|
||||||
} else {
|
|
||||||
await this.handleAction(event);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async handleAction(event) {
|
|
||||||
const payload = $(event.target).serialize();
|
|
||||||
// biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja
|
|
||||||
const request = new Request(clickApiUrl, {
|
|
||||||
method: "POST",
|
|
||||||
body: payload,
|
|
||||||
headers: {
|
|
||||||
// biome-ignore lint/style/useNamingConvention: this goes into http headers
|
|
||||||
Accept: "application/json",
|
|
||||||
// biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja
|
|
||||||
"X-CSRFToken": csrfToken,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const response = await fetch(request);
|
|
||||||
const json = await response.json();
|
|
||||||
this.basket = json.basket;
|
|
||||||
this.errors = json.errors;
|
|
||||||
$("form.code_form #code_field").val("").focus();
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
$(() => {
|
|
||||||
/* Autocompletion in the code field */
|
|
||||||
const codeField = $("#code_field");
|
|
||||||
|
|
||||||
let quantity = "";
|
|
||||||
codeField.autocomplete({
|
|
||||||
select: (event, ui) => {
|
|
||||||
event.preventDefault();
|
|
||||||
codeField.val(quantity + ui.item.value);
|
|
||||||
},
|
|
||||||
focus: (event, ui) => {
|
|
||||||
event.preventDefault();
|
|
||||||
codeField.val(quantity + ui.item.value);
|
|
||||||
},
|
|
||||||
source: (request, response) => {
|
|
||||||
// biome-ignore lint/performance/useTopLevelRegex: performance impact is minimal
|
|
||||||
const res = /^(\d+x)?(.*)/i.exec(request.term);
|
|
||||||
quantity = res[1] || "";
|
|
||||||
const search = res[2];
|
|
||||||
const matcher = new RegExp($.ui.autocomplete.escapeRegex(search), "i");
|
|
||||||
response(
|
|
||||||
// biome-ignore lint/correctness/noUndeclaredVariables: defined in counter_click.jinja
|
|
||||||
$.grep(productsAutocomplete, (value) => {
|
|
||||||
return matcher.test(value.tags);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Accordion UI between basket and refills */
|
|
||||||
$("#click_form").accordion({
|
|
||||||
heightStyle: "content",
|
|
||||||
activate: () => $(".focus").focus(),
|
|
||||||
});
|
|
||||||
$("#products").tabs();
|
|
||||||
|
|
||||||
codeField.focus();
|
|
||||||
});
|
|
@ -6,7 +6,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block additional_js %}
|
{% block additional_js %}
|
||||||
<script src="{{ static('counter/js/counter_click.js') }}" defer></script>
|
<script type="module" src="{{ static('bundled/counter/counter-click-index.ts') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block info_boxes %}
|
{% block info_boxes %}
|
||||||
@ -28,16 +28,11 @@
|
|||||||
<h5>{% trans %}Customer{% endtrans %}</h5>
|
<h5>{% trans %}Customer{% endtrans %}</h5>
|
||||||
{{ user_mini_profile(customer.user) }}
|
{{ user_mini_profile(customer.user) }}
|
||||||
{{ user_subscription(customer.user) }}
|
{{ user_subscription(customer.user) }}
|
||||||
<p>{% trans %}Amount: {% endtrans %}{{ customer.amount }} €</p>
|
<p>{% trans %}Amount: {% endtrans %}<span x-text="customerBalance"></span> €</p>
|
||||||
|
|
||||||
{% if counter.type == 'BAR' %}
|
|
||||||
<h5>{% trans %}Student card{% endtrans %}</h3>
|
|
||||||
{{ student_card_fragment }}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="click_form">
|
<div id="click_form" style="width: 20%;">
|
||||||
<h5>{% trans %}Selling{% endtrans %}</h5>
|
<h5 id="selling-accordion">{% trans %}Selling{% endtrans %}</h5>
|
||||||
<div>
|
<div>
|
||||||
{% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %}
|
{% set counter_click_url = url('counter:click', counter_id=counter.id, user_id=customer.user_id) %}
|
||||||
|
|
||||||
@ -105,17 +100,32 @@
|
|||||||
<input type="submit" value="{% trans %}Cancel{% endtrans %}"/>
|
<input type="submit" value="{% trans %}Cancel{% endtrans %}"/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% if (counter.type == 'BAR' and barmens_can_refill) %}
|
{% if object.type == "BAR" %}
|
||||||
<h5>{% trans %}Refilling{% endtrans %}</h5>
|
<h5>{% trans %}Refilling{% endtrans %}</h5>
|
||||||
<div>
|
{% if refilling_fragment %}
|
||||||
<form method="post"
|
<div
|
||||||
action="{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}">
|
@htmx:after-request="onRefillingSuccess"
|
||||||
{% csrf_token %}
|
>
|
||||||
{{ refill_form.as_p() }}
|
{{ refilling_fragment }}
|
||||||
<input type="hidden" name="action" value="refill">
|
|
||||||
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div>
|
||||||
|
<p class="alert alert-yellow">
|
||||||
|
{% trans trimmed %}
|
||||||
|
As a barman, you are not able to refill any account on your own.
|
||||||
|
An admin should be connected on this counter for that.
|
||||||
|
The customer can refill by using the eboutic.
|
||||||
|
{% endtrans %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if student_card_fragment %}
|
||||||
|
<h5>{% trans %}Student card{% endtrans %}</h3>
|
||||||
|
<div>
|
||||||
|
{{ student_card_fragment }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -155,9 +165,6 @@
|
|||||||
{% block script %}
|
{% block script %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<script>
|
||||||
const csrfToken = "{{ csrf_token }}";
|
|
||||||
const clickApiUrl = "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}";
|
|
||||||
const sessionBasket = {{ request.session["basket"]|tojson }};
|
|
||||||
const products = {
|
const products = {
|
||||||
{%- for p in products -%}
|
{%- for p in products -%}
|
||||||
{{ p.id }}: {
|
{{ p.id }}: {
|
||||||
@ -176,5 +183,14 @@
|
|||||||
},
|
},
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
];
|
];
|
||||||
|
window.addEventListener("DOMContentLoaded", () => {
|
||||||
|
loadCounter({
|
||||||
|
csrfToken: "{{ csrf_token }}",
|
||||||
|
clickApiUrl: "{{ url('counter:click', counter_id=counter.id, user_id=customer.user.id) }}",
|
||||||
|
sessionBasket: {{ request.session["basket"]|tojson }},
|
||||||
|
customerBalance: {{ customer.amount }},
|
||||||
|
customerId: {{ customer.pk }},
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock script %}
|
{% endblock script %}
|
9
counter/templates/counter/fragments/create_refill.jinja
Normal file
9
counter/templates/counter/fragments/create_refill.jinja
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<form
|
||||||
|
hx-trigger="submit"
|
||||||
|
hx-post="{{ action }}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p() }}
|
||||||
|
<input type="submit" value="{% trans %}Go{% endtrans %}"/>
|
||||||
|
</form>
|
@ -67,17 +67,22 @@ class TestCounter(TestCase):
|
|||||||
{"code": self.richard.customer.account_id, "counter_token": counter_token},
|
{"code": self.richard.customer.account_id, "counter_token": counter_token},
|
||||||
)
|
)
|
||||||
counter_url = response.get("location")
|
counter_url = response.get("location")
|
||||||
response = self.client.get(response.get("location"))
|
refill_url = reverse(
|
||||||
|
"counter:refilling_create",
|
||||||
|
kwargs={"customer_id": self.richard.customer.pk},
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(counter_url)
|
||||||
assert ">Richard Batsbak</" in str(response.content)
|
assert ">Richard Batsbak</" in str(response.content)
|
||||||
|
|
||||||
self.client.post(
|
self.client.post(
|
||||||
counter_url,
|
refill_url,
|
||||||
{
|
{
|
||||||
"action": "refill",
|
|
||||||
"amount": "5",
|
"amount": "5",
|
||||||
"payment_method": "CASH",
|
"payment_method": "CASH",
|
||||||
"bank": "OTHER",
|
"bank": "OTHER",
|
||||||
},
|
},
|
||||||
|
HTTP_REFERER=counter_url,
|
||||||
)
|
)
|
||||||
self.client.post(counter_url, "action=code&code=BARB", content_type="text/xml")
|
self.client.post(counter_url, "action=code&code=BARB", content_type="text/xml")
|
||||||
self.client.post(
|
self.client.post(
|
||||||
@ -110,15 +115,15 @@ class TestCounter(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
counter_url,
|
refill_url,
|
||||||
{
|
{
|
||||||
"action": "refill",
|
|
||||||
"amount": "5",
|
"amount": "5",
|
||||||
"payment_method": "CASH",
|
"payment_method": "CASH",
|
||||||
"bank": "OTHER",
|
"bank": "OTHER",
|
||||||
},
|
},
|
||||||
|
HTTP_REFERER=counter_url,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 302
|
||||||
|
|
||||||
self.client.post(
|
self.client.post(
|
||||||
reverse("counter:login", kwargs={"counter_id": self.foyer.id}),
|
reverse("counter:login", kwargs={"counter_id": self.foyer.id}),
|
||||||
@ -138,17 +143,23 @@ class TestCounter(TestCase):
|
|||||||
{"code": self.richard.customer.account_id, "counter_token": counter_token},
|
{"code": self.richard.customer.account_id, "counter_token": counter_token},
|
||||||
)
|
)
|
||||||
counter_url = response.get("location")
|
counter_url = response.get("location")
|
||||||
|
refill_url = reverse(
|
||||||
|
"counter:refilling_create",
|
||||||
|
kwargs={
|
||||||
|
"customer_id": self.richard.customer.pk,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
counter_url,
|
refill_url,
|
||||||
{
|
{
|
||||||
"action": "refill",
|
|
||||||
"amount": "5",
|
"amount": "5",
|
||||||
"payment_method": "CASH",
|
"payment_method": "CASH",
|
||||||
"bank": "OTHER",
|
"bank": "OTHER",
|
||||||
},
|
},
|
||||||
|
HTTP_REFERER=counter_url,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 403 # Krophil is not board admin
|
||||||
|
|
||||||
def test_annotate_has_barman_queryset(self):
|
def test_annotate_has_barman_queryset(self):
|
||||||
"""Test if the custom queryset method `annotate_has_barman` works as intended."""
|
"""Test if the custom queryset method `annotate_has_barman` works as intended."""
|
||||||
|
@ -39,7 +39,7 @@ from counter.views.cash import (
|
|||||||
CashSummaryListView,
|
CashSummaryListView,
|
||||||
CounterCashSummaryView,
|
CounterCashSummaryView,
|
||||||
)
|
)
|
||||||
from counter.views.click import CounterClick
|
from counter.views.click import CounterClick, RefillingCreateView
|
||||||
from counter.views.eticket import (
|
from counter.views.eticket import (
|
||||||
EticketCreateView,
|
EticketCreateView,
|
||||||
EticketEditView,
|
EticketEditView,
|
||||||
@ -60,6 +60,11 @@ from counter.views.student_card import (
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("<int:counter_id>/", CounterMain.as_view(), name="details"),
|
path("<int:counter_id>/", CounterMain.as_view(), name="details"),
|
||||||
path("<int:counter_id>/click/<int:user_id>/", CounterClick.as_view(), name="click"),
|
path("<int:counter_id>/click/<int:user_id>/", CounterClick.as_view(), name="click"),
|
||||||
|
path(
|
||||||
|
"refill/<int:customer_id>/",
|
||||||
|
RefillingCreateView.as_view(),
|
||||||
|
name="refilling_create",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"<int:counter_id>/last_ops/",
|
"<int:counter_id>/last_ops/",
|
||||||
CounterLastOperationsView.as_view(),
|
CounterLastOperationsView.as_view(),
|
||||||
|
@ -24,11 +24,13 @@ from django.http import Http404, HttpResponseRedirect, JsonResponse
|
|||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import DetailView
|
from django.views.generic import DetailView, FormView
|
||||||
|
|
||||||
|
from core.utils import FormFragmentTemplateData
|
||||||
from core.views import CanViewMixin
|
from core.views import CanViewMixin
|
||||||
from counter.forms import RefillForm
|
from counter.forms import RefillForm
|
||||||
from counter.models import Counter, Customer, Product, Selling
|
from counter.models import Counter, Customer, Product, Selling
|
||||||
|
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 StudentCardFormView
|
from counter.views.student_card import StudentCardFormView
|
||||||
|
|
||||||
@ -100,7 +102,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
request.session["too_young"] = False
|
request.session["too_young"] = False
|
||||||
request.session["not_allowed"] = False
|
request.session["not_allowed"] = False
|
||||||
request.session["no_age"] = False
|
request.session["no_age"] = False
|
||||||
self.refill_form = None
|
|
||||||
ret = super().get(request, *args, **kwargs)
|
ret = super().get(request, *args, **kwargs)
|
||||||
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
||||||
self.object.type == "BAR" and len(self.object.barmen_list) == 0
|
self.object.type == "BAR" and len(self.object.barmen_list) == 0
|
||||||
@ -111,7 +112,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""Handle the many possibilities of the post request."""
|
"""Handle the many possibilities of the post request."""
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
self.refill_form = None
|
|
||||||
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
if (self.object.type != "BAR" and not request.user.is_authenticated) or (
|
||||||
self.object.type == "BAR" and len(self.object.barmen_list) < 1
|
self.object.type == "BAR" and len(self.object.barmen_list) < 1
|
||||||
): # Check that at least one barman is logged in
|
): # Check that at least one barman is logged in
|
||||||
@ -137,7 +137,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
request.session["no_age"] = False
|
request.session["no_age"] = False
|
||||||
if self.object.type != "BAR":
|
if self.object.type != "BAR":
|
||||||
self.operator = request.user
|
self.operator = request.user
|
||||||
elif self.customer_is_barman():
|
elif self.object.customer_is_barman(self.customer):
|
||||||
self.operator = self.customer.user
|
self.operator = self.customer.user
|
||||||
else:
|
else:
|
||||||
self.operator = self.object.get_random_barman()
|
self.operator = self.object.get_random_barman()
|
||||||
@ -148,8 +148,6 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
self.add_product(request)
|
self.add_product(request)
|
||||||
elif action == "del_product":
|
elif action == "del_product":
|
||||||
self.del_product(request)
|
self.del_product(request)
|
||||||
elif action == "refill":
|
|
||||||
self.refill(request)
|
|
||||||
elif action == "code":
|
elif action == "code":
|
||||||
return self.parse_code(request)
|
return self.parse_code(request)
|
||||||
elif action == "cancel":
|
elif action == "cancel":
|
||||||
@ -159,16 +157,12 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
context = self.get_context_data(object=self.object)
|
context = self.get_context_data(object=self.object)
|
||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
def customer_is_barman(self) -> bool:
|
|
||||||
barmen = self.object.barmen_list
|
|
||||||
return self.object.type == "BAR" and self.customer.user in barmen
|
|
||||||
|
|
||||||
def get_product(self, pid):
|
def get_product(self, pid):
|
||||||
return Product.objects.filter(pk=int(pid)).first()
|
return Product.objects.filter(pk=int(pid)).first()
|
||||||
|
|
||||||
def get_price(self, pid):
|
def get_price(self, pid):
|
||||||
p = self.get_product(pid)
|
p = self.get_product(pid)
|
||||||
if self.customer_is_barman():
|
if self.object.customer_is_barman(self.customer):
|
||||||
price = p.special_selling_price
|
price = p.special_selling_price
|
||||||
else:
|
else:
|
||||||
price = p.selling_price
|
price = p.selling_price
|
||||||
@ -333,7 +327,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
for pid, infos in request.session["basket"].items():
|
for pid, infos in request.session["basket"].items():
|
||||||
# This duplicates code for DB optimization (prevent to load many times the same object)
|
# This duplicates code for DB optimization (prevent to load many times the same object)
|
||||||
p = Product.objects.filter(pk=pid).first()
|
p = Product.objects.filter(pk=pid).first()
|
||||||
if self.customer_is_barman():
|
if self.object.customer_is_barman(self.customer):
|
||||||
uprice = p.special_selling_price
|
uprice = p.special_selling_price
|
||||||
else:
|
else:
|
||||||
uprice = p.selling_price
|
uprice = p.selling_price
|
||||||
@ -383,24 +377,11 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
reverse_lazy("counter:details", args=self.args, kwargs=kwargs)
|
reverse_lazy("counter:details", args=self.args, kwargs=kwargs)
|
||||||
)
|
)
|
||||||
|
|
||||||
def refill(self, request):
|
|
||||||
"""Refill the customer's account."""
|
|
||||||
if not self.object.can_refill():
|
|
||||||
raise PermissionDenied
|
|
||||||
form = RefillForm(request.POST)
|
|
||||||
if form.is_valid():
|
|
||||||
form.instance.counter = self.object
|
|
||||||
form.instance.operator = self.operator
|
|
||||||
form.instance.customer = self.customer
|
|
||||||
form.instance.save()
|
|
||||||
else:
|
|
||||||
self.refill_form = form
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""Add customer to the context."""
|
"""Add customer to the context."""
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
products = self.object.products.select_related("product_type")
|
products = self.object.products.select_related("product_type")
|
||||||
if self.customer_is_barman():
|
if self.object.customer_is_barman(self.customer):
|
||||||
products = products.annotate(price=F("special_selling_price"))
|
products = products.annotate(price=F("special_selling_price"))
|
||||||
else:
|
else:
|
||||||
products = products.annotate(price=F("selling_price"))
|
products = products.annotate(price=F("selling_price"))
|
||||||
@ -413,9 +394,75 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
|||||||
)
|
)
|
||||||
kwargs["customer"] = self.customer
|
kwargs["customer"] = self.customer
|
||||||
kwargs["basket_total"] = self.sum_basket(self.request)
|
kwargs["basket_total"] = self.sum_basket(self.request)
|
||||||
kwargs["refill_form"] = self.refill_form or RefillForm()
|
|
||||||
kwargs["barmens_can_refill"] = self.object.can_refill()
|
if self.object.type == "BAR":
|
||||||
kwargs["student_card_fragment"] = StudentCardFormView.get_template_data(
|
kwargs["student_card_fragment"] = StudentCardFormView.get_template_data(
|
||||||
self.customer
|
self.customer
|
||||||
).render(self.request)
|
).render(self.request)
|
||||||
|
|
||||||
|
if self.object.can_refill():
|
||||||
|
kwargs["refilling_fragment"] = RefillingCreateView.get_template_data(
|
||||||
|
self.customer
|
||||||
|
).render(self.request)
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class RefillingCreateView(FormView):
|
||||||
|
"""This is a fragment only view which integrates with counter_click.jinja"""
|
||||||
|
|
||||||
|
form_class = RefillForm
|
||||||
|
template_name = "counter/fragments/create_refill.jinja"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_template_data(
|
||||||
|
cls, customer: Customer, *, form_instance: form_class | None = None
|
||||||
|
) -> FormFragmentTemplateData[form_class]:
|
||||||
|
return FormFragmentTemplateData(
|
||||||
|
form=form_instance if form_instance else cls.form_class(),
|
||||||
|
template=cls.template_name,
|
||||||
|
context={
|
||||||
|
"action": reverse_lazy(
|
||||||
|
"counter:refilling_create", kwargs={"customer_id": customer.pk}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
self.customer: Customer = get_object_or_404(Customer, pk=kwargs["customer_id"])
|
||||||
|
if not self.customer.can_buy:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
if not is_logged_in_counter(request):
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
self.counter: Counter = get_object_or_404(
|
||||||
|
Counter, token=request.session["counter_token"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.counter.can_refill():
|
||||||
|
raise PermissionDenied
|
||||||
|
|
||||||
|
if self.counter.customer_is_barman(self.customer):
|
||||||
|
self.operator = self.customer.user
|
||||||
|
else:
|
||||||
|
self.operator = self.counter.get_random_barman()
|
||||||
|
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
res = super().form_valid(form)
|
||||||
|
form.clean()
|
||||||
|
form.instance.counter = self.counter
|
||||||
|
form.instance.operator = self.operator
|
||||||
|
form.instance.customer = self.customer
|
||||||
|
form.instance.save()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
data = self.get_template_data(self.customer, form_instance=context["form"])
|
||||||
|
context.update(data.context)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_success_url(self, **kwargs):
|
||||||
|
return self.request.path
|
||||||
|
@ -70,11 +70,11 @@ class StudentCardFormView(FormView):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_template_data(
|
def get_template_data(
|
||||||
cls, customer: Customer
|
cls, customer: Customer, *, form_instance: form_class | None = None
|
||||||
) -> FormFragmentTemplateData[StudentCardForm]:
|
) -> FormFragmentTemplateData[form_class]:
|
||||||
"""Get necessary data to pre-render the fragment"""
|
"""Get necessary data to pre-render the fragment"""
|
||||||
return FormFragmentTemplateData(
|
return FormFragmentTemplateData(
|
||||||
form=cls.form_class(),
|
form=form_instance if form_instance else cls.form_class(),
|
||||||
template=cls.template_name,
|
template=cls.template_name,
|
||||||
context={
|
context={
|
||||||
"action": reverse(
|
"action": reverse(
|
||||||
@ -105,7 +105,7 @@ class StudentCardFormView(FormView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
data = self.get_template_data(self.customer)
|
data = self.get_template_data(self.customer, form_instance=context["form"])
|
||||||
context.update(data.context)
|
context.update(data.context)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -432,14 +432,6 @@ SITH_SUBSCRIPTION_LOCATIONS = [
|
|||||||
|
|
||||||
SITH_COUNTER_BARS = [(1, "MDE"), (2, "Foyer"), (35, "La Gommette")]
|
SITH_COUNTER_BARS = [(1, "MDE"), (2, "Foyer"), (35, "La Gommette")]
|
||||||
|
|
||||||
SITH_COUNTER_OFFICES = {2: "PdF", 1: "AE"}
|
|
||||||
|
|
||||||
SITH_COUNTER_PAYMENT_METHOD = [
|
|
||||||
("CHECK", _("Check")),
|
|
||||||
("CASH", _("Cash")),
|
|
||||||
("CARD", _("Credit card")),
|
|
||||||
]
|
|
||||||
|
|
||||||
SITH_COUNTER_BANK = [
|
SITH_COUNTER_BANK = [
|
||||||
("OTHER", "Autre"),
|
("OTHER", "Autre"),
|
||||||
("SOCIETE-GENERALE", "Société générale"),
|
("SOCIETE-GENERALE", "Société générale"),
|
||||||
|
@ -21,6 +21,7 @@ from django.utils.timezone import localdate
|
|||||||
from django.views.generic import CreateView, DetailView, TemplateView
|
from django.views.generic import CreateView, DetailView, TemplateView
|
||||||
from django.views.generic.edit import FormView
|
from django.views.generic.edit import FormView
|
||||||
|
|
||||||
|
from counter.apps import PAYMENT_METHOD
|
||||||
from subscription.forms import (
|
from subscription.forms import (
|
||||||
SelectionDateForm,
|
SelectionDateForm,
|
||||||
SubscriptionExistingUserForm,
|
SubscriptionExistingUserForm,
|
||||||
@ -108,6 +109,6 @@ class SubscriptionsStatsView(FormView):
|
|||||||
subscription_end__gte=self.end_date, subscription_start__lte=self.start_date
|
subscription_end__gte=self.end_date, subscription_start__lte=self.start_date
|
||||||
)
|
)
|
||||||
kwargs["subscriptions_types"] = settings.SITH_SUBSCRIPTIONS
|
kwargs["subscriptions_types"] = settings.SITH_SUBSCRIPTIONS
|
||||||
kwargs["payment_types"] = settings.SITH_COUNTER_PAYMENT_METHOD
|
kwargs["payment_types"] = PAYMENT_METHOD
|
||||||
kwargs["locations"] = settings.SITH_SUBSCRIPTION_LOCATIONS
|
kwargs["locations"] = settings.SITH_SUBSCRIPTION_LOCATIONS
|
||||||
return kwargs
|
return kwargs
|
||||||
|
Loading…
Reference in New Issue
Block a user