169 lines
6.8 KiB
Python
169 lines
6.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
# hub/ticket/domain/task.py
|
|
|
|
"""Доменная сущность задачи Ticket и сериализация её состояния."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from typing import Any, Mapping
|
|
|
|
from .ticket_types import TicketTaskSnapshot
|
|
|
|
|
|
def _normalize_datetime(value: Any) -> datetime | None:
|
|
"""Преобразовать строку или datetime в datetime."""
|
|
if value is None or value == "":
|
|
return None
|
|
if isinstance(value, datetime):
|
|
return value
|
|
if not isinstance(value, str):
|
|
return None
|
|
try:
|
|
if "T" in value:
|
|
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _normalize_color(value: Any) -> str:
|
|
"""Нормализовать представление цвета до hex-строки."""
|
|
if isinstance(value, str) and value.startswith("#") and len(value) >= 4:
|
|
return value
|
|
name_getter = getattr(value, "name", None)
|
|
if callable(name_getter):
|
|
try:
|
|
normalized = name_getter()
|
|
if isinstance(normalized, str) and normalized.startswith("#"):
|
|
return normalized
|
|
except Exception as _exc:
|
|
return "#FFFFFF"
|
|
return "#FFFFFF"
|
|
|
|
|
|
def _normalize_int(value: Any, default: int | None = None) -> int | None:
|
|
"""Нормализовать число, сохранив валидное значение 0."""
|
|
if value is None or value == "":
|
|
return default
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class TicketTask:
|
|
"""Каноническая доменная задача Ticket."""
|
|
|
|
task_id: int
|
|
location: str
|
|
state_code: int
|
|
state_name: str
|
|
action_text: str = ""
|
|
color_hex: str = "#FFFFFF"
|
|
created_at: datetime | None = None
|
|
completed_at: datetime | None = None
|
|
refused_from_state: int | None = None
|
|
refusal_reason: str = ""
|
|
assigned_specialist: str = ""
|
|
specialist_photo: str = ""
|
|
diagnostic_report_signed: bool = False
|
|
repair_report_signed: bool = False
|
|
acceptance_report_signed: bool = False
|
|
sequence_number: int = 0
|
|
|
|
@classmethod
|
|
def from_record(cls, record: Mapping[str, Any]) -> TicketTask | None:
|
|
"""Создать задачу из записи файлового хранилища."""
|
|
raw_task_id = record.get("button_id")
|
|
if raw_task_id is None:
|
|
return None
|
|
try:
|
|
task_id = int(raw_task_id)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
return cls(
|
|
task_id=task_id,
|
|
location=str(record.get("location", "")),
|
|
state_code=_normalize_int(record.get("state"), 1) or 0,
|
|
state_name=str(record.get("state_name", "")),
|
|
action_text=str(record.get("action", "")),
|
|
color_hex=_normalize_color(record.get("color", "#FFFFFF")),
|
|
created_at=_normalize_datetime(record.get("created_time")) or datetime.now(),
|
|
completed_at=_normalize_datetime(record.get("completed_time")),
|
|
refused_from_state=_normalize_int(record.get("refused_from_state")),
|
|
refusal_reason=str(record.get("refusal_reason", "")),
|
|
assigned_specialist=str(record.get("assigned_specialist", "")),
|
|
specialist_photo=str(record.get("specialist_photo", "")),
|
|
diagnostic_report_signed=bool(record.get("diagnostic_report_signed", False)),
|
|
repair_report_signed=bool(record.get("repair_report_signed", False)),
|
|
acceptance_report_signed=bool(record.get("acceptance_report_signed", False)),
|
|
sequence_number=_normalize_int(record.get("sequence_number"), 0) or 0,
|
|
)
|
|
|
|
@classmethod
|
|
def from_snapshot(cls, snapshot: TicketTaskSnapshot) -> TicketTask:
|
|
"""Создать доменную сущность из snapshot."""
|
|
return cls(
|
|
task_id=snapshot.task_id,
|
|
location=snapshot.location,
|
|
state_code=snapshot.state_code,
|
|
state_name=snapshot.state_name,
|
|
action_text=snapshot.action_text,
|
|
color_hex=snapshot.color_hex,
|
|
created_at=snapshot.created_at,
|
|
completed_at=snapshot.completed_at,
|
|
refused_from_state=snapshot.refused_from_state,
|
|
refusal_reason=snapshot.refusal_reason,
|
|
assigned_specialist=snapshot.assigned_specialist,
|
|
specialist_photo=snapshot.specialist_photo,
|
|
diagnostic_report_signed=snapshot.diagnostic_report_signed,
|
|
repair_report_signed=snapshot.repair_report_signed,
|
|
acceptance_report_signed=snapshot.acceptance_report_signed,
|
|
sequence_number=snapshot.sequence_number,
|
|
)
|
|
|
|
def to_record(self) -> dict[str, Any]:
|
|
"""Преобразовать доменную сущность в запись для JSON."""
|
|
return {
|
|
"button_id": self.task_id,
|
|
"location": self.location,
|
|
"state": self.state_code,
|
|
"action": self.action_text,
|
|
"state_name": self.state_name,
|
|
"color": self.color_hex,
|
|
"created_time": self.created_at,
|
|
"completed_time": self.completed_at,
|
|
"refused_from_state": self.refused_from_state,
|
|
"refusal_reason": self.refusal_reason,
|
|
"assigned_specialist": self.assigned_specialist,
|
|
"specialist_photo": self.specialist_photo,
|
|
"diagnostic_report_signed": self.diagnostic_report_signed,
|
|
"repair_report_signed": self.repair_report_signed,
|
|
"acceptance_report_signed": self.acceptance_report_signed,
|
|
"sequence_number": self.sequence_number,
|
|
}
|
|
|
|
def to_snapshot(self) -> TicketTaskSnapshot:
|
|
"""Вернуть неизменяемый снимок задачи для внешних слоёв."""
|
|
return TicketTaskSnapshot(
|
|
task_id=self.task_id,
|
|
location=self.location,
|
|
state_code=self.state_code,
|
|
state_name=self.state_name,
|
|
action_text=self.action_text,
|
|
color_hex=self.color_hex,
|
|
created_at=self.created_at,
|
|
completed_at=self.completed_at,
|
|
refused_from_state=self.refused_from_state,
|
|
refusal_reason=self.refusal_reason,
|
|
assigned_specialist=self.assigned_specialist,
|
|
specialist_photo=self.specialist_photo,
|
|
diagnostic_report_signed=self.diagnostic_report_signed,
|
|
repair_report_signed=self.repair_report_signed,
|
|
acceptance_report_signed=self.acceptance_report_signed,
|
|
sequence_number=self.sequence_number,
|
|
)
|