BUH_bot/gui_app.py
Игорь Надеин 5155e455d2
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
Add Gitea release updates and local Windows release pipeline
2026-09-16 15:03:17 +03:00

1114 lines
41 KiB
Python
Raw Permalink 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.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GUI приложение для работы с МойСклад
Генерация XML документов для 1С
"""
import sys
import os
import logging
from pathlib import Path
from typing import Optional, List, Dict
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QLineEdit, QTextEdit, QTableWidget,
QTableWidgetItem, QTabWidget, QFileDialog, QMessageBox,
QProgressBar, QGroupBox, QFormLayout, QComboBox, QSpinBox,
QCheckBox, QDialog, QDialogButtonBox
)
from PyQt6.QtCore import Qt, QThread, QTimer, QUrl, pyqtSignal
from PyQt6.QtGui import QDesktopServices, QFont, QIcon, QPalette, QColor
# Добавляем путь к modules
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'modules'))
from modules.moysklad_client import MoySkladClient
from modules.validator import OrderValidator, ValidationLevel
from modules.xml_generator import XML1CGenerator
from modules.release_updater import (
ReleaseInfo, download_installer, get_latest_release, update_available,
)
from app_version import APP_VERSION
from config import settings
class ThemeManager:
"""Управление системной темой приложения."""
LIGHT = "light"
DARK = "dark"
_original_palette: Optional[QPalette] = None
_original_style: Optional[str] = None
_original_stylesheet: Optional[str] = None
@staticmethod
def detect_system_theme() -> str:
"""Определение системной темы."""
if sys.platform == "win32":
try:
import winreg # type: ignore
key_path = (
r"Software\Microsoft\Windows\CurrentVersion"
r"\Themes\Personalize"
)
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) as key:
value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
if value == 0:
return ThemeManager.DARK
return ThemeManager.LIGHT
except OSError as error:
logger.warning(
"Не удалось прочитать системную тему Windows: %s",
error,
)
except ImportError:
logger.warning(
"Модуль winreg недоступен, используется светлая тема"
)
return ThemeManager.LIGHT
@classmethod
def apply(cls, app: QApplication) -> str:
"""Применение системной темы к приложению."""
theme = cls.detect_system_theme()
if cls._original_palette is None:
cls._original_palette = app.palette()
if cls._original_style is None:
cls._original_style = app.style().objectName()
if cls._original_stylesheet is None:
cls._original_stylesheet = app.styleSheet()
if theme == cls.DARK:
cls._apply_dark_theme(app)
else:
cls._apply_light_theme(app)
logger.info("Применена системная тема: %s", theme)
return theme
@staticmethod
def _apply_dark_theme(app: QApplication) -> None:
"""Применение темной темы."""
palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor(37, 37, 38))
palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.Base, QColor(30, 30, 30))
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(45, 45, 45))
palette.setColor(QPalette.ColorRole.ToolTipBase, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.ToolTipText, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.Button, QColor(45, 45, 45))
palette.setColor(QPalette.ColorRole.ButtonText, Qt.GlobalColor.white)
palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)
palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
palette.setColor(QPalette.ColorRole.Highlight, QColor(45, 140, 240))
palette.setColor(
QPalette.ColorRole.HighlightedText,
Qt.GlobalColor.black,
)
palette.setColor(
QPalette.ColorRole.PlaceholderText,
QColor(200, 200, 200, 120),
)
palette.setColor(QPalette.ColorRole.Shadow, QColor(12, 12, 12))
app.setStyle("Fusion")
app.setPalette(palette)
app.setStyleSheet(
"QToolTip {"
" background-color: #2a82da;"
" color: #ffffff;"
" border: 1px solid white;"
" }"
)
@classmethod
def _apply_light_theme(cls, app: QApplication) -> None:
"""Возврат к светлой теме системы."""
if cls._original_style:
app.setStyle(cls._original_style)
if cls._original_palette:
app.setPalette(cls._original_palette)
if cls._original_stylesheet is not None:
app.setStyleSheet(cls._original_stylesheet)
class WorkerThread(QThread):
"""Фоновый поток для долгих операций"""
progress = pyqtSignal(int, str)
finished = pyqtSignal(object)
error = pyqtSignal(str)
def __init__(self, func, *args, **kwargs):
super().__init__()
self.func = func
self.args = args
self.kwargs = kwargs
def run(self):
try:
logger.info(f"Запуск фоновой задачи: {self.func.__name__}")
result = self.func(*self.args, **self.kwargs)
logger.info(f"Задача завершена: {self.func.__name__}")
self.finished.emit(result)
except Exception as e:
logger.error(
f"Ошибка в задаче {self.func.__name__}: {e}",
exc_info=True
)
self.error.emit(str(e))
class UpdateWorker(QThread):
"""Run release checks and installer downloads outside the GUI thread."""
def __init__(self, operation, *args):
super().__init__()
self.operation = operation
self.args = args
self.result_value = None
self.error_message = None
def run(self):
try:
self.result_value = self.operation(*self.args)
except Exception as error:
logger.exception("Ошибка обновления приложения")
self.error_message = str(error)
class LoginDialog(QDialog):
"""Диалог авторизации"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Авторизация МойСклад")
self.setModal(True)
self.setMinimumWidth(400)
layout = QFormLayout()
self.login_input = QLineEdit()
self.login_input.setPlaceholderText("Логин")
layout.addRow("Логин:", self.login_input)
self.password_input = QLineEdit()
self.password_input.setPlaceholderText("Пароль")
self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
layout.addRow("Пароль:", self.password_input)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok |
QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addRow(buttons)
self.setLayout(layout)
def get_credentials(self):
return self.login_input.text(), self.password_input.text()
class MainWindow(QMainWindow):
"""Главное окно приложения"""
def __init__(self):
super().__init__()
self.client: Optional[MoySkladClient] = None
self.validator = OrderValidator()
self.xml_generator = XML1CGenerator()
self.current_orders: List[Dict] = []
self.selected_order_data: Optional[Dict] = None
self.worker_thread: Optional[WorkerThread] = None
self.update_worker: Optional[UpdateWorker] = None
self.latest_release: Optional[ReleaseInfo] = None
self.manual_update_check = False
self.downloading_update = False
self.current_page = 0
self.page_size = 25
self.total_orders = 0
self.init_ui()
self.check_credentials()
QTimer.singleShot(0, self.check_for_updates)
def init_ui(self):
"""Инициализация UI"""
self.setWindowTitle(f"МойСклад - Генератор документов {APP_VERSION}")
self.setGeometry(100, 100, 1200, 800)
# Установка иконки приложения
icon_path = os.path.join(os.path.dirname(__file__), 'app_icon.png')
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
# Центральный виджет
central_widget = QWidget()
self.setCentralWidget(central_widget)
# Главный layout
main_layout = QVBoxLayout()
central_widget.setLayout(main_layout)
# Строка статуса
status_layout = QHBoxLayout()
self.status_label = QLabel("Не авторизован")
self.status_label.setStyleSheet("color: red; font-weight: bold;")
status_layout.addWidget(self.status_label)
status_layout.addStretch()
self.version_label = QLabel(f"Версия {APP_VERSION}")
status_layout.addWidget(self.version_label)
self.update_label = QLabel("")
status_layout.addWidget(self.update_label)
self.update_btn = QPushButton("Проверить обновления")
self.update_btn.setMinimumWidth(190)
self.update_btn.setStyleSheet(
"QPushButton { background-color: #2676c9; color: white; "
"font-weight: bold; padding: 7px 12px; border-radius: 4px; }"
"QPushButton:disabled { background-color: #777; }"
)
self.update_btn.clicked.connect(self.on_update_clicked)
status_layout.addWidget(self.update_btn)
self.login_btn = QPushButton("Войти")
self.login_btn.clicked.connect(self.show_login_dialog)
status_layout.addWidget(self.login_btn)
self.logout_btn = QPushButton("Выйти")
self.logout_btn.clicked.connect(self.logout)
self.logout_btn.setEnabled(False)
status_layout.addWidget(self.logout_btn)
main_layout.addLayout(status_layout)
# Tabs
self.tabs = QTabWidget()
main_layout.addWidget(self.tabs)
# Вкладка поиска
self.search_tab = self.create_search_tab()
self.tabs.addTab(self.search_tab, "🔍 Поиск")
# Вкладка списка
self.list_tab = self.create_list_tab()
self.tabs.addTab(self.list_tab, "📋 Список отгрузок")
# Вкладка массовой генерации
self.batch_tab = self.create_batch_tab()
self.tabs.addTab(self.batch_tab, "📦 Массовая генерация")
# Progress bar
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
main_layout.addWidget(self.progress_bar)
def check_for_updates(self, manual: bool = False):
"""Check the latest published Gitea release without blocking the GUI."""
if self.update_worker and self.update_worker.isRunning():
return
self.manual_update_check = manual
self.downloading_update = False
self.update_btn.setEnabled(False)
self.update_label.setText("Проверка…")
self.update_worker = UpdateWorker(get_latest_release)
self.update_worker.finished.connect(self.on_check_finished)
self.update_worker.start()
def on_check_finished(self):
worker = self.update_worker
if worker.error_message:
self.on_update_failed(worker.error_message)
else:
self.on_release_checked(worker.result_value)
def on_release_checked(self, release: Optional[ReleaseInfo]):
self.latest_release = release
self.update_btn.setEnabled(True)
if release is None:
self.update_label.setText("Релизов пока нет")
self.update_btn.setText("Проверить обновления")
if self.manual_update_check:
QMessageBox.information(self, "Обновления", "Релизы пока не опубликованы.")
elif update_available(release):
self.update_label.setText(f"Доступна {release.tag_name}")
self.update_btn.setText(f"Обновить до {release.tag_name}")
if self.manual_update_check:
self.show_update_dialog(release)
else:
self.update_label.setText("Актуальная версия")
self.update_btn.setText("Проверить обновления")
if self.manual_update_check:
QMessageBox.information(
self, "Обновления",
f"Установлена версия {APP_VERSION}. Последний релиз: {release.tag_name}.",
)
def on_update_clicked(self):
if self.latest_release and update_available(self.latest_release):
self.show_update_dialog(self.latest_release)
else:
self.check_for_updates(manual=True)
def show_update_dialog(self, release: ReleaseInfo):
dialog = QMessageBox(self)
dialog.setWindowTitle("Обновление приложения")
dialog.setTextFormat(Qt.TextFormat.PlainText)
dialog.setText(f"Доступна версия {release.tag_name} (установлена {APP_VERSION}).")
if release.notes:
dialog.setInformativeText(release.notes[:700])
if len(release.notes) > 700:
dialog.setDetailedText(release.notes)
install_button = dialog.addButton(
"Скачать и установить", QMessageBox.ButtonRole.AcceptRole
)
release_button = dialog.addButton(
"Открыть релиз", QMessageBox.ButtonRole.ActionRole
)
dialog.addButton("Позже", QMessageBox.ButtonRole.RejectRole)
dialog.exec()
if dialog.clickedButton() == release_button:
QDesktopServices.openUrl(QUrl(release.page_url))
elif dialog.clickedButton() == install_button:
self.download_update(release)
def download_update(self, release: ReleaseInfo):
if self.update_worker and self.update_worker.isRunning():
return
self.downloading_update = True
self.update_btn.setEnabled(False)
self.update_btn.setText("Загрузка…")
self.update_label.setText("Загрузка установщика…")
self.update_worker = UpdateWorker(download_installer, release)
self.update_worker.finished.connect(self.on_download_finished)
self.update_worker.start()
def on_download_finished(self):
worker = self.update_worker
if worker.error_message:
self.on_update_failed(worker.error_message)
else:
self.on_installer_ready(worker.result_value)
def on_installer_ready(self, installer_path: Path):
try:
os.startfile(str(installer_path))
except OSError as error:
self.on_update_failed(f"Не удалось запустить установщик: {error}")
return
QApplication.instance().quit()
def on_update_failed(self, message: str):
self.update_btn.setEnabled(True)
self.update_btn.setText(
f"Обновить до {self.latest_release.tag_name}"
if self.latest_release and update_available(self.latest_release)
else "Проверить обновления"
)
self.update_label.setText("Не удалось загрузить" if self.downloading_update else "Нет связи с Gitea")
if self.manual_update_check or self.downloading_update:
QMessageBox.warning(self, "Обновление", message)
def create_search_tab(self) -> QWidget:
"""Создание вкладки поиска"""
widget = QWidget()
layout = QVBoxLayout()
# Поиск
search_group = QGroupBox("Поиск отгрузки")
search_layout = QHBoxLayout()
self.search_input = QLineEdit()
self.search_input.setPlaceholderText(
"Введите номер отгрузки..."
)
self.search_input.returnPressed.connect(self.search_orders)
search_layout.addWidget(self.search_input)
self.search_btn = QPushButton("Найти")
self.search_btn.clicked.connect(self.search_orders)
search_layout.addWidget(self.search_btn)
search_group.setLayout(search_layout)
layout.addWidget(search_group)
# Результаты
results_group = QGroupBox("Результаты")
results_layout = QVBoxLayout()
self.search_results_table = QTableWidget()
self.search_results_table.setColumnCount(5)
self.search_results_table.setHorizontalHeaderLabels([
"№", "Дата", "Контрагент", "Сумма", "Действия"
])
self.search_results_table.horizontalHeader().setStretchLastSection(
True
)
self.search_results_table.setSelectionBehavior(
QTableWidget.SelectionBehavior.SelectRows
)
results_layout.addWidget(self.search_results_table)
results_group.setLayout(results_layout)
layout.addWidget(results_group)
widget.setLayout(layout)
return widget
def create_list_tab(self) -> QWidget:
"""Создание вкладки списка"""
widget = QWidget()
layout = QVBoxLayout()
# Кнопки управления
controls_layout = QHBoxLayout()
self.load_list_btn = QPushButton("Загрузить список")
self.load_list_btn.clicked.connect(self.load_orders_list)
controls_layout.addWidget(self.load_list_btn)
controls_layout.addStretch()
self.prev_page_btn = QPushButton("← Предыдущая")
self.prev_page_btn.clicked.connect(self.prev_page)
self.prev_page_btn.setEnabled(False)
controls_layout.addWidget(self.prev_page_btn)
self.page_label = QLabel("Страница: 1")
controls_layout.addWidget(self.page_label)
self.next_page_btn = QPushButton("Следующая →")
self.next_page_btn.clicked.connect(self.next_page)
self.next_page_btn.setEnabled(False)
controls_layout.addWidget(self.next_page_btn)
layout.addLayout(controls_layout)
# Таблица
self.orders_table = QTableWidget()
self.orders_table.setColumnCount(5)
self.orders_table.setHorizontalHeaderLabels([
"№", "Дата", "Контрагент", "Сумма", "Действия"
])
self.orders_table.horizontalHeader().setStretchLastSection(True)
self.orders_table.setSelectionBehavior(
QTableWidget.SelectionBehavior.SelectRows
)
layout.addWidget(self.orders_table)
widget.setLayout(layout)
return widget
def create_batch_tab(self) -> QWidget:
"""Создание вкладки массовой генерации"""
widget = QWidget()
layout = QVBoxLayout()
# Ввод номеров
input_group = QGroupBox("Номера отгрузок")
input_layout = QVBoxLayout()
info_label = QLabel(
"Введите номера отгрузок через запятую (максимум 50):"
)
input_layout.addWidget(info_label)
self.batch_input = QTextEdit()
self.batch_input.setPlaceholderText(
"Например: 4926,4927,4928"
)
self.batch_input.setMaximumHeight(100)
input_layout.addWidget(self.batch_input)
input_group.setLayout(input_layout)
layout.addWidget(input_group)
# Настройки
settings_group = QGroupBox("Настройки")
settings_layout = QFormLayout()
self.batch_mode_combo = QComboBox()
self.batch_mode_combo.addItems([
"Один файл (все документы)",
"Отдельные файлы (каждый документ)"
])
settings_layout.addRow("Режим:", self.batch_mode_combo)
self.validate_checkbox = QCheckBox("Валидировать перед генерацией")
self.validate_checkbox.setChecked(True)
settings_layout.addRow("", self.validate_checkbox)
settings_group.setLayout(settings_layout)
layout.addWidget(settings_group)
# Кнопка генерации
self.batch_generate_btn = QPushButton("Сгенерировать")
self.batch_generate_btn.clicked.connect(self.batch_generate)
layout.addWidget(self.batch_generate_btn)
# Результаты
results_group = QGroupBox("Результаты")
results_layout = QVBoxLayout()
self.batch_results = QTextEdit()
self.batch_results.setReadOnly(True)
results_layout.addWidget(self.batch_results)
results_group.setLayout(results_layout)
layout.addWidget(results_group)
widget.setLayout(layout)
return widget
def check_credentials(self):
"""Проверка сохраненных учетных данных"""
try:
self.client = MoySkladClient(
settings.moysklad_login,
settings.moysklad_password,
settings.moysklad_api_url
)
# Тестовый запрос
self.client.get_orders(limit=1)
self.update_status(True)
except Exception:
self.update_status(False)
def show_login_dialog(self):
"""Показ диалога авторизации"""
dialog = LoginDialog(self)
if dialog.exec() == QDialog.DialogCode.Accepted:
login, password = dialog.get_credentials()
self.login(login, password)
def login(self, login: str, password: str):
"""Авторизация"""
try:
self.client = MoySkladClient(
login, password, settings.moysklad_api_url
)
# Тестовый запрос
self.client.get_orders(limit=1)
self.update_status(True)
QMessageBox.information(
self, "Успех", "Авторизация успешна!"
)
except Exception as e:
QMessageBox.critical(
self, "Ошибка", f"Ошибка авторизации:\n{e}"
)
self.update_status(False)
def logout(self):
"""Выход"""
self.client = None
self.update_status(False)
self.current_orders = []
self.orders_table.setRowCount(0)
self.search_results_table.setRowCount(0)
def update_status(self, logged_in: bool):
"""Обновление статуса авторизации"""
if logged_in:
self.status_label.setText("✓ Авторизован")
self.status_label.setStyleSheet(
"color: green; font-weight: bold;"
)
self.login_btn.setEnabled(False)
self.logout_btn.setEnabled(True)
self.tabs.setEnabled(True)
else:
self.status_label.setText("✗ Не авторизован")
self.status_label.setStyleSheet("color: red; font-weight: bold;")
self.login_btn.setEnabled(True)
self.logout_btn.setEnabled(False)
self.tabs.setEnabled(False)
def search_orders(self):
"""Поиск отгрузок"""
if not self.client:
QMessageBox.warning(self, "Ошибка", "Необходима авторизация")
return
query = self.search_input.text().strip()
if not query:
QMessageBox.warning(self, "Ошибка", "Введите номер отгрузки")
return
# Блокируем кнопку на время поиска
self.search_btn.setEnabled(False)
self.show_progress("Поиск...")
def search():
try:
return self.client.search_orders(query, limit=10)
except Exception as e:
raise Exception(f"Ошибка поиска: {str(e)}")
self.run_in_thread(search, self.on_search_complete)
def on_search_complete(self, result):
"""Обработка результатов поиска"""
self.hide_progress()
self.search_btn.setEnabled(True)
if not result or 'rows' not in result:
QMessageBox.warning(
self, "Ошибка", "Не удалось выполнить поиск"
)
return
orders = result.get('rows', [])
if not orders:
QMessageBox.information(
self, "Результаты", "Отгрузки не найдены"
)
return
self.display_orders_in_table(orders, self.search_results_table)
def load_orders_list(self):
"""Загрузка списка отгрузок"""
if not self.client:
QMessageBox.warning(self, "Ошибка", "Необходима авторизация")
return
# Сброс на первую страницу
self.current_page = 0
self.load_page()
def load_page(self):
"""Загрузка текущей страницы"""
if not self.client:
return
# Блокируем кнопки на время загрузки
self.load_list_btn.setEnabled(False)
self.prev_page_btn.setEnabled(False)
self.next_page_btn.setEnabled(False)
offset = self.current_page * self.page_size
self.show_progress(
f"Загрузка страницы {self.current_page + 1}..."
)
def load():
try:
result = self.client.get_orders(
limit=self.page_size,
offset=offset
)
# Получаем общее количество
total = result.get('meta', {}).get('size', 0)
return result, total
except Exception as e:
raise Exception(f"Ошибка загрузки списка: {str(e)}")
self.run_in_thread(load, self.on_list_complete)
def on_list_complete(self, result):
"""Обработка списка отгрузок"""
self.hide_progress()
self.load_list_btn.setEnabled(True)
if not result:
QMessageBox.warning(
self, "Ошибка", "Не удалось загрузить список отгрузок"
)
return
# result теперь кортеж (data, total)
data, total = result
if not data or 'rows' not in data:
QMessageBox.warning(
self, "Ошибка", "Не удалось загрузить список отгрузок"
)
return
self.current_orders = data.get('rows', [])
self.total_orders = total
# Обновляем кнопки пагинации
self.update_pagination_buttons()
# Отображаем отгрузки
self.display_orders_in_table(self.current_orders, self.orders_table)
def update_pagination_buttons(self):
"""Обновление кнопок пагинации"""
total_pages = (
(self.total_orders + self.page_size - 1) // self.page_size
)
# Обновляем текст страницы
self.page_label.setText(
f"Страница: {self.current_page + 1} из {total_pages} "
f"(всего: {self.total_orders})"
)
# Включаем/выключаем кнопки
self.prev_page_btn.setEnabled(self.current_page > 0)
self.next_page_btn.setEnabled(
(self.current_page + 1) * self.page_size < self.total_orders
)
def display_orders_in_table(self, orders: List[Dict], table: QTableWidget):
"""Отображение отгрузок в таблице"""
table.setRowCount(len(orders))
for i, order in enumerate(orders):
# Номер
table.setItem(i, 0, QTableWidgetItem(order.get('name', 'N/A')))
# Дата
date_str = order.get('moment', '')[:10]
table.setItem(i, 1, QTableWidgetItem(date_str))
# Контрагент
agent = order.get('agent', {})
table.setItem(i, 2, QTableWidgetItem(agent.get('name', 'N/A')))
# Сумма
sum_val = order.get('sum', 0) / 100
table.setItem(i, 3, QTableWidgetItem(f"{sum_val:,.2f} ₽"))
# Кнопки действий
actions_widget = QWidget()
actions_layout = QHBoxLayout()
actions_layout.setContentsMargins(2, 2, 2, 2)
validate_btn = QPushButton("Валидировать")
validate_btn.clicked.connect(
lambda checked, o=order: self.validate_order(o)
)
actions_layout.addWidget(validate_btn)
generate_btn = QPushButton("Генерировать XML")
generate_btn.clicked.connect(
lambda checked, o=order: self.generate_xml_for_order(o)
)
actions_layout.addWidget(generate_btn)
actions_widget.setLayout(actions_layout)
table.setCellWidget(i, 4, actions_widget)
def validate_order(self, order: Dict):
"""Валидация отгрузки"""
if not self.client:
return
order_id = order['id']
self.show_progress("Валидация...")
def validate():
order_data = self.client.get_full_order_data(order_id)
results = self.validator.validate_order(order_data)
return order_data, results
self.run_in_thread(validate, self.on_validate_complete)
def on_validate_complete(self, result):
"""Обработка результатов валидации"""
self.hide_progress()
order_data, validation_results = result
level, summary = self.validator.get_validation_summary(
validation_results
)
if level == ValidationLevel.CRITICAL:
QMessageBox.critical(self, "Ошибки валидации", summary)
elif level == ValidationLevel.WARNING:
QMessageBox.warning(self, "Предупреждения", summary)
else:
QMessageBox.information(self, "Валидация", summary)
def generate_xml_for_order(self, order: Dict):
"""Генерация XML для отгрузки"""
if not self.client:
return
order_id = order['id']
self.show_progress("Генерация XML...")
def generate():
order_data = self.client.get_full_order_data(order_id)
xml_content = self.xml_generator.generate_realization(order_data)
return order_data, xml_content
self.run_in_thread(generate, self.on_generate_complete)
def on_generate_complete(self, result):
"""Обработка сгенерированного XML"""
self.hide_progress()
order_data, xml_content = result
order_name = order_data['order'].get('name', 'order')
filename, _ = QFileDialog.getSaveFileName(
self,
"Сохранить XML",
f"order_{order_name}.xml",
"XML Files (*.xml)"
)
if filename:
with open(filename, 'w', encoding='utf-8') as f:
f.write(xml_content)
QMessageBox.information(
self, "Успех", f"XML сохранен:\n{filename}"
)
def batch_generate(self):
"""Массовая генерация"""
if not self.client:
QMessageBox.warning(self, "Ошибка", "Необходима авторизация")
return
order_numbers = [
num.strip()
for num in self.batch_input.toPlainText().split(',')
if num.strip()
]
if not order_numbers:
QMessageBox.warning(
self, "Ошибка", "Введите номера отгрузок"
)
return
if len(order_numbers) > 50:
QMessageBox.warning(
self, "Ошибка", "Максимум 50 отгрузок за раз"
)
return
batch_mode = self.batch_mode_combo.currentIndex() == 0
validate = self.validate_checkbox.isChecked()
self.show_progress("Обработка...")
self.batch_results.clear()
def process():
return self.process_batch(order_numbers, batch_mode, validate)
self.run_in_thread(process, self.on_batch_complete)
def process_batch(
self,
order_numbers: List[str],
batch_mode: bool,
validate: bool
) -> Dict:
"""Обработка пакета отгрузок"""
orders_data = []
not_found = []
errors = []
validation_errors = {}
for order_num in order_numbers:
try:
# Поиск
search_result = self.client.search_orders(order_num, limit=1)
rows = search_result.get('rows', [])
if not rows:
not_found.append(order_num)
continue
order_id = rows[0]['id']
order_data = self.client.get_full_order_data(order_id)
# Валидация
if validate:
results = self.validator.validate_order(order_data)
critical = [
r for r in results
if r.level == ValidationLevel.CRITICAL
]
if critical:
validation_errors[order_num] = [
f"{r.field}: {r.message}" for r in critical
]
continue
orders_data.append(order_data)
except Exception as e:
errors.append(f"{order_num}: {str(e)[:50]}")
return {
'orders_data': orders_data,
'not_found': not_found,
'errors': errors,
'validation_errors': validation_errors,
'batch_mode': batch_mode
}
def on_batch_complete(self, result):
"""Обработка результатов пакетной генерации"""
self.hide_progress()
orders_data = result['orders_data']
not_found = result['not_found']
errors = result['errors']
validation_errors = result['validation_errors']
batch_mode = result['batch_mode']
# Формируем отчет
report = []
report.append(f"Обработано: {len(orders_data)} отгрузок")
if validation_errors:
report.append(f"\n❌ Ошибки валидации: {len(validation_errors)}")
for order_num, errs in validation_errors.items():
report.append(f"\n {order_num}:")
for err in errs:
report.append(f" • {err}")
if not_found:
report.append(f"\n⚠️ Не найдено: {', '.join(not_found)}")
if errors:
report.append("\n⚠️ Ошибки обработки:")
for err in errors:
report.append(f" • {err}")
self.batch_results.setText('\n'.join(report))
# Генерация XML
if orders_data:
if batch_mode:
self.generate_batch_xml(orders_data)
else:
self.generate_separate_xmls(orders_data)
else:
QMessageBox.warning(
self,
"Предупреждение",
"Нет отгрузок для генерации"
)
def generate_batch_xml(self, orders_data: List[Dict]):
"""Генерация одного XML со всеми документами"""
filename, _ = QFileDialog.getSaveFileName(
self,
"Сохранить пакетный XML",
f"batch_{len(orders_data)}_orders.xml",
"XML Files (*.xml)"
)
if filename:
self.xml_generator.generate_batch_realization(
orders_data, filename
)
QMessageBox.information(
self, "Успех", f"Пакетный XML сохранен:\n{filename}"
)
def generate_separate_xmls(self, orders_data: List[Dict]):
"""Генерация отдельных XML файлов"""
directory = QFileDialog.getExistingDirectory(
self, "Выберите папку для сохранения"
)
if directory:
for order_data in orders_data:
order_name = order_data['order'].get('name', 'order')
filename = os.path.join(
directory, f"order_{order_name}.xml"
)
xml_content = self.xml_generator.generate_realization(
order_data
)
with open(filename, 'w', encoding='utf-8') as f:
f.write(xml_content)
QMessageBox.information(
self,
"Успех",
f"Сохранено {len(orders_data)} файлов в:\n{directory}"
)
def run_in_thread(self, func, callback):
"""Запуск функции в отдельном потоке"""
# Останавливаем предыдущий поток если есть
if self.worker_thread and self.worker_thread.isRunning():
self.worker_thread.wait()
self.worker_thread = WorkerThread(func)
self.worker_thread.finished.connect(callback)
self.worker_thread.error.connect(self.on_thread_error)
self.worker_thread.start()
def on_thread_error(self, error_msg: str):
"""Обработка ошибки в потоке"""
self.hide_progress()
# Разблокируем кнопки
self.search_btn.setEnabled(True)
self.load_list_btn.setEnabled(True)
QMessageBox.critical(self, "Ошибка", error_msg)
def show_progress(self, message: str):
"""Показ прогресса"""
self.progress_bar.setVisible(True)
self.progress_bar.setRange(0, 0) # Неопределенный прогресс
self.statusBar().showMessage(message)
def hide_progress(self):
"""Скрытие прогресса"""
self.progress_bar.setVisible(False)
self.statusBar().clearMessage()
def prev_page(self):
"""Предыдущая страница"""
if self.current_page > 0:
self.current_page -= 1
self.load_page()
def next_page(self):
"""Следующая страница"""
if (self.current_page + 1) * self.page_size < self.total_orders:
self.current_page += 1
self.load_page()
def main():
"""Запуск приложения"""
app = QApplication(sys.argv)
app.setApplicationName("МойСклад - Генератор документов")
app.setOrganizationName("BUH_bot")
# Установка иконки приложения для всех окон
icon_path = os.path.join(os.path.dirname(__file__), 'app_icon.png')
if os.path.exists(icon_path):
app.setWindowIcon(QIcon(icon_path))
ThemeManager.apply(app)
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()