diff --git a/pyproject.toml b/pyproject.toml index 4ab8653..74d382d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ requires-python = "~=3.13.0" dependencies = [ "alembic>=1.17.0", "fastapi[standard]>=0.116.0", + "pygrister>=0.8.0", "python-dotenv>=1.1.1", "reportlab>=4.4.4", "sqlalchemy>=2.0.44", diff --git a/src/meal_manager/grist.py b/src/meal_manager/grist.py new file mode 100644 index 0000000..c90885f --- /dev/null +++ b/src/meal_manager/grist.py @@ -0,0 +1,56 @@ +import datetime +import os + +from dotenv import load_dotenv + +load_dotenv() +from pygrister.api import GristApi + +config = { + "GRIST_API_SERVER": "allmende-gufi.de", + "GRIST_TEAM_SITE": "grist", + "GRIST_API_KEY": os.getenv("GRIST_API_KEY"), + "GRIST_DOC_ID": "xmEcaq5pvxUB3mBfEY8BLe", +} + + +def sync_with_grist(event): + grist = GristApi(config=config) + + status_code, grist_response = grist.list_records( + "Transactions", filter={"meal_manager_event_id": [event.id]} + ) + + existing_records = set() + for entry in grist_response: + existing_records.add(entry["Partei_Konto"]) + + new_records = [] + + today = datetime.date.today().isoformat() + for registration in event.registrations: + if registration.household.name in existing_records: + continue + if registration.num_adult_meals > 0: + new_records.append( + { + "Datum": event.event_time.date().isoformat(), + "Typ": "Essen", + "Partei_Konto": registration.household.name, + "Betrag": -3.5 * registration.num_adult_meals, + "Date_Added": today, + "meal_manager_event_id": event.id, + } + ) + if registration.num_children_meals > 0: + new_records.append( + { + "Datum": event.event_time.date().isoformat(), + "Typ": "Essen Kind", + "Partei_Konto": registration.household.name, + "Betrag": -2 * registration.num_children_meals, + "Date_Added": today, + "meal_manager_event_id": event.id, + } + ) + grist.add_records("Transactions", new_records) diff --git a/src/meal_manager/main.py b/src/meal_manager/main.py index 9604165..008b188 100644 --- a/src/meal_manager/main.py +++ b/src/meal_manager/main.py @@ -13,6 +13,7 @@ from fastapi.templating import Jinja2Templates from sqlalchemy import create_engine, select from sqlalchemy.orm import Session +from meal_manager.grist import sync_with_grist from meal_manager.models import ( Base, Event, @@ -295,7 +296,11 @@ async def delete_event( @app.get("/event/{event_id}") async def read_event( - request: Request, event_id: int, session: SessionDep, user: UserDep + request: Request, + event_id: int, + session: SessionDep, + user: UserDep, + message: str | None = None, ): statement = select(Event).where(Event.id == event_id) event = session.scalars(statement).one() @@ -318,6 +323,7 @@ async def read_event( "households": households, "now": datetime.now(), "user": user, + "message": message, }, ) @@ -423,3 +429,17 @@ def get_event_attendance_pdf(event_id: int, session: SessionDep): return Response( content=pdf_buffer.getvalue(), media_type="application/pdf", headers=headers ) + + +@app.get("/event/{event_id}/sync_with_grist") +def sync_with_grist_route(event_id: int, session: SessionDep, user: StrictUserDep): + + statement = select(Event).where(Event.id == event_id) + event = session.scalars(statement).one() + + sync_with_grist(event) + + return RedirectResponse( + url=f"/event/{event_id}?message=Erfolgreich%20an%20Abrechnung%20%C3%BCbertragen", + status_code=status.HTTP_302_FOUND, + ) diff --git a/src/meal_manager/pdf.py b/src/meal_manager/pdf.py index 86d6908..5e09c88 100644 --- a/src/meal_manager/pdf.py +++ b/src/meal_manager/pdf.py @@ -12,7 +12,14 @@ def build_dinner_overview_pdf(event: Event) -> BytesIO: """Build a PDF with an overview of the event's attendance.""" # Create an in-memory PDF buffer = BytesIO() - doc = SimpleDocTemplate(buffer, pagesize=portrait(A4), topMargin=30, bottomMargin=30, leftMargin=40, rightMargin=40) + doc = SimpleDocTemplate( + buffer, + pagesize=portrait(A4), + topMargin=30, + bottomMargin=30, + leftMargin=40, + rightMargin=40, + ) styles = getSampleStyleSheet() elements = [] @@ -20,8 +27,12 @@ def build_dinner_overview_pdf(event: Event) -> BytesIO: title_style = styles["Title"] title_style.fontSize = 16 title_style.spaceAfter = 20 - elements.append(Paragraph(f"Anwesenheitsliste – {event.title} ({event.event_time.date().strftime('%d.%m.%y')})", - title_style)) + elements.append( + Paragraph( + f"Anwesenheitsliste – {event.title} ({event.event_time.date().strftime('%d.%m.%y')})", + title_style, + ) + ) elements.append(Spacer(1, 20)) # Team overview section @@ -32,23 +43,27 @@ def build_dinner_overview_pdf(event: Event) -> BytesIO: team_types = { "Kochen": [r for r in event.team if r.work_type == "cooking"], "Abwaschen": [r for r in event.team if r.work_type == "dishes"], - "Tische decken": [r for r in event.team if r.work_type == "tables"] + "Tische decken": [r for r in event.team if r.work_type == "tables"], } for team_name, registrations in team_types.items(): members = ", ".join(r.person_name for r in registrations) team_data.append([team_name, members]) - team_table = Table(team_data, repeatRows=1, colWidths=[100, '*']) - team_table.setStyle(TableStyle([ - ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor('#E8E8E8')), - ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor('#A0A0A0')), - ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), - ("FONTSIZE", (0, 0), (-1, -1), 10), - ("BOTTOMPADDING", (0, 0), (-1, -1), 8), - ("TOPPADDING", (0, 0), (-1, -1), 8), - ])) + team_table = Table(team_data, repeatRows=1, colWidths=[100, "*"]) + team_table.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#E8E8E8")), + ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#A0A0A0")), + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), + ("FONTSIZE", (0, 0), (-1, -1), 10), + ("BOTTOMPADDING", (0, 0), (-1, -1), 8), + ("TOPPADDING", (0, 0), (-1, -1), 8), + ] + ) + ) elements.append(team_table) elements.append(Spacer(1, 25)) @@ -63,37 +78,49 @@ def build_dinner_overview_pdf(event: Event) -> BytesIO: # Attendance section elements.append(Paragraph("Teilnehmende", styles["Heading2"])) - elements.append(Paragraph(f"Gesamt: {sum_adults} Erwachsene, {sum_children} Kinder, {sum_small_children} Kleinkinder")) + elements.append( + Paragraph( + f"Gesamt: {sum_adults} Erwachsene, {sum_children} Kinder, {sum_small_children} Kleinkinder" + ) + ) elements.append(Spacer(1, 12)) # Table header - data = [["Haushalt", "Erwachsene", "Kinder >7", "Kinder <7", "Kommentar", "Anwesend?"]] + data = [ + ["Haushalt", "Erwachsene", "Kinder >7", "Kinder <7", "Kommentar", "Anwesend?"] + ] # Table rows for r in event.registrations: - data.append([ - r.household.name, - r.num_adult_meals, - r.num_children_meals, - r.num_small_children_meals, - Paragraph(r.comment or ""), - "" - ]) + data.append( + [ + r.household.name, + r.num_adult_meals, + r.num_children_meals, + r.num_small_children_meals, + Paragraph(r.comment or ""), + "", + ] + ) for _ in range(5): data.append([""] * 6) # Create table - table = Table(data, repeatRows=1, colWidths=[120, 70, 60, 60, '*', 65]) - table.setStyle(TableStyle([ - ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor('#E8E8E8')), - ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor('#A0A0A0')), - ("ALIGN", (1, 1), (-2, -1), "CENTER"), - ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), - ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), - ("FONTSIZE", (0, 0), (-1, -1), 10), - ("BOTTOMPADDING", (0, 0), (-1, -1), 3), - ("TOPPADDING", (0, 0), (-1, -1), 3), - ])) + table = Table(data, repeatRows=1, colWidths=[120, 70, 60, 60, "*", 65]) + table.setStyle( + TableStyle( + [ + ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#E8E8E8")), + ("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#A0A0A0")), + ("ALIGN", (1, 1), (-2, -1), "CENTER"), + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), + ("FONTSIZE", (0, 0), (-1, -1), 10), + ("BOTTOMPADDING", (0, 0), (-1, -1), 3), + ("TOPPADDING", (0, 0), (-1, -1), 3), + ] + ) + ) elements.append(table) doc.build(elements) diff --git a/src/meal_manager/templates/event.html b/src/meal_manager/templates/event.html index 23a243d..f0b4630 100644 --- a/src/meal_manager/templates/event.html +++ b/src/meal_manager/templates/event.html @@ -20,7 +20,15 @@

{{ event.event_time.strftime('%A, %d.%m.%Y') }}

{{ event.description }}


+{% if message %} + +{% endif %}
+

Anmeldungen

{% if not user %}
@@ -62,9 +70,20 @@
{% if user and user.admin %} - +
+
+ +
+
+ + Abrechnen + +
+
+ + {% endif %}
diff --git a/uv.lock b/uv.lock index ece29e5..c2d7f59 100644 --- a/uv.lock +++ b/uv.lock @@ -364,6 +364,7 @@ source = { editable = "." } dependencies = [ { name = "alembic" }, { name = "fastapi", extra = ["standard"] }, + { name = "pygrister" }, { name = "python-dotenv" }, { name = "reportlab" }, { name = "sqlalchemy" }, @@ -380,6 +381,7 @@ dev = [ requires-dist = [ { name = "alembic", specifier = ">=1.17.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.116.0" }, + { name = "pygrister", specifier = ">=0.8.0" }, { name = "python-dotenv", specifier = ">=1.1.1" }, { name = "reportlab", specifier = ">=4.4.4" }, { name = "sqlalchemy", specifier = ">=2.0.44" }, @@ -518,6 +520,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pygrister" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/a2/804b3e63bce91fb0c7f093b6f3e522e476d9f3074cd3384d713f73fff78b/pygrister-0.8.0.tar.gz", hash = "sha256:4faaad23b27c9ae46dc7b321a0de376f3bfbdaa5faa7ffd769566105667cd478", size = 41472, upload-time = "2025-08-10T10:53:47.561Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/47/e43f2d8b88477a9b27b39d97e3c61534ae3cda4c99771f8b3c81d2469486/pygrister-0.8.0-py3-none-any.whl", hash = "sha256:b882a93db0aae642435d23f8e6f6a50f737befa35e3cce72f234bedd0ef4bee6", size = 31863, upload-time = "2025-08-10T10:53:46.345Z" }, +] + [[package]] name = "python-dotenv" version = "1.1.1" @@ -566,6 +581,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/66/e040586fe6f9ae7f3a6986186653791fb865947f0b745290ee4ab026b834/reportlab-4.4.4-py3-none-any.whl", hash = "sha256:299b3b0534e7202bb94ed2ddcd7179b818dcda7de9d8518a57c85a58a1ebaadb", size = 1954981, upload-time = "2025-09-19T10:43:33.589Z" }, ] +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + [[package]] name = "rich" version = "14.1.0"