1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-10-14 09:38:32 +02:00

Compare commits

...

4 Commits

Author SHA1 Message Date
Ehouarn
5af69f719d First step to re-write logic of SogeCredit validity 2025-09-28 22:13:52 +02:00
Ehouarn
4f6b1d5b6c More open food 2025-09-28 21:51:54 +02:00
Ehouarn
27a1f36183 Export club members 2025-09-28 21:15:01 +02:00
Ehouarn
83c8b9a3d0 Export activity guests 2025-09-28 21:12:48 +02:00
10 changed files with 155 additions and 11 deletions

View File

@@ -37,6 +37,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div id="guests_table"> <div id="guests_table">
{% render_table guests %} {% render_table guests %}
</div> </div>
<div class="card-footer text-center">
<button class="btn btn-block btn-primary mb-3" onclick="window.location.href='?_export=1&table=guests'">
{% trans "Export to CSV" %}
</button>
</div>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -136,12 +136,19 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix
model = Activity model = Activity
context_object_name = "activity" context_object_name = "activity"
extra_context = {"title": _("Activity detail")} extra_context = {"title": _("Activity detail")}
export_formats = ["csv"]
tables = [ tables = [
lambda data: GuestTable(data, prefix="guests-"), GuestTable,
lambda data: OpenerTable(data, prefix="opener-"), OpenerTable,
] ]
def get_tables(self):
tables = super().get_tables()
tables[0].prefix = "guests"
tables[1].prefix = "opener"
return tables
def get_tables_data(self): def get_tables_data(self):
return [ return [
Guest.objects.filter(activity=self.object) Guest.objects.filter(activity=self.object)
@@ -150,6 +157,51 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix
.filter(PermissionBackend.filter_queryset(self.request, Opener, "view")), .filter(PermissionBackend.filter_queryset(self.request, Opener, "view")),
] ]
def render_to_response(self, context, **response_kwargs):
"""
Gère l'export CSV manuel pour MultiTableMixin.
"""
if "_export" in self.request.GET:
import tablib
table_name = self.request.GET.get("table")
if table_name:
tables = self.get_tables()
data_list = self.get_tables_data()
for t, d in zip(tables, data_list):
if t.prefix == table_name:
# Préparer le CSV
dataset = tablib.Dataset()
columns = list(t.base_columns) # noms des colonnes
dataset.headers = columns
for row in d:
values = []
for col in columns:
try:
val = getattr(row, col, "")
# Gestion spéciale pour la colonne 'entry'
if col == "entry":
if getattr(row, "has_entry", False):
val = timezone.localtime(row.entry.time).strftime("%Y-%m-%d %H:%M:%S")
else:
val = ""
values.append(str(val) if val is not None else "")
except Exception: # RelatedObjectDoesNotExist ou autre
values.append("")
dataset.append(values)
csv_bytes = dataset.export("csv")
if isinstance(csv_bytes, str):
csv_bytes = csv_bytes.encode("utf-8")
response = HttpResponse(csv_bytes, content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="{table_name}.csv"'
return response
# Sinon rendu normal
return super().render_to_response(context, **response_kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data() context = super().get_context_data()

View File

@@ -74,11 +74,15 @@ class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, Li
search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view')) search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view'))
# table open # table open
open_table = self.get_queryset().order_by('expiry_date').filter( open_table = self.get_queryset().filter(
Q(polymorphic_ctype__model='transformedfood') Q(polymorphic_ctype__model='transformedfood')
| Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC')).filter( | Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC')).filter(
expiry_date__lt=timezone.now(), end_of_life='').filter( expiry_date__lt=timezone.now(), end_of_life='').filter(
PermissionBackend.filter_queryset(self.request, Food, 'view')) PermissionBackend.filter_queryset(self.request, Food, 'view'))
open_table = open_table.union(self.get_queryset().filter(
Q(end_of_life='', order__iexact='open')
).filter(
PermissionBackend.filter_queryset(self.request, Food, 'view'))).order_by('expiry_date')
# table served # table served
served_table = self.get_queryset().order_by('-pk').filter( served_table = self.get_queryset().order_by('-pk').filter(
end_of_life='', is_ready=True).exclude( end_of_life='', is_ready=True).exclude(

View File

@@ -92,6 +92,20 @@ class MembershipTable(tables.Table):
} }
) )
user_email = tables.Column(
verbose_name="Email",
accessor="user.email",
orderable=False,
visible=False,
)
user_full_name = tables.Column(
verbose_name=_("Full name"),
accessor="user.get_full_name",
orderable=False,
visible=False,
)
def render_user(self, value): def render_user(self, value):
# If the user has the right, link the displayed user with the page of its detail. # If the user has the right, link the displayed user with the page of its detail.
s = value.username s = value.username
@@ -149,6 +163,16 @@ class MembershipTable(tables.Table):
+ "'>" + s + "</a>") + "'>" + s + "</a>")
return s return s
def value_user(self, record):
return record.user.username if record.user else ""
def value_club(self, record):
return record.club.name if record.club else ""
def value_roles(self, record):
roles = record.roles.all()
return ", ".join(str(role) for role in roles)
class Meta: class Meta:
attrs = { attrs = {
'class': 'table table-condensed table-striped', 'class': 'table table-condensed table-striped',

View File

@@ -36,7 +36,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% trans "There is no membership found with this pattern." %} {% trans "There is no membership found with this pattern." %}
</div> </div>
{% endif %} {% endif %}
<div class="card-footer text-center">
<button class="btn btn-block btn-primary mb-3" onclick="window.location.href='?_export=csv'">
{% trans "Export to CSV" %}
</button>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -17,6 +17,7 @@ from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, UpdateView, TemplateView from django.views.generic import DetailView, UpdateView, TemplateView
from django.views.generic.edit import FormMixin from django.views.generic.edit import FormMixin
from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView
from django_tables2.export.views import ExportMixin
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from api.viewsets import is_regex from api.viewsets import is_regex
from note.models import Alias, NoteClub, NoteUser, Trust from note.models import Alias, NoteClub, NoteUser, Trust
@@ -950,11 +951,12 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id}) return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id})
class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): class ClubMembersListView(ExportMixin, ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
model = Membership model = Membership
table_class = MembershipTable table_class = MembershipTable
template_name = "member/club_members.html" template_name = "member/club_members.html"
extra_context = {"title": _("Members of the club")} extra_context = {"title": _("Members of the club")}
export_formats = ["csv"]
def get_queryset(self, **kwargs): def get_queryset(self, **kwargs):
qs = super().get_queryset().filter(club_id=self.kwargs["pk"]) qs = super().get_queryset().filter(club_id=self.kwargs["pk"])
@@ -986,6 +988,14 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
return qs.distinct() return qs.distinct()
def get_export_filename(self, export_format):
return "members.csv"
def get_export_content_type(self, export_format):
if export_format == "csv":
return "text/csv"
return super().get_export_content_type(export_format)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
club = Club.objects.filter( club = Club.objects.filter(

View File

@@ -4702,6 +4702,22 @@
"description": "Supprimer un succès" "description": "Supprimer un succès"
} }
}, },
{
"model": "permission.permission",
"pk": 330,
"fields": {
"model": [
"auth",
"user"
],
"query": "{\"memberships__club\": [\"club\"]}",
"type": "view",
"mask": 2,
"field": "email",
"permanent": false,
"description": "Voir l'adresse mail des membres de son club"
}
},
{ {
"model": "permission.role", "model": "permission.role",
"pk": 1, "pk": 1,
@@ -4852,7 +4868,8 @@
259, 259,
260, 260,
263, 263,
265 265,
330
] ]
} }
}, },
@@ -5201,6 +5218,7 @@
"permissions": [ "permissions": [
37, 37,
41, 41,
42,
53, 53,
54, 54,
55, 55,

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.6 on 2025-09-28 20:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('treasury', '0010_alter_invoice_bde'),
]
operations = [
migrations.AddField(
model_name='sogecredit',
name='valid',
field=models.BooleanField(blank=True, default=False, verbose_name='Valid'),
),
]

View File

@@ -308,6 +308,12 @@ class SogeCredit(models.Model):
null=True, null=True,
) )
valid = models.BooleanField(
default=False,
verbose_name=_("Valid"),
blank=True,
)
class Meta: class Meta:
verbose_name = _("Credit from the Société générale") verbose_name = _("Credit from the Société générale")
verbose_name_plural = _("Credits from the Société générale") verbose_name_plural = _("Credits from the Société générale")
@@ -338,7 +344,7 @@ class SogeCredit(models.Model):
credit_transaction.save() credit_transaction.save()
credit_transaction.refresh_from_db() credit_transaction.refresh_from_db()
self.credit_transaction = credit_transaction self.credit_transaction = credit_transaction
elif not self.valid: elif not self.valid_legacy:
self.credit_transaction.amount = self.amount self.credit_transaction.amount = self.amount
self.credit_transaction._force_save = True self.credit_transaction._force_save = True
self.credit_transaction.save() self.credit_transaction.save()
@@ -346,12 +352,12 @@ class SogeCredit(models.Model):
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@property @property
def valid(self): def valid_legacy(self):
return self.credit_transaction and self.credit_transaction.valid return self.credit_transaction and self.credit_transaction.valid
@property @property
def amount(self): def amount(self):
if self.valid: if self.valid_legacy:
return self.credit_transaction.total return self.credit_transaction.total
amount = 0 amount = 0
transactions_wei = self.transactions.filter(membership__club__weiclub__isnull=False) transactions_wei = self.transactions.filter(membership__club__weiclub__isnull=False)
@@ -365,7 +371,7 @@ class SogeCredit(models.Model):
The Sogé credit may be created after the user already paid its memberships. The Sogé credit may be created after the user already paid its memberships.
We query transactions and update the credit, if it is unvalid. We query transactions and update the credit, if it is unvalid.
""" """
if self.valid or not self.pk: if self.valid_legacy or not self.pk:
return return
# Soge do not pay BDE and kfet memberships since 2022 # Soge do not pay BDE and kfet memberships since 2022
@@ -405,7 +411,7 @@ class SogeCredit(models.Model):
Invalidating a Société générale delete the transaction of the bank if it was already created. Invalidating a Société générale delete the transaction of the bank if it was already created.
Treasurers must know what they do, With Great Power Comes Great Responsibility... Treasurers must know what they do, With Great Power Comes Great Responsibility...
""" """
if self.valid: if self.valid_legacy:
self.credit_transaction.valid = False self.credit_transaction.valid = False
self.credit_transaction.save() self.credit_transaction.save()
for tr in self.transactions.all(): for tr in self.transactions.all():
@@ -414,7 +420,7 @@ class SogeCredit(models.Model):
tr.save() tr.save()
def validate(self, force=False): def validate(self, force=False):
if self.valid and not force: if self.valid_legacy and not force:
# The credit is already done # The credit is already done
return return

View File

@@ -18,4 +18,5 @@ django-rest-polymorphic~=0.1.10
django-tables2~=2.7.5 django-tables2~=2.7.5
python-memcached~=1.62 python-memcached~=1.62
phonenumbers~=9.0.8 phonenumbers~=9.0.8
tablib~=3.8.0
Pillow>=11.3.0 Pillow>=11.3.0