Skip to content

Commit 96f09cf

Browse files
Pas: Generating various exports.
TYPE: Feature LINK: OGC-1878
1 parent 4adcb66 commit 96f09cf

31 files changed

+2504
-103
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,15 @@ dependencies:
142142

143143
#### MacOS
144144
```shell
145-
brew install curl libffi libjpeg libpq libxml2 libxslt zlib libev poppler pv libxmlsec1
145+
brew install curl libffi libjpeg libpq libxml2 libxslt zlib libev poppler pv libxmlsec1 weasyprint
146146
```
147147

148148
#### Ubuntu
149149
```shell
150150
sudo apt-get install libcurl4-openssl-dev libffi-dev libjpeg-dev libpq-dev \
151151
libxml2-dev libxslt1-dev zlib1g-dev libev-dev libgnutls28-dev libkrb5-dev \
152-
libpoppler-cpp-dev pv libzbar0 openssl libssl-dev xmlsec1 libxmlsec1-openssl
152+
libpoppler-cpp-dev pv libzbar0 openssl libssl-dev xmlsec1 libxmlsec1-openssl \
153+
weasyprint
153154
```
154155

155156
## Installation

setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ install_requires =
159159
urlextract
160160
vobject
161161
watchdog
162+
weasyprint
162163
webcolors
163164
webob
164165
websockets

src/onegov/pas/calculate_pay.py

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from decimal import Decimal
2+
3+
4+
from typing import TYPE_CHECKING
5+
if TYPE_CHECKING:
6+
from onegov.pas.models.rate_set import RateSet
7+
8+
9+
def calculate_rate(
10+
rate_set: 'RateSet',
11+
attendence_type: str,
12+
duration_minutes: int,
13+
is_president: bool,
14+
commission_type: str | None = None,
15+
) -> Decimal:
16+
"""Calculate the rate for an attendance based on type, duration and role.
17+
"""
18+
19+
if attendence_type == 'plenary':
20+
# Entry of plenary session is always half a day. Duration (minutes)
21+
# therefore ignored (!)
22+
base_rate = (
23+
rate_set.plenary_none_president_halfday
24+
if is_president
25+
else rate_set.plenary_none_member_halfday
26+
)
27+
return Decimal(str(base_rate))
28+
29+
elif attendence_type == 'commission' and commission_type:
30+
if commission_type == 'normal':
31+
# First 2 hours have initial rate, then additional per 30min
32+
if duration_minutes <= 120: # 2 hours
33+
rate = (
34+
rate_set.commission_normal_president_initial
35+
if is_president
36+
else rate_set.commission_normal_member_initial
37+
)
38+
else:
39+
initial = (
40+
rate_set.commission_normal_president_initial
41+
if is_president
42+
else rate_set.commission_normal_member_initial
43+
)
44+
additional_per_30min = (
45+
rate_set.commission_normal_president_additional
46+
if is_president
47+
else rate_set.commission_normal_member_additional
48+
)
49+
# This line calculates how many additional 30-minute periods
50+
# are needed after the initial 2 hours (120 minutes), with
51+
# rounding up.
52+
additional_periods = (
53+
duration_minutes - 120 + 29
54+
) // 30 # round up
55+
rate = initial + (
56+
additional_periods * additional_per_30min
57+
)
58+
59+
elif commission_type == 'intercantonal':
60+
# Per half day rates
61+
rate = (
62+
rate_set.commission_intercantonal_president_halfday
63+
if is_president
64+
else rate_set.commission_intercantonal_member_halfday
65+
)
66+
67+
else: # official
68+
# Has both half day and full day rates
69+
# todo: What is the way to determine if it is full day?
70+
is_full_day = duration_minutes > 240 # more than 4 hours
71+
if is_president:
72+
rate = (
73+
rate_set.commission_official_president_fullday
74+
if is_full_day
75+
else rate_set.commission_official_president_halfday
76+
)
77+
else:
78+
rate = (
79+
rate_set.commission_official_vice_president_fullday
80+
if is_full_day
81+
else rate_set.commission_official_vice_president_halfday
82+
)
83+
84+
return Decimal(str(rate))
85+
86+
elif attendence_type == 'study' and commission_type:
87+
# Study time is per half hour or hour depending on commission type
88+
if commission_type == 'normal':
89+
rate_per_30min = (
90+
rate_set.study_normal_president_halfhour
91+
if is_president
92+
else rate_set.study_normal_member_halfhour
93+
)
94+
periods = (
95+
duration_minutes + 29
96+
) // 30 # round up to next 30min
97+
return Decimal(str(rate_per_30min * periods))
98+
99+
elif commission_type == 'intercantonal':
100+
rate_per_hour = (
101+
rate_set.study_intercantonal_president_hour
102+
if is_president
103+
else rate_set.study_intercantonal_member_hour
104+
)
105+
periods = (
106+
duration_minutes + 59
107+
) // 60 # round up to next hour
108+
return Decimal(str(rate_per_hour * periods))
109+
110+
else: # official
111+
rate_per_30min = (
112+
rate_set.study_official_president_halfhour
113+
if is_president
114+
else rate_set.study_official_member_halfhour
115+
)
116+
periods = (
117+
duration_minutes + 29
118+
) // 30 # round up to next 30min
119+
return Decimal(str(rate_per_30min * periods))
120+
121+
elif attendence_type == 'shortest':
122+
# Shortest meetings are per 30min
123+
rate_per_30min = (
124+
rate_set.shortest_all_president_halfhour
125+
if is_president
126+
else rate_set.shortest_all_member_halfhour
127+
)
128+
periods = (duration_minutes + 29) // 30 # round up to next 30min
129+
return Decimal(str(rate_per_30min * periods))
130+
131+
raise ValueError(
132+
f'Invalid attendance type {attendence_type} or commission '
133+
f'type {commission_type}'
134+
)
+118-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,130 @@
11
from onegov.core.collection import GenericCollection
2-
from onegov.pas.models import Attendence
3-
from sqlalchemy import desc
2+
from onegov.pas.models import (
3+
Attendence,
4+
SettlementRun,
5+
Parliamentarian,
6+
ParliamentarianRole,
7+
)
8+
from sqlalchemy import desc, or_
49

5-
from typing import TYPE_CHECKING
10+
11+
from typing import TYPE_CHECKING, Self
612
if TYPE_CHECKING:
7-
from sqlalchemy.orm import Query
13+
from datetime import date
14+
from sqlalchemy.orm import Query, Session
815

916

1017
class AttendenceCollection(GenericCollection[Attendence]):
18+
def __init__(
19+
self,
20+
session: 'Session',
21+
settlement_run_id: str | None = None,
22+
date_from: 'date | None' = None,
23+
date_to: 'date | None' = None,
24+
type: str | None = None,
25+
parliamentarian_id: str | None = None,
26+
commission_id: str | None = None,
27+
party_id: str | None = None, # New parameter
28+
):
29+
super().__init__(session)
30+
self.settlement_run_id = settlement_run_id
31+
self.date_from = date_from
32+
self.date_to = date_to
33+
self.type = type
34+
self.parliamentarian_id = parliamentarian_id
35+
self.commission_id = commission_id
36+
self.party_id = party_id
1137

1238
@property
1339
def model_class(self) -> type[Attendence]:
1440
return Attendence
1541

1642
def query(self) -> 'Query[Attendence]':
17-
return super().query().order_by(desc(Attendence.date))
43+
query = super().query()
44+
45+
if self.settlement_run_id:
46+
settlement_run = self.session.query(SettlementRun).get(
47+
self.settlement_run_id
48+
)
49+
if settlement_run:
50+
query = query.filter(
51+
Attendence.date >= settlement_run.start,
52+
Attendence.date <= settlement_run.end,
53+
)
54+
55+
if self.date_from:
56+
query = query.filter(Attendence.date >= self.date_from)
57+
if self.date_to:
58+
query = query.filter(Attendence.date <= self.date_to)
59+
if self.type:
60+
query = query.filter(Attendence.type == self.type)
61+
if self.parliamentarian_id:
62+
query = query.filter(
63+
Attendence.parliamentarian_id == self.parliamentarian_id
64+
)
65+
if self.commission_id:
66+
query = query.filter(
67+
Attendence.commission_id == self.commission_id
68+
)
69+
70+
# Check for any overlap in party membership period
71+
if self.party_id:
72+
query = (
73+
query.join(Attendence.parliamentarian)
74+
.join(Parliamentarian.roles)
75+
.filter(
76+
ParliamentarianRole.party_id == self.party_id,
77+
or_(
78+
ParliamentarianRole.start.is_(None),
79+
ParliamentarianRole.start <= Attendence.date
80+
),
81+
or_(
82+
ParliamentarianRole.end.is_(None),
83+
ParliamentarianRole.end >= Attendence.date
84+
)
85+
)
86+
)
87+
88+
return query.order_by(desc(Attendence.date))
89+
90+
def for_filter(
91+
self,
92+
settlement_run_id: str | None = None,
93+
date_from: 'date | None' = None,
94+
date_to: 'date | None' = None,
95+
type: str | None = None,
96+
parliamentarian_id: str | None = None,
97+
commission_id: str | None = None,
98+
party_id: str | None = None, # New parameter
99+
) -> Self:
100+
return self.__class__(
101+
self.session,
102+
settlement_run_id=settlement_run_id,
103+
date_from=date_from,
104+
date_to=date_to,
105+
type=type,
106+
parliamentarian_id=parliamentarian_id,
107+
commission_id=commission_id,
108+
party_id=party_id,
109+
)
110+
111+
def by_party(
112+
self,
113+
party_id: str,
114+
start_date: 'date',
115+
end_date: 'date'
116+
) -> Self:
117+
"""
118+
Filter attendances by party membership during a period.
119+
Returns attendances where the parliamentarian belonged to the party
120+
at any point during the period.
121+
"""
122+
return self.for_filter(
123+
settlement_run_id=self.settlement_run_id,
124+
date_from=start_date,
125+
date_to=end_date,
126+
type=self.type,
127+
parliamentarian_id=self.parliamentarian_id,
128+
commission_id=self.commission_id,
129+
party_id=party_id,
130+
)

src/onegov/pas/custom.py

+19
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
from onegov.pas.collections import AttendenceCollection
66
from onegov.pas.collections import ChangeCollection
77
from onegov.user import Auth
8+
from onegov.pas.models import SettlementRun, RateSet
89

910
from typing import TYPE_CHECKING
1011
if TYPE_CHECKING:
1112
from collections.abc import Iterator
1213
from onegov.town6.request import TownRequest
14+
from sqlalchemy.orm import Session
1315

1416

1517
def get_global_tools(request: 'TownRequest') -> 'Iterator[Link | LinkGroup]':
@@ -62,3 +64,20 @@ def get_global_tools(request: 'TownRequest') -> 'Iterator[Link | LinkGroup]':
6264

6365
def get_top_navigation(request: 'TownRequest') -> 'list[Link]':
6466
return []
67+
68+
69+
def get_current_settlement_run(session: 'Session') -> SettlementRun:
70+
query = session.query(SettlementRun)
71+
query = query.filter(SettlementRun.active == True)
72+
return query.one()
73+
74+
75+
def get_current_rate_set(session: 'Session', run: SettlementRun) -> RateSet:
76+
rat_set = (
77+
session.query(RateSet).filter(RateSet.year == run.start.year).first()
78+
)
79+
# We get the first one we find by year. This works because we are only
80+
# allowing to create one rate set per year
81+
if rat_set is None:
82+
raise ValueError('No rate set found for the current year')
83+
return rat_set

0 commit comments

Comments
 (0)