Add Dispatch_V0.1.1
This commit is contained in:
208
Dispatch_V0.1.1/domain/ticket_state_service.py
Normal file
208
Dispatch_V0.1.1/domain/ticket_state_service.py
Normal 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
|
||||
Reference in New Issue
Block a user