from __future__ import annotations
from onegov.pas.calculate_pay import calculate_rate
from dataclasses import dataclass
from typing import cast
from onegov.pas.collections import (
AttendenceCollection,
)
from onegov.pas.custom import get_current_rate_set
from decimal import Decimal
from weasyprint import HTML, CSS # type: ignore[import-untyped]
from weasyprint.text.fonts import ( # type: ignore[import-untyped]
FontConfiguration,
)
from onegov.pas.models.attendence import TYPES, Attendence
from datetime import date # noqa: TC003
from onegov.pas.utils import format_swiss_number
from typing import TYPE_CHECKING, Literal, TypedDict
if TYPE_CHECKING:
from onegov.pas.models import Parliamentarian
from onegov.pas.models.settlement_run import SettlementRun
@dataclass
[docs]
class ParliamentarianEntry:
[docs]
type_description: str
[docs]
calculated_value: Decimal
[docs]
additional_value: Decimal
[docs]
base_rate: Decimal
[docs]
attendance_type: AttendenceType
[docs]
AttendenceType = Literal['plenary', 'commission', 'study', 'shortest']
[docs]
class TypeTotal(TypedDict):
[docs]
entries: list[ParliamentarianEntry]
[docs]
TotalType = Literal['plenary', 'commission', 'study', 'shortest', 'expenses']
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from onegov.town6.request import TownRequest
[docs]
def generate_parliamentarian_settlement_pdf(
settlement_run: SettlementRun,
request: TownRequest,
parliamentarian: Parliamentarian,
) -> bytes:
"""Generate PDF for parliamentarian settlement data."""
font_config = FontConfiguration()
session = request.session
year = settlement_run.end.year
quarter = settlement_run.get_run_number_for_year(session, year)
css = CSS(
string="""
@page {
size: A4;
margin: 2.5cm 0.75cm 2cm 0.75cm; /* top right bottom left */
@top-right {
content: "Staatskanzlei";
font-family: Helvetica, Arial, sans-serif;
font-size: 8pt;
}
}
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 7pt;
line-height: 1.0;
}
.first-line {
font-size: 7pt;
text-decoration: underline;
margin-left: 1.0cm;
margin-bottom: 0.2cm;
}
.address {
margin-left: 1.0cm;
margin-bottom: 0.5cm;
font-size: 8pt;
line-height: 1.4;
}
.date {
margin-left: 1.0cm;
margin-bottom: 2cm;
font-size: 8pt;
}
table {
border-collapse: collapse;
width: 100%;
table-layout: auto;
white-space: nowrap;
}
.col-types {
background-color: #d5d7d9;
}
.header-row td {
background-color: #d5d7d9;
font-weight: bold;
}
*/ Makes the text sit on the line of the cell.
Workaround for `vertical-align` not working. */
table td th {
padding-top: 7pt;
padding-bottom: 1pt;
}
th, td {
padding: 2pt;
border: 1pt solid #000;
}
/* Column widths for main table */
.first-table td:first-child { width: 20%; }
.first-table td:nth-child(2) { width: 50%; } /* Type */
.first-table td:nth-child(3) { width: 15%; } /* Value column */
.first-table td:last-child { width: 15%; } /* CHF column */
.parliamentarian-summary-table td:first-child { width: 70%; }
.parliamentarian-summary-table td:nth-child(2) { width: 15%; }
.parliamentarian-summary-table td:last-child { width: 15%; }
.numeric { text-align: right; }
.first-table tr:nth-child(2) td {
background-color: #d5d7d9;
}
.first-table tr:nth-child(even):not(.total-row) td {
background-color: #f3f3f3;
}
.parliamentarian-summary-table {
page-break-inside: avoid;
margin-top: 1cm;
}
.parliamentarian-summary-table td {
font-weight: bold;
background-color: #d5d7d9;
}
"""
)
data = get_parliamentarian_settlement_data(
settlement_run, request, parliamentarian
)
html = f"""
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<div class="first-line">
<p>Staatskanzlei, Seestrasse 2, 6300 Zug</p><br>
</div>
<div class="address">
{parliamentarian.formal_greeting}<br>
{parliamentarian.shipping_address}<br>
{parliamentarian.shipping_address_zip_code}
{parliamentarian.shipping_address_city}
</div>
<div class="date">
Zug {settlement_run.end.strftime('%d.%m.%Y')}
</div>
<h2 class="title">
Abrechnung {quarter}. Quartal {year}
</h2>
<table class="first-table">
<thead>
<tr class="col-types">
<th class="data-column-date">Datum</th>
<th class="data-column-type">Typ</th>
<th class="data-column-value">Wert</th>
<th class="data-column-chf">CHF ohne Tz</th>
</tr>
</thead>
<tbody>
"""
type_totals: dict[TotalType, TypeTotal] = {
cast('TotalType', 'plenary'): {'entries': [], 'total': Decimal('0')},
cast('TotalType', 'commission'): {'entries': [],
'total': Decimal('0')},
cast('TotalType', 'study'): {'entries': [], 'total': Decimal('0')},
cast('TotalType', 'shortest'): {'entries': [], 'total': Decimal('0')},
cast('TotalType', 'expenses'): {'entries': [], 'total': Decimal('0')},
}
for entry in data['entries']:
html += f"""
<tr>
<td>{entry.date.strftime('%d.%m.%Y')}</td>
<td>{entry.type_description}</td>
<td class="numeric">{format_swiss_number(
entry.calculated_value)}</td>
<td class="numeric">{format_swiss_number(entry.base_rate)}</td>
</tr>
"""
if entry.type_description not in ['Total', 'Auszahlung']:
type_totals[entry.attendance_type]['entries'].append(entry)
type_totals[entry.attendance_type]['total'] += entry.base_rate
html += """
</tbody>
</table>
<table class="parliamentarian-summary-table">
<tbody>
"""
total = Decimal('0')
type_mappings = [
('Total aller Plenarsitzungen inkl. Teuerungszulage',
cast('TotalType', 'plenary')),
('Total aller Kommissionssitzungen inkl. Teuerungszulage',
cast('TotalType', 'commission')),
('Total aller Aktenstudium inkl. Teuerungszulage',
cast('TotalType', 'study')),
('Total aller Kürzestsitzungen inkl. Teuerungszulage',
cast('TotalType', 'shortest')),
('Total Spesen inkl. Teuerungszulage',
cast('TotalType', 'expenses')),
]
for type_name, type_key in type_mappings:
total_value = sum(
entry.calculated_value
for entry in type_totals[type_key]['entries']
)
total_value_str = (
format_swiss_number(total_value) if type_key != 'expenses' else '-'
)
total_chf = type_totals[type_key]['total']
total += total_chf
html += f"""
<tr>
<td>{type_name}</td>
<td class="numeric">{total_value_str}</td>
<td class="numeric">{format_swiss_number(total_chf)}</td>
</tr>
"""
html += f"""
<tr class="merge-cells">
<td>Auszahlung</td>
<td colspan="2" class="numeric">{format_swiss_number(total)}
</td>
</tr>
</tbody>
</table>
</body>
</html>
"""
return HTML(string=html).write_pdf(
stylesheets=[css], font_config=font_config
)
[docs]
def get_parliamentarian_settlement_data(
settlement_run: SettlementRun,
request: TownRequest,
parliamentarian: Parliamentarian,
) -> dict[str, list[ParliamentarianEntry]]:
"""Get settlement data for a specific parliamentarian."""
session = request.session
rate_set = get_current_rate_set(session, settlement_run)
attendences = (
AttendenceCollection(
session,
date_from=settlement_run.start,
date_to=settlement_run.end,
)
.query()
.filter(Attendence.parliamentarian_id == parliamentarian.id)
)
result = []
for attendence in attendences:
is_president = any(
r.role == 'president' for r in parliamentarian.roles
)
base_rate = calculate_rate(
rate_set=rate_set,
attendence_type=attendence.type,
duration_minutes=int(attendence.duration),
is_president=is_president,
commission_type=(
attendence.commission.type
if attendence.commission
else None
),
)
# Build type description
type_desc = request.translate(TYPES[attendence.type])
if attendence.commission:
type_desc = f'{type_desc} - {attendence.commission.name}'
entry = ParliamentarianEntry(
date=attendence.date,
type_description=type_desc,
calculated_value=attendence.calculate_value(),
additional_value=Decimal('0'),
base_rate=Decimal(str(base_rate)),
attendance_type=attendence.type,
)
result.append(entry)
# Sort by date
result.sort(key=lambda x: x.date)
return {'entries': result}