Initial commit – TimeMaster Zeiterfassung & HR-Tool
Stand: agent-06 (Audit-Log), agent-05 (Krankmeldung), agent-07 Phase 1 (Personalnummer), Busylight-Pull-Integration, TOTP/2FA, Abwesenheiten, Zeiterfassung, Kiosk-Grundgerüst. Migrations 0001–0023 deployed auf 192.168.1.137 + .164. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
from app.models.company import Company
|
||||
from app.models.department import Department
|
||||
from app.models.user import User, UserRole
|
||||
from app.models.session import Session
|
||||
from app.models.password_reset import PasswordReset
|
||||
from app.models.audit_log import AuditLog
|
||||
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.vacation_balance import VacationBalance
|
||||
from app.models.overtime_balance import OvertimeBalance
|
||||
from app.models.public_holiday import PublicHoliday
|
||||
from app.models.smtp_config import SmtpConfig
|
||||
from app.models.caldav_config import CaldavCompanyConfig, CaldavUserConfig
|
||||
from app.models.kiosk_device import KioskDevice, KioskAuthMethod
|
||||
|
||||
__all__ = [
|
||||
"Company",
|
||||
"Department",
|
||||
"User",
|
||||
"UserRole",
|
||||
"Session",
|
||||
"PasswordReset",
|
||||
"AuditLog",
|
||||
"WorkSchedule",
|
||||
"TimeEntry",
|
||||
"EntryStatus",
|
||||
"EntrySource",
|
||||
"AbsenceType",
|
||||
"Absence",
|
||||
"AbsenceStatus",
|
||||
"VacationBalance",
|
||||
"OvertimeBalance",
|
||||
"PublicHoliday",
|
||||
"KioskDevice",
|
||||
"KioskAuthMethod",
|
||||
]
|
||||
@@ -0,0 +1,87 @@
|
||||
import uuid
|
||||
import enum
|
||||
from datetime import date, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, Date, DateTime, Enum, ForeignKey, Numeric, String, Text, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.user import User
|
||||
from app.models.absence_type import AbsenceType
|
||||
|
||||
|
||||
class AbsenceStatus(str, enum.Enum):
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class Absence(Base):
|
||||
__tablename__ = "absences"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
type_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("absence_types.id", ondelete="RESTRICT"), nullable=False
|
||||
)
|
||||
start_date: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
end_date: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
half_day_start: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
half_day_end: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
working_days: Mapped[float] = mapped_column(Numeric(5, 1), default=0)
|
||||
status: Mapped[AbsenceStatus] = mapped_column(
|
||||
Enum(AbsenceStatus, name="absencestatus", values_callable=lambda x: [e.value for e in x]),
|
||||
nullable=False, default=AbsenceStatus.PENDING,
|
||||
)
|
||||
approved_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL")
|
||||
)
|
||||
substitute_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL")
|
||||
)
|
||||
note: Mapped[str | None] = mapped_column(Text)
|
||||
rejection_reason: Mapped[str | None] = mapped_column(Text)
|
||||
correction_note: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
# Zusatzinformationen (Weiterbildung, Dienstreise, etc.)
|
||||
# Struktur je Kategorie:
|
||||
# training: {"course_name": str, "provider": str, "location": str}
|
||||
# business_trip: {"destination": str, "purpose": str}
|
||||
meta: Mapped[dict | None] = mapped_column(JSONB)
|
||||
|
||||
# Krankheit: Arbeitsunfähigkeitsbescheinigung
|
||||
certificate_required_by: Mapped[date | None] = mapped_column(Date)
|
||||
certificate_received_at: Mapped[date | None] = mapped_column(Date)
|
||||
|
||||
# CalDAV-Sync
|
||||
caldav_uid: Mapped[str | None] = mapped_column(String(255))
|
||||
caldav_user_etag: Mapped[str | None] = mapped_column(Text)
|
||||
caldav_company_etag: Mapped[str | None] = mapped_column(Text)
|
||||
caldav_last_error: Mapped[str | None] = mapped_column(Text)
|
||||
caldav_synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
user: Mapped["User"] = relationship(
|
||||
"User", primaryjoin="Absence.user_id == User.id",
|
||||
foreign_keys="[Absence.user_id]", lazy="noload",
|
||||
)
|
||||
absence_type: Mapped["AbsenceType"] = relationship("AbsenceType", lazy="noload")
|
||||
approver: Mapped["User | None"] = relationship(
|
||||
"User", primaryjoin="Absence.approved_by == User.id",
|
||||
foreign_keys="[Absence.approved_by]", lazy="noload",
|
||||
)
|
||||
substitute: Mapped["User | None"] = relationship(
|
||||
"User", primaryjoin="Absence.substitute_id == User.id",
|
||||
foreign_keys="[Absence.substitute_id]", lazy="noload",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Absence {self.user_id} {self.start_date}–{self.end_date} [{self.status}]>"
|
||||
@@ -0,0 +1,49 @@
|
||||
import enum
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, Enum, ForeignKey, Integer, String
|
||||
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.company import Company
|
||||
|
||||
|
||||
class AbsenceCategory(str, enum.Enum):
|
||||
VACATION = "vacation"
|
||||
SICK = "sick"
|
||||
OVERTIME_COMP = "overtime_comp"
|
||||
TRAINING = "training"
|
||||
BUSINESS_TRIP = "business_trip"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class AbsenceType(Base):
|
||||
__tablename__ = "absence_types"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
company_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
color: Mapped[str] = mapped_column(String(7), default="#3B82F6")
|
||||
category: Mapped[AbsenceCategory] = mapped_column(
|
||||
Enum(AbsenceCategory, name="absencecategory", values_callable=lambda x: [e.value for e in x]),
|
||||
nullable=False, default=AbsenceCategory.OTHER,
|
||||
)
|
||||
requires_approval: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
deducts_vacation: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
affects_overtime_balance: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
requires_certificate: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
certificate_after_days: Mapped[int] = mapped_column(Integer, default=3)
|
||||
is_paid: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
max_days_per_year: Mapped[int | None] = mapped_column(Integer)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
|
||||
company: Mapped["Company"] = relationship("Company", lazy="noload")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<AbsenceType {self.name} [{self.category}]>"
|
||||
@@ -0,0 +1,23 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class AuditLog(Base):
|
||||
__tablename__ = "audit_logs"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
company_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("companies.id", ondelete="SET NULL"), index=True)
|
||||
user_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), index=True)
|
||||
action: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
entity_type: Mapped[str | None] = mapped_column(String(100))
|
||||
entity_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
|
||||
old_value: Mapped[dict | None] = mapped_column(JSONB)
|
||||
new_value: Mapped[dict | None] = mapped_column(JSONB)
|
||||
ip: Mapped[str | None] = mapped_column(String(45))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
|
||||
@@ -0,0 +1,64 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, func # noqa: F401
|
||||
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.company import Company
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class CaldavCompanyConfig(Base):
|
||||
"""Zentraler Firmenkalender – alle genehmigten Abwesenheiten landen hier."""
|
||||
__tablename__ = "caldav_company_configs"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
company_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"),
|
||||
nullable=False, unique=True, index=True,
|
||||
)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
principal_url: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
calendar_url: Mapped[str | None] = mapped_column(Text)
|
||||
username: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
password_encrypted: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
calendar_display_name: Mapped[str] = mapped_column(String(255), default="")
|
||||
verify_ssl: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
name_template: Mapped[str] = mapped_column(Text, default="$vorname $nachname – $typ")
|
||||
last_error: Mapped[str | None] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
company: Mapped["Company"] = relationship("Company", lazy="noload")
|
||||
|
||||
|
||||
class CaldavUserConfig(Base):
|
||||
"""Persönlicher Kalender des Mitarbeiters."""
|
||||
__tablename__ = "caldav_user_configs"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False, unique=True, index=True,
|
||||
)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
principal_url: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
calendar_url: Mapped[str | None] = mapped_column(Text)
|
||||
username: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
password_encrypted: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
calendar_display_name: Mapped[str] = mapped_column(String(255), default="")
|
||||
verify_ssl: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
last_error: Mapped[str | None] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
user: Mapped["User"] = relationship("User", lazy="noload")
|
||||
@@ -0,0 +1,52 @@
|
||||
import enum
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Integer, String, Text
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.user import User
|
||||
from app.models.department import Department
|
||||
|
||||
|
||||
class PersonnelNumberMode(str, enum.Enum):
|
||||
MANUAL = "manual"
|
||||
AUTO = "auto"
|
||||
|
||||
|
||||
class Company(Base):
|
||||
__tablename__ = "companies"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
|
||||
plan: Mapped[str] = mapped_column(String(50), default="trial")
|
||||
logo_url: Mapped[str | None] = mapped_column(Text)
|
||||
country: Mapped[str] = mapped_column(String(10), default="DE")
|
||||
state: Mapped[str | None] = mapped_column(String(10))
|
||||
settings: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||
|
||||
# Personalnummern-Konfiguration
|
||||
personnel_number_required: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
personnel_number_mode: Mapped[str] = mapped_column(String(10), nullable=False, default=PersonnelNumberMode.MANUAL.value)
|
||||
personnel_number_next: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
|
||||
# Krankmeldungs-Konfiguration: Default-Schwelle für AU-Pflicht (in Tagen).
|
||||
# Pro AbsenceType via certificate_after_days überschreibbar.
|
||||
sick_note_required_after_days: Mapped[int] = mapped_column(Integer, nullable=False, default=3)
|
||||
|
||||
# Busylight-Pull: SHA-256-Hash des per-Firma-Tokens (Klartext nie in DB).
|
||||
busylight_pull_token_hash: Mapped[str | None] = mapped_column(String(64), unique=True)
|
||||
busylight_token_created_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
|
||||
# Relationships
|
||||
users: Mapped[list["User"]] = relationship("User", back_populates="company", lazy="noload")
|
||||
departments: Mapped[list["Department"]] = relationship("Department", back_populates="company", lazy="noload")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Company {self.name}>"
|
||||
@@ -0,0 +1,40 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
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.company import Company
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
class Department(Base):
|
||||
__tablename__ = "departments"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
company_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"), nullable=False)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
manager_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL", use_alter=True, name="fk_dep_manager"))
|
||||
|
||||
# Relationships
|
||||
company: Mapped["Company"] = relationship("Company", back_populates="departments")
|
||||
members: Mapped[list["User"]] = relationship(
|
||||
"User",
|
||||
primaryjoin="User.department_id == Department.id",
|
||||
foreign_keys="[User.department_id]",
|
||||
back_populates="department",
|
||||
lazy="noload",
|
||||
)
|
||||
manager: Mapped["User | None"] = relationship(
|
||||
"User",
|
||||
primaryjoin="Department.manager_id == User.id",
|
||||
foreign_keys="[Department.manager_id]",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Department {self.name}>"
|
||||
@@ -0,0 +1,41 @@
|
||||
import uuid
|
||||
import enum
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, String, func
|
||||
from sqlalchemy.dialects.postgresql import ARRAY, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.company import Company
|
||||
|
||||
|
||||
class KioskAuthMethod(str, enum.Enum):
|
||||
PIN = "pin"
|
||||
NFC = "nfc"
|
||||
QR = "qr"
|
||||
LIST = "list" # Mitarbeiter-Liste
|
||||
|
||||
|
||||
class KioskDevice(Base):
|
||||
__tablename__ = "kiosk_devices"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
company_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"),
|
||||
nullable=False, index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
location: Mapped[str | None] = mapped_column(String(255))
|
||||
token_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
company: Mapped["Company"] = relationship("Company", lazy="noload")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<KioskDevice {self.name} ({self.company_id})>"
|
||||
@@ -0,0 +1,60 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, 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.company import Company
|
||||
|
||||
|
||||
class LdapConfig(Base):
|
||||
__tablename__ = "ldap_configs"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
company_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"),
|
||||
nullable=False, unique=True, index=True
|
||||
)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# Server
|
||||
host: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
port: Mapped[int] = mapped_column(Integer, default=389)
|
||||
use_ssl: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
use_tls: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
tls_verify: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# Bind credentials
|
||||
bind_dn: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
bind_password_encrypted: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
|
||||
# Search
|
||||
base_dn: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
user_search_filter: Mapped[str] = mapped_column(
|
||||
String(512), nullable=False, default="(objectClass=person)"
|
||||
)
|
||||
|
||||
# Attribute mapping
|
||||
attr_email: Mapped[str] = mapped_column(String(100), default="mail")
|
||||
attr_firstname: Mapped[str] = mapped_column(String(100), default="givenName")
|
||||
attr_lastname: Mapped[str] = mapped_column(String(100), default="sn")
|
||||
attr_username: Mapped[str] = mapped_column(String(100), default="sAMAccountName")
|
||||
attr_department: Mapped[str | None] = mapped_column(String(100))
|
||||
attr_personnel_number: Mapped[str | None] = mapped_column(String(100))
|
||||
|
||||
# Sync state
|
||||
last_sync_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
company: Mapped["Company"] = relationship("Company", lazy="noload")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<LdapConfig {self.host} company={self.company_id}>"
|
||||
@@ -0,0 +1,50 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Numeric, 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
|
||||
from app.models.company import Company
|
||||
|
||||
|
||||
class OvertimeBalance(Base):
|
||||
"""Kumuliertes Überstundenguthaben pro Mitarbeiter.
|
||||
|
||||
total_hours = Summe aller genehmigten Überstunden aus time_entries
|
||||
taken_hours = bereits als Freizeitausgleich genommene Stunden
|
||||
available = total_hours - taken_hours
|
||||
"""
|
||||
__tablename__ = "overtime_balances"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False, unique=True, index=True,
|
||||
)
|
||||
company_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
total_hours: Mapped[Decimal] = mapped_column(Numeric(8, 2), default=Decimal("0"))
|
||||
taken_hours: Mapped[Decimal] = mapped_column(Numeric(8, 2), default=Decimal("0"))
|
||||
last_calculated: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
user: Mapped["User"] = relationship("User", lazy="noload")
|
||||
company: Mapped["Company"] = relationship("Company", lazy="noload")
|
||||
|
||||
@property
|
||||
def available_hours(self) -> Decimal:
|
||||
return self.total_hours - self.taken_hours
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<OvertimeBalance user={self.user_id} total={self.total_hours}h taken={self.taken_hours}h>"
|
||||
@@ -0,0 +1,25 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import 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 PasswordReset(Base):
|
||||
__tablename__ = "password_resets"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
token_hash: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
user: Mapped["User"] = relationship("User")
|
||||
@@ -0,0 +1,27 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Numeric, String, Text, func
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class Project(Base):
|
||||
__tablename__ = "projects"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
company_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
color: Mapped[str] = mapped_column(String(7), default="#3B82F6") # Tailwind blue-500
|
||||
budget_hours: Mapped[float | None] = mapped_column(Numeric(8, 2))
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Project {self.name} ({self.company_id})>"
|
||||
@@ -0,0 +1,26 @@
|
||||
import uuid
|
||||
from datetime import date
|
||||
|
||||
from sqlalchemy import Boolean, Date, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class PublicHoliday(Base):
|
||||
__tablename__ = "public_holidays"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("country", "state", "date", name="uq_public_holiday"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
country: Mapped[str] = mapped_column(String(10), nullable=False)
|
||||
state: Mapped[str | None] = mapped_column(String(10)) # z.B. "BY", "NW" für Bundesländer
|
||||
date: Mapped[date] = mapped_column(Date, nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
year: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
|
||||
is_high_rate: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<PublicHoliday {self.name} {self.date}>"
|
||||
@@ -0,0 +1,30 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy.dialects.postgresql import INET, 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 Session(Base):
|
||||
__tablename__ = "sessions"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
refresh_token_hash: Mapped[str] = mapped_column(Text, nullable=False, unique=True)
|
||||
device: Mapped[str | None] = mapped_column(String(255))
|
||||
ip: Mapped[str | None] = mapped_column(String(45)) # supports IPv6
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
user: Mapped["User"] = relationship("User", back_populates="sessions")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Session user={self.user_id} expires={self.expires_at}>"
|
||||
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
SMTP-Konfiguration pro Firma.
|
||||
Passwort wird Fernet-verschlüsselt gespeichert (gleiche Methode wie ldap_service).
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import Boolean, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class SmtpConfig(Base):
|
||||
__tablename__ = "smtp_configs"
|
||||
__table_args__ = (UniqueConstraint("company_id"),)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
company_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
host: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
port: Mapped[int] = mapped_column(Integer, default=587, nullable=False)
|
||||
use_tls: Mapped[bool] = mapped_column(Boolean, default=False) # SMTPS port 465
|
||||
use_starttls: Mapped[bool] = mapped_column(Boolean, default=True) # STARTTLS port 587
|
||||
|
||||
username: Mapped[str | None] = mapped_column(String(255))
|
||||
password_encrypted: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
from_email: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
from_name: Mapped[str] = mapped_column(String(255), default="TimeMaster", nullable=False)
|
||||
|
||||
is_enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
@@ -0,0 +1,81 @@
|
||||
import uuid
|
||||
import enum
|
||||
from datetime import date, datetime, time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Date, DateTime, Enum, ForeignKey, Integer, String, Text, Time, 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 EntryStatus(str, enum.Enum):
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
REJECTED = "rejected"
|
||||
|
||||
|
||||
class EntrySource(str, enum.Enum):
|
||||
WEB = "web"
|
||||
KIOSK = "kiosk"
|
||||
API = "api"
|
||||
MANUAL = "manual"
|
||||
|
||||
|
||||
class TimeEntry(Base):
|
||||
__tablename__ = "time_entries"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
date: Mapped[date] = mapped_column(Date, nullable=False, index=True)
|
||||
start_time: Mapped[time] = mapped_column(Time(timezone=False), nullable=False)
|
||||
end_time: Mapped[time | None] = mapped_column(Time(timezone=False))
|
||||
break_minutes: Mapped[int] = mapped_column(Integer, default=0)
|
||||
break_start: Mapped[time | None] = mapped_column(Time(timezone=False)) # Aktive Pause tracken
|
||||
project_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True))
|
||||
note: Mapped[str | None] = mapped_column(Text)
|
||||
status: Mapped[EntryStatus] = mapped_column(
|
||||
Enum(EntryStatus, name="entrystatus", values_callable=lambda x: [e.value for e in x]), nullable=False, default=EntryStatus.PENDING
|
||||
)
|
||||
source: Mapped[EntrySource] = mapped_column(
|
||||
Enum(EntrySource, name="entrysource", values_callable=lambda x: [e.value for e in x]), nullable=False, default=EntrySource.WEB
|
||||
)
|
||||
approved_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL")
|
||||
)
|
||||
correction_note: Mapped[str | None] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
user: Mapped["User"] = relationship(
|
||||
"User", primaryjoin="TimeEntry.user_id == User.id",
|
||||
foreign_keys="[TimeEntry.user_id]", lazy="noload"
|
||||
)
|
||||
approver: Mapped["User | None"] = relationship(
|
||||
"User", primaryjoin="TimeEntry.approved_by == User.id",
|
||||
foreign_keys="[TimeEntry.approved_by]", lazy="noload"
|
||||
)
|
||||
@property
|
||||
def worked_minutes(self) -> int | None:
|
||||
"""Gearbeitete Minuten (ohne Pausen), None wenn noch offen."""
|
||||
if self.end_time is None:
|
||||
return None
|
||||
start_total = self.start_time.hour * 60 + self.start_time.minute
|
||||
end_total = self.end_time.hour * 60 + self.end_time.minute
|
||||
if end_total <= start_total:
|
||||
end_total += 24 * 60 # overnight shift
|
||||
return max(0, end_total - start_total - self.break_minutes)
|
||||
|
||||
@property
|
||||
def worked_hours(self) -> float | None:
|
||||
mins = self.worked_minutes
|
||||
return round(mins / 60, 2) if mins is not None else None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<TimeEntry {self.user_id} {self.date} {self.start_time}-{self.end_time}>"
|
||||
@@ -0,0 +1,94 @@
|
||||
import uuid
|
||||
import enum
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, String, 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.company import Company
|
||||
from app.models.department import Department
|
||||
from app.models.session import Session
|
||||
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
SUPER_ADMIN = "SUPER_ADMIN"
|
||||
COMPANY_ADMIN = "COMPANY_ADMIN"
|
||||
HR = "HR"
|
||||
MANAGER = "MANAGER"
|
||||
EMPLOYEE = "EMPLOYEE"
|
||||
|
||||
|
||||
class AuthProvider(str, enum.Enum):
|
||||
LOCAL = "local"
|
||||
LDAP = "ldap"
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
company_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"))
|
||||
department_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("departments.id", ondelete="SET NULL"))
|
||||
|
||||
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
|
||||
password_hash: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
first_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
last_name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
role: Mapped[UserRole] = mapped_column(Enum(UserRole), nullable=False, default=UserRole.EMPLOYEE)
|
||||
auth_provider: Mapped[AuthProvider] = mapped_column(
|
||||
Enum(AuthProvider, name="authprovider", values_callable=lambda x: [e.value for e in x]),
|
||||
nullable=False, default=AuthProvider.LOCAL,
|
||||
)
|
||||
ldap_dn: Mapped[str | None] = mapped_column(Text)
|
||||
work_schedule_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("work_schedules.id", ondelete="SET NULL")
|
||||
)
|
||||
|
||||
# Kiosk auth
|
||||
kiosk_pin_hash: Mapped[str | None] = mapped_column(Text)
|
||||
kiosk_qr_token: Mapped[str | None] = mapped_column(Text, unique=True)
|
||||
|
||||
# Kalender-Kürzel (vom Manager setzbar, für CalDAV-Template $kuerzel)
|
||||
kuerzel: Mapped[str | None] = mapped_column(String(20))
|
||||
|
||||
# Personalnummer (numerisch, eindeutig pro Firma; bleibt nach Deaktivierung reserviert)
|
||||
personnel_number: Mapped[str | None] = mapped_column(String(50))
|
||||
|
||||
# TOTP / 2FA
|
||||
totp_secret: Mapped[str | None] = mapped_column(String(64))
|
||||
totp_enabled: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
# Permissions
|
||||
can_manual_time_entry: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
|
||||
# Account state
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
invite_token_hash: Mapped[str | None] = mapped_column(Text)
|
||||
invite_expires: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
last_login: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
# Relationships
|
||||
company: Mapped["Company"] = relationship("Company", back_populates="users")
|
||||
department: Mapped["Department | None"] = relationship(
|
||||
"Department",
|
||||
primaryjoin="User.department_id == Department.id",
|
||||
foreign_keys="[User.department_id]",
|
||||
back_populates="members",
|
||||
)
|
||||
sessions: Mapped[list["Session"]] = relationship("Session", back_populates="user", cascade="all, delete-orphan", lazy="noload")
|
||||
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
|
||||
def is_admin_or_above(self) -> bool:
|
||||
return self.role in (UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<User {self.email} [{self.role}]>"
|
||||
@@ -0,0 +1,39 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, Integer, UniqueConstraint
|
||||
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 VacationBalance(Base):
|
||||
__tablename__ = "vacation_balances"
|
||||
__table_args__ = (UniqueConstraint("user_id", "year", name="uq_vacation_balance_user_year"),)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
||||
)
|
||||
year: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
entitled_days: Mapped[int] = mapped_column(Integer, default=30) # Grundurlaub
|
||||
special_days: Mapped[int] = mapped_column(Integer, default=0) # Sondertage
|
||||
carried_over: Mapped[int] = mapped_column(Integer, default=0) # Resturlaub aus Vorjahr
|
||||
used_days: Mapped[int] = mapped_column(Integer, default=0) # Verbraucht
|
||||
|
||||
user: Mapped["User"] = relationship("User", lazy="noload")
|
||||
|
||||
@property
|
||||
def total_days(self) -> int:
|
||||
return self.entitled_days + self.special_days + self.carried_over
|
||||
|
||||
@property
|
||||
def remaining_days(self) -> int:
|
||||
return self.total_days - self.used_days
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<VacationBalance {self.user_id} {self.year}: {self.remaining_days} left>"
|
||||
@@ -0,0 +1,45 @@
|
||||
import uuid
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import Date, ForeignKey, Numeric, String
|
||||
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.company import Company
|
||||
|
||||
|
||||
class WorkSchedule(Base):
|
||||
__tablename__ = "work_schedules"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
company_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
mon_h: Mapped[Decimal] = mapped_column(Numeric(4, 2), default=Decimal("8.00"))
|
||||
tue_h: Mapped[Decimal] = mapped_column(Numeric(4, 2), default=Decimal("8.00"))
|
||||
wed_h: Mapped[Decimal] = mapped_column(Numeric(4, 2), default=Decimal("8.00"))
|
||||
thu_h: Mapped[Decimal] = mapped_column(Numeric(4, 2), default=Decimal("8.00"))
|
||||
fri_h: Mapped[Decimal] = mapped_column(Numeric(4, 2), default=Decimal("8.00"))
|
||||
sat_h: Mapped[Decimal] = mapped_column(Numeric(4, 2), default=Decimal("0.00"))
|
||||
sun_h: Mapped[Decimal] = mapped_column(Numeric(4, 2), default=Decimal("0.00"))
|
||||
valid_from: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
|
||||
company: Mapped["Company"] = relationship("Company", lazy="noload")
|
||||
|
||||
@property
|
||||
def weekly_hours(self) -> Decimal:
|
||||
return self.mon_h + self.tue_h + self.wed_h + self.thu_h + self.fri_h + self.sat_h + self.sun_h
|
||||
|
||||
def hours_for_weekday(self, weekday: int) -> Decimal:
|
||||
"""weekday: 0=Mon, 1=Tue, ..., 6=Sun"""
|
||||
mapping = [self.mon_h, self.tue_h, self.wed_h, self.thu_h, self.fri_h, self.sat_h, self.sun_h]
|
||||
return mapping[weekday]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<WorkSchedule {self.name}>"
|
||||
Reference in New Issue
Block a user