M1: Grundgerüst, Datenmodell und Admin

Django-5.2-Projekt mit uv, django-environ und Docker-Compose-Setup
(web + Postgres, WeasyPrint-Systembibliotheken im Image). Vollständiges
Datenmodell (Firma, Therapeut, Auftraggeber, Teilnehmer, Leistungsart,
Leistung, InvoiceLog, InvoiceCounter) mit Admin, Login-Flow und
Platzhalter-Dashboard. Fortschritt in FORTSCHRITT.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yy3uodsai6Ytt3GJ7tBdz
This commit is contained in:
Claude
2026-07-10 23:53:56 +00:00
parent 7b60be2933
commit cfa2266dd9
31 changed files with 2044 additions and 0 deletions

13
.dockerignore Normal file
View File

@@ -0,0 +1,13 @@
.git
.gitignore
.venv
**/__pycache__
*.pyc
*.pyo
db.sqlite3
staticfiles/
media/
.env
.idea/
*.md
Architektur_Planung_Abrechnungssystem.pdf

17
.env.example Normal file
View File

@@ -0,0 +1,17 @@
# Kopiere diese Datei nach .env und passe die Werte an.
# Docker Compose liest .env für die Web- und DB-Container.
# Django
SECRET_KEY=change-me-to-a-long-random-value
DEBUG=1
ALLOWED_HOSTS=localhost,127.0.0.1
CSRF_TRUSTED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000
# Postgres (von docker-compose.yml verwendet)
POSTGRES_DB=abrechnung
POSTGRES_USER=abrechnung
POSTGRES_PASSWORD=abrechnung
# Wird im Compose-File aus den POSTGRES_*-Werten zusammengesetzt; für lokalen
# (nicht-Docker) Betrieb hier eine eigene DATABASE_URL setzen, z.B.:
# DATABASE_URL=postgres://abrechnung:abrechnung@localhost:5432/abrechnung

10
.gitignore vendored
View File

@@ -1,2 +1,12 @@
Architektur_Planung_Abrechnungssystem.pdf
.idea/
# Python / Django
__pycache__/
*.py[cod]
.venv/
db.sqlite3
staticfiles/
media/
.env
.pytest_cache/

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.13

48
Dockerfile Normal file
View File

@@ -0,0 +1,48 @@
# syntax=docker/dockerfile:1
# --- Builder: resolve & install dependencies with uv into a venv ---------------
FROM python:3.13-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:0.8 /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_PROJECT_ENVIRONMENT=/app/.venv
WORKDIR /app
# Install deps first (cached layer) using only the lockfiles.
COPY pyproject.toml uv.lock .python-version ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --no-install-project
# --- Runtime: slim image + WeasyPrint system libraries -------------------------
FROM python:3.13-slim AS runtime
# WeasyPrint needs pango/cairo/gdk-pixbuf + fonts to render PDFs.
RUN apt-get update && apt-get install -y --no-install-recommends \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libcairo2 \
libgdk-pixbuf-2.0-0 \
libffi8 \
fonts-dejavu-core \
fonts-liberation \
&& rm -rf /var/lib/apt/lists/*
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
DJANGO_SETTINGS_MODULE=config.settings
WORKDIR /app
# Copy the pre-built virtualenv from the builder, then the app source.
COPY --from=builder /app/.venv /app/.venv
COPY . .
RUN chmod +x entrypoint.sh
EXPOSE 8000
ENTRYPOINT ["./entrypoint.sh"]
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]

61
FORTSCHRITT.md Normal file
View File

@@ -0,0 +1,61 @@
# Umsetzungsfortschritt (Meilensteine)
> Ausführungsplan für das MVP aus [`SPEZIFIKATION.md`](./SPEZIFIKATION.md) /
> [`UMSETZUNGSPLAN.md`](./UMSETZUNGSPLAN.md). Diese Datei hält den Fortschritt fest,
> damit die Arbeit später fortgesetzt werden kann.
**Auslieferung:** in Meilenstein-Checkpoints (nach jedem Meilenstein Review).
**Kanonische Laufzeit:** Docker Compose (`web` + Postgres). WeasyPrint-Systembibliotheken
kommen aus dem Docker-Image, nicht vom Host.
## Status
| MS | Inhalt | Status |
|----|--------|--------|
| **M1** | Grundgerüst + Datenmodell + Admin | ✅ **fertig** |
| **M2** | Auth + Leistungserfassung (Monatsblatt-Grid) | ⬜ offen |
| **M3** | On-the-fly Rechnungslegung + ZUGFeRD | ⬜ offen |
| **M4** | Storno / Korrektur-Workflow | ⬜ offen |
| **M5** | Dashboard (Controlling) + Test-Suite | ⬜ offen |
## M1 — erledigt
- `uv`-Projekt (`pyproject.toml`, `uv.lock`, `.python-version`=3.13), Django 5.2, psycopg3,
WeasyPrint, drafthorse, factur-x, crispy-forms/bootstrap5, django-tables2, whitenoise, pytest-django.
- `config/` (Settings via `django-environ`, `DATABASE_URL`, deutsche Lokalisierung).
- Datenmodell `abrechnung/models.py`: `Firma` (Singleton), `Therapeut`, `Auftraggeber`,
`Teilnehmer` (fest 1 Therapeut), `Leistungsart`, `InvoiceLog` (Journal, keine Dokumente),
`Leistung` (ehem. `TherapySession`; `auftraggeber` über Teilnehmer abgeleitet; `betrag`),
`InvoiceCounter` (Zähler-Zeile für lückenlose Nummern in M3).
- Admin für alle Entitäten; `Firma` auf eine Instanz begrenzt.
- Auth (Login/Logout), `base.html`, Platzhalter-Dashboard.
- Docker: multi-stage `Dockerfile` (uv-Builder → schlanke Runtime mit pango/cairo/gdk-pixbuf +
Fonts), `entrypoint.sh` (wait-for-db → migrate → collectstatic → gunicorn),
`docker-compose.yml` (web + postgres:16) + Dev-Override, `.dockerignore`, `.env.example`.
**Verifiziert:** `manage.py check` sauber; Migrationen erzeugt+angewandt; HTTP-Smoketest
(Login 200, `/` anon→Login 302 / auth 200, Admin 200); `pytest` grün (3 Modelltests);
`docker compose config` valide.
**Offen (Umgebung ohne Docker-Daemon):** `docker compose up` (Image-Build + web+db live)
muss in einer Umgebung mit laufendem Docker-Daemon bestätigt werden.
## Als Nächstes: M2
Django-Login-Flow steht bereits. Zu bauen: gruppierte Erfassungs-View
**Therapeut + Monat** → je zugeordnetem Teilnehmer ein einklappbares Monatsblatt
(`modelformset_factory(Leistung)`: Datum · Leistungsart · Stunden · Satz · Beschreibung),
Stundensatz aus Leistungsart vorbelegt, Bestandseinträge vorgeladen, minimales JS
(`static/abrechnung/monatsblatt.js`: Zeilen klonen, Tab/Enter-Navigation, Live-Summen).
## Lokaler Start (Entwicklung)
```bash
uv sync
uv run python manage.py migrate # nutzt SQLite, wenn keine DATABASE_URL gesetzt
uv run python manage.py createsuperuser
uv run python manage.py runserver
```
## Start via Docker (kanonisch)
```bash
cp .env.example .env # Werte anpassen
docker compose up --build
docker compose exec web python manage.py createsuperuser
```

0
abrechnung/__init__.py Normal file
View File

80
abrechnung/admin.py Normal file
View File

@@ -0,0 +1,80 @@
"""Admin-Registrierung. Stammdatenpflege läuft im MVP über den Django-Admin."""
from django.contrib import admin
from .models import (
Auftraggeber,
Firma,
InvoiceCounter,
InvoiceLog,
Leistung,
Leistungsart,
Teilnehmer,
Therapeut,
)
@admin.register(Firma)
class FirmaAdmin(admin.ModelAdmin):
list_display = ("name", "ort", "ust_idnr")
def has_add_permission(self, request):
# Singleton: nur eine Firma zulassen.
return not Firma.objects.exists()
@admin.register(Therapeut)
class TherapeutAdmin(admin.ModelAdmin):
list_display = ("name", "kuerzel", "aktiv")
list_filter = ("aktiv",)
search_fields = ("name", "kuerzel")
@admin.register(Auftraggeber)
class AuftraggeberAdmin(admin.ModelAdmin):
list_display = ("name", "typ", "ort", "ust_idnr")
list_filter = ("typ",)
search_fields = ("name",)
@admin.register(Teilnehmer)
class TeilnehmerAdmin(admin.ModelAdmin):
list_display = ("name", "auftraggeber", "therapeut", "aktiv")
list_filter = ("aktiv", "therapeut", "auftraggeber")
search_fields = ("name", "aktenzeichen")
autocomplete_fields = ("auftraggeber", "therapeut")
@admin.register(Leistungsart)
class LeistungsartAdmin(admin.ModelAdmin):
list_display = ("bezeichnung", "standard_stundensatz", "aktiv")
list_filter = ("aktiv",)
search_fields = ("bezeichnung",)
@admin.register(Leistung)
class LeistungAdmin(admin.ModelAdmin):
list_display = (
"datum", "teilnehmer", "therapeut", "leistungsart",
"stundenanzahl", "stundensatz", "abgerechnet_mit",
)
list_filter = ("therapeut", "leistungsart", "datum")
search_fields = ("teilnehmer__name", "beschreibung")
autocomplete_fields = ("teilnehmer", "therapeut", "leistungsart")
date_hierarchy = "datum"
@admin.register(InvoiceLog)
class InvoiceLogAdmin(admin.ModelAdmin):
list_display = (
"rechnungsnummer", "datum", "auftraggeber", "gesamtbetrag",
"typ", "status", "storno_von",
)
list_filter = ("status", "typ", "datum")
search_fields = ("rechnungsnummer", "auftraggeber__name")
readonly_fields = ("created_at", "created_by")
@admin.register(InvoiceCounter)
class InvoiceCounterAdmin(admin.ModelAdmin):
list_display = ("praefix", "letzter_wert")

6
abrechnung/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class AbrechnungConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "abrechnung"

View File

@@ -0,0 +1,493 @@
# Generated by Django 5.2.16 on 2026-07-10 22:50
import django.core.validators
import django.db.models.deletion
from decimal import Decimal
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Auftraggeber",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=200, verbose_name="Name")),
(
"typ",
models.CharField(
choices=[
("AMT", "Amt / Behörde"),
("FIRMA", "Firma"),
("PRIVAT", "Privatzahler"),
],
default="AMT",
max_length=10,
verbose_name="Typ",
),
),
(
"strasse",
models.CharField(
max_length=200, verbose_name="Straße & Hausnummer"
),
),
("plz", models.CharField(max_length=10, verbose_name="PLZ")),
("ort", models.CharField(max_length=100, verbose_name="Ort")),
(
"land",
models.CharField(
default="DE", max_length=2, verbose_name="Land (ISO)"
),
),
(
"ust_idnr",
models.CharField(
blank=True, max_length=20, verbose_name="USt-IdNr."
),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="E-Mail"
),
),
(
"leitweg_id",
models.CharField(
blank=True,
help_text="Nur für Behörden mit XRechnung-Pflicht.",
max_length=50,
verbose_name="Leitweg-ID",
),
),
],
options={
"verbose_name": "Auftraggeber",
"verbose_name_plural": "Auftraggeber",
"ordering": ["name"],
},
),
migrations.CreateModel(
name="Firma",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=200, verbose_name="Firmenname")),
(
"strasse",
models.CharField(
max_length=200, verbose_name="Straße & Hausnummer"
),
),
("plz", models.CharField(max_length=10, verbose_name="PLZ")),
("ort", models.CharField(max_length=100, verbose_name="Ort")),
(
"land",
models.CharField(
default="DE", max_length=2, verbose_name="Land (ISO)"
),
),
(
"ust_idnr",
models.CharField(
blank=True, max_length=20, verbose_name="USt-IdNr."
),
),
(
"steuernummer",
models.CharField(
blank=True, max_length=30, verbose_name="Steuernummer"
),
),
(
"iban",
models.CharField(blank=True, max_length=34, verbose_name="IBAN"),
),
(
"bic",
models.CharField(blank=True, max_length=11, verbose_name="BIC"),
),
(
"bank",
models.CharField(blank=True, max_length=100, verbose_name="Bank"),
),
(
"email",
models.EmailField(
blank=True, max_length=254, verbose_name="E-Mail"
),
),
(
"telefon",
models.CharField(blank=True, max_length=40, verbose_name="Telefon"),
),
(
"logo",
models.ImageField(
blank=True, null=True, upload_to="firma/", verbose_name="Logo"
),
),
(
"ust_befreiungsgrund",
models.CharField(
default="Steuerfrei gemäß § 4 UStG",
help_text="Erscheint als Befreiungsgrund auf Rechnung und im ZUGFeRD-XML.",
max_length=255,
verbose_name="USt-Befreiungsgrund",
),
),
],
options={
"verbose_name": "Firma",
"verbose_name_plural": "Firma",
},
),
migrations.CreateModel(
name="InvoiceCounter",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"praefix",
models.CharField(max_length=20, unique=True, verbose_name="Präfix"),
),
(
"letzter_wert",
models.PositiveIntegerField(
default=0, verbose_name="letzter vergebener Wert"
),
),
],
options={
"verbose_name": "Nummernkreis-Zähler",
"verbose_name_plural": "Nummernkreis-Zähler",
},
),
migrations.CreateModel(
name="Leistungsart",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"bezeichnung",
models.CharField(
max_length=100, unique=True, verbose_name="Bezeichnung"
),
),
(
"standard_stundensatz",
models.DecimalField(
blank=True,
decimal_places=2,
max_digits=8,
null=True,
validators=[
django.core.validators.MinValueValidator(Decimal("0"))
],
verbose_name="Standard-Stundensatz (€)",
),
),
("aktiv", models.BooleanField(default=True, verbose_name="aktiv")),
],
options={
"verbose_name": "Leistungsart",
"verbose_name_plural": "Leistungsarten",
"ordering": ["bezeichnung"],
},
),
migrations.CreateModel(
name="Therapeut",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=150, verbose_name="Name")),
(
"kuerzel",
models.CharField(max_length=10, unique=True, verbose_name="Kürzel"),
),
("aktiv", models.BooleanField(default=True, verbose_name="aktiv")),
],
options={
"verbose_name": "Therapeut",
"verbose_name_plural": "Therapeuten",
"ordering": ["name"],
},
),
migrations.CreateModel(
name="InvoiceLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"rechnungsnummer",
models.CharField(
max_length=30, unique=True, verbose_name="Rechnungsnummer"
),
),
("datum", models.DateField(verbose_name="Rechnungsdatum")),
(
"gesamtbetrag",
models.DecimalField(
decimal_places=2, max_digits=12, verbose_name="Gesamtbetrag (€)"
),
),
(
"status",
models.CharField(
choices=[("GUELTIG", "Gültig"), ("STORNIERT", "Storniert")],
default="GUELTIG",
max_length=10,
verbose_name="Status",
),
),
(
"typ",
models.CharField(
choices=[("RECHNUNG", "Rechnung"), ("STORNO", "Storno")],
default="RECHNUNG",
max_length=10,
verbose_name="Typ",
),
),
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="erstellt am"),
),
(
"auftraggeber",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="rechnungen",
to="abrechnung.auftraggeber",
verbose_name="Auftraggeber",
),
),
(
"created_by",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="erstellte_rechnungen",
to=settings.AUTH_USER_MODEL,
verbose_name="erstellt von",
),
),
(
"storno_von",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="storno_belege",
to="abrechnung.invoicelog",
verbose_name="Storno von",
),
),
],
options={
"verbose_name": "Rechnungsjournal-Eintrag",
"verbose_name_plural": "Rechnungsjournal",
"ordering": ["-datum", "-id"],
},
),
migrations.CreateModel(
name="Teilnehmer",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=200, verbose_name="Name")),
(
"aktenzeichen",
models.CharField(
blank=True, max_length=50, verbose_name="Aktenzeichen"
),
),
(
"geburtsdatum",
models.DateField(
blank=True, null=True, verbose_name="Geburtsdatum"
),
),
("aktiv", models.BooleanField(default=True, verbose_name="aktiv")),
(
"auftraggeber",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="teilnehmer",
to="abrechnung.auftraggeber",
verbose_name="Auftraggeber",
),
),
(
"therapeut",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="teilnehmer",
to="abrechnung.therapeut",
verbose_name="Zugeordneter Therapeut",
),
),
],
options={
"verbose_name": "Teilnehmer",
"verbose_name_plural": "Teilnehmer",
"ordering": ["name"],
},
),
migrations.CreateModel(
name="Leistung",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("datum", models.DateField(verbose_name="Datum")),
(
"stundenanzahl",
models.DecimalField(
decimal_places=2,
max_digits=6,
validators=[
django.core.validators.MinValueValidator(Decimal("0"))
],
verbose_name="Stunden",
),
),
(
"stundensatz",
models.DecimalField(
decimal_places=2,
max_digits=8,
validators=[
django.core.validators.MinValueValidator(Decimal("0"))
],
verbose_name="Stundensatz (€)",
),
),
(
"beschreibung",
models.CharField(
blank=True, max_length=255, verbose_name="Beschreibung"
),
),
(
"abgerechnet_mit",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="leistungen",
to="abrechnung.invoicelog",
verbose_name="abgerechnet mit",
),
),
(
"leistungsart",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="leistungen",
to="abrechnung.leistungsart",
verbose_name="Leistungsart",
),
),
(
"teilnehmer",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="leistungen",
to="abrechnung.teilnehmer",
verbose_name="Teilnehmer",
),
),
(
"therapeut",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="leistungen",
to="abrechnung.therapeut",
verbose_name="Therapeut",
),
),
],
options={
"verbose_name": "Leistung",
"verbose_name_plural": "Leistungen",
"ordering": ["datum", "id"],
"indexes": [
models.Index(
fields=["therapeut", "datum"],
name="abrechnung__therape_3934a3_idx",
),
models.Index(
fields=["abgerechnet_mit"],
name="abrechnung__abgerec_00377c_idx",
),
],
},
),
]

View File

254
abrechnung/models.py Normal file
View File

@@ -0,0 +1,254 @@
"""Datenmodell des Abrechnungssystems (siehe UMSETZUNGSPLAN.md §"Data model")."""
from __future__ import annotations
from decimal import Decimal
from django.conf import settings
from django.core.validators import MinValueValidator
from django.db import models
class Firma(models.Model):
"""Verkäufer / Rechnungssteller (Singleton). Basis für ZUGFeRD-Seller & Kopf."""
name = models.CharField("Firmenname", max_length=200)
strasse = models.CharField("Straße & Hausnummer", max_length=200)
plz = models.CharField("PLZ", max_length=10)
ort = models.CharField("Ort", max_length=100)
land = models.CharField("Land (ISO)", max_length=2, default="DE")
ust_idnr = models.CharField("USt-IdNr.", max_length=20, blank=True)
steuernummer = models.CharField("Steuernummer", max_length=30, blank=True)
iban = models.CharField("IBAN", max_length=34, blank=True)
bic = models.CharField("BIC", max_length=11, blank=True)
bank = models.CharField("Bank", max_length=100, blank=True)
email = models.EmailField("E-Mail", blank=True)
telefon = models.CharField("Telefon", max_length=40, blank=True)
logo = models.ImageField("Logo", upload_to="firma/", blank=True, null=True)
ust_befreiungsgrund = models.CharField(
"USt-Befreiungsgrund",
max_length=255,
default="Steuerfrei gemäß § 4 UStG",
help_text="Erscheint als Befreiungsgrund auf Rechnung und im ZUGFeRD-XML.",
)
class Meta:
verbose_name = "Firma"
verbose_name_plural = "Firma"
def __str__(self) -> str:
return self.name
class Therapeut(models.Model):
"""Leistungserbringer. Basis für Leistungsstatistiken."""
name = models.CharField("Name", max_length=150)
kuerzel = models.CharField("Kürzel", max_length=10, unique=True)
aktiv = models.BooleanField("aktiv", default=True)
class Meta:
verbose_name = "Therapeut"
verbose_name_plural = "Therapeuten"
ordering = ["name"]
def __str__(self) -> str:
return f"{self.name} ({self.kuerzel})"
class Auftraggeber(models.Model):
"""Rechnungsempfänger / Bezahler. → ZUGFeRD-Buyer."""
class Typ(models.TextChoices):
AMT = "AMT", "Amt / Behörde"
FIRMA = "FIRMA", "Firma"
PRIVAT = "PRIVAT", "Privatzahler"
name = models.CharField("Name", max_length=200)
typ = models.CharField("Typ", max_length=10, choices=Typ.choices, default=Typ.AMT)
strasse = models.CharField("Straße & Hausnummer", max_length=200)
plz = models.CharField("PLZ", max_length=10)
ort = models.CharField("Ort", max_length=100)
land = models.CharField("Land (ISO)", max_length=2, default="DE")
ust_idnr = models.CharField("USt-IdNr.", max_length=20, blank=True)
email = models.EmailField("E-Mail", blank=True)
leitweg_id = models.CharField(
"Leitweg-ID", max_length=50, blank=True,
help_text="Nur für Behörden mit XRechnung-Pflicht.",
)
class Meta:
verbose_name = "Auftraggeber"
verbose_name_plural = "Auftraggeber"
ordering = ["name"]
def __str__(self) -> str:
return self.name
class Teilnehmer(models.Model):
"""Leistungsempfänger (Patient/Klient). Fest einem Auftraggeber zugeordnet."""
name = models.CharField("Name", max_length=200)
aktenzeichen = models.CharField("Aktenzeichen", max_length=50, blank=True)
geburtsdatum = models.DateField("Geburtsdatum", null=True, blank=True)
auftraggeber = models.ForeignKey(
Auftraggeber, on_delete=models.PROTECT, related_name="teilnehmer",
verbose_name="Auftraggeber",
)
therapeut = models.ForeignKey(
Therapeut, on_delete=models.PROTECT, related_name="teilnehmer",
verbose_name="Zugeordneter Therapeut",
)
aktiv = models.BooleanField("aktiv", default=True)
class Meta:
verbose_name = "Teilnehmer"
verbose_name_plural = "Teilnehmer"
ordering = ["name"]
def __str__(self) -> str:
return self.name
class Leistungsart(models.Model):
"""Admin-konfigurierbare Leistungsart (Sitzung, Vorbereitung, Elterngespräch, …)."""
bezeichnung = models.CharField("Bezeichnung", max_length=100, unique=True)
standard_stundensatz = models.DecimalField(
"Standard-Stundensatz (€)", max_digits=8, decimal_places=2,
null=True, blank=True, validators=[MinValueValidator(Decimal("0"))],
)
aktiv = models.BooleanField("aktiv", default=True)
class Meta:
verbose_name = "Leistungsart"
verbose_name_plural = "Leistungsarten"
ordering = ["bezeichnung"]
def __str__(self) -> str:
return self.bezeichnung
class InvoiceLog(models.Model):
"""Rechnungsjournal: nur Metadaten, keine Dokumente. Lückenloser Nummernkreis."""
class Status(models.TextChoices):
GUELTIG = "GUELTIG", "Gültig"
STORNIERT = "STORNIERT", "Storniert"
class Typ(models.TextChoices):
RECHNUNG = "RECHNUNG", "Rechnung"
STORNO = "STORNO", "Storno"
rechnungsnummer = models.CharField("Rechnungsnummer", max_length=30, unique=True)
datum = models.DateField("Rechnungsdatum")
auftraggeber = models.ForeignKey(
Auftraggeber, on_delete=models.PROTECT, related_name="rechnungen",
verbose_name="Auftraggeber",
)
gesamtbetrag = models.DecimalField("Gesamtbetrag (€)", max_digits=12, decimal_places=2)
status = models.CharField(
"Status", max_length=10, choices=Status.choices, default=Status.GUELTIG,
)
typ = models.CharField("Typ", max_length=10, choices=Typ.choices, default=Typ.RECHNUNG)
storno_von = models.ForeignKey(
"self", on_delete=models.PROTECT, null=True, blank=True,
related_name="storno_belege", verbose_name="Storno von",
)
created_at = models.DateTimeField("erstellt am", auto_now_add=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.PROTECT, null=True, blank=True,
related_name="erstellte_rechnungen", verbose_name="erstellt von",
)
class Meta:
verbose_name = "Rechnungsjournal-Eintrag"
verbose_name_plural = "Rechnungsjournal"
ordering = ["-datum", "-id"]
def __str__(self) -> str:
return f"{self.rechnungsnummer} ({self.get_status_display()})"
class InvoiceCounter(models.Model):
"""Zähler-Zeile für die lückenlose Nummernvergabe (siehe services/numbering.py).
Genau eine Zeile pro Präfix; wird per select_for_update gesperrt, damit
parallele Abrechnungen keine Nummern doppelt oder mit Lücken vergeben.
"""
praefix = models.CharField("Präfix", max_length=20, unique=True)
letzter_wert = models.PositiveIntegerField("letzter vergebener Wert", default=0)
class Meta:
verbose_name = "Nummernkreis-Zähler"
verbose_name_plural = "Nummernkreis-Zähler"
def __str__(self) -> str:
return f"{self.praefix}: {self.letzter_wert}"
class Leistung(models.Model):
"""Erbrachte Leistung (Sitzung, Vorbereitung, Elterngespräch, Beratung …).
Umbenannt von TherapySession. Auftraggeber wird über den Teilnehmer abgeleitet.
"""
therapeut = models.ForeignKey(
Therapeut, on_delete=models.PROTECT, related_name="leistungen",
verbose_name="Therapeut",
)
teilnehmer = models.ForeignKey(
Teilnehmer, on_delete=models.PROTECT, related_name="leistungen",
verbose_name="Teilnehmer",
)
leistungsart = models.ForeignKey(
Leistungsart, on_delete=models.PROTECT, related_name="leistungen",
verbose_name="Leistungsart",
)
datum = models.DateField("Datum")
stundenanzahl = models.DecimalField(
"Stunden", max_digits=6, decimal_places=2,
validators=[MinValueValidator(Decimal("0"))],
)
stundensatz = models.DecimalField(
"Stundensatz (€)", max_digits=8, decimal_places=2,
validators=[MinValueValidator(Decimal("0"))],
)
beschreibung = models.CharField("Beschreibung", max_length=255, blank=True)
abgerechnet_mit = models.ForeignKey(
InvoiceLog, on_delete=models.PROTECT, null=True, blank=True,
related_name="leistungen", verbose_name="abgerechnet mit",
)
class Meta:
verbose_name = "Leistung"
verbose_name_plural = "Leistungen"
ordering = ["datum", "id"]
indexes = [
models.Index(fields=["therapeut", "datum"]),
models.Index(fields=["abgerechnet_mit"]),
]
def __str__(self) -> str:
return f"{self.datum} · {self.teilnehmer} · {self.leistungsart}"
@property
def betrag(self) -> Decimal:
"""Zeilensumme = Stunden × Stundensatz."""
return (self.stundenanzahl or Decimal("0")) * (self.stundensatz or Decimal("0"))
@property
def auftraggeber(self) -> Auftraggeber:
"""Abgeleitet über den Teilnehmer (kein direkter FK)."""
return self.teilnehmer.auftraggeber

View File

View File

@@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %}Dashboard · Abrechnungssystem{% endblock %}
{% block content %}
<div class="card">
<h1>Willkommen, {{ user.get_username }}</h1>
<p>Das Grundgerüst steht (Meilenstein&nbsp;1). Über <a href="{% url 'admin:index' %}">Stammdaten</a>
lassen sich Firma, Auftraggeber, Therapeuten, Teilnehmer, Leistungsarten und Leistungen pflegen.</p>
<p>Leistungserfassung, Rechnungslegung, Storno und das Controlling-Dashboard folgen in den
nächsten Meilensteinen.</p>
</div>
{% endblock %}

View File

View File

@@ -0,0 +1,46 @@
"""Grundlegende Modelltests (M1). Umfangreichere Tests folgen in M5."""
from datetime import date
from decimal import Decimal
import pytest
from abrechnung.models import (
Auftraggeber,
Leistung,
Leistungsart,
Teilnehmer,
Therapeut,
)
@pytest.fixture
def leistung(db):
therapeut = Therapeut.objects.create(name="Anna Muster", kuerzel="AM")
auftraggeber = Auftraggeber.objects.create(
name="Jugendamt Musterstadt", strasse="Amtsweg 1", plz="12345", ort="Musterstadt",
)
teilnehmer = Teilnehmer.objects.create(
name="Max Klient", auftraggeber=auftraggeber, therapeut=therapeut,
)
art = Leistungsart.objects.create(
bezeichnung="Sitzung", standard_stundensatz=Decimal("80.00"),
)
return Leistung.objects.create(
therapeut=therapeut, teilnehmer=teilnehmer, leistungsart=art,
datum=date(2026, 5, 4), stundenanzahl=Decimal("1.50"),
stundensatz=Decimal("80.00"),
)
def test_betrag_is_hours_times_rate(leistung):
assert leistung.betrag == Decimal("120.00")
def test_auftraggeber_derived_via_teilnehmer(leistung):
assert leistung.auftraggeber == leistung.teilnehmer.auftraggeber
assert leistung.auftraggeber.name == "Jugendamt Musterstadt"
def test_leistung_starts_unbilled(leistung):
assert leistung.abgerechnet_mit is None

10
abrechnung/views.py Normal file
View File

@@ -0,0 +1,10 @@
"""Views. In M1 nur ein Platzhalter-Dashboard; wird in späteren Meilensteinen ausgebaut."""
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
@login_required
def dashboard(request):
"""Platzhalter-Startseite (echtes Controlling-Dashboard folgt in M5)."""
return render(request, "abrechnung/dashboard_placeholder.html")

0
config/__init__.py Normal file
View File

16
config/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for config project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_asgi_application()

128
config/settings.py Normal file
View File

@@ -0,0 +1,128 @@
"""
Django settings for the Abrechnungssystem project.
Settings are read from environment variables via django-environ so the same
image runs in dev (docker-compose.override.yml) and prod (docker-compose.yml).
"""
from pathlib import Path
import environ
BASE_DIR = Path(__file__).resolve().parent.parent
env = environ.Env(
DEBUG=(bool, False),
ALLOWED_HOSTS=(list, ["*"]),
)
# Read a local .env if present (Docker passes env directly, so it's optional).
environ.Env.read_env(BASE_DIR / ".env")
SECRET_KEY = env("SECRET_KEY", default="django-insecure-dev-only-change-me")
DEBUG = env("DEBUG")
ALLOWED_HOSTS = env("ALLOWED_HOSTS")
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[])
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"crispy_forms",
"crispy_bootstrap5",
"django_tables2",
"abrechnung",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "config.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "config.wsgi.application"
# Database — DATABASE_URL, e.g. postgres://user:pass@db:5432/abrechnung.
# Falls back to a local SQLite file when DATABASE_URL is unset (e.g. quick tests).
DATABASES = {
"default": env.db(
"DATABASE_URL",
default=f"sqlite:///{BASE_DIR / 'db.sqlite3'}",
)
}
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internationalization — German-facing UI.
LANGUAGE_CODE = "de-de"
TIME_ZONE = "Europe/Berlin"
USE_I18N = True
USE_TZ = True
# Static files
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / "static"]
STORAGES = {
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# Media (only used for the seller logo — not for invoice documents).
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Auth flow
LOGIN_URL = "login"
LOGIN_REDIRECT_URL = "dashboard"
LOGOUT_REDIRECT_URL = "login"
# crispy-forms
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"
DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap5.html"

23
config/urls.py Normal file
View File

@@ -0,0 +1,23 @@
"""URL-Konfiguration des Abrechnungssystems."""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import path
from abrechnung import views
urlpatterns = [
path("admin/", admin.site.urls),
path(
"login/",
auth_views.LoginView.as_view(template_name="registration/login.html"),
name="login",
),
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
path("", views.dashboard, name="dashboard"),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

16
config/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for config project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()

View File

@@ -0,0 +1,12 @@
# Dev-Overrides: Quellcode als Bind-Mount, runserver mit Auto-Reload, DEBUG an.
# Wird von `docker compose up` automatisch zusätzlich geladen.
services:
web:
build:
context: .
environment:
DEBUG: "1"
DJANGO_COLLECTSTATIC: "0"
volumes:
- .:/app
command: ["python", "manage.py", "runserver", "0.0.0.0:8000"]

31
docker-compose.yml Normal file
View File

@@ -0,0 +1,31 @@
services:
db:
image: postgres:16
environment:
POSTGRES_DB: ${POSTGRES_DB:-abrechnung}
POSTGRES_USER: ${POSTGRES_USER:-abrechnung}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-abrechnung}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-abrechnung}"]
interval: 5s
timeout: 3s
retries: 10
web:
build: .
env_file:
- .env
environment:
DB_HOST: db
DB_PORT: "5432"
DATABASE_URL: postgres://${POSTGRES_USER:-abrechnung}:${POSTGRES_PASSWORD:-abrechnung}@db:5432/${POSTGRES_DB:-abrechnung}
depends_on:
db:
condition: service_healthy
ports:
- "8000:8000"
volumes:
pgdata:

22
entrypoint.sh Normal file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Wartet auf die DB, migriert, sammelt Statics und startet dann das übergebene CMD.
set -euo pipefail
# Warte auf Postgres, falls ein Host konfiguriert ist.
if [[ -n "${DB_HOST:-}" ]]; then
echo "Warte auf Datenbank ${DB_HOST}:${DB_PORT:-5432} ..."
until python -c "import socket,sys,os; s=socket.socket(); s.settimeout(2); \
s.connect((os.environ['DB_HOST'], int(os.environ.get('DB_PORT','5432')))); s.close()" \
2>/dev/null; do
sleep 1
done
echo "Datenbank erreichbar."
fi
python manage.py migrate --noinput
if [[ "${DJANGO_COLLECTSTATIC:-1}" == "1" ]]; then
python manage.py collectstatic --noinput
fi
exec "$@"

23
manage.py Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == "__main__":
main()

32
pyproject.toml Normal file
View File

@@ -0,0 +1,32 @@
[project]
name = "abrechnungssystem"
version = "0.1.0"
description = "Django ZUGFeRD Abrechnungssystem für Therapie- und Sozialdienste"
requires-python = ">=3.13,<3.14"
dependencies = [
"django>=5.2,<5.3",
"django-environ>=0.11",
"psycopg[binary]>=3.2",
"gunicorn>=23.0",
"whitenoise>=6.8",
"weasyprint>=63",
"drafthorse>=2024.1.0",
"factur-x>=3.3",
"django-crispy-forms>=2.3",
"crispy-bootstrap5>=2024.10",
"django-tables2>=2.7",
]
[dependency-groups]
dev = [
"pytest>=8.3",
"pytest-django>=4.9",
]
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings"
python_files = ["test_*.py", "tests.py"]
testpaths = ["abrechnung/tests"]
[tool.uv]
package = false

View File

53
templates/base.html Normal file
View File

@@ -0,0 +1,53 @@
{% load static %}
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Abrechnungssystem{% endblock %}</title>
<style>
:root { --bg:#f6f7f9; --fg:#1c2430; --brand:#2b5c8a; --border:#d9dde3; }
* { box-sizing: border-box; }
body { margin:0; font-family: system-ui, sans-serif; color:var(--fg); background:var(--bg); }
header.app { background:var(--brand); color:#fff; padding:.6rem 1.2rem; display:flex; align-items:center; gap:1.2rem; }
header.app a { color:#fff; text-decoration:none; }
header.app nav { display:flex; gap:1rem; margin-left:auto; font-size:.95rem; }
main { max-width:1100px; margin:1.4rem auto; padding:0 1.2rem; }
.card { background:#fff; border:1px solid var(--border); border-radius:8px; padding:1.2rem 1.4rem; }
.messages { list-style:none; padding:0; margin:0 0 1rem; }
.messages li { padding:.6rem 1rem; border-radius:6px; margin-bottom:.5rem; }
.messages li.success { background:#e5f4ea; color:#1c5b34; }
.messages li.error { background:#fbe6e6; color:#7a1f1f; }
.messages li.warning { background:#fdf3e0; color:#7a5a1f; }
.messages li.info { background:#e7eefb; color:#22406e; }
</style>
{% block extrahead %}{% endblock %}
</head>
<body>
<header class="app">
<a href="{% url 'dashboard' %}"><strong>Abrechnungssystem</strong></a>
<nav>
{% if user.is_authenticated %}
<a href="{% url 'dashboard' %}">Dashboard</a>
<a href="{% url 'admin:index' %}">Stammdaten</a>
<form action="{% url 'logout' %}" method="post" style="display:inline">
{% csrf_token %}
<button type="submit" style="background:none;border:none;color:#fff;cursor:pointer;font:inherit;">Abmelden</button>
</form>
{% else %}
<a href="{% url 'login' %}">Anmelden</a>
{% endif %}
</nav>
</header>
<main>
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li class="{{ message.tags }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% block content %}{% endblock %}
</main>
</body>
</html>

View File

@@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block title %}Anmelden · Abrechnungssystem{% endblock %}
{% block content %}
<div class="card" style="max-width:380px;margin:2rem auto;">
<h1 style="margin-top:0;">Anmelden</h1>
{% if form.errors %}
<p style="color:#7a1f1f;">Benutzername oder Passwort ist falsch.</p>
{% endif %}
<form method="post">
{% csrf_token %}
<p>
<label for="id_username">Benutzername</label><br>
<input type="text" name="username" id="id_username" autofocus required
style="width:100%;padding:.5rem;">
</p>
<p>
<label for="id_password">Passwort</label><br>
<input type="password" name="password" id="id_password" required
style="width:100%;padding:.5rem;">
</p>
<input type="hidden" name="next" value="{{ next }}">
<button type="submit" style="padding:.55rem 1.2rem;background:var(--brand);color:#fff;border:none;border-radius:6px;cursor:pointer;">
Anmelden
</button>
</form>
</div>
{% endblock %}

611
uv.lock generated Normal file
View File

@@ -0,0 +1,611 @@
version = 1
revision = 3
requires-python = "==3.13.*"
[[package]]
name = "abrechnungssystem"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "crispy-bootstrap5" },
{ name = "django" },
{ name = "django-crispy-forms" },
{ name = "django-environ" },
{ name = "django-tables2" },
{ name = "drafthorse" },
{ name = "factur-x" },
{ name = "gunicorn" },
{ name = "psycopg", extra = ["binary"] },
{ name = "weasyprint" },
{ name = "whitenoise" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-django" },
]
[package.metadata]
requires-dist = [
{ name = "crispy-bootstrap5", specifier = ">=2024.10" },
{ name = "django", specifier = ">=5.2,<5.3" },
{ name = "django-crispy-forms", specifier = ">=2.3" },
{ name = "django-environ", specifier = ">=0.11" },
{ name = "django-tables2", specifier = ">=2.7" },
{ name = "drafthorse", specifier = ">=2024.1.0" },
{ name = "factur-x", specifier = ">=3.3" },
{ name = "gunicorn", specifier = ">=23.0" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2" },
{ name = "weasyprint", specifier = ">=63" },
{ name = "whitenoise", specifier = ">=6.8" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=8.3" },
{ name = "pytest-django", specifier = ">=4.9" },
]
[[package]]
name = "asgiref"
version = "3.11.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" },
]
[[package]]
name = "brotli"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" },
{ url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" },
{ url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" },
{ url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" },
{ url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" },
{ url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" },
{ url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" },
{ url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" },
{ url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" },
{ url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" },
]
[[package]]
name = "brotlicffi"
version = "1.2.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8a/b6/017dc5f852ed9b8735af77774509271acbf1de02d238377667145fcee01d/brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c", size = 478156, upload-time = "2026-03-05T19:54:11.547Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9a/9f/b98dcd4af47994cee97aebac866996a006a2e5fc1fd1e2b82a8ad95cf09c/brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4", size = 432608, upload-time = "2026-03-05T19:53:56.736Z" },
{ url = "https://files.pythonhosted.org/packages/b1/7a/ac4ee56595a061e3718a6d1ea7e921f4df156894acffb28ed88a1fd52022/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce", size = 1534257, upload-time = "2026-03-05T19:53:58.667Z" },
{ url = "https://files.pythonhosted.org/packages/99/39/e7410db7f6f56de57744ea52a115084ceb2735f4d44973f349bb92136586/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a", size = 1536838, upload-time = "2026-03-05T19:54:00.705Z" },
{ url = "https://files.pythonhosted.org/packages/a6/75/6e7977d1935fc3fbb201cbd619be8f2c7aea25d40a096967132854b34708/brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187", size = 343337, upload-time = "2026-03-05T19:54:02.446Z" },
{ url = "https://files.pythonhosted.org/packages/d8/ef/e7e485ce5e4ba3843a0a92feb767c7b6098fd6e65ce752918074d175ae71/brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede", size = 379026, upload-time = "2026-03-05T19:54:04.322Z" },
]
[[package]]
name = "certifi"
version = "2026.6.17"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
]
[[package]]
name = "cffi"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" },
{ url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" },
{ url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" },
{ url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" },
{ url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" },
{ url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" },
{ url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" },
{ url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" },
{ url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" },
{ url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" },
{ url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" },
{ url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" },
{ url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.9"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" },
{ url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" },
{ url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" },
{ url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" },
{ url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" },
{ url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" },
{ url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" },
{ url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" },
{ url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" },
{ url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" },
{ url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" },
{ url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" },
{ url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" },
{ url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "crispy-bootstrap5"
version = "2026.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "django-crispy-forms" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/e8/05e1170f6b8fbfe4098392bdca813c2094659853a438919234d4663009b1/crispy_bootstrap5-2026.3.tar.gz", hash = "sha256:e7f5adb36acfbb456444c46e82c436931c796c539e9c620be4fa9dc9c9d6679c", size = 23452, upload-time = "2026-03-01T10:08:00.459Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/2d/93f78072f203aa28d961add6a130b929b47f7aa3fef6905898b4bb9a637d/crispy_bootstrap5-2026.3-py3-none-any.whl", hash = "sha256:e0fff85c0503e9aed610a0ee31368e2191d340657f813669491c288c1c2e2dfa", size = 24770, upload-time = "2026-03-01T10:07:59.058Z" },
]
[[package]]
name = "cssselect2"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tinycss2" },
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" },
]
[[package]]
name = "django"
version = "5.2.16"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a9/26/889449d521ae508b26de715954faecd8bcf3f740affb81b2d146a83b42a5/django-5.2.16.tar.gz", hash = "sha256:59ea02020c3136fce14bef0bbece21a10a4febef5eed1c51c22ae468efa22200", size = 10890894, upload-time = "2026-07-07T13:52:17.005Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/13/1e5e3e4c15dcecb04281b3cb2a46a4670e1cef131068e202f6040df19224/django-5.2.16-py3-none-any.whl", hash = "sha256:04f354bf9d807a86ad1a8392fe3808d362358a8eafc322848e0e43e59b24371d", size = 8311943, upload-time = "2026-07-07T13:52:11.223Z" },
]
[[package]]
name = "django-crispy-forms"
version = "2.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/73/42/c2cfb672493730b963ef377b103e29871c56348a215d0ae8cf362fe8ab1e/django_crispy_forms-2.6.tar.gz", hash = "sha256:4921a1087c6cd4f9fa3c139654c1de1c1c385f8bd6729aaee530bc0121ab4b93", size = 1097838, upload-time = "2026-03-01T09:03:37.138Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/96/e3/4c5915a732d6ab54da8871400852b67529518eedfb6b78ecf10bbccfcabb/django_crispy_forms-2.6-py3-none-any.whl", hash = "sha256:8ee0ae28b6b0ac41ff48a65944480c049fe8d1b0047086874fd7efabf4ec1374", size = 31479, upload-time = "2026-03-01T09:03:36.048Z" },
]
[[package]]
name = "django-environ"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/e1/4c4ddcf6e90f023e89edb1d92f9a7fffce61ad4b691df497f2f5c0be6e26/django_environ-0.14.0.tar.gz", hash = "sha256:b6c48d93b9d2ff8a3ea14099e90c35aa4f101c1b4d5f262dfee0d27b06742ed7", size = 60417, upload-time = "2026-06-18T22:49:55.847Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/fe/67b423fc30f16d10259e901320fbc121746bf168aaaad9043e6ebb0110c1/django_environ-0.14.0-py3-none-any.whl", hash = "sha256:8dbe8a57f0a540ab8abd6f54f230de5e99e3a2c9d797cb9caecb037bca3d47d8", size = 20934, upload-time = "2026-06-18T22:49:54.355Z" },
]
[[package]]
name = "django-tables2"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e2/95/8e28e76c04b91d15cf595607040e2e821fd48b517428c2bae5d62af89b18/django_tables2-3.0.0.tar.gz", hash = "sha256:3c5343f72663b0d6684e8e6cbcb16caae10d7621a51c1cf3c581f57ce605f8ee", size = 130242, upload-time = "2026-04-13T07:42:44.463Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/61/55caa8304cdfdf40717227640d2cdfe8118e8f32b9d5cc1b81b972147185/django_tables2-3.0.0-py3-none-any.whl", hash = "sha256:2a5b5447f10d7a8cfb7a2e8f0b139d969c7eb2e675079a4b8ba0107956345bfa", size = 96405, upload-time = "2026-04-13T07:42:42.651Z" },
]
[[package]]
name = "drafthorse"
version = "2025.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "lxml" },
{ name = "pypdf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/15/73/328aee3552406f2c3d7e24ff2bef944ac210dfdaf7b1fa1b18b1e85e5697/drafthorse-2025.2.0.tar.gz", hash = "sha256:3a86d17cc7d2796dabb130f599dc119737a50ce8847e32226aecb9bea3af00d0", size = 96419, upload-time = "2025-09-15T07:57:55.183Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/0d/96b2524ec93aa394546a820b589a6e52649905d9f6a7094dba7835663ce5/drafthorse-2025.2.0-py3-none-any.whl", hash = "sha256:172bae98d0ce888efdf3cc363de3c5cbaf9e5491ef725abc67364234e7d300b6", size = 107509, upload-time = "2025-09-15T07:57:53.169Z" },
]
[[package]]
name = "factur-x"
version = "6.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "lxml" },
{ name = "pypdf" },
{ name = "python-stdnum" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/f15922486dce627189a1671d5626ca39189aaa9788ce9a79e5a3ed6b2c21/factur_x-6.2.tar.gz", hash = "sha256:e3b8add147be66f05e4dffae887f536499c39728d7602e43d26efc0783b39423", size = 1106360, upload-time = "2026-07-09T22:17:43.145Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/c1/f659da747d280557bcdaf7bd0a1033d051b67fb472a0c716b027c2fdfabc/factur_x-6.2-py3-none-any.whl", hash = "sha256:f84f9ebb2a29fabe526305c080d6b2bc96d089c6ab487345d8f4fb394c9159b9", size = 899799, upload-time = "2026-07-09T22:17:41.226Z" },
]
[[package]]
name = "fonttools"
version = "4.63.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" },
{ url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" },
{ url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" },
{ url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" },
{ url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" },
{ url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" },
{ url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" },
{ url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" },
{ url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" },
]
[package.optional-dependencies]
woff = [
{ name = "brotli", marker = "platform_python_implementation == 'CPython'" },
{ name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" },
{ name = "zopfli" },
]
[[package]]
name = "gunicorn"
version = "26.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" },
]
[[package]]
name = "idna"
version = "3.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "lxml"
version = "6.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" },
{ url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" },
{ url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" },
{ url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" },
{ url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" },
{ url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" },
{ url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" },
{ url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" },
{ url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" },
{ url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" },
{ url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" },
{ url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" },
{ url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" },
{ url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" },
{ url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" },
{ url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" },
{ url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" },
{ url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" },
]
[[package]]
name = "packaging"
version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]]
name = "pillow"
version = "12.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
{ url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
{ url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
{ url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
{ url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
{ url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
{ url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
{ url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
{ url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
{ url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
{ url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "psycopg"
version = "3.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" },
]
[package.optional-dependencies]
binary = [
{ name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
]
[[package]]
name = "psycopg-binary"
version = "3.3.4"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" },
{ url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" },
{ url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" },
{ url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" },
{ url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" },
{ url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" },
{ url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" },
{ url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" },
{ url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" },
{ url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" },
{ url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]]
name = "pydyf"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/36/ee/fb410c5c854b6a081a49077912a9765aeffd8e07cbb0663cfda310b01fb4/pydyf-0.12.1.tar.gz", hash = "sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095", size = 17716, upload-time = "2025-12-02T14:52:14.244Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/11/47efe2f66ba848a107adfd490b508f5c0cedc82127950553dca44d29e6c4/pydyf-0.12.1-py3-none-any.whl", hash = "sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc", size = 8028, upload-time = "2025-12-02T14:52:12.938Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pypdf"
version = "6.14.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/03/72/7dfd5ff1c9c37de97a731701f51af091325f123d9d4270361c9c69e4431f/pypdf-6.14.2.tar.gz", hash = "sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25", size = 6491182, upload-time = "2026-06-23T14:18:30.859Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/e6/136aa8993a2ae7214e0b0ef2edaa0d2e08d1d4e4982635b08a835ff31ec8/pypdf-6.14.2-py3-none-any.whl", hash = "sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946", size = 349514, upload-time = "2026-06-23T14:18:28.867Z" },
]
[[package]]
name = "pyphen"
version = "0.17.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/69/56/e4d7e1bd70d997713649c5ce530b2d15a5fc2245a74ca820fc2d51d89d4d/pyphen-0.17.2.tar.gz", hash = "sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3", size = 2079470, upload-time = "2025-01-20T13:18:36.296Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" },
]
[[package]]
name = "pytest"
version = "9.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
]
[[package]]
name = "pytest-django"
version = "4.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/a5/41d091f697c09609e7ef1d5d61925494e0454ebf51de7de05f0f0a728f1d/pytest_django-4.12.0-py3-none-any.whl", hash = "sha256:3ff300c49f8350ba2953b90297d23bf5f589db69545f56f1ec5f8cff5da83e85", size = 26123, upload-time = "2026-02-14T18:40:47.381Z" },
]
[[package]]
name = "python-stdnum"
version = "2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/7f/96c2b9de6024353177dc6139c33730d5ac25877bc33215515d6b95b84555/python_stdnum-2.2.tar.gz", hash = "sha256:e95fcfa858a703d4a40130cb3eaac133c60d8808a7f3c98efeedac968c2479b9", size = 1311813, upload-time = "2026-01-04T19:36:16.753Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/61/aa32d9c79f83a2fae033cd6496fb2a24aba918d31c73704271dfcfb48375/python_stdnum-2.2-py3-none-any.whl", hash = "sha256:bdf98fd117a0ca152e4047aa8ad254bae63853d4e915ddd4e0effb33ba0e9260", size = 1193213, upload-time = "2026-01-04T19:36:14.812Z" },
]
[[package]]
name = "requests"
version = "2.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]]
name = "sqlparse"
version = "0.5.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
]
[[package]]
name = "tinycss2"
version = "1.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" },
]
[[package]]
name = "tinyhtml5"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/1f/cfe2f6b30557c92b3f31d41707e09cef5c1efbd87392bc6c0430c46b0e4d/tinyhtml5-2.1.0.tar.gz", hash = "sha256:60a50ec3d938a37e491efa01af895853060943dcebb5627de5b10d188b338a67", size = 179242, upload-time = "2026-03-05T17:06:30.704Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/48/01695a036b695f83fea7aef6955d735db0f517b1c8e25ddb399ac0bdbcbf/tinyhtml5-2.1.0-py3-none-any.whl", hash = "sha256:6e11cfff38515834268daf89d5f85bbde0b6dd02e8d9e212d1385c2289b89f0a", size = 39686, upload-time = "2026-03-05T17:06:28.498Z" },
]
[[package]]
name = "tzdata"
version = "2026.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" },
]
[[package]]
name = "urllib3"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]
[[package]]
name = "weasyprint"
version = "69.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
{ name = "cssselect2" },
{ name = "fonttools", extra = ["woff"] },
{ name = "pillow" },
{ name = "pydyf" },
{ name = "pyphen" },
{ name = "tinycss2" },
{ name = "tinyhtml5" },
]
sdist = { url = "https://files.pythonhosted.org/packages/59/53/dcc3885c2f7a47faa45f6b8b801412f5f9e055173a52801ef01c09943c5a/weasyprint-69.0.tar.gz", hash = "sha256:a7a32f39ca16bd82ef11de99c92ea4b5f14951c9033af035e451ce4f4ee0a88c", size = 1549834, upload-time = "2026-06-02T14:42:17.765Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/cb/208525c6bd5033d7b2589b55e07bec23d9c61bb00703cbaf20ef52c3811f/weasyprint-69.0-py3-none-any.whl", hash = "sha256:475951cfd917014de6d4d005caff48c6aa867e7e42b80cd5b16a0484a1609ee6", size = 322872, upload-time = "2026-06-02T14:42:15.871Z" },
]
[[package]]
name = "webencodings"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
]
[[package]]
name = "whitenoise"
version = "6.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" },
]
[[package]]
name = "zopfli"
version = "0.4.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/74/21/3b6af43a663b22b00e738bb0642931a2579e15da6852613d56c6aa535d28/zopfli-0.4.3.tar.gz", hash = "sha256:d3a50f91a13cea9bafe025de8fd87a005eb26de02a4f0c193127ddbf23ac8ebe", size = 179156, upload-time = "2026-06-10T09:10:19.96Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/5f/b7d81b670daf990e15a0f7551da96c3c0700f69ae6d96b0245d6a19f51f3/zopfli-0.4.3-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:88f4fbe429aad72bc206275d81fab11a097e0f951a5848d1f51083c37ea73073", size = 291492, upload-time = "2026-06-10T09:10:06.621Z" },
{ url = "https://files.pythonhosted.org/packages/55/c8/d8d8d731e0b192024567b7198fb77b748821d355f3c8bf0109de27191f43/zopfli-0.4.3-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:769875152d0625c46707bcca57d4b2233fe653482067acd55fbf6ec525cb9bdc", size = 829354, upload-time = "2026-06-10T09:10:07.909Z" },
{ url = "https://files.pythonhosted.org/packages/0e/2b/fbe8ba2ec40f5986b8983a4752f7a32672a80a10ea6e68213324a7055469/zopfli-0.4.3-cp310-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0c9c1d40a8cb1d58762d7e57290ccb753e0828c4d01be8acb59aae5d0ca206", size = 818436, upload-time = "2026-06-10T09:10:09.063Z" },
{ url = "https://files.pythonhosted.org/packages/de/d9/63568c54c8b68b9135f3456c5add83797a5528d596657f0e4f4910173b08/zopfli-0.4.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7fa3c35193475290e3f007bbcdebdbae64ba2f012d75c632da0d727e1da50d5e", size = 1778931, upload-time = "2026-06-10T09:10:10.282Z" },
{ url = "https://files.pythonhosted.org/packages/7a/05/8f3aac10a858e89c2146d3a1f6ce33634c3db757365b4148fef1b85784d2/zopfli-0.4.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:47604eee5c6704bdf0e94d8391fe3b74ddb2abd84128fbcfdc3ee0fc265feaef", size = 1864132, upload-time = "2026-06-10T09:10:11.595Z" },
{ url = "https://files.pythonhosted.org/packages/8d/20/9ca59d14b91f9fbc631793b4b085b309777edadaca496aa518a180817827/zopfli-0.4.3-cp310-abi3-win32.whl", hash = "sha256:628c3e941752880b3491db8d44163d0aedb221944e22a17187ff7fc549b050f6", size = 271715, upload-time = "2026-06-10T09:10:12.7Z" },
{ url = "https://files.pythonhosted.org/packages/9d/3a/4ff4fdead77ef30f5832b38a47eb7a1283e98b3c678576b83f8fdfff53eb/zopfli-0.4.3-cp310-abi3-win_amd64.whl", hash = "sha256:921c2c9907f4364963848da5ad194b46d68865e07fdb975d04fd09bc42d47357", size = 288550, upload-time = "2026-06-10T09:10:13.639Z" },
{ url = "https://files.pythonhosted.org/packages/e6/44/6264f929057236fde72dd6d271f54612b4811ce37288e002f5d5339d696a/zopfli-0.4.3-cp310-abi3-win_arm64.whl", hash = "sha256:7e9703ca6e7ef66c8d05e0826b6f558b680c9db8206f84f05a3ee93430a12e42", size = 451343, upload-time = "2026-06-10T09:10:14.72Z" },
]