Source code for pas.models.attendence

from __future__ import annotations

import datetime
from decimal import ROUND_HALF_UP, Decimal
from onegov.core.orm import Base
from onegov.core.orm.mixins import TimestampMixin
from onegov.pas import _
from sqlalchemy import Enum
from sqlalchemy import ForeignKey
from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Mapped
from uuid import uuid4
from uuid import UUID


from typing import Literal
from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from onegov.pas.models import PASCommission
    from onegov.pas.models import PASParliamentarian
    from typing import TypeAlias

[docs] AttendenceType: TypeAlias = Literal[ 'plenary', 'commission', 'study', 'shortest', ]
[docs] TYPES: dict[AttendenceType, str] = { 'plenary': _('Plenary session'), 'commission': _('Commission meeting'), 'study': _('File study'), 'shortest': _('Shortest meeting'), }
[docs] class Attendence(Base, TimestampMixin):
[docs] __tablename__ = 'par_attendence'
#: The polymorphic type of attendence
[docs] poly_type: Mapped[str] = mapped_column(default=lambda: 'generic')
[docs] __mapper_args__ = { 'polymorphic_on': poly_type, 'polymorphic_identity': 'pas_attendence', }
#: Internal ID
[docs] id: Mapped[UUID] = mapped_column( primary_key=True, default=uuid4 )
#: The date
[docs] date: Mapped[datetime.date]
#: The duration in minutes
[docs] duration: Mapped[int]
#: The type
[docs] type: Mapped[AttendenceType] = mapped_column( Enum( *TYPES.keys(), name='par_attendence_type' ), default='plenary' )
#: Tracks grouped attendance records to enable future batch #: modifications. Only relevant if added in bulk.
[docs] bulk_edit_id: Mapped[UUID | None]
#: Whether this attendance submission is closed/completed #: This is only relevant for commission attendance, not plenary sessions. #: Parliamentarians use this to signal they have recorded all their #: commission activities for a settlement run.
[docs] abschluss: Mapped[bool] = mapped_column(default=False)
#: The type as translated text @property
[docs] def type_label(self) -> str: return TYPES.get(self.type, '')
#: The id of the parliamentarian
[docs] parliamentarian_id: Mapped[UUID] = mapped_column( ForeignKey('par_parliamentarians.id'), )
#: The parliamentarian
[docs] parliamentarian: Mapped[PASParliamentarian] = relationship( back_populates='attendences' )
#: the id of the commission
[docs] commission_id: Mapped[UUID | None] = mapped_column( ForeignKey('par_commissions.id'), )
#: the related commission (which may have any number of memberships)
[docs] commission: Mapped[PASCommission | None] = relationship( back_populates='attendences' )
[docs] def calculate_value(self) -> Decimal: """Calculate the value (in hours) for an attendance record. The calculation follows these business rules: - Plenary sessions: * Returns actual hours from duration field for display * CHF calculation is independent and always uses half-day rate * This allows storing actual hours while paying fixed rate - Everything else is counted as actual hours: * First 2 hours are counted as given * After 2 hours, time is rounded to nearest 30-minute increment, * and there is another rate applied for the additional time * Example: 2h 40min would be calculated as 2.5 hours Examples: >>> # Plenary session >>> attendence.type = 'plenary' >>> attendence.duration = 180 # 3 hours >>> calculate_value(attendence) '3.0' >>> # Commission meeting, 2 hours >>> attendence.type = 'commission' >>> attendence.duration = 120 # minutes >>> calculate_value(attendence) '2.0' >>> # Study session, 2h 40min >>> attendence.type = 'study' >>> attendence.duration = 160 # minutes >>> calculate_value(attendence) '2.5' """ if self.duration < 0: raise ValueError('Duration cannot be negative') if self.type == 'plenary': duration_hours = Decimal(str(self.duration)) / Decimal('60') return duration_hours.quantize( Decimal('0.1'), rounding=ROUND_HALF_UP ) if self.type in ('commission', 'study', 'shortest'): # Convert minutes to hours with Decimal for precise calculation duration_hours = Decimal(str(self.duration)) / Decimal('60') if duration_hours <= Decimal('2'): # Round to 1 decimal place return duration_hours.quantize( Decimal('0.1'), rounding=ROUND_HALF_UP ) else: base_hours = Decimal('2') additional_hours = (duration_hours - base_hours) # Round additional time to nearest 0.5 additional_hours = (additional_hours * 2).quantize( Decimal('1.0'), rounding=ROUND_HALF_UP ) / 2 total_hours = base_hours + additional_hours return total_hours.quantize( Decimal('0.1'), rounding=ROUND_HALF_UP ) raise ValueError(f'Unknown attendance type: {self.type}')
[docs] def __repr__(self) -> str: return f'<Attendence {self.date} {self.type}>'