update
This commit is contained in:
1
AzA march 2026/workforce_planner/core/__init__.py
Normal file
1
AzA march 2026/workforce_planner/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
36
AzA march 2026/workforce_planner/core/enums.py
Normal file
36
AzA march 2026/workforce_planner/core/enums.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Zentrale Enums für das workforce_planner Paket."""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class AbsenceStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class AbsenceCategory(str, Enum):
|
||||
URLAUB = "urlaub"
|
||||
UNBEZAHLT = "unbezahlt"
|
||||
KRANK = "krank"
|
||||
BUERO = "buero"
|
||||
WEITERBILDUNG = "weiterbildung"
|
||||
SONSTIGES = "sonstiges"
|
||||
|
||||
|
||||
ABSENCE_META = {
|
||||
AbsenceCategory.URLAUB: {"label": "Urlaub", "color": "#7EC8E3", "deducts_balance": True, "requires_approval": True},
|
||||
AbsenceCategory.UNBEZAHLT: {"label": "Unbezahlter Urlaub","color": "#E8C87E", "deducts_balance": False, "requires_approval": True},
|
||||
AbsenceCategory.KRANK: {"label": "Krank", "color": "#F5A3A3", "deducts_balance": False, "requires_approval": False},
|
||||
AbsenceCategory.BUERO: {"label": "Bürozeit", "color": "#A8D5BA", "deducts_balance": False, "requires_approval": False},
|
||||
AbsenceCategory.WEITERBILDUNG: {"label": "Weiterbildung", "color": "#B8C8F0", "deducts_balance": False, "requires_approval": True},
|
||||
AbsenceCategory.SONSTIGES: {"label": "Sonstiges", "color": "#D4C5F9", "deducts_balance": False, "requires_approval": False},
|
||||
}
|
||||
|
||||
|
||||
class EmployeeRole(str, Enum):
|
||||
ADMIN = "admin"
|
||||
MANAGER = "manager"
|
||||
EMPLOYEE = "employee"
|
||||
154
AzA march 2026/workforce_planner/core/models.py
Normal file
154
AzA march 2026/workforce_planner/core/models.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""SQLAlchemy-Datenbankmodelle für workforce_planner."""
|
||||
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import (
|
||||
Column, String, Integer, Float, Date, DateTime, Boolean,
|
||||
ForeignKey, Text, Enum as SAEnum, UniqueConstraint, CheckConstraint,
|
||||
Index, JSON,
|
||||
)
|
||||
from sqlalchemy.orm import relationship, DeclarativeBase
|
||||
|
||||
from .enums import AbsenceCategory, AbsenceStatus, EmployeeRole
|
||||
|
||||
|
||||
def _new_id() -> str:
|
||||
return uuid.uuid4().hex[:12]
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# MULTI-TENANT: Praxis / Organisation
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class Practice(Base):
|
||||
"""Eine Praxis / Organisation – Mandant im Multi-Tenant-Modell."""
|
||||
__tablename__ = "practices"
|
||||
|
||||
id = Column(String(32), primary_key=True, default=_new_id)
|
||||
name = Column(String(200), nullable=False)
|
||||
address = Column(Text, default="")
|
||||
phone = Column(String(40), default="")
|
||||
email = Column(String(200), nullable=True)
|
||||
settings = Column(JSON, default=dict)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
employees = relationship("Employee", back_populates="practice")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Practice {self.name!r}>"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# EMPLOYEE
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class Employee(Base):
|
||||
__tablename__ = "employees"
|
||||
|
||||
id = Column(String(32), primary_key=True, default=_new_id)
|
||||
practice_id = Column(String(32), ForeignKey("practices.id"), nullable=True)
|
||||
name = Column(String(120), nullable=False)
|
||||
email = Column(String(200), unique=True, nullable=True)
|
||||
password_hash = Column(String(256), nullable=True)
|
||||
department = Column(String(80), nullable=True)
|
||||
role = Column(SAEnum(EmployeeRole), default=EmployeeRole.EMPLOYEE, nullable=False)
|
||||
workload_percent = Column(Integer, default=100)
|
||||
vacation_days_per_year = Column(Integer, default=25)
|
||||
is_active = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
last_login = Column(DateTime, nullable=True)
|
||||
|
||||
practice = relationship("Practice", back_populates="employees")
|
||||
absences = relationship("Absence", back_populates="employee", cascade="all, delete-orphan",
|
||||
foreign_keys="Absence.employee_id")
|
||||
balance_accounts = relationship("BalanceAccount", back_populates="employee", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Employee {self.name!r}>"
|
||||
|
||||
|
||||
class Absence(Base):
|
||||
__tablename__ = "absences"
|
||||
|
||||
id = Column(String(32), primary_key=True, default=_new_id)
|
||||
employee_id = Column(String(32), ForeignKey("employees.id", ondelete="CASCADE"), nullable=False)
|
||||
category = Column(SAEnum(AbsenceCategory), nullable=False)
|
||||
status = Column(SAEnum(AbsenceStatus), default=AbsenceStatus.PENDING, nullable=False)
|
||||
start_date = Column(Date, nullable=False)
|
||||
end_date = Column(Date, nullable=False)
|
||||
reason = Column(Text, default="")
|
||||
approver_id = Column(String(32), ForeignKey("employees.id"), nullable=True)
|
||||
approved_at = Column(DateTime, nullable=True)
|
||||
business_days = Column(Float, default=0)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
employee = relationship("Employee", back_populates="absences", foreign_keys=[employee_id])
|
||||
approver = relationship("Employee", foreign_keys=[approver_id])
|
||||
|
||||
__table_args__ = (
|
||||
CheckConstraint("end_date >= start_date", name="ck_absence_dates"),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Absence {self.employee_id} {self.category.value} {self.start_date}–{self.end_date}>"
|
||||
|
||||
|
||||
class BalanceAccount(Base):
|
||||
"""Ferientage-Konto pro Mitarbeiter und Jahr."""
|
||||
__tablename__ = "balance_accounts"
|
||||
|
||||
id = Column(String(32), primary_key=True, default=_new_id)
|
||||
employee_id = Column(String(32), ForeignKey("employees.id", ondelete="CASCADE"), nullable=False)
|
||||
year = Column(Integer, nullable=False)
|
||||
yearly_quota = Column(Integer, default=25)
|
||||
carry_over = Column(Float, default=0)
|
||||
manual_adjustment = Column(Float, default=0)
|
||||
|
||||
employee = relationship("Employee", back_populates="balance_accounts")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("employee_id", "year", name="uq_balance_emp_year"),
|
||||
)
|
||||
|
||||
@property
|
||||
def total_available(self) -> float:
|
||||
return self.yearly_quota + self.carry_over + self.manual_adjustment
|
||||
|
||||
def __repr__(self):
|
||||
return f"<BalanceAccount {self.employee_id} {self.year} quota={self.yearly_quota}>"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# AUDIT LOG
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
class AuditLog(Base):
|
||||
"""Vollständiges Änderungsprotokoll – wer hat was wann gemacht."""
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id = Column(String(32), primary_key=True, default=_new_id)
|
||||
timestamp = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
user_id = Column(String(32), ForeignKey("employees.id"), nullable=True)
|
||||
action = Column(String(50), nullable=False)
|
||||
entity_type = Column(String(50), nullable=False)
|
||||
entity_id = Column(String(32), nullable=True)
|
||||
practice_id = Column(String(32), ForeignKey("practices.id"), nullable=True)
|
||||
old_values = Column(JSON, nullable=True)
|
||||
new_values = Column(JSON, nullable=True)
|
||||
ip_address = Column(String(45), nullable=True)
|
||||
user_agent = Column(String(300), nullable=True)
|
||||
|
||||
user = relationship("Employee", foreign_keys=[user_id])
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_audit_entity", "entity_type", "entity_id"),
|
||||
Index("ix_audit_user", "user_id"),
|
||||
Index("ix_audit_time", "timestamp"),
|
||||
)
|
||||
123
AzA march 2026/workforce_planner/core/schemas.py
Normal file
123
AzA march 2026/workforce_planner/core/schemas.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Pydantic Schemas – Validierung & Serialisierung für API und Clients."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field, EmailStr, model_validator
|
||||
|
||||
from .enums import AbsenceCategory, AbsenceStatus, EmployeeRole
|
||||
|
||||
|
||||
# ───────────────────────── Employee ─────────────────────────
|
||||
|
||||
class EmployeeCreate(BaseModel):
|
||||
name: str = Field(..., min_length=1, max_length=120)
|
||||
email: Optional[str] = None
|
||||
department: Optional[str] = None
|
||||
role: EmployeeRole = EmployeeRole.EMPLOYEE
|
||||
workload_percent: int = Field(100, ge=10, le=100)
|
||||
vacation_days_per_year: int = Field(25, ge=0, le=60)
|
||||
|
||||
|
||||
class EmployeeUpdate(BaseModel):
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=120)
|
||||
email: Optional[str] = None
|
||||
department: Optional[str] = None
|
||||
role: Optional[EmployeeRole] = None
|
||||
workload_percent: Optional[int] = Field(None, ge=10, le=100)
|
||||
vacation_days_per_year: Optional[int] = Field(None, ge=0, le=60)
|
||||
is_active: Optional[bool] = None
|
||||
|
||||
|
||||
class EmployeeRead(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
email: Optional[str]
|
||||
department: Optional[str]
|
||||
role: EmployeeRole
|
||||
workload_percent: int
|
||||
vacation_days_per_year: int
|
||||
is_active: bool
|
||||
created_at: datetime.datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ───────────────────────── Absence ──────────────────────────
|
||||
|
||||
class AbsenceCreate(BaseModel):
|
||||
employee_id: str
|
||||
category: AbsenceCategory
|
||||
start_date: datetime.date
|
||||
end_date: datetime.date
|
||||
reason: str = ""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_dates(self):
|
||||
if self.end_date < self.start_date:
|
||||
raise ValueError("end_date darf nicht vor start_date liegen")
|
||||
return self
|
||||
|
||||
|
||||
class AbsenceUpdate(BaseModel):
|
||||
category: Optional[AbsenceCategory] = None
|
||||
start_date: Optional[datetime.date] = None
|
||||
end_date: Optional[datetime.date] = None
|
||||
reason: Optional[str] = None
|
||||
status: Optional[AbsenceStatus] = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_dates(self):
|
||||
if self.start_date and self.end_date and self.end_date < self.start_date:
|
||||
raise ValueError("end_date darf nicht vor start_date liegen")
|
||||
return self
|
||||
|
||||
|
||||
class AbsenceRead(BaseModel):
|
||||
id: str
|
||||
employee_id: str
|
||||
category: AbsenceCategory
|
||||
status: AbsenceStatus
|
||||
start_date: datetime.date
|
||||
end_date: datetime.date
|
||||
reason: str
|
||||
approver_id: Optional[str]
|
||||
approved_at: Optional[datetime.datetime]
|
||||
business_days: float
|
||||
created_at: datetime.datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
# ───────────────────────── Balance ──────────────────────────
|
||||
|
||||
class BalanceRead(BaseModel):
|
||||
employee_id: str
|
||||
employee_name: str
|
||||
year: int
|
||||
yearly_quota: int
|
||||
carry_over: float
|
||||
manual_adjustment: float
|
||||
total_available: float
|
||||
used_days: float
|
||||
remaining: float
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class BalanceAdjust(BaseModel):
|
||||
carry_over: Optional[float] = None
|
||||
manual_adjustment: Optional[float] = None
|
||||
yearly_quota: Optional[int] = Field(None, ge=0, le=60)
|
||||
|
||||
|
||||
# ───────────────────────── Approval ─────────────────────────
|
||||
|
||||
class ApprovalDecision(BaseModel):
|
||||
absence_id: str
|
||||
approved: bool
|
||||
approver_id: str
|
||||
comment: str = ""
|
||||
Reference in New Issue
Block a user