diff --git a/counter/forms.py b/counter/forms.py
index 9b1a9ab6..b2e9a678 100644
--- a/counter/forms.py
+++ b/counter/forms.py
@@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import ClockedSchedule
from phonenumber_field.widgets import RegionalPhoneNumberWidget
+from club.models import Club
from club.widgets.ajax_select import AutoCompleteSelectClub
from core.models import User
from core.views.forms import (
@@ -29,6 +30,7 @@ from counter.models import (
Counter,
Customer,
Eticket,
+ InvoiceCall,
Product,
Refilling,
ReturnableProduct,
@@ -478,3 +480,39 @@ class BaseBasketForm(forms.BaseFormSet):
BasketForm = forms.formset_factory(
BasketProductForm, formset=BaseBasketForm, absolute_max=None, min_num=1
)
+
+
+class InvoiceCallForm(forms.Form):
+ def __init__(self, *args, month, clubs: list[Club] | None = None, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.month = month
+ self.clubs = clubs
+
+ if self.clubs is None:
+ self.clubs = []
+
+ invoices = {
+ i["club_id"]: i["is_validated"]
+ for i in InvoiceCall.objects.filter(
+ club__in=self.clubs, month=self.month
+ ).values("club_id", "is_validated")
+ }
+
+ for club in self.clubs:
+ is_validated = invoices.get(club.id, False)
+
+ self.fields[f"club_{club.id}"] = forms.BooleanField(
+ required=False, initial=is_validated
+ )
+
+ def save(self):
+ for club in self.clubs:
+ field_name = f"club_{club.id}"
+ is_validated = self.cleaned_data.get(field_name, False)
+
+ InvoiceCall.objects.update_or_create(
+ month=self.month, club=club, defaults={"is_validated": is_validated}
+ )
+
+ def get_club_name(self, club_id):
+ return f"club_{club_id}"
diff --git a/counter/migrations/0032_invoicecall.py b/counter/migrations/0032_invoicecall.py
new file mode 100644
index 00000000..b02edf20
--- /dev/null
+++ b/counter/migrations/0032_invoicecall.py
@@ -0,0 +1,45 @@
+# Generated by Django 5.2.3 on 2025-09-09 10:24
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+import counter.models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("club", "0014_alter_club_options_rename_unix_name_club_slug_name_and_more"),
+ ("counter", "0031_alter_counter_options"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="InvoiceCall",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "is_validated",
+ models.BooleanField(default=False, verbose_name="is validated"),
+ ),
+ ("month", counter.models.MonthField(verbose_name="invoice date")),
+ (
+ "club",
+ models.ForeignKey(
+ on_delete=django.db.models.deletion.CASCADE, to="club.club"
+ ),
+ ),
+ ],
+ options={
+ "verbose_name": "Invoice call",
+ "verbose_name_plural": "Invoice calls",
+ },
+ ),
+ ]
diff --git a/counter/models.py b/counter/models.py
index 685071c3..6e5bdba4 100644
--- a/counter/models.py
+++ b/counter/models.py
@@ -1395,3 +1395,49 @@ class ScheduledProductAction(PeriodicTask):
# adapted in the case of scheduled product action,
# so we skip it and execute directly Model.validate_unique
return super(PeriodicTask, self).validate_unique(*args, **kwargs)
+
+
+class MonthField(models.DateField):
+ description = _("Year + month field (day forced to 1)")
+ default_error_messages = {
+ "invalid": _(
+ "“%(value)s” value has an invalid date format. It must be "
+ "in YYYY-MM format."
+ ),
+ "invalid_date": _(
+ "“%(value)s” value has the correct format (YYYY-MM) "
+ "but it is an invalid date."
+ ),
+ }
+
+ def to_python(self, value):
+ if isinstance(value, date):
+ return value.replace(day=1)
+
+ if isinstance(value, str):
+ try:
+ year, month = map(int, value.split("-"))
+ return date(year, month, 1)
+ except (ValueError, TypeError) as err:
+ raise ValueError(
+ self.error_messages["invalid"] % {"value": value}
+ ) from err
+
+ return super().to_python(value)
+
+
+class InvoiceCall(models.Model):
+ is_validated = models.BooleanField(verbose_name=_("is validated"), default=False)
+ club = models.ForeignKey(Club, on_delete=models.CASCADE)
+ month = MonthField(verbose_name=_("invoice date"))
+
+ class Meta:
+ verbose_name = _("Invoice call")
+ verbose_name_plural = _("Invoice calls")
+
+ def __str__(self):
+ return f"invoice call of {self.month} made by {self.club}"
+
+ def save(self, *args, **kwargs):
+ self.month = self._meta.get_field("month").to_python(self.month)
+ super().save(*args, **kwargs)
diff --git a/counter/templates/counter/invoices_call.jinja b/counter/templates/counter/invoices_call.jinja
index 973021fb..9085d05d 100644
--- a/counter/templates/counter/invoices_call.jinja
+++ b/counter/templates/counter/invoices_call.jinja
@@ -15,24 +15,32 @@
-
-
{% trans %}CB Payments{% endtrans %} : {{ sum_cb }} €
-{% trans %}Club{% endtrans %} | -{% trans %}Sum{% endtrans %} | - - - {% for i in sums %} -
{{ i['club__name'] }} | -{{ i['selling_sum'] }} € | -