feat: agent-11 PR1 – Vertretung, Storno-Re-Genehmigung, Kommentare
Abwesenheits-Modul abgerundet (Feature-Parität mit Urlaubsverwaltung):
- Vertretung: Overlap-Warnung beim Anlegen, E-Mail an Vertretung bei
Genehmigung, GET /absences/?as_substitute=true, neuer schlanker
GET /users/colleagues (alle Rollen, RLS-gefenced) für die Auswahl;
Vertreter-Dropdown + Anzeige in der Liste.
- Stornierung mit Re-Genehmigung: neuer Status CANCELLATION_REQUESTED,
POST /absences/{id}/request-cancellation; Manager genehmigt/lehnt über
bestehende approve/reject ab (Urlaub + FZA-Rückbuchung via _apply_cancellation).
- Kommentare: Model AbsenceComment (company_id-RLS), GET/POST comments,
System-Kommentare bei Statuswechsel, AbsenceCommentsModal.
- Fix: CalDAV fire-and-forget nutzte die Request-Session weiter (in Tests
geteilt -> "another operation in progress"); jetzt sync_*_bg mit eigener
Session + RLS-Bypass.
Migration 0035. 178/178 Tests grün. Deployed auf 137 + 164.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ from app.models.work_schedule import WorkSchedule
|
||||
from app.models.time_entry import TimeEntry, EntryStatus, EntrySource
|
||||
from app.models.absence_type import AbsenceType
|
||||
from app.models.absence import Absence, AbsenceStatus
|
||||
from app.models.absence_comment import AbsenceComment
|
||||
from app.models.vacation_balance import VacationBalance
|
||||
from app.models.overtime_balance import OvertimeBalance
|
||||
from app.models.public_holiday import PublicHoliday
|
||||
@@ -32,6 +33,7 @@ __all__ = [
|
||||
"AbsenceType",
|
||||
"Absence",
|
||||
"AbsenceStatus",
|
||||
"AbsenceComment",
|
||||
"VacationBalance",
|
||||
"OvertimeBalance",
|
||||
"PublicHoliday",
|
||||
|
||||
@@ -16,10 +16,13 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class AbsenceStatus(str, enum.Enum):
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
CANCELLED = "cancelled"
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
CANCELLED = "cancelled"
|
||||
# Mitarbeiter hat für einen bereits genehmigten Antrag eine Stornierung
|
||||
# beantragt – Manager muss zustimmen (→ cancelled) oder ablehnen (→ approved).
|
||||
CANCELLATION_REQUESTED = "cancellation_requested"
|
||||
|
||||
|
||||
class Absence(Base):
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Text, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class AbsenceComment(Base):
|
||||
"""Kommentar-Thread an einem Abwesenheitsantrag.
|
||||
|
||||
`company_id` wird redundant gespeichert, damit die Tabelle über die normale
|
||||
company_id-RLS-Policy gefenced werden kann (kein Join nötig). System-Kommentare
|
||||
(`is_system=True`) werden bei Statuswechseln automatisch erzeugt.
|
||||
"""
|
||||
__tablename__ = "absence_comments"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
absence_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("absences.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
company_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
author_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL")
|
||||
)
|
||||
body: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
is_system: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
author: Mapped["User | None"] = relationship(
|
||||
"User", primaryjoin="AbsenceComment.author_id == User.id",
|
||||
foreign_keys="[AbsenceComment.author_id]", lazy="noload",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<AbsenceComment {self.absence_id} by {self.author_id}>"
|
||||
Reference in New Issue
Block a user