1 Commits

Author SHA1 Message Date
dependabot[bot]
8bdf7c7eba [UPDATE] Update ical requirement from <12,>=11.1.0 to >=11.1.0,<13
Updates the requirements on [ical](https://github.com/allenporter/ical) to permit the latest version.
- [Release notes](https://github.com/allenporter/ical/releases)
- [Commits](https://github.com/allenporter/ical/compare/11.1.0...12.0.0)

---
updated-dependencies:
- dependency-name: ical
  dependency-version: 12.0.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 08:03:55 +00:00
33 changed files with 219 additions and 343 deletions

View File

@@ -8,7 +8,7 @@ from django.utils.crypto import constant_time_compare
class Sha512ApiKeyHasher(BasePasswordHasher): class Sha512ApiKeyHasher(BasePasswordHasher):
""" """
An API key hasher using the sha512 algorithm. An API key hasher using the sha256 algorithm.
This hasher shouldn't be used in Django's `PASSWORD_HASHERS` setting. This hasher shouldn't be used in Django's `PASSWORD_HASHERS` setting.
It is insecure for use in hashing passwords, but is safe for hashing It is insecure for use in hashing passwords, but is safe for hashing

View File

@@ -7,7 +7,7 @@ from django.conf import settings
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Max from django.db.models import Max
from django.test import Client, TestCase from django.test import TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.timezone import localdate, localtime, now from django.utils.timezone import localdate, localtime, now
from model_bakery import baker from model_bakery import baker
@@ -532,35 +532,6 @@ class TestMembership(TestClub):
assert new_board == initial_board assert new_board == initial_board
@pytest.mark.django_db
def test_membership_set_old(client: Client):
membership = baker.make(Membership, end_date=None, user=(subscriber_user.make()))
client.force_login(membership.user)
response = client.post(
reverse("club:membership_set_old", kwargs={"membership_id": membership.id})
)
assertRedirects(
response, reverse("core:user_clubs", kwargs={"user_id": membership.user_id})
)
membership.refresh_from_db()
assert membership.end_date == localdate()
@pytest.mark.django_db
def test_membership_delete(client: Client):
user = baker.make(User, is_superuser=True)
membership = baker.make(Membership)
client.force_login(user)
url = reverse("club:membership_delete", kwargs={"membership_id": membership.id})
response = client.get(url)
assert response.status_code == 200
response = client.post(url)
assertRedirects(
response, reverse("core:user_clubs", kwargs={"user_id": membership.user_id})
)
assert not Membership.objects.filter(id=membership.id).exists()
@pytest.mark.django_db @pytest.mark.django_db
class TestJoinClub: class TestJoinClub:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)

View File

@@ -34,7 +34,7 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError from django.core.exceptions import NON_FIELD_ERRORS, PermissionDenied, ValidationError
from django.core.paginator import InvalidPage, Paginator from django.core.paginator import InvalidPage, Paginator
from django.db.models import F, Q, Sum from django.db.models import F, Q, Sum
from django.http import Http404, StreamingHttpResponse from django.http import Http404, HttpResponseRedirect, StreamingHttpResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils import timezone from django.utils import timezone
@@ -43,7 +43,6 @@ from django.utils.timezone import now
from django.utils.translation import gettext from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, ListView, View from django.views.generic import DetailView, ListView, View
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.edit import CreateView, DeleteView, UpdateView
from club.forms import ( from club.forms import (
@@ -545,17 +544,33 @@ class ClubCreateView(PermissionRequiredMixin, CreateView):
permission_required = "club.add_club" permission_required = "club.add_club"
class MembershipSetOldView(CanEditMixin, SingleObjectMixin, View): class MembershipSetOldView(CanEditMixin, DetailView):
"""Set a membership as being old.""" """Set a membership as beeing old."""
model = Membership model = Membership
pk_url_kwarg = "membership_id" pk_url_kwarg = "membership_id"
def post(self, *_args, **_kwargs): def get(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
self.object.end_date = timezone.now() self.object.end_date = timezone.now()
self.object.save() self.object.save()
return redirect("core:user_clubs", user_id=self.object.user_id) return HttpResponseRedirect(
reverse(
"club:club_members",
args=self.args,
kwargs={"club_id": self.object.club.id},
)
)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return HttpResponseRedirect(
reverse(
"club:club_members",
args=self.args,
kwargs={"club_id": self.object.club.id},
)
)
class MembershipDeleteView(PermissionRequiredMixin, DeleteView): class MembershipDeleteView(PermissionRequiredMixin, DeleteView):
@@ -567,7 +582,7 @@ class MembershipDeleteView(PermissionRequiredMixin, DeleteView):
permission_required = "club.delete_membership" permission_required = "club.delete_membership"
def get_success_url(self): def get_success_url(self):
return reverse_lazy("core:user_clubs", kwargs={"user_id": self.object.user_id}) return reverse_lazy("core:user_clubs", kwargs={"user_id": self.object.user.id})
class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView): class ClubMailingView(ClubTabsMixin, CanEditMixin, DetailFormView):

View File

@@ -1,3 +1,4 @@
from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
@@ -17,6 +18,16 @@ from core.markdown import markdown
from core.models import User from core.models import User
@dataclass
class MockResponse:
ok: bool
value: str
@property
def content(self):
return self.value.encode("utf8")
def accel_redirect_to_file(response: HttpResponse) -> Path | None: def accel_redirect_to_file(response: HttpResponse) -> Path | None:
redirect = Path(response.headers.get("X-Accel-Redirect", "")) redirect = Path(response.headers.get("X-Accel-Redirect", ""))
if not redirect.is_relative_to(Path("/") / settings.MEDIA_ROOT.stem): if not redirect.is_relative_to(Path("/") / settings.MEDIA_ROOT.stem):

View File

@@ -1,6 +1,6 @@
from typing import Annotated, Any, Literal from typing import Annotated, Any, Literal
from annotated_types import Ge, Le, MinLen import annotated_types
from django.conf import settings from django.conf import settings
from django.db.models import F from django.db.models import F
from django.http import HttpResponse from django.http import HttpResponse
@@ -28,7 +28,6 @@ from core.schemas import (
UserSchema, UserSchema,
) )
from core.templatetags.renderer import markdown from core.templatetags.renderer import markdown
from counter.utils import is_logged_in_counter
@api_controller("/markdown") @api_controller("/markdown")
@@ -73,7 +72,7 @@ class MailingListController(ControllerBase):
@api_controller("/user") @api_controller("/user")
class UserController(ControllerBase): class UserController(ControllerBase):
@route.get("", response=list[UserProfileSchema]) @route.get("", response=list[UserProfileSchema], permissions=[CanAccessLookup])
def fetch_profiles(self, pks: Query[set[int]]): def fetch_profiles(self, pks: Query[set[int]]):
return User.objects.viewable_by(self.context.request.user).filter(pk__in=pks) return User.objects.viewable_by(self.context.request.user).filter(pk__in=pks)
@@ -86,18 +85,15 @@ class UserController(ControllerBase):
"/search", "/search",
response=PaginatedResponseSchema[UserProfileSchema], response=PaginatedResponseSchema[UserProfileSchema],
url_name="search_users", url_name="search_users",
# logged in barmen aren't authenticated stricto sensu, so no auth here permissions=[CanAccessLookup],
auth=None,
) )
@paginate(PageNumberPaginationExtra, page_size=20) @paginate(PageNumberPaginationExtra, page_size=20)
def search_users(self, filters: Query[UserFilterSchema]): def search_users(self, filters: Query[UserFilterSchema]):
qs = User.objects return filters.filter(
# the logged in barmen can see all users (even the hidden one), User.objects.viewable_by(self.context.request.user).order_by(
# because they have a temporary administrative function during F("last_login").desc(nulls_last=True)
# which they may have to deal with hidden users )
if not is_logged_in_counter(self.context.request): )
qs = qs.viewable_by(self.context.request.user)
return filters.filter(qs.order_by(F("last_login").desc(nulls_last=True)))
@api_controller("/file") @api_controller("/file")
@@ -109,7 +105,7 @@ class SithFileController(ControllerBase):
permissions=[CanAccessLookup], permissions=[CanAccessLookup],
) )
@paginate(PageNumberPaginationExtra, page_size=50) @paginate(PageNumberPaginationExtra, page_size=50)
def search_files(self, search: Annotated[str, MinLen(1)]): def search_files(self, search: Annotated[str, annotated_types.MinLen(1)]):
return SithFile.objects.filter(is_in_sas=False).filter(name__icontains=search) return SithFile.objects.filter(is_in_sas=False).filter(name__icontains=search)
@@ -122,11 +118,11 @@ class GroupController(ControllerBase):
permissions=[CanAccessLookup], permissions=[CanAccessLookup],
) )
@paginate(PageNumberPaginationExtra, page_size=50) @paginate(PageNumberPaginationExtra, page_size=50)
def search_group(self, search: Annotated[str, MinLen(1)]): def search_group(self, search: Annotated[str, annotated_types.MinLen(1)]):
return Group.objects.filter(name__icontains=search).values() return Group.objects.filter(name__icontains=search).values()
DepthValue = Annotated[int, Ge(0), Le(10)] DepthValue = Annotated[int, annotated_types.Ge(0), annotated_types.Le(10)]
DEFAULT_DEPTH = 4 DEFAULT_DEPTH = 4

View File

@@ -350,6 +350,7 @@ class Command(BaseCommand):
date=make_aware( date=make_aware(
self.faker.date_time_between(customer.since, localdate()) self.faker.date_time_between(customer.since, localdate())
), ),
is_validated=True,
) )
) )
sales.extend(this_customer_sales) sales.extend(this_customer_sales)

View File

@@ -23,13 +23,12 @@
# #
from __future__ import annotations from __future__ import annotations
import difflib
import string import string
import unicodedata import unicodedata
from datetime import timedelta from datetime import timedelta
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Final, Self from typing import TYPE_CHECKING, Self
from uuid import uuid4 from uuid import uuid4
from django.conf import settings from django.conf import settings
@@ -1345,9 +1344,6 @@ class PageRev(models.Model):
The content is in PageRev.title and PageRev.content . The content is in PageRev.title and PageRev.content .
""" """
MERGE_TIME_THRESHOLD: Final[timedelta] = timedelta(minutes=20)
MERGE_DIFF_THRESHOLD: Final[float] = 0.2
revision = models.IntegerField(_("revision")) revision = models.IntegerField(_("revision"))
title = models.CharField(_("page title"), max_length=255, blank=True) title = models.CharField(_("page title"), max_length=255, blank=True)
content = models.TextField(_("page content"), blank=True) content = models.TextField(_("page content"), blank=True)
@@ -1389,32 +1385,6 @@ class PageRev(models.Model):
def is_owned_by(self, user: User) -> bool: def is_owned_by(self, user: User) -> bool:
return any(g.id == self.page.owner_group_id for g in user.cached_groups) return any(g.id == self.page.owner_group_id for g in user.cached_groups)
def similarity_ratio(self, text: str) -> float:
"""Similarity ratio between this revision's content and the given text.
The result is a float in [0; 1], 0 meaning the contents are entirely different,
and 1 they are strictly the same.
"""
# cf. https://docs.python.org/3/library/difflib.html#difflib.SequenceMatcher.ratio
return difflib.SequenceMatcher(None, self.content, text).quick_ratio()
def should_merge(self, other: Self) -> bool:
"""Return True if `other` should be merged into `self`, else False.
It's considered the other revision should be merged into this one if :
- it was made less than 20 minutes after
- by the same author
- with a similarity ratio higher than 80%
"""
return (
not self._state.adding # cannot merge if the original rev doesn't exist
and self.author == other.author
and (other.date - self.date) < self.MERGE_TIME_THRESHOLD
and (not other._state.adding or other.revision == self.revision + 1)
and self.similarity_ratio(other.content) >= (1 - other.MERGE_DIFF_THRESHOLD)
)
def get_notification_types(): def get_notification_types():
return settings.SITH_NOTIFICATIONS return settings.SITH_NOTIFICATIONS

View File

@@ -21,8 +21,6 @@ $secondary-neutral-dark-color: hsl(40, 57.6%, 17%);
$white-color: hsl(219.6, 20.8%, 98%); $white-color: hsl(219.6, 20.8%, 98%);
$black-color: hsl(0, 0%, 17%); $black-color: hsl(0, 0%, 17%);
$red-text-color: #eb2f06;
$hovered-red-text-color: #ff4d4d;
$faceblue: hsl(221, 44%, 41%); $faceblue: hsl(221, 44%, 41%);
$twitblue: hsl(206, 82%, 63%); $twitblue: hsl(206, 82%, 63%);

View File

@@ -745,32 +745,4 @@ form {
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: var(--nf-input-size); background-size: var(--nf-input-size);
} }
&.no-margin {
margin:0;
}
// a submit input that should look like a regular <a>
input[type="submit"], button {
&.link-like {
color: $primary-dark-color;
&:hover {
color: $primary-light-color;
}
&.link-red {
color: $red-text-color;
&:hover {
color: $hovered-red-text-color;
}
}
font-weight: normal;
font-size: 100%;
margin: auto;
background: none;
border: none;
cursor: pointer;
padding: 0;
}
}
} }

View File

@@ -5,6 +5,9 @@ $text-color: white;
$background-color-hovered: #283747; $background-color-hovered: #283747;
$red-text-color: #eb2f06;
$hovered-red-text-color: #ff4d4d;
.header { .header {
box-sizing: border-box; box-sizing: border-box;
background-color: $deepblue; background-color: $deepblue;
@@ -248,15 +251,12 @@ $background-color-hovered: #283747;
justify-content: flex-start; justify-content: flex-start;
} }
a {
color: $text-color;
}
a, a,
button { button {
font-size: 100%; font-size: 100%;
margin: 0; margin: 0;
text-align: right; text-align: right;
color: $text-color;
margin-top: auto; margin-top: auto;
&:hover { &:hover {
@@ -268,6 +268,19 @@ $background-color-hovered: #283747;
margin: 0; margin: 0;
display: inline; display: inline;
} }
#logout-form button {
color: $red-text-color;
&:hover {
color: $hovered-red-text-color;
}
background: none;
border: none;
cursor: pointer;
padding: 0;
}
} }
} }
} }

View File

@@ -519,6 +519,7 @@ th {
td { td {
margin: 5px; margin: 5px;
border-collapse: collapse; border-collapse: collapse;
vertical-align: top;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@@ -61,9 +61,7 @@
<a href="{{ url('core:user_tools') }}">{% trans %}Tools{% endtrans %}</a> <a href="{{ url('core:user_tools') }}">{% trans %}Tools{% endtrans %}</a>
<form id="logout-form" method="post" action="{{ url("core:logout") }}"> <form id="logout-form" method="post" action="{{ url("core:logout") }}">
{% csrf_token %} {% csrf_token %}
<button type="submit" class="link-like link-red"> <button type="submit">{% trans %}Logout{% endtrans %}</button>
{% trans %}Logout{% endtrans %}
</button>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -17,9 +17,7 @@
<td>{% trans %}Description{% endtrans %}</td> <td>{% trans %}Description{% endtrans %}</td>
<td>{% trans %}Since{% endtrans %}</td> <td>{% trans %}Since{% endtrans %}</td>
<td></td> <td></td>
{% if user.has_perm("club.delete_membership") %} <td></td>
<td></td>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -30,16 +28,7 @@
<td>{{ m.description }}</td> <td>{{ m.description }}</td>
<td>{{ m.start_date }}</td> <td>{{ m.start_date }}</td>
{% if m.can_be_edited_by(user) %} {% if m.can_be_edited_by(user) %}
<td> <td><a href="{{ url('club:membership_set_old', membership_id=m.id) }}">{% trans %}Mark as old{% endtrans %}</a></td>
<form
method="post"
action="{{ url('club:membership_set_old', membership_id=m.id) }}"
class="no-margin"
>
{% csrf_token %}
<input type="submit" class="link-like" value="{% trans %}Mark as old{% endtrans %}" />
</form>
</td>
{% endif %} {% endif %}
{% if user.has_perm("club.delete_membership") %} {% if user.has_perm("club.delete_membership") %}
<td><a href="{{ url('club:membership_delete', membership_id=m.id) }}">{% trans %}Delete{% endtrans %}</a></td> <td><a href="{{ url('club:membership_delete', membership_id=m.id) }}">{% trans %}Delete{% endtrans %}</a></td>
@@ -59,9 +48,7 @@
<td>{% trans %}Description{% endtrans %}</td> <td>{% trans %}Description{% endtrans %}</td>
<td>{% trans %}From{% endtrans %}</td> <td>{% trans %}From{% endtrans %}</td>
<td>{% trans %}To{% endtrans %}</td> <td>{% trans %}To{% endtrans %}</td>
{% if user.has_perm("club.delete_membership") %}
<td></td>
{% endif %}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@@ -1,5 +1,4 @@
from datetime import timedelta from datetime import timedelta
from unittest import mock
import pytest import pytest
from django.conf import settings from django.conf import settings
@@ -24,7 +23,6 @@ from core.models import AnonymousUser, Group, User
from core.views import UserTabsMixin from core.views import UserTabsMixin
from counter.baker_recipes import sale_recipe from counter.baker_recipes import sale_recipe
from counter.models import Counter, Customer, Refilling, Selling from counter.models import Counter, Customer, Refilling, Selling
from counter.utils import is_logged_in_counter
from eboutic.models import Invoice, InvoiceItem from eboutic.models import Invoice, InvoiceItem
@@ -62,9 +60,7 @@ class TestSearchUsersAPI(TestSearchUsers):
"""Test that users are ordered by last login date.""" """Test that users are ordered by last login date."""
self.client.force_login(subscriber_user.make()) self.client.force_login(subscriber_user.make())
response = self.client.get( response = self.client.get(reverse("api:search_users") + "?search=First")
reverse("api:search_users", query={"search": "First"})
)
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["count"] == 11 assert response.json()["count"] == 11
# The users are ordered by last login date, so we need to reverse the list # The users are ordered by last login date, so we need to reverse the list
@@ -73,7 +69,7 @@ class TestSearchUsersAPI(TestSearchUsers):
] ]
def test_search_case_insensitive(self): def test_search_case_insensitive(self):
"""Test that the search is case-insensitive.""" """Test that the search is case insensitive."""
self.client.force_login(subscriber_user.make()) self.client.force_login(subscriber_user.make())
expected = [u.id for u in self.users[::-1]] expected = [u.id for u in self.users[::-1]]
@@ -86,19 +82,14 @@ class TestSearchUsersAPI(TestSearchUsers):
assert [r["id"] for r in response.json()["results"]] == expected assert [r["id"] for r in response.json()["results"]] == expected
def test_search_nick_name(self): def test_search_nick_name(self):
"""Test that the search can be done on the nickname.""" """Test that the search can be done on the nick name."""
# hidden users should not be in the final result,
# even when the nickname matches
self.users[10].is_viewable = False
self.users[10].save()
self.client.force_login(subscriber_user.make()) self.client.force_login(subscriber_user.make())
# this should return users with nicknames Nick11, Nick10 and Nick1 # this should return users with nicknames Nick11, Nick10 and Nick1
response = self.client.get( response = self.client.get(reverse("api:search_users") + "?search=Nick1")
reverse("api:search_users", query={"search": "Nick1"})
)
assert response.status_code == 200 assert response.status_code == 200
assert [r["id"] for r in response.json()["results"]] == [ assert [r["id"] for r in response.json()["results"]] == [
self.users[10].id,
self.users[9].id, self.users[9].id,
self.users[0].id, self.users[0].id,
] ]
@@ -110,25 +101,10 @@ class TestSearchUsersAPI(TestSearchUsers):
self.client.force_login(subscriber_user.make()) self.client.force_login(subscriber_user.make())
# this should return users with first names First1 and First10 # this should return users with first names First1 and First10
response = self.client.get(reverse("api:search_users", query={"search": "bél"})) response = self.client.get(reverse("api:search_users") + "?search=bél")
assert response.status_code == 200 assert response.status_code == 200
assert [r["id"] for r in response.json()["results"]] == [belix.id] assert [r["id"] for r in response.json()["results"]] == [belix.id]
@mock.create_autospec(is_logged_in_counter, return_value=True)
def test_search_as_barman(self):
# barmen should also see hidden users
self.users[10].is_viewable = False
self.users[10].save()
response = self.client.get(
reverse("api:search_users", query={"search": "Nick1"})
)
assert response.status_code == 200
assert [r["id"] for r in response.json()["results"]] == [
self.users[10].id,
self.users[9].id,
self.users[0].id,
]
class TestSearchUsersView(TestSearchUsers): class TestSearchUsersView(TestSearchUsers):
"""Test the search user view (`GET /search`).""" """Test the search user view (`GET /search`)."""
@@ -187,7 +163,11 @@ class TestFilterInactive(TestCase):
time_inactive = time_active - timedelta(days=3) time_inactive = time_active - timedelta(days=3)
counter, seller = baker.make(Counter), baker.make(User) counter, seller = baker.make(Counter), baker.make(User)
sale_recipe = Recipe( sale_recipe = Recipe(
Selling, counter=counter, club=counter.club, seller=seller, unit_price=0 Selling,
counter=counter,
club=counter.club,
seller=seller,
is_validated=True,
) )
cls.users = [ cls.users = [

View File

@@ -20,9 +20,9 @@
# Place - Suite 330, Boston, MA 02111-1307, USA. # Place - Suite 330, Boston, MA 02111-1307, USA.
# #
# #
import difflib
import re import re
from copy import copy from datetime import date, datetime, timedelta
from datetime import date, datetime
from io import BytesIO from io import BytesIO
from captcha.fields import CaptchaField from captcha.fields import CaptchaField
@@ -390,11 +390,14 @@ class PageRevisionForm(forms.ModelForm):
- less than 20 minutes ago - less than 20 minutes ago
- by the same author - by the same author
- with a similarity ratio higher than 80% - with a diff ratio higher than 20%
then the latter will be edited and the new revision won't be created. then the latter will be edited and the new revision won't be created.
""" """
TIME_THRESHOLD = timedelta(minutes=20)
DIFF_THRESHOLD = 0.2
class Meta: class Meta:
model = PageRev model = PageRev
fields = ["title", "content"] fields = ["title", "content"]
@@ -406,11 +409,21 @@ class PageRevisionForm(forms.ModelForm):
super().__init__(*args, instance=instance, **kwargs) super().__init__(*args, instance=instance, **kwargs)
self.author = author self.author = author
self.page = page self.page = page
self.initial_obj: PageRev = copy(self.instance) self.initial_content = instance.content if instance else ""
def diff_ratio(self, new_str: str) -> float:
return difflib.SequenceMatcher(
None, self.initial_content, new_str
).quick_ratio()
def save(self, commit=True): # noqa FBT002 def save(self, commit=True): # noqa FBT002
revision: PageRev = self.instance revision: PageRev = self.instance
if not self.initial_obj.should_merge(self.instance): if (
revision._state.adding
or revision.author != self.author
or revision.date + self.TIME_THRESHOLD < now()
or self.diff_ratio(revision.content) < (1 - self.DIFF_THRESHOLD)
):
revision.author = self.author revision.author = self.author
revision.page = self.page revision.page = self.page
revision.id = None # if id is None, Django will create a new record revision.id = None # if id is None, Django will create a new record

View File

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

View File

@@ -136,10 +136,7 @@ class GetUserForm(forms.Form):
class RefillForm(forms.ModelForm): class RefillForm(forms.ModelForm):
allowed_refilling_methods = [ allowed_refilling_methods = ["CASH", "CARD"]
Refilling.PaymentMethod.CASH,
Refilling.PaymentMethod.CARD,
]
error_css_class = "error" error_css_class = "error"
required_css_class = "required" required_css_class = "required"
@@ -149,7 +146,7 @@ class RefillForm(forms.ModelForm):
class Meta: class Meta:
model = Refilling model = Refilling
fields = ["amount", "payment_method"] fields = ["amount", "payment_method", "bank"]
widgets = {"payment_method": forms.RadioSelect} widgets = {"payment_method": forms.RadioSelect}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -163,6 +160,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]
if "CHECK" not in self.allowed_refilling_methods:
del self.fields["bank"]
class CounterEditForm(forms.ModelForm): class CounterEditForm(forms.ModelForm):
class Meta: class Meta:

View File

@@ -119,6 +119,7 @@ class Command(BaseCommand):
quantity=1, quantity=1,
unit_price=account.amount, unit_price=account.amount,
date=now(), date=now(),
is_validated=True,
) )
for account in accounts for account in accounts
] ]

View File

@@ -1,84 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-19 17:59
from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.models import Case, When
def migrate_selling_payment_method(apps: StateApps, schema_editor):
# 0 <=> SITH_ACCOUNT is the default value, so no need to migrate it
Selling = apps.get_model("counter", "Selling")
Selling.objects.filter(payment_method_str="CARD").update(payment_method=1)
def migrate_selling_payment_method_reverse(apps: StateApps, schema_editor):
Selling = apps.get_model("counter", "Selling")
Selling.objects.filter(payment_method=1).update(payment_method_str="CARD")
def migrate_refilling_payment_method(apps: StateApps, schema_editor):
Refilling = apps.get_model("counter", "Refilling")
Refilling.objects.update(
payment_method=Case(
When(payment_method_str="CARD", then=0),
When(payment_method_str="CASH", then=1),
When(payment_method_str="CHECK", then=2),
)
)
def migrate_refilling_payment_method_reverse(apps: StateApps, schema_editor):
Refilling = apps.get_model("counter", "Refilling")
Refilling.objects.update(
payment_method_str=Case(
When(payment_method=0, then="CARD"),
When(payment_method=1, then="CASH"),
When(payment_method=2, then="CHECK"),
)
)
class Migration(migrations.Migration):
dependencies = [("counter", "0034_alter_selling_date_selling_date_month_idx")]
operations = [
migrations.RemoveField(model_name="selling", name="is_validated"),
migrations.RemoveField(model_name="refilling", name="is_validated"),
migrations.RemoveField(model_name="refilling", name="bank"),
migrations.RenameField(
model_name="selling",
old_name="payment_method",
new_name="payment_method_str",
),
migrations.AddField(
model_name="selling",
name="payment_method",
field=models.PositiveSmallIntegerField(
choices=[(0, "Sith account"), (1, "Credit card")],
default=0,
verbose_name="payment method",
),
),
migrations.RunPython(
migrate_selling_payment_method, migrate_selling_payment_method_reverse
),
migrations.RemoveField(model_name="selling", name="payment_method_str"),
migrations.RenameField(
model_name="refilling",
old_name="payment_method",
new_name="payment_method_str",
),
migrations.AddField(
model_name="refilling",
name="payment_method",
field=models.PositiveSmallIntegerField(
choices=[(0, "Credit card"), (1, "Cash"), (2, "Check")],
default=0,
verbose_name="payment method",
),
),
migrations.RunPython(
migrate_refilling_payment_method, migrate_refilling_payment_method_reverse
),
migrations.RemoveField(model_name="refilling", name="payment_method_str"),
]

View File

@@ -44,6 +44,7 @@ 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 counter.apps import PAYMENT_METHOD
from counter.fields import CurrencyField from counter.fields import CurrencyField
from subscription.models import Subscription from subscription.models import Subscription
@@ -79,8 +80,7 @@ class CustomerQuerySet(models.QuerySet):
) )
money_out = Subquery( money_out = Subquery(
Selling.objects.filter( Selling.objects.filter(
customer=OuterRef("pk"), customer=OuterRef("pk"), payment_method="SITH_ACCOUNT"
payment_method=Selling.PaymentMethod.SITH_ACCOUNT,
) )
.values("customer_id") .values("customer_id")
.annotate(res=Sum(F("unit_price") * F("quantity"), default=0)) .annotate(res=Sum(F("unit_price") * F("quantity"), default=0))
@@ -731,11 +731,6 @@ class RefillingQuerySet(models.QuerySet):
class Refilling(models.Model): class Refilling(models.Model):
"""Handle the refilling.""" """Handle the refilling."""
class PaymentMethod(models.IntegerChoices):
CARD = 0, _("Credit card")
CASH = 1, _("Cash")
CHECK = 2, _("Check")
counter = models.ForeignKey( counter = models.ForeignKey(
Counter, related_name="refillings", blank=False, on_delete=models.CASCADE Counter, related_name="refillings", blank=False, on_delete=models.CASCADE
) )
@@ -750,9 +745,16 @@ class Refilling(models.Model):
Customer, related_name="refillings", blank=False, on_delete=models.CASCADE Customer, related_name="refillings", blank=False, on_delete=models.CASCADE
) )
date = models.DateTimeField(_("date")) date = models.DateTimeField(_("date"))
payment_method = models.PositiveSmallIntegerField( payment_method = models.CharField(
_("payment method"), choices=PaymentMethod, default=PaymentMethod.CARD _("payment method"),
max_length=255,
choices=PAYMENT_METHOD,
default="CARD",
) )
bank = models.CharField(
_("bank"), max_length=255, choices=settings.SITH_COUNTER_BANK, default="OTHER"
)
is_validated = models.BooleanField(_("is validated"), default=False)
objects = RefillingQuerySet.as_manager() objects = RefillingQuerySet.as_manager()
@@ -769,9 +771,10 @@ class Refilling(models.Model):
if not self.date: if not self.date:
self.date = timezone.now() self.date = timezone.now()
self.full_clean() self.full_clean()
if self._state.adding: if not self.is_validated:
self.customer.amount += self.amount self.customer.amount += self.amount
self.customer.save() self.customer.save()
self.is_validated = True
if self.customer.user.preferences.notify_on_refill: if self.customer.user.preferences.notify_on_refill:
Notification( Notification(
user=self.customer.user, user=self.customer.user,
@@ -811,10 +814,6 @@ class SellingQuerySet(models.QuerySet):
class Selling(models.Model): class Selling(models.Model):
"""Handle the sellings.""" """Handle the sellings."""
class PaymentMethod(models.IntegerChoices):
SITH_ACCOUNT = 0, _("Sith account")
CARD = 1, _("Credit card")
# We make sure that sellings have a way begger label than any product name is allowed to # We make sure that sellings have a way begger label than any product name is allowed to
label = models.CharField(_("label"), max_length=128) label = models.CharField(_("label"), max_length=128)
product = models.ForeignKey( product = models.ForeignKey(
@@ -851,9 +850,13 @@ class Selling(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
date = models.DateTimeField(_("date"), db_index=True) date = models.DateTimeField(_("date"), db_index=True)
payment_method = models.PositiveSmallIntegerField( payment_method = models.CharField(
_("payment method"), choices=PaymentMethod, default=PaymentMethod.SITH_ACCOUNT _("payment method"),
max_length=255,
choices=[("SITH_ACCOUNT", _("Sith account")), ("CARD", _("Credit card"))],
default="SITH_ACCOUNT",
) )
is_validated = models.BooleanField(_("is validated"), default=False)
objects = SellingQuerySet.as_manager() objects = SellingQuerySet.as_manager()
@@ -872,12 +875,10 @@ class Selling(models.Model):
if not self.date: if not self.date:
self.date = timezone.now() self.date = timezone.now()
self.full_clean() self.full_clean()
if ( if not self.is_validated:
self._state.adding
and self.payment_method == self.PaymentMethod.SITH_ACCOUNT
):
self.customer.amount -= self.quantity * self.unit_price self.customer.amount -= self.quantity * self.unit_price
self.customer.save(allow_negative=allow_negative) self.customer.save(allow_negative=allow_negative)
self.is_validated = True
user = self.customer.user user = self.customer.user
if user.was_subscribed: if user.was_subscribed:
if ( if (
@@ -947,9 +948,7 @@ class Selling(models.Model):
def is_owned_by(self, user: User) -> bool: def is_owned_by(self, user: User) -> bool:
if user.is_anonymous: if user.is_anonymous:
return False return False
return self.payment_method != self.PaymentMethod.CARD and user.is_owner( return self.payment_method != "CARD" and user.is_owner(self.counter)
self.counter
)
def can_be_viewed_by(self, user: User) -> bool: def can_be_viewed_by(self, user: User) -> bool:
if ( if (
@@ -959,7 +958,7 @@ class Selling(models.Model):
return user == self.customer.user return user == self.customer.user
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
if self.payment_method == Selling.PaymentMethod.SITH_ACCOUNT: if self.payment_method == "SITH_ACCOUNT":
self.customer.amount += self.quantity * self.unit_price self.customer.amount += self.quantity * self.unit_price
self.customer.save() self.customer.save()
super().delete(*args, **kwargs) super().delete(*args, **kwargs)

View File

@@ -116,6 +116,7 @@ class TestAccountDumpCommand(TestAccountDump):
operation: Selling = customer.buyings.order_by("date").last() operation: Selling = customer.buyings.order_by("date").last()
assert operation.unit_price == initial_amount assert operation.unit_price == initial_amount
assert operation.counter_id == settings.SITH_COUNTER_ACCOUNT_DUMP_ID assert operation.counter_id == settings.SITH_COUNTER_ACCOUNT_DUMP_ID
assert operation.is_validated is True
dump = customer.dumps.last() dump = customer.dumps.last()
assert dump.dump_operation == operation assert dump.dump_operation == operation

View File

@@ -53,7 +53,7 @@ def set_age(user: User, age: int):
def force_refill_user(user: User, amount: Decimal | int): def force_refill_user(user: User, amount: Decimal | int):
baker.make(Refilling, amount=amount, customer=user.customer) baker.make(Refilling, amount=amount, customer=user.customer, is_validated=False)
class TestFullClickBase(TestCase): class TestFullClickBase(TestCase):
@@ -115,10 +115,18 @@ class TestRefilling(TestFullClickBase):
) -> HttpResponse: ) -> HttpResponse:
used_client = client if client is not None else self.client used_client = client if client is not None else self.client
return used_client.post( return used_client.post(
reverse("counter:refilling_create", kwargs={"customer_id": user.pk}), reverse(
{"amount": str(amount), "payment_method": Refilling.PaymentMethod.CASH}, "counter:refilling_create",
kwargs={"customer_id": user.pk},
),
{
"amount": str(amount),
"payment_method": "CASH",
"bank": "OTHER",
},
HTTP_REFERER=reverse( HTTP_REFERER=reverse(
"counter:click", kwargs={"counter_id": counter.id, "user_id": user.pk} "counter:click",
kwargs={"counter_id": counter.id, "user_id": user.pk},
), ),
) )
@@ -141,7 +149,11 @@ class TestRefilling(TestFullClickBase):
"counter:refilling_create", "counter:refilling_create",
kwargs={"customer_id": self.customer.pk}, kwargs={"customer_id": self.customer.pk},
), ),
{"amount": "10", "payment_method": "CASH"}, {
"amount": "10",
"payment_method": "CASH",
"bank": "OTHER",
},
) )
self.client.force_login(self.club_admin) self.client.force_login(self.club_admin)

View File

@@ -298,6 +298,7 @@ def test_update_balance():
_quantity=len(customers), _quantity=len(customers),
unit_price=10, unit_price=10,
quantity=1, quantity=1,
payment_method="SITH_ACCOUNT",
_save_related=True, _save_related=True,
), ),
*sale_recipe.prepare( *sale_recipe.prepare(
@@ -305,12 +306,14 @@ def test_update_balance():
_quantity=3, _quantity=3,
unit_price=5, unit_price=5,
quantity=2, quantity=2,
payment_method="SITH_ACCOUNT",
_save_related=True, _save_related=True,
), ),
sale_recipe.prepare( sale_recipe.prepare(
customer=customers[4], customer=customers[4],
quantity=1, quantity=1,
unit_price=50, unit_price=50,
payment_method="SITH_ACCOUNT",
_save_related=True, _save_related=True,
), ),
*sale_recipe.prepare( *sale_recipe.prepare(
@@ -321,7 +324,7 @@ def test_update_balance():
_quantity=len(customers), _quantity=len(customers),
unit_price=50, unit_price=50,
quantity=1, quantity=1,
payment_method=Selling.PaymentMethod.CARD, payment_method="CARD",
_save_related=True, _save_related=True,
), ),
] ]

View File

@@ -67,13 +67,15 @@ class InvoiceCallView(
end_date = start_date + relativedelta(months=1) end_date = start_date + relativedelta(months=1)
kwargs["sum_cb"] = Refilling.objects.filter( kwargs["sum_cb"] = Refilling.objects.filter(
payment_method=Refilling.PaymentMethod.CARD, payment_method="CARD",
is_validated=True,
date__gte=start_date, date__gte=start_date,
date__lte=end_date, date__lte=end_date,
).aggregate(res=Sum("amount", default=0))["res"] ).aggregate(res=Sum("amount", default=0))["res"]
kwargs["sum_cb"] += ( kwargs["sum_cb"] += (
Selling.objects.filter( Selling.objects.filter(
payment_method=Selling.PaymentMethod.CARD, payment_method="CARD",
is_validated=True,
date__gte=start_date, date__gte=start_date,
date__lte=end_date, date__lte=end_date,
) )

View File

@@ -110,9 +110,7 @@ class Basket(models.Model):
)["total"] )["total"]
) )
def generate_sales( def generate_sales(self, counter, seller: User, payment_method: str):
self, counter, seller: User, payment_method: Selling.PaymentMethod
):
"""Generate a list of sold items corresponding to the items """Generate a list of sold items corresponding to the items
of this basket WITHOUT saving them NOR deleting the basket. of this basket WITHOUT saving them NOR deleting the basket.
@@ -253,7 +251,8 @@ class Invoice(models.Model):
customer=customer, customer=customer,
operator=self.user, operator=self.user,
amount=i.product_unit_price * i.quantity, amount=i.product_unit_price * i.quantity,
payment_method=Refilling.PaymentMethod.CARD, payment_method="CARD",
bank="OTHER",
date=self.date, date=self.date,
) )
new.save() new.save()
@@ -268,7 +267,8 @@ class Invoice(models.Model):
customer=customer, customer=customer,
unit_price=i.product_unit_price, unit_price=i.product_unit_price,
quantity=i.quantity, quantity=i.quantity,
payment_method=Selling.PaymentMethod.CARD, payment_method="CARD",
is_validated=True,
date=self.date, date=self.date,
) )
new.save() new.save()

View File

@@ -108,22 +108,12 @@ def test_eboutic_basket_expiry(
client.force_login(customer.user) client.force_login(customer.user)
if sellings: for date in sellings:
sale_recipe.make( sale_recipe.make(
customer=customer, customer=customer, counter=eboutic, date=date, is_validated=True
counter=eboutic,
date=iter(sellings),
_quantity=len(sellings),
_bulk_create=True,
)
if refillings:
refill_recipe.make(
customer=customer,
counter=eboutic,
date=iter(refillings),
_quantity=len(refillings),
_bulk_create=True,
) )
for date in refillings:
refill_recipe.make(customer=customer, counter=eboutic, date=date)
assert ( assert (
f'x-data="basket({int(expected.timestamp() * 1000) if expected else "null"})"' f'x-data="basket({int(expected.timestamp() * 1000) if expected else "null"})"'

View File

@@ -114,13 +114,13 @@ class TestPaymentSith(TestPaymentBase):
"quantity" "quantity"
) )
assert len(sellings) == 2 assert len(sellings) == 2
assert sellings[0].payment_method == Selling.PaymentMethod.SITH_ACCOUNT assert sellings[0].payment_method == "SITH_ACCOUNT"
assert sellings[0].quantity == 1 assert sellings[0].quantity == 1
assert sellings[0].unit_price == self.snack.selling_price assert sellings[0].unit_price == self.snack.selling_price
assert sellings[0].counter.type == "EBOUTIC" assert sellings[0].counter.type == "EBOUTIC"
assert sellings[0].product == self.snack assert sellings[0].product == self.snack
assert sellings[1].payment_method == Selling.PaymentMethod.SITH_ACCOUNT assert sellings[1].payment_method == "SITH_ACCOUNT"
assert sellings[1].quantity == 2 assert sellings[1].quantity == 2
assert sellings[1].unit_price == self.beer.selling_price assert sellings[1].unit_price == self.beer.selling_price
assert sellings[1].counter.type == "EBOUTIC" assert sellings[1].counter.type == "EBOUTIC"
@@ -198,13 +198,13 @@ class TestPaymentCard(TestPaymentBase):
"quantity" "quantity"
) )
assert len(sellings) == 2 assert len(sellings) == 2
assert sellings[0].payment_method == Selling.PaymentMethod.CARD assert sellings[0].payment_method == "CARD"
assert sellings[0].quantity == 1 assert sellings[0].quantity == 1
assert sellings[0].unit_price == self.snack.selling_price assert sellings[0].unit_price == self.snack.selling_price
assert sellings[0].counter.type == "EBOUTIC" assert sellings[0].counter.type == "EBOUTIC"
assert sellings[0].product == self.snack assert sellings[0].product == self.snack
assert sellings[1].payment_method == Selling.PaymentMethod.CARD assert sellings[1].payment_method == "CARD"
assert sellings[1].quantity == 2 assert sellings[1].quantity == 2
assert sellings[1].unit_price == self.beer.selling_price assert sellings[1].unit_price == self.beer.selling_price
assert sellings[1].counter.type == "EBOUTIC" assert sellings[1].counter.type == "EBOUTIC"

View File

@@ -275,9 +275,7 @@ class EbouticPayWithSith(CanViewMixin, SingleObjectMixin, View):
return redirect("eboutic:payment_result", "failure") return redirect("eboutic:payment_result", "failure")
eboutic = get_eboutic() eboutic = get_eboutic()
sales = basket.generate_sales( sales = basket.generate_sales(eboutic, basket.user, "SITH_ACCOUNT")
eboutic, basket.user, Selling.PaymentMethod.SITH_ACCOUNT
)
try: try:
with transaction.atomic(): with transaction.atomic():
# Selling.save has some important business logic in it. # Selling.save has some important business logic in it.

View File

@@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-19 21:00+0100\n" "POT-Creation-Date: 2025-11-12 21:44+0100\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@@ -2928,6 +2928,18 @@ msgstr "Photos"
msgid "Account" msgid "Account"
msgstr "Compte" msgstr "Compte"
#: counter/apps.py sith/settings.py
msgid "Check"
msgstr "Chèque"
#: counter/apps.py sith/settings.py
msgid "Cash"
msgstr "Espèces"
#: counter/apps.py counter/models.py sith/settings.py
msgid "Credit card"
msgstr "Carte bancaire"
#: counter/apps.py counter/models.py #: counter/apps.py counter/models.py
msgid "counter" msgid "counter"
msgstr "comptoir" msgstr "comptoir"
@@ -3140,29 +3152,21 @@ msgstr "vendeurs"
msgid "token" msgid "token"
msgstr "jeton" msgstr "jeton"
#: counter/models.py sith/settings.py
msgid "Credit card"
msgstr "Carte bancaire"
#: counter/models.py sith/settings.py
msgid "Cash"
msgstr "Espèces"
#: counter/models.py sith/settings.py
msgid "Check"
msgstr "Chèque"
#: counter/models.py subscription/models.py #: counter/models.py subscription/models.py
msgid "payment method" msgid "payment method"
msgstr "méthode de paiement" msgstr "méthode de paiement"
#: counter/models.py #: counter/models.py
msgid "refilling" msgid "bank"
msgstr "rechargement" msgstr "banque"
#: counter/models.py #: counter/models.py
msgid "Sith account" msgid "is validated"
msgstr "Compte utilisateur" msgstr "est validé"
#: counter/models.py
msgid "refilling"
msgstr "rechargement"
#: counter/models.py eboutic/models.py #: counter/models.py eboutic/models.py
msgid "unit price" msgid "unit price"
@@ -3172,6 +3176,10 @@ msgstr "prix unitaire"
msgid "quantity" msgid "quantity"
msgstr "quantité" msgstr "quantité"
#: counter/models.py
msgid "Sith account"
msgstr "Compte utilisateur"
#: counter/models.py #: counter/models.py
msgid "selling" msgid "selling"
msgstr "vente" msgstr "vente"
@@ -3324,10 +3332,6 @@ msgid ""
"%(value)s” value has the correct format (YYYY-MM) but it is an invalid date." "%(value)s” value has the correct format (YYYY-MM) but it is an invalid date."
msgstr "La valeur « %(value)s » a le bon format, mais est une date invalide." msgstr "La valeur « %(value)s » a le bon format, mais est une date invalide."
#: counter/models.py
msgid "is validated"
msgstr "est validé"
#: counter/models.py #: counter/models.py
msgid "invoice date" msgid "invoice date"
msgstr "date de la facture" msgstr "date de la facture"

View File

@@ -43,7 +43,7 @@ dependencies = [
"tomli>=2.3.0,<3.0.0", "tomli>=2.3.0,<3.0.0",
"django-honeypot>=1.3.0,<2", "django-honeypot>=1.3.0,<2",
"pydantic-extra-types>=2.10.6,<3.0.0", "pydantic-extra-types>=2.10.6,<3.0.0",
"ical>=11.1.0,<12", "ical>=11.1.0,<13",
"redis[hiredis]<7,>=5.3.0", "redis[hiredis]<7,>=5.3.0",
"environs[django]>=14.5.0,<15.0.0", "environs[django]>=14.5.0,<15.0.0",
"requests>=2.32.5,<3.0.0", "requests>=2.32.5,<3.0.0",

View File

@@ -114,6 +114,7 @@ class TestMergeUser(TestCase):
seller=self.root, seller=self.root,
unit_price=2, unit_price=2,
quantity=2, quantity=2,
payment_method="SITH_ACCOUNT",
).save() ).save()
Selling( Selling(
label="barbar", label="barbar",
@@ -124,6 +125,7 @@ class TestMergeUser(TestCase):
seller=self.root, seller=self.root,
unit_price=2, unit_price=2,
quantity=4, quantity=4,
payment_method="SITH_ACCOUNT",
).save() ).save()
today = localtime(now()).date() today = localtime(now()).date()
# both subscriptions began last month and shall end in 5 months # both subscriptions began last month and shall end in 5 months
@@ -195,6 +197,7 @@ class TestMergeUser(TestCase):
seller=self.root, seller=self.root,
unit_price=2, unit_price=2,
quantity=4, quantity=4,
payment_method="SITH_ACCOUNT",
).save() ).save()
data = {"user1": self.to_keep.id, "user2": self.to_delete.id} data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
res = self.client.post(reverse("rootplace:merge"), data) res = self.client.post(reverse("rootplace:merge"), data)
@@ -222,6 +225,7 @@ class TestMergeUser(TestCase):
seller=self.root, seller=self.root,
unit_price=2, unit_price=2,
quantity=4, quantity=4,
payment_method="SITH_ACCOUNT",
).save() ).save()
data = {"user1": self.to_keep.id, "user2": self.to_delete.id} data = {"user1": self.to_keep.id, "user2": self.to_delete.id}
res = self.client.post(reverse("rootplace:merge"), data) res = self.client.post(reverse("rootplace:merge"), data)

View File

@@ -440,6 +440,19 @@ 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_BANK = [
("OTHER", "Autre"),
("SOCIETE-GENERALE", "Société générale"),
("BANQUE-POPULAIRE", "Banque populaire"),
("BNP", "BNP"),
("CAISSE-EPARGNE", "Caisse d'épargne"),
("CIC", "CIC"),
("CREDIT-AGRICOLE", "Crédit Agricole"),
("CREDIT-MUTUEL", "Credit Mutuel"),
("CREDIT-LYONNAIS", "Credit Lyonnais"),
("LA-POSTE", "La Poste"),
]
SITH_PEDAGOGY_UV_TYPE = [ SITH_PEDAGOGY_UV_TYPE = [
("FREE", _("Free")), ("FREE", _("Free")),
("CS", _("CS")), ("CS", _("CS")),

View File

@@ -24,6 +24,7 @@ from django.views.generic import CreateView, DetailView, TemplateView
from django.views.generic.edit import FormView from django.views.generic.edit import FormView
from core.views.group import PermissionGroupsUpdateView from core.views.group import PermissionGroupsUpdateView
from counter.apps import PAYMENT_METHOD
from subscription.forms import ( from subscription.forms import (
SelectionDateForm, SelectionDateForm,
SubscriptionExistingUserForm, SubscriptionExistingUserForm,
@@ -128,6 +129,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_SUBSCRIPTION_PAYMENT_METHOD kwargs["payment_types"] = PAYMENT_METHOD
kwargs["locations"] = settings.SITH_SUBSCRIPTION_LOCATIONS kwargs["locations"] = settings.SITH_SUBSCRIPTION_LOCATIONS
return kwargs return kwargs