#!/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()