Files
Dispatch/Dispatch_V0.1.1/application/task_application_service.py
2026-04-29 08:18:54 +04:00

322 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
# hub/ticket/application/task_application_service.py
"""Единый application-фасад Ticket поверх state, domain и hardware gateway."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from PySide6.QtCore import QObject, Signal
from error_logger import log_exception
from domain import (
ArchiveRecordSnapshot,
TicketConnectionStatus,
TicketDocumentSnapshot,
TicketHardwareStatus,
TicketStateService,
TicketTaskSnapshot,
)
from domain.task import TicketTask
from domain.ticket_constants import STATE_COMPLETED, STATE_REFUSED
from services import ServiceManager, TicketHardwareGateway
from state import TicketRuntimeState
from .archive_service import ArchiveService
from .document_flow_service import DocumentFlowService
from .report_signing_service import ReportSigningService
from .specialist_assignment_service import SpecialistAssignmentService
class TaskApplicationService(QObject):
"""Канонический application facade Ticket."""
task_updated = Signal(object)
task_removed = Signal(int)
connection_changed = Signal(str, str)
active_view_changed = Signal(str)
state_loaded = Signal()
com_connection_changed = Signal(bool, str)
button_initialization_changed = Signal(bool, int)
def __init__(
self,
state: TicketRuntimeState | None = None,
hardware_gateway: TicketHardwareGateway | None = None,
state_service: TicketStateService | None = None,
specialist_service: SpecialistAssignmentService | None = None,
report_service: ReportSigningService | None = None,
archive_service: ArchiveService | None = None,
document_service: DocumentFlowService | None = None,
parent: QObject | None = None,
):
super().__init__(parent)
self._state = state or TicketRuntimeState()
self._hardware_gateway = hardware_gateway or ServiceManager(parent=self)
self._state_service = state_service or TicketStateService()
self._specialist_service = specialist_service or SpecialistAssignmentService(self._state)
self._report_service = report_service or ReportSigningService(self._state)
self._document_service = document_service or DocumentFlowService(self._state)
self._archive_service = archive_service or ArchiveService(
self._state,
self._state_service,
document_service=self._document_service,
)
self._started = False
self._connect_state_signals()
def start(self) -> None:
"""Загрузить state, синхронизировать gateway и запустить сервисы."""
if self._started:
return
self._started = True
self._state.load()
self._hardware_gateway.reset_button_states()
for task in self._state.list_tasks():
self._hardware_gateway.set_button_state(task.task_id, task.state_code)
self._hardware_gateway.set_observer(self)
self._hardware_gateway.start()
def stop(self) -> None:
"""Остановить gateway и снять observer."""
if not self._started:
return
self._started = False
self._hardware_gateway.set_observer(None)
self._hardware_gateway.stop()
def list_tasks(self) -> list[TicketTaskSnapshot]:
return list(self._state.list_tasks())
def list_active_tasks(self) -> list[TicketTaskSnapshot]:
return list(self._state.list_active_tasks())
def list_archived_tasks(self) -> list[TicketTaskSnapshot]:
return list(self._state.list_archived_tasks())
def list_archive_records(self) -> list[ArchiveRecordSnapshot]:
return self._archive_service.list_archive_records()
def get_task(self, task_id: int) -> TicketTaskSnapshot | None:
return self._state.get_task(task_id)
def handle_task_action(self, raw_action: Mapping[str, Any]) -> TicketTaskSnapshot | None:
"""Обработать аппаратное действие через доменную state-machine."""
button_id = self._normalize_int(raw_action.get("button_id"))
hardware_state = self._normalize_int(raw_action.get("hardware_state"))
if button_id is None or hardware_state is None:
return None
current_snapshot = self._state.get_task(button_id)
current_task = (
TicketTask.from_snapshot(current_snapshot)
if current_snapshot is not None
else None
)
result = self._state_service.handle_hardware_signal(
button_id,
hardware_state,
current_task,
)
if result.task is not None:
self._assign_sequence_on_terminal_state(result.task)
updated_snapshot = result.task.to_snapshot()
self._state.upsert_task(updated_snapshot)
self._hardware_gateway.set_button_state(
updated_snapshot.task_id,
updated_snapshot.state_code,
)
self._auto_save_archive_record(updated_snapshot)
return updated_snapshot
if current_snapshot is not None:
self._hardware_gateway.set_button_state(
current_snapshot.task_id,
current_snapshot.state_code,
)
return current_snapshot
def set_active_view(self, view_name: str) -> None:
self._state.active_view = view_name
def submit_new_task(self, snapshot: TicketTaskSnapshot) -> TicketTaskSnapshot | None:
"""Зарегистрировать заявку, созданную через форму Dispatch.
Назначение: добавляет в runtime-state готовый снимок задачи в
состоянии «Новая заявка» (`STATE_TODO`). Вызывает `upsert_task`,
который эмитит `task_updated` — доска подхватывает карточку без
дополнительной шины событий. Аппаратный шлюз в Dispatch
отключён (`NullHardwareGateway`), поэтому синхронизация
состояния кнопок не выполняется.
"""
if snapshot is None:
return None
self._state.upsert_task(snapshot)
return self._state.get_task(snapshot.task_id)
def allocate_new_task_id(self) -> int:
"""Выдать свободный идентификатор для заявки, созданной диспетчером."""
existing_ids = [task.task_id for task in self._state.list_tasks()]
base = max(existing_ids, default=0)
# Резервируем диапазон 1..99 за аппаратными button_id, чтобы при
# будущей реальной интеграции с COM-каналом идентификаторы заявок
# из формы не пересекались с физическими кнопками.
return max(base, 99) + 1
def assign_specialist(
self,
task_id: int,
specialist_name: str,
) -> TicketTaskSnapshot | None:
snapshot = self._specialist_service.assign_specialist(task_id, specialist_name)
return self._sync_gateway_state(snapshot)
def list_specialists(self) -> list[str]:
return self._specialist_service.list_specialists()
def sign_report(
self,
task_id: int,
report_type: str,
) -> TicketTaskSnapshot | None:
snapshot = self._report_service.sign_report(task_id, report_type)
return self._sync_gateway_state(snapshot)
def sign_acceptance_report(self, task_id: int) -> TicketTaskSnapshot | None:
snapshot = self._report_service.sign_acceptance_report(task_id)
return self._sync_gateway_state(snapshot)
def archive_task(self, task_id: int) -> TicketTaskSnapshot | None:
snapshot = self._archive_service.archive_task(task_id)
return self._sync_gateway_state(snapshot)
def refuse_task(
self,
task_id: int,
refusal_reason: str,
) -> TicketTaskSnapshot | None:
normalized_reason = str(refusal_reason or "").strip()
if not normalized_reason:
return None
current_snapshot = self._state.get_task(task_id)
current_task = (
TicketTask.from_snapshot(current_snapshot)
if current_snapshot is not None
else None
)
result = self._state_service.mark_task_as_refused(
task_id,
current_task,
normalized_reason,
)
if result.task is None:
return None
self._assign_sequence_on_terminal_state(result.task)
snapshot = result.task.to_snapshot()
self._state.upsert_task(snapshot)
self._auto_save_archive_record(snapshot)
return self._sync_gateway_state(snapshot)
def create_diagnostic_report(
self, task_id: int, initial_cause: str, actual_cause: str,
) -> TicketDocumentSnapshot:
return self._document_service.create_diagnostic_report(
task_id, initial_cause, actual_cause,
)
def create_repair_report(
self, task_id: int, work_done: str, used_parts: str, recommendations: str,
) -> TicketDocumentSnapshot:
return self._document_service.create_repair_report(
task_id, work_done, used_parts, recommendations,
)
def create_acceptance_report(
self, task_id: int, work_description: str,
executor_signature: str, customer_signature: str,
) -> TicketDocumentSnapshot:
return self._document_service.create_acceptance_report(
task_id, work_description, executor_signature, customer_signature,
)
def list_documents(
self,
document_type: str | None = None,
) -> list[TicketDocumentSnapshot]:
return self._document_service.list_documents(document_type)
def can_advance_to_confirmation(self, task_id: int) -> bool:
return self._report_service.can_advance_to_confirmation(task_id)
def can_advance_to_completed(self, task_id: int) -> bool:
return self._report_service.can_advance_to_completed(task_id)
def get_active_view(self) -> str:
return self._state.active_view
def get_gateway_status(self) -> TicketHardwareStatus:
return self._hardware_gateway.get_status()
def on_task_action(self, raw_action: Mapping[str, Any]) -> None:
"""Observer callback от hardware gateway."""
try:
self.handle_task_action(raw_action)
except Exception as exc:
log_exception(__name__, "TaskApplicationService.on_task_action", exc)
def on_gateway_status(self, status: TicketHardwareStatus) -> None:
"""Observer callback обновления статуса hardware gateway."""
if status.connection_status == TicketConnectionStatus.ERROR:
self._state.set_error(status.message)
else:
self._state.connection_status = status.connection_status
self._state.set_com_connection(
status.connection_status == TicketConnectionStatus.CONNECTED,
status.message,
)
self._state.set_button_initialization(
status.buttons_initialized,
status.button_count,
)
def on_gateway_error(self, message: str) -> None:
"""Observer callback ошибки hardware gateway."""
self._state.set_error(message)
def _connect_state_signals(self) -> None:
self._state.task_updated.connect(self.task_updated.emit)
self._state.task_removed.connect(self.task_removed.emit)
self._state.connection_changed.connect(self.connection_changed.emit)
self._state.active_view_changed.connect(self.active_view_changed.emit)
self._state.state_loaded.connect(self.state_loaded.emit)
self._state.com_connection_changed.connect(self.com_connection_changed.emit)
self._state.button_initialization_changed.connect(
self.button_initialization_changed.emit
)
def _sync_gateway_state(
self,
snapshot: TicketTaskSnapshot | None,
) -> TicketTaskSnapshot | None:
if snapshot is None:
return None
self._hardware_gateway.set_button_state(snapshot.task_id, snapshot.state_code)
return snapshot
def _assign_sequence_on_terminal_state(self, task: TicketTask) -> None:
"""Присвоить сквозной номер при переходе в Выполненные/Отказ."""
if task.state_code in {STATE_COMPLETED, STATE_REFUSED}:
task.sequence_number = self._state.next_sequence_number()
def _auto_save_archive_record(self, snapshot: TicketTaskSnapshot) -> None:
if snapshot.state_code in {STATE_COMPLETED, STATE_REFUSED}:
self._archive_service.ensure_archive_record(snapshot)
@staticmethod
def _normalize_int(value: Any) -> int | None:
try:
return int(value)
except (TypeError, ValueError):
return None