Add Dispatch_V0.1.1
This commit is contained in:
20
Dispatch_V0.1.1/application/__init__.py
Normal file
20
Dispatch_V0.1.1/application/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/application/__init__.py
|
||||
|
||||
"""Публичный application-контур Ticket."""
|
||||
|
||||
from .archive_service import ArchiveService
|
||||
from .document_flow_service import DocumentFlowService
|
||||
from .report_signing_service import ReportSigningService
|
||||
from .specialist_assignment_service import SpecialistAssignmentService
|
||||
from .task_application_service import TaskApplicationService
|
||||
from .ticket_application_api import TicketApplicationApi
|
||||
|
||||
__all__ = [
|
||||
"ArchiveService",
|
||||
"DocumentFlowService",
|
||||
"ReportSigningService",
|
||||
"SpecialistAssignmentService",
|
||||
"TaskApplicationService",
|
||||
"TicketApplicationApi",
|
||||
]
|
||||
BIN
Dispatch_V0.1.1/application/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
Dispatch_V0.1.1/application/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
80
Dispatch_V0.1.1/application/archive_service.py
Normal file
80
Dispatch_V0.1.1/application/archive_service.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/application/archive_service.py
|
||||
|
||||
"""Application-сервис архивации задач Ticket."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from error_logger import log_exception
|
||||
|
||||
from domain import ArchiveRecordSnapshot, TicketStateService
|
||||
from domain.task import TicketTask
|
||||
from state import ArchiveRecordRepository, TicketStateApi
|
||||
from .document_flow_service import DocumentFlowService
|
||||
|
||||
|
||||
class ArchiveService:
|
||||
"""Команда архивации поверх доменного сервиса и state API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
state: TicketStateApi,
|
||||
state_service: TicketStateService,
|
||||
archive_repository: ArchiveRecordRepository | None = None,
|
||||
document_service: DocumentFlowService | None = None,
|
||||
):
|
||||
self._state = state
|
||||
self._state_service = state_service
|
||||
self._archive_repository = archive_repository or ArchiveRecordRepository()
|
||||
self._document_service = document_service
|
||||
|
||||
def archive_task(self, task_id: int):
|
||||
"""Перевести задачу в архив и сохранить архивную запись."""
|
||||
snapshot = self._state.get_task(task_id)
|
||||
if snapshot is None:
|
||||
return None
|
||||
|
||||
self.ensure_archive_record(snapshot)
|
||||
|
||||
result = self._state_service.move_task_to_archive(
|
||||
task_id,
|
||||
TicketTask.from_snapshot(snapshot),
|
||||
)
|
||||
if result.task is None:
|
||||
return None
|
||||
archived_snapshot = result.task.to_snapshot()
|
||||
self._state.upsert_task(archived_snapshot)
|
||||
return archived_snapshot
|
||||
|
||||
def list_archive_records(self) -> list[ArchiveRecordSnapshot]:
|
||||
"""Вернуть все архивные записи из файлового хранилища."""
|
||||
return self._archive_repository.list_records()
|
||||
|
||||
def ensure_archive_record(self, snapshot) -> None:
|
||||
"""Сохранить архивную запись, если её ещё нет для данного цикла задачи."""
|
||||
try:
|
||||
cycle_token = self._build_cycle_token(snapshot)
|
||||
if self._archive_repository.has_record(snapshot.task_id, cycle_token):
|
||||
return
|
||||
documents = self._collect_cycle_documents(snapshot)
|
||||
self._archive_repository.save_record(snapshot, documents)
|
||||
except Exception as exc:
|
||||
log_exception(__name__, "ArchiveService.ensure_archive_record", exc)
|
||||
|
||||
def _collect_cycle_documents(self, snapshot) -> list:
|
||||
"""Вернуть документы только текущего цикла задачи."""
|
||||
if self._document_service is None:
|
||||
return []
|
||||
cycle_token = self._build_cycle_token(snapshot)
|
||||
all_docs = self._document_service.list_documents()
|
||||
return [
|
||||
d for d in all_docs
|
||||
if d.task_id == snapshot.task_id and cycle_token in d.document_id
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _build_cycle_token(snapshot) -> str:
|
||||
from datetime import datetime as _dt
|
||||
if snapshot.created_at is not None:
|
||||
return snapshot.created_at.strftime("%Y%m%d_%H%M%S")
|
||||
return _dt.now().strftime("%Y%m%d_%H%M%S")
|
||||
230
Dispatch_V0.1.1/application/document_flow_service.py
Normal file
230
Dispatch_V0.1.1/application/document_flow_service.py
Normal file
@@ -0,0 +1,230 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/application/document_flow_service.py
|
||||
|
||||
"""Application-сервис генерации документов Ticket."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from domain import TicketDocumentSnapshot, TicketTaskSnapshot, parse_location_parts
|
||||
from domain.task import TicketTask
|
||||
from domain.ticket_constants import STATE_CONFIRMATION, STATE_IN_PROGRESS
|
||||
from state import TicketDocumentRepository, TicketStateApi
|
||||
|
||||
|
||||
class DocumentFlowService:
|
||||
"""Канонический document-flow Ticket поверх state и файлового репозитория."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
state: TicketStateApi,
|
||||
repository: TicketDocumentRepository | None = None,
|
||||
):
|
||||
self._state = state
|
||||
self._repository = repository or TicketDocumentRepository()
|
||||
|
||||
def create_diagnostic_report(
|
||||
self,
|
||||
task_id: int,
|
||||
initial_cause: str,
|
||||
actual_cause: str,
|
||||
) -> TicketDocumentSnapshot:
|
||||
task = self._get_task_or_raise(task_id)
|
||||
self._ensure_in_progress(task, "Диагностический отчёт")
|
||||
self._ensure_specialist_assigned(task)
|
||||
if task.diagnostic_report_signed:
|
||||
raise ValueError("Диагностический отчёт уже подписан.")
|
||||
if not initial_cause.strip() or not actual_cause.strip():
|
||||
raise ValueError("Заполните первичное и вторичное заключения.")
|
||||
payload = self._base_payload(task)
|
||||
payload.update(
|
||||
{
|
||||
"initial_cause": initial_cause.strip(),
|
||||
"actual_cause": actual_cause.strip(),
|
||||
}
|
||||
)
|
||||
document = self._save_document(
|
||||
task=task,
|
||||
document_type="diagnostic",
|
||||
title=f"Диагностический отчёт по задаче #{task.sequence_number or task.task_id}",
|
||||
summary=actual_cause.strip(),
|
||||
payload=payload,
|
||||
content_builder=self._render_diagnostic_report,
|
||||
)
|
||||
self._state.sign_report(task.task_id, "diagnostic")
|
||||
return document
|
||||
|
||||
def create_repair_report(
|
||||
self,
|
||||
task_id: int,
|
||||
work_done: str,
|
||||
used_parts: str,
|
||||
recommendations: str,
|
||||
) -> TicketDocumentSnapshot:
|
||||
task = self._get_task_or_raise(task_id)
|
||||
self._ensure_in_progress(task, "Ремонтный отчёт")
|
||||
self._ensure_specialist_assigned(task)
|
||||
if task.repair_report_signed:
|
||||
raise ValueError("Ремонтный отчёт уже подписан.")
|
||||
if not work_done.strip():
|
||||
raise ValueError("Заполните поле 'Выполненные работы'.")
|
||||
payload = self._base_payload(task)
|
||||
payload.update(
|
||||
{
|
||||
"work_done": work_done.strip(),
|
||||
"used_parts": used_parts.strip(),
|
||||
"recommendations": recommendations.strip(),
|
||||
}
|
||||
)
|
||||
document = self._save_document(
|
||||
task=task,
|
||||
document_type="repair",
|
||||
title=f"Ремонтный отчёт по задаче #{task.sequence_number or task.task_id}",
|
||||
summary=work_done.strip(),
|
||||
payload=payload,
|
||||
content_builder=self._render_repair_report,
|
||||
)
|
||||
self._state.sign_report(task.task_id, "repair")
|
||||
return document
|
||||
|
||||
def create_acceptance_report(
|
||||
self,
|
||||
task_id: int,
|
||||
work_description: str,
|
||||
executor_signature: str,
|
||||
customer_signature: str,
|
||||
) -> TicketDocumentSnapshot:
|
||||
task = self._get_task_or_raise(task_id)
|
||||
if task.state_code != STATE_CONFIRMATION:
|
||||
raise ValueError("Акт приёмки доступен только в состоянии подтверждения.")
|
||||
if task.acceptance_report_signed:
|
||||
raise ValueError("Акт приёмки уже подписан.")
|
||||
if not task.diagnostic_report_signed or not task.repair_report_signed:
|
||||
raise ValueError("Сначала подпишите диагностический и ремонтный отчёты.")
|
||||
if not work_description.strip():
|
||||
raise ValueError("Заполните описание выполненных работ.")
|
||||
if not executor_signature.strip() or not customer_signature.strip():
|
||||
raise ValueError("Укажите подписи исполнителя и заказчика.")
|
||||
payload = self._base_payload(task)
|
||||
payload.update(
|
||||
{
|
||||
"work_description": work_description.strip(),
|
||||
"executor_signature": executor_signature.strip(),
|
||||
"customer_signature": customer_signature.strip(),
|
||||
}
|
||||
)
|
||||
document = self._save_document(
|
||||
task=task,
|
||||
document_type="acceptance",
|
||||
title=f"Акт приёмки по задаче #{task.sequence_number or task.task_id}",
|
||||
summary=work_description.strip(),
|
||||
payload=payload,
|
||||
content_builder=self._render_acceptance_report,
|
||||
)
|
||||
task_data = TicketTask.from_snapshot(task).to_record()
|
||||
task_data["acceptance_report_signed"] = True
|
||||
self._state.update_task(task_data)
|
||||
return document
|
||||
|
||||
def list_documents(
|
||||
self,
|
||||
document_type: str | None = None,
|
||||
) -> list[TicketDocumentSnapshot]:
|
||||
"""Вернуть отсортированный список документов Ticket."""
|
||||
return self._repository.list_documents(document_type)
|
||||
|
||||
def _save_document(
|
||||
self,
|
||||
task: TicketTaskSnapshot,
|
||||
document_type: str,
|
||||
title: str,
|
||||
summary: str,
|
||||
payload: dict[str, str],
|
||||
content_builder: Callable[[dict[str, str]], str],
|
||||
) -> TicketDocumentSnapshot:
|
||||
document = self._repository.save_document(
|
||||
task=task,
|
||||
document_type=document_type,
|
||||
title=title,
|
||||
summary=summary,
|
||||
content=content_builder(payload),
|
||||
payload=payload,
|
||||
)
|
||||
if document is None:
|
||||
raise ValueError("Не удалось сохранить документ Ticket.")
|
||||
return document
|
||||
|
||||
def _get_task_or_raise(self, task_id: int) -> TicketTaskSnapshot:
|
||||
task = self._state.get_task(task_id)
|
||||
if task is None:
|
||||
raise ValueError(f"Задача #{task_id} не найдена.")
|
||||
return task
|
||||
|
||||
@staticmethod
|
||||
def _ensure_in_progress(task: TicketTaskSnapshot, document_name: str) -> None:
|
||||
if task.state_code != STATE_IN_PROGRESS:
|
||||
raise ValueError(f"{document_name} можно подписать только в состоянии 'В работе'.")
|
||||
|
||||
@staticmethod
|
||||
def _ensure_specialist_assigned(task: TicketTaskSnapshot) -> None:
|
||||
if not task.assigned_specialist.strip():
|
||||
raise ValueError("Сначала назначьте специалиста.")
|
||||
|
||||
@staticmethod
|
||||
def _base_payload(task: TicketTaskSnapshot) -> dict[str, str]:
|
||||
institution, room, device = parse_location_parts(task.location)
|
||||
return {
|
||||
"task_id": str(task.sequence_number or task.task_id),
|
||||
"institution": institution,
|
||||
"room": room,
|
||||
"device": device,
|
||||
"location": task.location,
|
||||
"specialist": task.assigned_specialist,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _render_diagnostic_report(payload: dict[str, str]) -> str:
|
||||
return (
|
||||
f"ДИАГНОСТИЧЕСКИЙ ОТЧЁТ #{payload['task_id']}\n\n"
|
||||
f"Учреждение: {payload.get('institution', '—')}\n"
|
||||
f"Оборудование: {payload.get('device', '—')}\n"
|
||||
f"Кабинет: {payload.get('room', '—')}\n"
|
||||
f"Специалист: {payload.get('specialist', '—')}\n\n"
|
||||
"Первичное заключение:\n"
|
||||
f"{payload.get('initial_cause', '—')}\n\n"
|
||||
"Вторичное заключение:\n"
|
||||
f"{payload.get('actual_cause', '—')}\n"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _render_repair_report(payload: dict[str, str]) -> str:
|
||||
return (
|
||||
f"РЕМОНТНЫЙ ОТЧЁТ #{payload['task_id']}\n\n"
|
||||
f"Учреждение: {payload.get('institution', '—')}\n"
|
||||
f"Оборудование: {payload.get('device', '—')}\n"
|
||||
f"Кабинет: {payload.get('room', '—')}\n"
|
||||
f"Специалист: {payload.get('specialist', '—')}\n\n"
|
||||
"Выполненные работы:\n"
|
||||
f"{payload.get('work_done', '—')}\n\n"
|
||||
"Использованные запчасти:\n"
|
||||
f"{payload.get('used_parts', '—')}\n\n"
|
||||
"Рекомендации:\n"
|
||||
f"{payload.get('recommendations', '—')}\n"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _render_acceptance_report(payload: dict[str, str]) -> str:
|
||||
return (
|
||||
f"АКТ ПРИЁМКИ #{payload['task_id']}\n\n"
|
||||
f"Учреждение: {payload.get('institution', '—')}\n"
|
||||
f"Оборудование: {payload.get('device', '—')}\n"
|
||||
f"Кабинет: {payload.get('room', '—')}\n"
|
||||
f"Специалист: {payload.get('specialist', '—')}\n\n"
|
||||
"Описание выполненных работ:\n"
|
||||
f"{payload.get('work_description', '—')}\n\n"
|
||||
"Исполнитель:\n"
|
||||
f"{payload.get('executor_signature', '—')}\n\n"
|
||||
"Заказчик:\n"
|
||||
f"{payload.get('customer_signature', '—')}\n"
|
||||
)
|
||||
50
Dispatch_V0.1.1/application/report_signing_service.py
Normal file
50
Dispatch_V0.1.1/application/report_signing_service.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/application/report_signing_service.py
|
||||
|
||||
"""Application-сервис подписания отчётов Ticket."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from domain.task import TicketTask
|
||||
from domain.ticket_constants import STATE_CONFIRMATION
|
||||
from state import TicketStateApi
|
||||
|
||||
|
||||
class ReportSigningService:
|
||||
"""Команды подписания отчётов поверх канонического state API."""
|
||||
|
||||
def __init__(self, state: TicketStateApi):
|
||||
self._state = state
|
||||
|
||||
def sign_report(self, task_id: int, report_type: str):
|
||||
"""Подписать диагностический или ремонтный отчёт."""
|
||||
snapshot = self._state.get_task(task_id)
|
||||
if snapshot is None:
|
||||
return None
|
||||
if report_type not in {"diagnostic", "repair"}:
|
||||
return None
|
||||
self._state.sign_report(task_id, report_type)
|
||||
return self._state.get_task(task_id)
|
||||
|
||||
def sign_acceptance_report(self, task_id: int):
|
||||
"""Подписать акт приёмки без смены доменного состояния."""
|
||||
snapshot = self._state.get_task(task_id)
|
||||
if snapshot is None:
|
||||
return None
|
||||
task_data = TicketTask.from_snapshot(snapshot).to_record()
|
||||
task_data["acceptance_report_signed"] = True
|
||||
return self._state.update_task(task_data)
|
||||
|
||||
def can_advance_to_confirmation(self, task_id: int) -> bool:
|
||||
"""Проверить готовность задачи к переходу в подтверждение."""
|
||||
return self._state.can_advance_to_confirmation(task_id)
|
||||
|
||||
def can_advance_to_completed(self, task_id: int) -> bool:
|
||||
"""Проверить готовность задачи к переходу в выполненные."""
|
||||
snapshot = self._state.get_task(task_id)
|
||||
if snapshot is None:
|
||||
return False
|
||||
return (
|
||||
snapshot.state_code == STATE_CONFIRMATION
|
||||
and snapshot.acceptance_report_signed
|
||||
)
|
||||
42
Dispatch_V0.1.1/application/specialist_assignment_service.py
Normal file
42
Dispatch_V0.1.1/application/specialist_assignment_service.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/application/specialist_assignment_service.py
|
||||
|
||||
"""Application-сервис назначения специалистов в Ticket."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from domain.task import TicketTask
|
||||
from state import TicketStateApi
|
||||
|
||||
|
||||
SPECIALIST_PHOTOS = {
|
||||
"Иванов Алексей Сергеевич": "specialist1.png",
|
||||
"Петрова Мария Владимировна": "specialist2.png",
|
||||
"Сидоров Дмитрий Иванович": "specialist3.png",
|
||||
"Козлова Анна Петровна": "specialist4.png",
|
||||
"Васильев Сергей Николаевич": "specialist5.png",
|
||||
"Николаева Ольга Дмитриевна": "specialist6.png",
|
||||
"Фёдоров Андрей Викторович": "specialist7.png",
|
||||
"Орлова Екатерина Александровна": "specialist8.png",
|
||||
}
|
||||
|
||||
|
||||
class SpecialistAssignmentService:
|
||||
"""Команда назначения специалиста поверх канонического state API."""
|
||||
|
||||
def __init__(self, state: TicketStateApi):
|
||||
self._state = state
|
||||
|
||||
def assign_specialist(self, task_id: int, specialist_name: str):
|
||||
"""Назначить специалиста и сохранить фотографию профиля."""
|
||||
snapshot = self._state.get_task(task_id)
|
||||
if snapshot is None:
|
||||
return None
|
||||
task_data = TicketTask.from_snapshot(snapshot).to_record()
|
||||
task_data["assigned_specialist"] = specialist_name
|
||||
task_data["specialist_photo"] = SPECIALIST_PHOTOS.get(specialist_name, "")
|
||||
return self._state.update_task(task_data)
|
||||
|
||||
def list_specialists(self) -> list[str]:
|
||||
"""Вернуть канонический список доступных специалистов."""
|
||||
return list(SPECIALIST_PHOTOS.keys())
|
||||
321
Dispatch_V0.1.1/application/task_application_service.py
Normal file
321
Dispatch_V0.1.1/application/task_application_service.py
Normal file
@@ -0,0 +1,321 @@
|
||||
# -*- 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
|
||||
115
Dispatch_V0.1.1/application/ticket_application_api.py
Normal file
115
Dispatch_V0.1.1/application/ticket_application_api.py
Normal file
@@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# hub/ticket/application/ticket_application_api.py
|
||||
|
||||
"""Публичный application API модуля Ticket."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Mapping, Protocol, Sequence
|
||||
|
||||
from domain import TicketDocumentSnapshot, TicketTaskSnapshot
|
||||
|
||||
|
||||
class TicketApplicationApi(Protocol):
|
||||
"""Контракт orchestration-слоя между UI, state и сервисами."""
|
||||
|
||||
def start(self) -> None:
|
||||
"""Инициализировать application-слой Ticket."""
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Остановить активные операции и освободить ресурсы."""
|
||||
|
||||
def list_tasks(self) -> Sequence[TicketTaskSnapshot]:
|
||||
"""Вернуть текущий срез задач для UI."""
|
||||
|
||||
def list_active_tasks(self) -> Sequence[TicketTaskSnapshot]:
|
||||
"""Вернуть активные задачи для UI."""
|
||||
|
||||
def list_archived_tasks(self) -> Sequence[TicketTaskSnapshot]:
|
||||
"""Вернуть архивные задачи для UI."""
|
||||
|
||||
def get_task(self, task_id: int) -> TicketTaskSnapshot | None:
|
||||
"""Вернуть задачу по идентификатору."""
|
||||
|
||||
def handle_task_action(
|
||||
self,
|
||||
raw_action: Mapping[str, Any],
|
||||
) -> TicketTaskSnapshot | None:
|
||||
"""Обработать входящее действие от аппаратного или mock-шлюза."""
|
||||
|
||||
def set_active_view(self, view_name: str) -> None:
|
||||
"""Переключить активное представление Ticket."""
|
||||
|
||||
def assign_specialist(
|
||||
self,
|
||||
task_id: int,
|
||||
specialist_name: str,
|
||||
) -> TicketTaskSnapshot | None:
|
||||
"""Назначить специалиста на задачу."""
|
||||
|
||||
def list_specialists(self) -> Sequence[str]:
|
||||
"""Вернуть список доступных специалистов."""
|
||||
|
||||
def sign_report(
|
||||
self,
|
||||
task_id: int,
|
||||
report_type: str,
|
||||
) -> TicketTaskSnapshot | None:
|
||||
"""Подписать диагностический или ремонтный отчёт."""
|
||||
|
||||
def sign_acceptance_report(self, task_id: int) -> TicketTaskSnapshot | None:
|
||||
"""Подписать акт приёмки."""
|
||||
|
||||
def archive_task(self, task_id: int) -> TicketTaskSnapshot | None:
|
||||
"""Перевести задачу в архив."""
|
||||
|
||||
def refuse_task(
|
||||
self,
|
||||
task_id: int,
|
||||
refusal_reason: str,
|
||||
) -> TicketTaskSnapshot | None:
|
||||
"""Перевести задачу в отказ."""
|
||||
|
||||
def create_diagnostic_report(
|
||||
self,
|
||||
task_id: int,
|
||||
initial_cause: str,
|
||||
actual_cause: str,
|
||||
) -> TicketDocumentSnapshot:
|
||||
"""Создать и подписать диагностический отчёт."""
|
||||
|
||||
def create_repair_report(
|
||||
self,
|
||||
task_id: int,
|
||||
work_done: str,
|
||||
used_parts: str,
|
||||
recommendations: str,
|
||||
) -> TicketDocumentSnapshot:
|
||||
"""Создать и подписать ремонтный отчёт."""
|
||||
|
||||
def create_acceptance_report(
|
||||
self,
|
||||
task_id: int,
|
||||
work_description: str,
|
||||
executor_signature: str,
|
||||
customer_signature: str,
|
||||
) -> TicketDocumentSnapshot:
|
||||
"""Создать и подписать акт приёмки."""
|
||||
|
||||
def list_documents(
|
||||
self,
|
||||
document_type: str | None = None,
|
||||
) -> Sequence[TicketDocumentSnapshot]:
|
||||
"""Вернуть список документов Ticket."""
|
||||
|
||||
def can_advance_to_confirmation(self, task_id: int) -> bool:
|
||||
"""Проверить готовность задачи к переходу в подтверждение."""
|
||||
|
||||
def can_advance_to_completed(self, task_id: int) -> bool:
|
||||
"""Проверить готовность задачи к переходу в выполненные."""
|
||||
|
||||
def get_active_view(self) -> str:
|
||||
"""Вернуть имя активного внутреннего представления Ticket."""
|
||||
|
||||
def get_gateway_status(self):
|
||||
"""Вернуть последний известный статус hardware gateway."""
|
||||
Reference in New Issue
Block a user