Add Dispatch_V0.1.1

This commit is contained in:
2026-04-29 08:18:54 +04:00
commit a7ede6ded4
404 changed files with 39167 additions and 0 deletions

View File

@@ -0,0 +1,208 @@
# -*- coding: utf-8 -*-
# hub/ticket/domain/ticket_state_service.py
"""Доменный сервис переходов и представления состояния Ticket."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from .location_catalog import LocationCatalog
from .task import TicketTask
from .ticket_constants import (
HARDWARE_SIGNAL_ADVANCE,
HARDWARE_SIGNAL_INITIALIZE,
STATE_ARCHIVED,
STATE_COMPLETED,
STATE_CONFIRMATION,
STATE_IN_PROGRESS,
STATE_REFUSED,
STATE_TODO,
TICKET_STATE_ACTIONS,
TICKET_STATE_COLORS,
TICKET_STATE_NAMES,
)
from .ticket_transition_policy import TicketTransitionPolicy
STATE_NAMES = TICKET_STATE_NAMES
STATE_COLORS = TICKET_STATE_COLORS
@dataclass(frozen=True, slots=True)
class TicketTransitionResult:
"""Результат обработки доменного перехода Ticket."""
changed: bool
task: TicketTask | None
message: str
blocked_reason: str = ""
class TicketStateService:
"""Каноническая state-machine Ticket без зависимости от GUI и COM-сервиса."""
def __init__(
self,
location_catalog: LocationCatalog | None = None,
transition_policy: TicketTransitionPolicy | None = None,
):
self._location_catalog = location_catalog or LocationCatalog()
self._transition_policy = transition_policy or TicketTransitionPolicy()
def handle_hardware_signal(
self,
button_id: int,
hardware_state: int,
current_task: TicketTask | None,
) -> TicketTransitionResult:
"""Обработать аппаратный сигнал и вычислить доменный результат."""
if hardware_state == HARDWARE_SIGNAL_INITIALIZE:
return TicketTransitionResult(
changed=False,
task=current_task,
message=f"Получен запрос инициализации для кнопки {button_id}.",
)
if hardware_state not in HARDWARE_SIGNAL_ADVANCE:
return TicketTransitionResult(
changed=False,
task=current_task,
message=f"Неизвестное аппаратное состояние: {hardware_state:02X}.",
blocked_reason="unsupported_hardware_signal",
)
return self.advance_task(button_id, current_task)
def advance_task(self, button_id: int, current_task: TicketTask | None) -> TicketTransitionResult:
"""Перевести задачу в следующее допустимое состояние."""
if current_task is None:
task = self._build_task_for_state(button_id, STATE_TODO)
return TicketTransitionResult(
changed=True,
task=task,
message=f"Создана новая задача #{button_id} в состоянии '{task.state_name}'.",
)
if current_task.state_code == STATE_ARCHIVED:
task = self._reset_task_cycle(current_task, STATE_TODO)
return TicketTransitionResult(
changed=True,
task=task,
message=f"Архивная задача #{button_id} начала новый цикл.",
)
if current_task.state_code == STATE_REFUSED:
task = self._reset_task_cycle(current_task, STATE_TODO)
return TicketTransitionResult(
changed=True,
task=task,
message=f"Задача #{button_id} сброшена из отказа в новое выполнение.",
)
if current_task.state_code == STATE_COMPLETED:
task = self._reset_task_cycle(current_task, STATE_TODO)
return TicketTransitionResult(
changed=True,
task=task,
message=f"Выполненная задача #{button_id} начала новый цикл.",
)
decision = self._transition_policy.can_advance(current_task)
if not decision.allowed:
return TicketTransitionResult(
changed=False,
task=current_task,
message=f"Переход задачи #{button_id} отклонён.",
blocked_reason=decision.reason,
)
next_state = self._next_state(current_task.state_code)
task = self._clone_with_state(current_task, next_state)
return TicketTransitionResult(
changed=True,
task=task,
message=(
f"Задача #{button_id} перешла из состояния "
f"'{STATE_NAMES[current_task.state_code]}' в '{task.state_name}'."
),
)
def mark_task_as_refused(
self,
button_id: int,
current_task: TicketTask | None,
refusal_reason: str = "",
) -> TicketTransitionResult:
"""Перевести задачу в состояние отказа."""
task = current_task or self._build_task_for_state(button_id, STATE_TODO)
refused_task = self._clone_with_state(task, STATE_REFUSED)
refused_task.refused_from_state = task.state_code
refused_task.refusal_reason = str(refusal_reason or "").strip()
return TicketTransitionResult(
changed=True,
task=refused_task,
message=f"Задача #{button_id} переведена в отказ.",
)
def move_task_to_archive(
self,
button_id: int,
current_task: TicketTask | None,
) -> TicketTransitionResult:
"""Переместить задачу в архив как отдельное доменное правило."""
task = current_task or self._build_task_for_state(button_id, STATE_TODO)
archived_task = self._clone_with_state(task, STATE_ARCHIVED)
return TicketTransitionResult(
changed=True,
task=archived_task,
message=f"Задача #{button_id} перемещена в архив.",
)
def _next_state(self, current_state: int) -> int:
if current_state == STATE_TODO:
return STATE_IN_PROGRESS
if current_state == STATE_IN_PROGRESS:
return STATE_CONFIRMATION
if current_state == STATE_CONFIRMATION:
return STATE_COMPLETED
return current_state
def _build_task_for_state(self, button_id: int, state_code: int) -> TicketTask:
"""Создать новую задачу для button_id в заданном состоянии."""
return TicketTask(
task_id=button_id,
location=self._location_catalog.get_location(button_id),
state_code=state_code,
state_name=STATE_NAMES[state_code],
action_text=TICKET_STATE_ACTIONS[state_code],
color_hex=STATE_COLORS[state_code],
created_at=datetime.now(),
completed_at=datetime.now() if state_code == STATE_COMPLETED else None,
)
def _clone_with_state(self, task: TicketTask, state_code: int) -> TicketTask:
"""Склонировать задачу с новым доменным состоянием."""
return TicketTask(
task_id=task.task_id,
location=task.location,
state_code=state_code,
state_name=STATE_NAMES[state_code],
action_text=TICKET_STATE_ACTIONS[state_code],
color_hex=STATE_COLORS[state_code],
created_at=task.created_at,
completed_at=datetime.now() if state_code == STATE_COMPLETED else task.completed_at,
refused_from_state=task.refused_from_state,
refusal_reason=task.refusal_reason,
assigned_specialist=task.assigned_specialist,
specialist_photo=task.specialist_photo,
diagnostic_report_signed=task.diagnostic_report_signed,
repair_report_signed=task.repair_report_signed,
acceptance_report_signed=task.acceptance_report_signed,
sequence_number=task.sequence_number,
)
def _reset_task_cycle(self, task: TicketTask, state_code: int) -> TicketTask:
"""Начать новый цикл задачи на основе архивной или отказной записи."""
fresh_task = self._build_task_for_state(task.task_id, state_code)
fresh_task.location = task.location
return fresh_task