Add sync to grist functionality
This commit is contained in:
56
src/meal_manager/grist.py
Normal file
56
src/meal_manager/grist.py
Normal file
@@ -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)
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -20,7 +20,15 @@
|
||||
<p class="text-muted">{{ event.event_time.strftime('%A, %d.%m.%Y') }}</p>
|
||||
<p>{{ event.description }}</p>
|
||||
<hr class="hr"/>
|
||||
{% if message %}
|
||||
<div class="alert alert-info alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="container">
|
||||
|
||||
<p class="h3">Anmeldungen</p>
|
||||
{% if not user %}
|
||||
<div class="alert alert-warning m-2">
|
||||
@@ -62,9 +70,20 @@
|
||||
</div>
|
||||
</div>
|
||||
{% if user and user.admin %}
|
||||
<button type="button" class="btn btn-danger mb-2 w-100" data-bs-toggle="modal" data-bs-target="#deleteEvent">
|
||||
Event Löschen
|
||||
</button>
|
||||
<div class="row w-100">
|
||||
<div class="col-6 p-1">
|
||||
<button type="button" class="btn btn-danger w-100" data-bs-toggle="modal" data-bs-target="#deleteEvent">
|
||||
Event Löschen
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6 p-1">
|
||||
<a href="/event/{{event.id}}/sync_with_grist" class="btn btn-secondary w-100">
|
||||
<i class="bi bi-cash-coin m-2"></i> Abrechnen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
|
||||
Reference in New Issue
Block a user